aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomasz Mieszkowski <tomasz.mieszkowski@ext.allegro.pl>2014-08-28 08:54:15 +0200
committerTomasz Mieszkowski <tomasz.mieszkowski@ext.allegro.pl>2014-08-28 08:54:15 +0200
commit60c073a6ed451cf91d9877a3305407756b9ffdce (patch)
tree955542ec282745e31d78d1b2b9515d8cebe67e78
downloadtipboard-60c073a6ed451cf91d9877a3305407756b9ffdce.tar.gz
tipboard-60c073a6ed451cf91d9877a3305407756b9ffdce.tar.bz2
tipboard-60c073a6ed451cf91d9877a3305407756b9ffdce.zip
Tipboard got open-sourced!1.4.0
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml15
-rw-r--r--CHANGES.rst123
-rw-r--r--LICENSE.rst13
-rw-r--r--MANIFEST.in6
-rw-r--r--README.rst84
-rw-r--r--doc/Makefile177
-rw-r--r--doc/api.rst216
-rw-r--r--doc/changelog.rst1
-rw-r--r--doc/conf.py265
-rw-r--r--doc/configuration.rst241
-rw-r--r--doc/extras.rst58
-rw-r--r--doc/img/advanced-plot.pngbin0 -> 123312 bytes
-rw-r--r--doc/img/bar-chart.pngbin0 -> 15504 bytes
-rw-r--r--doc/img/big-value.pngbin0 -> 26190 bytes
-rw-r--r--doc/img/cumulative-flow.pngbin0 -> 36749 bytes
-rw-r--r--doc/img/fancy-listing.pngbin0 -> 36610 bytes
-rw-r--r--doc/img/just-value.pngbin0 -> 19944 bytes
-rw-r--r--doc/img/line-chart.pngbin0 -> 46423 bytes
-rw-r--r--doc/img/listing.pngbin0 -> 32683 bytes
-rw-r--r--doc/img/norm-chart.pngbin0 -> 14919 bytes
-rw-r--r--doc/img/pie-chart.pngbin0 -> 17345 bytes
-rw-r--r--doc/img/simple-percentage.pngbin0 -> 21221 bytes
-rw-r--r--doc/img/smaller/advanced-plot.pngbin0 -> 104095 bytes
-rw-r--r--doc/img/smaller/bar-chart.pngbin0 -> 19455 bytes
-rw-r--r--doc/img/smaller/big-value.pngbin0 -> 35928 bytes
-rw-r--r--doc/img/smaller/cumulative-flow.pngbin0 -> 42946 bytes
-rw-r--r--doc/img/smaller/fancy-listing.pngbin0 -> 53125 bytes
-rw-r--r--doc/img/smaller/just-value.pngbin0 -> 25660 bytes
-rw-r--r--doc/img/smaller/line-chart.pngbin0 -> 56810 bytes
-rw-r--r--doc/img/smaller/listing.pngbin0 -> 40311 bytes
-rw-r--r--doc/img/smaller/norm-chart.pngbin0 -> 19436 bytes
-rw-r--r--doc/img/smaller/pie-chart.pngbin0 -> 23370 bytes
-rw-r--r--doc/img/smaller/simple-percentage.pngbin0 -> 36279 bytes
-rw-r--r--doc/img/smaller/text.pngbin0 -> 56207 bytes
-rw-r--r--doc/img/text.pngbin0 -> 19447 bytes
-rw-r--r--doc/index.rst16
-rw-r--r--doc/installation.rst98
-rw-r--r--doc/license.rst5
-rw-r--r--doc/overview.rst21
-rw-r--r--doc/tile__advanced_plot.rst140
-rw-r--r--doc/tile__bar_chart.rst61
-rw-r--r--doc/tile__big_value.rst97
-rw-r--r--doc/tile__cumulative_flow.rst68
-rw-r--r--doc/tile__fancy_listing.rst91
-rw-r--r--doc/tile__just_value.rst67
-rw-r--r--doc/tile__line_chart.rst81
-rw-r--r--doc/tile__listing.rst34
-rw-r--r--doc/tile__norm_chart.rst79
-rw-r--r--doc/tile__pie_chart.rst64
-rw-r--r--doc/tile__simple_percentage.rst81
-rw-r--r--doc/tile__text.rst61
-rw-r--r--doc/tiles.rst99
-rw-r--r--requirements.txt11
-rw-r--r--setup.py38
-rw-r--r--tipboard/__init__.py11
-rw-r--r--tipboard/api.py180
-rw-r--r--tipboard/app.py299
-rw-r--r--tipboard/console.py245
-rw-r--r--tipboard/defaults/layout_config.yaml52
-rw-r--r--tipboard/defaults/settings-local.py8
-rw-r--r--tipboard/extras/README.rst5
-rw-r--r--tipboard/extras/client_code_example.py104
-rw-r--r--tipboard/extras/fabfile.py52
-rw-r--r--tipboard/extras/jira-ds.js191
-rwxr-xr-xtipboard/extras/jira-ds.py182
-rw-r--r--tipboard/parser.py117
-rw-r--r--tipboard/redis_utils.py104
-rw-r--r--tipboard/settings.py143
-rw-r--r--tipboard/static/css/jquery.jqplot.css259
-rw-r--r--tipboard/static/css/layout.css544
-rw-r--r--tipboard/static/css/reset.css2
-rw-r--r--tipboard/static/fonts/MISO_LICENSE41
-rw-r--r--tipboard/static/fonts/miso-light-webfont.eotbin0 -> 23391 bytes
-rw-r--r--tipboard/static/fonts/miso-light-webfont.svg420
-rw-r--r--tipboard/static/fonts/miso-light-webfont.ttfbin0 -> 49396 bytes
-rw-r--r--tipboard/static/fonts/miso-light-webfont.woffbin0 -> 27432 bytes
-rw-r--r--tipboard/static/fonts/miso-webfont.eotbin0 -> 23595 bytes
-rw-r--r--tipboard/static/fonts/miso-webfont.svg420
-rw-r--r--tipboard/static/fonts/miso-webfont.ttfbin0 -> 50524 bytes
-rw-r--r--tipboard/static/fonts/miso-webfont.woffbin0 -> 27468 bytes
-rw-r--r--tipboard/static/js/flipboard.js73
-rw-r--r--tipboard/static/js/lib/html5shiv.js8
-rw-r--r--tipboard/static/js/lib/jqplot/excanvas.min.js3
-rw-r--r--tipboard/static/js/lib/jqplot/jquery.jqplot.js11411
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.BezierCurveRenderer.js314
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.barRenderer.js801
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.blockRenderer.js235
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.bubbleRenderer.js759
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js203
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js253
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.canvasOverlay.js1021
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js449
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js679
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.ciParser.js116
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.cursor.js1108
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js741
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.donutRenderer.js805
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.dragable.js225
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js305
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.funnelRenderer.js943
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.highlighter.js465
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.json2.js475
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.logAxisRenderer.js534
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js611
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoRenderer.js437
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js1029
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.mobile.js45
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.ohlcRenderer.js373
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.pieRenderer.js904
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.pointLabels.js377
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js728
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js429
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidRenderer.js514
-rw-r--r--tipboard/static/js/lib/jqplot/plugins/jqplot.trendline.js223
-rw-r--r--tipboard/static/js/lib/jquery.fullscreen.js123
-rw-r--r--tipboard/static/js/lib/jquery.js9597
-rw-r--r--tipboard/static/js/tipboard.js783
-rw-r--r--tipboard/templates/base.html22
-rw-r--r--tipboard/templates/flipboard.html21
-rw-r--r--tipboard/templates/layout.html49
-rw-r--r--tipboard/tests/__init__.py0
-rw-r--r--tipboard/tests/frontend/SpecRunner.html397
-rw-r--r--tipboard/tests/frontend/lib/jasmine-2.0.0/boot.js181
-rw-r--r--tipboard/tests/frontend/lib/jasmine-2.0.0/console.js160
-rw-r--r--tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine-html.js359
-rw-r--r--tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.css55
-rw-r--r--tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.js2402
-rw-r--r--tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine_favicon.pngbin0 -> 2057 bytes
-rw-r--r--tipboard/tests/frontend/spec/tipboardSpec.js705
-rw-r--r--tipboard/tests/test_config_parser.py201
-rw-r--r--tipboard/tests/test_console_commands.py129
-rw-r--r--tipboard/tests/test_rest_api.py187
-rw-r--r--tipboard/tiles/advanced_plot.html14
-rw-r--r--tipboard/tiles/advanced_plot.js35
-rw-r--r--tipboard/tiles/bar_chart.css1
-rw-r--r--tipboard/tiles/bar_chart.html10
-rw-r--r--tipboard/tiles/bar_chart.js69
-rw-r--r--tipboard/tiles/big_value.css40
-rw-r--r--tipboard/tiles/big_value.html35
-rw-r--r--tipboard/tiles/big_value.js33
-rw-r--r--tipboard/tiles/cumulative_flow.css18
-rw-r--r--tipboard/tiles/cumulative_flow.html16
-rw-r--r--tipboard/tiles/cumulative_flow.js103
-rw-r--r--tipboard/tiles/empty.css3
-rw-r--r--tipboard/tiles/empty.html8
-rw-r--r--tipboard/tiles/fancy_listing.css46
-rw-r--r--tipboard/tiles/fancy_listing.html19
-rw-r--r--tipboard/tiles/fancy_listing.js78
-rw-r--r--tipboard/tiles/just_value.css39
-rw-r--r--tipboard/tiles/just_value.html16
-rw-r--r--tipboard/tiles/just_value.js29
-rw-r--r--tipboard/tiles/line_chart.html10
-rw-r--r--tipboard/tiles/line_chart.js92
-rw-r--r--tipboard/tiles/listing.css14
-rw-r--r--tipboard/tiles/listing.html13
-rw-r--r--tipboard/tiles/listing.js35
-rw-r--r--tipboard/tiles/norm_chart.html10
-rw-r--r--tipboard/tiles/norm_chart.js142
-rw-r--r--tipboard/tiles/pie_chart.html10
-rw-r--r--tipboard/tiles/pie_chart.js64
-rw-r--r--tipboard/tiles/simple_percentage.css31
-rw-r--r--tipboard/tiles/simple_percentage.html22
-rw-r--r--tipboard/tiles/simple_percentage.js18
-rw-r--r--tipboard/tiles/text.html9
-rw-r--r--tipboard/tiles/text.js28
166 files changed, 49871 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4c68c24
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+tipboard.egg-info/
+/.idea/*
+*.pyc
+*.swp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..6046636
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,15 @@
+language: python
+python:
+ - "2.7"
+# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
+install:
+ - pip install flake8 --use-mirrors
+ - pip install . --use-mirrors
+before_script:
+ - flake8 --statistics tipboard
+# command to run tests, e.g. python setup.py test
+script:
+ - tipboard test test_config_parser
+ - tipboard test test_console_commands
+ - tipboard test test_rest_api.TestRestApi.test_01_api_key
+ - tipboard test test_rest_api.TestRestApi.test_02_info_resource
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..ce81671
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,123 @@
+Change Log
+----------
+
+1.4.0
+~~~~~
+
+Released on August 28, 2014.
+
+* Tipboard got open-sourced!
+
+
+1.3.1
+~~~~~
+
+Released on July 23, 2014.
+
+* Added extensive documentation.
+
+* Numerous fixes in 'jira-ds' script (e.g added timeouts).
+
+* Fixed definitions of colors available for tiles.
+
+* Fixed checking for expired data (+ made it timezone aware).
+
+* Added integration with Travis.
+
+* Changed default size of the log files.
+
+
+1.3.0
+~~~~~
+
+Released on February 17, 2014.
+
+New features:
+
+* Fading highlighter (for just_value, big_value and simple_percentage tiles).
+
+* Fancy centering options for fancy_listing tile.
+
+* Notifications on data expiration.
+
+* New tile: norm_chart.
+
+* Possibility to define more than one dashboard per application instance.
+
+
+Bug fixes:
+
+* Tiles no longer vanish when flipping is enabled.
+
+* Characters like '.' or '-' (and some others) in tiles' ids are no longer
+ causing problems.
+
+* Renderer names (like OHLCRenderer, MarkerRenderer, ShadowRenderer and
+ ShapeRenderer) can now safely be passed to tiles' configs.
+
+
+Others:
+
+* Error messages displayed on tiles got more emphasis.
+
+* Renderer names (in tiles' configs) are now case insensitive.
+
+* Added frontend tests and selector for tests.
+
+
+1.2.0
+~~~~~
+
+Released on December 19, 2013.
+
+This release brings new features and some minor bugfixes.
+
+* New tiles: big_value, just_value, advanced_plot.
+
+* Rewritten 'jira-ds' script with some new options (e.g. 'maxResults' for JQL).
+
+* Completely new graphic theme - with new colors, fonts etc.
+
+* Fixed existing tests and some new added.
+
+* Exceptions raised by JavaScript are now displayed on the tiles.
+
+* Improved config handling for bar_chart, pie_chart and line_chart.
+
+* Added possibility to specify specialized renderers for almost all plots
+ (except cumulative_flow).
+
+
+1.1.0
+~~~~~
+
+Released on November 20, 2013.
+
+This release contains multiple improvements and bugfixes:
+
+* Tiles are no longer packages (i.e. folders).
+
+* Reorganized files/folders structure.
+
+* Massively reduced app's settings.
+
+* Simplified layout config (no more classes, only one keyword needed to get
+ tile flips working).
+
+* New tiles: bar_chart, fancy_listing.
+
+* Improved scaling of tiles + some cosmetic changes.
+
+* Unique API key is generated automatically for every project.
+
+* Fabric script for administrative installs
+
+
+1.0.0
+~~~~~
+
+Released on November 06, 2013.
+
+This is the first release of Tipboard.
+
+* initial release
diff --git a/LICENSE.rst b/LICENSE.rst
new file mode 100644
index 0000000..7dcd463
--- /dev/null
+++ b/LICENSE.rst
@@ -0,0 +1,13 @@
+ Copyright 2013-2014 Allegro Group
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..d70e5a7
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include README.rst CHANGES.rst LICENSE.rst requirements.txt
+recursive-include tipboard/static *
+recursive-include tipboard/templates *
+recursive-include tipboard/tiles *
+recursive-include tipboard/defaults *
+recursive-include tipboard/extras *
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..be6ef7e
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,84 @@
+========
+Tipboard
+========
+
+Introduction
+------------
+
+Tipboard is a system for creating dashboards, written in JavaScript and Python.
+Its widgets ('tiles' in Tipboard's terminology) are completely separated from
+data sources, which provides great flexibility and relatively high degree of
+possible customizations.
+
+Because of its intended target (displaying various data and statistics in your
+office), it is optimized for larger screens.
+
+Similar projects: `Geckoboard <http://www.geckoboard.com/>`_,
+`Dashing <http://shopify.github.io/dashing/>`_.
+
+A detailed, technical documentation for Tipboard can be found
+`here <http://tipboard.readthedocs.org/en/latest/>`_.
+
+
+Quick start
+-----------
+
+Requirements
+~~~~~~~~~~~~
+
+Assuming Ubuntu or similar Linux distribution, some required packages need
+to be installed first::
+
+ $ sudo apt-get install python-dev python-virtualenv redis-server
+
+Virtual environment
+~~~~~~~~~~~~~~~~~~~
+
+Continue by creating a virtual environment that will help you conveniently
+separate your instance from what you already have installed in the system
+(let's say we will name it "tb-env")::
+
+ $ virtualenv tb-env
+
+Activate the created virtual environment with the following command::
+
+ $ source tb-env/bin/activate
+
+Installation with pip
+~~~~~~~~~~~~~~~~~~~~~
+
+After creating and activating virtualenv, install the latest (current) version
+of Tipboard package available on `pypi <https://pypi.python.org/pypi>`_
+("Python Package Index") with the following command::
+
+ (tb-env)$ pip install tipboard
+
+Next, you need to create a configuration template for your dashboard - let's
+say we will call it 'my_test_dashboard'::
+
+ (tb-env)$ tipboard create_project my_test_dashboard
+
+This command will create ``.tipboard`` directory in your home dir and will
+fill it with default settings for your dashboard.
+
+Verification
+~~~~~~~~~~~~
+
+To verify your installation, launch this command::
+
+ (tb-env)$ tipboard runserver
+
+If you see the message ``Listening on port...`` instead of any errors, it means
+that installation was successful and you may now
+`configure <http://tipboard.readthedocs.org/en/latest/configuration.html>`_
+your newly installed Tipboard's instance. You may also point your favourite
+web browser to ``http://localhost:7272`` to see the current state of your
+dashboard.
+
+
+License
+-------
+
+Tipboard is licensed under the `Apache License, v2.0 <http://tipboard.readthedocs.org/en/latest/license.html>`_.
+
+Copyright (c) 2013-2014 `Allegro Group <http://allegrogroup.com>`_.
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..9826b9d
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tipboard.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tipboard.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Tipboard"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tipboard"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/doc/api.rst b/doc/api.rst
new file mode 100644
index 0000000..50ae7e4
--- /dev/null
+++ b/doc/api.rst
@@ -0,0 +1,216 @@
+===
+API
+===
+
+One of the advantages of Tipboard is flexibility in feeding tiles with data. We
+achieve that by providing a simple, REST API - that way, your feeding scripts
+may be written in any language (Python, Ruby, Bash, Perl, PHP - you name it).
+The only limitation is the format of input data accepted by a given tile type
+(see :ref:`tiles_library` for the details).
+
+To experiment with resources specified below you can use tools like `Advanced
+REST Client <http://chromerestclient.appspot.com/>`_ (Chrome extension), or
+`cURL <http://curl.haxx.se/>`_, if you prefer working from command line. For
+Python programmers, there's an excellent `Requests
+<http://docs.python-requests.org/en/latest/>`_ library, which we strongly
+recommend.
+
+.. _api_key:
+
+API key
+-------
+
+To send anything to your tiles, first you have to get your API key. This unique
+key is generated for you automatically during Tipboard's installation and may
+be read in the ``~/.tipboard/settings-local.py`` file - it is a sequence of
+characters starting with ``API_KEY``, e.g.::
+
+ API_KEY = 'e2c3275d0e1a4bc0da360dd225d74a43'
+
+If you can't see any such string, just add the key manually (it doesn't have
+to be as long and hard to memorise as the one above, though).
+
+.. note::
+
+ Every change in ``settings-local.py`` file requires restart of the
+ application.
+
+Available resources
+-------------------
+
+Current API version: **v0.1**
+
+.. note::
+
+ In 99% of cases, probably only ``push`` and ``tileconfig`` will be of
+ interest to you.
+
+
+.. http:post:: /api/(api_version)/(api_key)/push
+
+ Feeds tiles with data. Input data should be provided in the format that
+ complies with the one used in a desired tile. **Note:** a tile to which data
+ will be sent is defined by the key included in the data sent rather than by
+ `tile_id` as in cases below.
+
+ :param api_version: version of API to be used
+ :param api_key: your API key
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ POST /api/v0.1/my_key/push
+ Host: localhost:7272
+ POST data: tile=text key=id_1 data={"text": "Hello world!"}
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: text/html; charset=UTF-8
+
+ Tile's data pushed successfully.
+
+.. http:post:: /api/(api_version)/(api_key)/tileconfig/(tile_id)
+
+ Configures tile specified by `tile_id`. The configuration should comply with
+ the specification of a given tile type.
+
+ :param api_version: version of API to be used
+ :param api_key: your API key
+ :param tile_id: unique tile's ID from your ``layout_config.yaml`` file
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v0.1/my_key/tileconfig/id_1
+ Host: localhost:7272
+ POST data: value={"font_color": "#00FF00"}
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: text/html; charset=UTF-8
+
+ Tile's config updated.
+
+
+.. http:delete:: /api/(api_version)/(api_key)/tileconfig/(tile_id)
+
+ Resets configuration of the tile specified by `tile_id`.
+
+ :param api_version: version of API to be used
+ :param api_key: your API key
+ :param tile_id: unique tile's ID from your ``layout_config.yaml`` file
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ DELETE /api/v0.1/my_key/tileconfig/id_1
+ Host: localhost:7272
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: text/html; charset=UTF-8
+
+ Tile's config deleted.
+
+.. http:get:: /api/(api_version)/(api_key)/tiledata/(tile_id)
+
+ Retrieves data belonging to the tile specified by `tile_id`. May be useful for diagnostics.
+
+ :param api_version: version of API to be used
+ :param api_key: your API key
+ :param tile_id: unique tile's ID from your ``layout_config.yaml`` file
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v0.1/my_key/tiledata/id_1
+ Host: localhost:7272
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "tile_template": "text",
+ "meta": {
+ "font_color": "#ff9618",
+ "font_size": "45px"
+ },
+ "data": {
+ "text": "Lorem ipsum."
+ },
+ "id": "id_1"
+ }
+
+.. http:delete:: /api/(api_version)/(api_key)/tiledata/(tile_id)
+
+ Removes everything belonging to the tile given by `tile_id` from Redis.
+
+ :param api_version: version of API to be used
+ :param api_key: your API key
+ :param tile_id: unique tile's ID from your ``layout_config.yaml`` file
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ DELETE /api/v0.1/my_key/tiledata/id_1
+ Host: localhost:7272
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: text/html; charset=UTF-8
+
+ Tile's data deleted.
+
+.. http:get:: /api/(api_version)/(api_key)/info
+
+ Provides information on project and user configuration. This resource has
+ been created for debugging purposes.
+
+ :param api_version: version of API to be used
+ :param api_key: your API key
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v0.1/my_key/info
+ Host: localhost:7272
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "tipboard_version": "1.3.0",
+ "project_layout_config": "/home/pylabs/.tipboard/layout_config.yaml",
+ "redis_db": {
+ "host": "localhost",
+ "db": 4,
+ "port": 6379
+ },
+ "project_name": "pylabs"
+ }
diff --git a/doc/changelog.rst b/doc/changelog.rst
new file mode 100644
index 0000000..d9e113e
--- /dev/null
+++ b/doc/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGES.rst
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..7fe4c27
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+#
+# Tipboard documentation build configuration file, created by
+# sphinx-quickstart on Mon Mar 17 10:55:17 2014.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.todo',
+ 'sphinxcontrib.httpdomain',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Tipboard'
+copyright = u'2013-2014, Allegro Group'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.4.0'
+# The full version, including alpha/beta/rc tags.
+release = '1.4.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+html_domain_indices = False
+
+# If false, no index is generated.
+html_use_index = False
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = False
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Tipboarddoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'Tipboard.tex', u'Tipboard Documentation',
+ u'Allegro Group', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'tipboard', u'Tipboard Documentation',
+ [u'Allegro Group'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'Tipboard', u'Tipboard Documentation',
+ u'Allegro Group', 'Tipboard', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+
+# -- Misc options ---------------------------------------------------------
+todo_include_todos = True
diff --git a/doc/configuration.rst b/doc/configuration.rst
new file mode 100644
index 0000000..a4d8f0b
--- /dev/null
+++ b/doc/configuration.rst
@@ -0,0 +1,241 @@
+=============
+Configuration
+=============
+
+The description below assumes that you have installed Tipboard correctly and
+use a default configuration that is the starting point for steps presented
+below (see section :ref:`installation`).
+
+Default configuration
+---------------------
+
+Tipboard launched after installation present a basic, empty layout – empty
+tiles in 2 lines with 4 columns each. If you want to modify them, create a
+"clean" config, where your changes will be introduced. Use the command::
+
+ (tb-env)$ tipboard create_project <name_of_project>
+
+It will create the ``~/.tipboard`` dir with the following content:
+
+* ``settings-local.yaml`` file that defines the layout of tiles on the
+ dashboard you are creating;
+
+* ``settings-local.py`` file in which you can overwrite default (global)
+ application settings; a description of options and their default values has
+ been presented in `this file
+ <https://github.com/allegro/tipboard/blob/develop/tipboard/settings.py>`_;
+
+* ``custom_tiles`` subdir to place your own tiles.
+
+.. note::
+
+ Before you send anything to your tiles, you have to get your API key
+ first, which is described in the :ref:`api_key` section.
+
+Launching Tipboard app
+----------------------
+
+After you have logged in to your machine, you may launch Tipboard with the
+command::
+
+ (tb-env)$ tipboard runserver [<host>] [<port>]
+
+...where ``host`` and ``port`` parameters are optional (by default these are
+``localhost`` and ``7272``; if you want the application to listen on all the
+network interfaces, set ``host`` to ``0.0.0.0``).
+
+Customising tile layout
+-----------------------
+
+As mentioned previously, the layout of tiles in a dashboard is defined by
+``layout_config.yaml`` file. The file is in the `YAML <http://yaml.org>`_
+format, the description of which is beyond the scope of this manual. However,
+it is worth indicating that YAML has certain format requirements –
+**indentation should have a unified structure** (be a multiplication of a
+number, e.g. 4), when creating indentations **spaces should not be mixed with
+tabs**.
+
+Below you can find a list of options that can be saved in the file.
+Indentations indicate the position of a given option in the configuration (e.g.
+``details`` are superior to ``page_title``).
+
+::
+
+ details
+ page_tile
+ layout
+ row_X_of_Y
+ col_X_of_Y
+ tile_template
+ tile_id
+ tile
+ timeout
+
+where:
+
+.. describe:: details
+
+ A section that contains additional configuration parameters; for the time
+ being it is only ‘page_title’; depending on his needs, the users add other
+ elements.
+
+.. describe:: page_tile
+
+ A section that defines the title of a page to appear in the web browser
+ after entering the dashboard.
+
+.. describe:: layout
+
+ A section that contains a proper configuration of the tile layout.
+
+.. describe:: row_X_of_Y
+
+ Defines a row hight; a sum of Xs should equal Y.
+
+.. describe:: col_X_of_Y
+
+ Similar to above but concerns a column width in a given row.
+
+.. describe:: tile_template
+
+ The name of a tile template to be displayed (e.g. ``pie_chart``,
+ ``line_chart``, ``cumulative_flow``)
+
+.. describe:: tile_id
+
+ A tile identifier in a HTML document and key identifier in Redis.
+
+.. describe:: title
+
+ A title to be displayed in the upper part of the tile.
+
+.. describe:: timeout
+
+ The length (in seconds) of data life (if data is not sent during this time,
+ you will be informed that the data is stalled). Since interval used by the
+ application to check for those timeouts is 5 seconds, it doesn't make sense
+ to set this value smaller than this.
+
+ .. versionadded:: 1.3.0
+
+The method of using ``row_X_of_Y`` and ``col_X_of_Y`` has been presented in the
+examples below. If you want to see how it's done "from the kitchen", and you
+have some basic knowledge of CSS styling, have a look `here
+<https://github.com/allegro/tipboard/blob/develop/tipboard/static/css/layout.css>`_;
+
+.. note::
+
+ If you want to present a lot of data on your dashboard, consider dividing
+ all your tiles into two (or more) separate dashboards. Tiles offer a limited
+ capacity and if you "feed" them with too much data (e.g. long lines of
+ text), it is possible the dashboard will get broken.
+
+Setting tiles' rotation
+~~~~~~~~~~~~~~~~~~~~~~~
+
+One of the most useful functions is defining tiles to rotate. In a single
+container (i.e. in one of the fields indicated by ``col_X_of_Y`` and
+``row_X_of_Y``), you may define a few tiles to be displayed in this location as
+items rotating at intervals defined in the configuration (similar to ads
+rotating on bus/tram stops, so-called *citylights*). To achieve that:
+
+* add the ``flip-time-xx`` class to a container, where ``xx`` is rotation
+ interval in seconds;
+* add tile to the container.
+
+The example below presents a container with two tiles (one of the ``empty`` type,
+the other of the ``text`` type) to rotate every 2 seconds (``flip-time-2``).
+The rotation will start with the ``empty`` type tile::
+
+ layout:
+ - row_1_of_2:
+ - col_1_of_4 flip-time-2:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile 2
+
+ - tile_template: text
+ tile_id: text
+ title: Empty Tile
+
+Sample layout
+~~~~~~~~~~~~~
+
+Let's assume we want to define a layout as on the scheme below (i.e. a division
+into 2 equal rows, with the upper one divided into 4 columns, and the lower one
+divided into 3 columns)::
+
+ +-------+--------+--------+-------+
+ | | | | |
+ | | | | |
+ | | | | |
+ | | | | |
+ +-------+--+-----+----+---+-------+
+ | | | |
+ | | | |
+ | | | |
+ | | | |
+ +----------+----------+-----------+
+
+...its corresponding configuration file should look as follows (for brevity, I
+will present only the ``layout`` section, skipping the ``tile_template``,
+``title_id``, etc.)::
+
+ layout:
+ row_1_of_2:
+ col_1_of_4:
+ col_1_of_4:
+ col_1_of_4:
+ col_1_of_4:
+   row_1_of_2:
+ col_1_of_3:
+ col_1_of_3:
+ col_1_of_3:
+
+Multiple dashboards per application's instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. versionadded:: 1.3.0
+
+It is possible to define multiple dashboards per application's instance. In
+order to achieve that, you just create separate layout config files (one per
+every dashboard) - having done that, your dashboards will be available at::
+
+ http://localhost:7272/<name_of_layout_config_file>
+
+For example, having two layout config files ``my_first_dashboard.yaml`` and
+``my_second_dashboard.yaml``, the corresponding dashboards can be accessed
+via::
+
+ http://localhost:7272/my_first_dashboard
+ http://localhost:7272/my_second_dashboard
+
+.. note::
+
+ You have to strip the ``.yaml`` file extension when constructing your URLs.
+
+When it comes to feeding those dashboards with data, the future data location
+is specified by tile IDs (unique within application instance). Therefore, there
+is no need to specify different URLs for different dashboards - having tiles'
+IDs, Tipboard will make sure that your data is delivered where it should be.
+
+Multiple rotating dashboards
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.3.0
+
+If you have defined several dashboards (as described above), you may want to
+rotate (flip) them periodically. If you are unsure what that means, think of
+extensions like Revolver (Chrome) or Tab Slideshow (Firefox).
+
+To achieve that, you need:
+
+* at least two dashboards (well, that's kind of obvious)
+* in the file ``settings-local.py`` add the variable ``FLIPBOARD_INTERVAL =
+ <seconds>`` (e.g. ``FLIPBOARD_INTERVAL = 5``)
+
+The above solution will make all your dashboards rotate - if you want to limit
+this behavior and rotate only certain dashboards, just add another parameter
+``FLIPBOARD_SEQUENCE`` which is just a list of dashboard names that should be
+taken into account, e.g.::
+
+ FLIPBOARD_SEQUENCE = ['my_first_dashboard', 'my_third_dashboard']
diff --git a/doc/extras.rst b/doc/extras.rst
new file mode 100644
index 0000000..396fb35
--- /dev/null
+++ b/doc/extras.rst
@@ -0,0 +1,58 @@
+======
+Extras
+======
+
+Here you will find description of components which are not a part of the
+Tipboard project *per se*, although they may be useful to some of its users.
+Assuming standard installation, you can find them here::
+
+ <path_to_your_virtualenv>/lib/python2.7/site-packages/tipboard/extras
+
+.. note::
+
+ If you have developed something of similar nature and you are willing to
+ share it, we encourage you to make a pull request to our repo. Thanks!
+
+``jira-ds.py``
+--------------
+
+Script for fetching frequently used data from `JIRA
+<https://www.atlassian.com/software/jira>`_ issue tracker, in order to present
+it on your dashboards. Returns requested data to stdout in JSON format. For
+the list of available options see ``jira-ds.py --help``.
+
+This script is basically a wrapper around ``jira-ds.js``, so those two files
+shouldn't be separated. Requires `CasperJS <http://casperjs.org/>`_ and
+`PhantomJS <http://phantomjs.org/>`_ installed somewhere in your path (we
+suggest using `npm <http://nodejs.org/>`_ for that).
+
+Before you start using them, remember to fill in ``JIRA_CREDENTIALS`` and
+``JIRA_BASE_URL`` (in ``jira-ds.py``) as well as ``url_jira`` and
+``url_jira_login`` (in ``jira-ds.js``) with the your JIRA credentials, location
+of your JIRA instance and its login page.
+
+Tested with JIRA 6.1.x.
+
+
+``client_code_example.py``
+--------------------------
+
+Simple Python script targeted to novice users serving as an example how to glue
+together three steps: fetching data, processing it and then sending it to the
+tile. See comments in the source code for further explaination.
+
+
+``fabfile.py``
+--------------
+
+Script for quick, automated installations on remote machines.
+
+You need to have `fabric <https://github.com/ronnix/fabtools>`_ and
+`fabtools <http://fabtools.readthedocs.org>`_ to use remote install script.
+
+Run::
+
+ fab -H root@host install
+
+-- it will install all needed ``.deb`` packages, create virualenv and set up
+Tipboard service using master branch from our main repo.
diff --git a/doc/img/advanced-plot.png b/doc/img/advanced-plot.png
new file mode 100644
index 0000000..94da500
--- /dev/null
+++ b/doc/img/advanced-plot.png
Binary files differ
diff --git a/doc/img/bar-chart.png b/doc/img/bar-chart.png
new file mode 100644
index 0000000..7cf3048
--- /dev/null
+++ b/doc/img/bar-chart.png
Binary files differ
diff --git a/doc/img/big-value.png b/doc/img/big-value.png
new file mode 100644
index 0000000..b59bb52
--- /dev/null
+++ b/doc/img/big-value.png
Binary files differ
diff --git a/doc/img/cumulative-flow.png b/doc/img/cumulative-flow.png
new file mode 100644
index 0000000..13c9aaa
--- /dev/null
+++ b/doc/img/cumulative-flow.png
Binary files differ
diff --git a/doc/img/fancy-listing.png b/doc/img/fancy-listing.png
new file mode 100644
index 0000000..e5ab907
--- /dev/null
+++ b/doc/img/fancy-listing.png
Binary files differ
diff --git a/doc/img/just-value.png b/doc/img/just-value.png
new file mode 100644
index 0000000..43458d4
--- /dev/null
+++ b/doc/img/just-value.png
Binary files differ
diff --git a/doc/img/line-chart.png b/doc/img/line-chart.png
new file mode 100644
index 0000000..9bf030d
--- /dev/null
+++ b/doc/img/line-chart.png
Binary files differ
diff --git a/doc/img/listing.png b/doc/img/listing.png
new file mode 100644
index 0000000..0bedee5
--- /dev/null
+++ b/doc/img/listing.png
Binary files differ
diff --git a/doc/img/norm-chart.png b/doc/img/norm-chart.png
new file mode 100644
index 0000000..15bf7bd
--- /dev/null
+++ b/doc/img/norm-chart.png
Binary files differ
diff --git a/doc/img/pie-chart.png b/doc/img/pie-chart.png
new file mode 100644
index 0000000..20521fa
--- /dev/null
+++ b/doc/img/pie-chart.png
Binary files differ
diff --git a/doc/img/simple-percentage.png b/doc/img/simple-percentage.png
new file mode 100644
index 0000000..2a86b31
--- /dev/null
+++ b/doc/img/simple-percentage.png
Binary files differ
diff --git a/doc/img/smaller/advanced-plot.png b/doc/img/smaller/advanced-plot.png
new file mode 100644
index 0000000..d11f307
--- /dev/null
+++ b/doc/img/smaller/advanced-plot.png
Binary files differ
diff --git a/doc/img/smaller/bar-chart.png b/doc/img/smaller/bar-chart.png
new file mode 100644
index 0000000..34241f6
--- /dev/null
+++ b/doc/img/smaller/bar-chart.png
Binary files differ
diff --git a/doc/img/smaller/big-value.png b/doc/img/smaller/big-value.png
new file mode 100644
index 0000000..7516c43
--- /dev/null
+++ b/doc/img/smaller/big-value.png
Binary files differ
diff --git a/doc/img/smaller/cumulative-flow.png b/doc/img/smaller/cumulative-flow.png
new file mode 100644
index 0000000..c0cfd5a
--- /dev/null
+++ b/doc/img/smaller/cumulative-flow.png
Binary files differ
diff --git a/doc/img/smaller/fancy-listing.png b/doc/img/smaller/fancy-listing.png
new file mode 100644
index 0000000..b241ac6
--- /dev/null
+++ b/doc/img/smaller/fancy-listing.png
Binary files differ
diff --git a/doc/img/smaller/just-value.png b/doc/img/smaller/just-value.png
new file mode 100644
index 0000000..d3c8cf0
--- /dev/null
+++ b/doc/img/smaller/just-value.png
Binary files differ
diff --git a/doc/img/smaller/line-chart.png b/doc/img/smaller/line-chart.png
new file mode 100644
index 0000000..2873303
--- /dev/null
+++ b/doc/img/smaller/line-chart.png
Binary files differ
diff --git a/doc/img/smaller/listing.png b/doc/img/smaller/listing.png
new file mode 100644
index 0000000..0b11eb9
--- /dev/null
+++ b/doc/img/smaller/listing.png
Binary files differ
diff --git a/doc/img/smaller/norm-chart.png b/doc/img/smaller/norm-chart.png
new file mode 100644
index 0000000..739ffcd
--- /dev/null
+++ b/doc/img/smaller/norm-chart.png
Binary files differ
diff --git a/doc/img/smaller/pie-chart.png b/doc/img/smaller/pie-chart.png
new file mode 100644
index 0000000..d312bdf
--- /dev/null
+++ b/doc/img/smaller/pie-chart.png
Binary files differ
diff --git a/doc/img/smaller/simple-percentage.png b/doc/img/smaller/simple-percentage.png
new file mode 100644
index 0000000..790b77a
--- /dev/null
+++ b/doc/img/smaller/simple-percentage.png
Binary files differ
diff --git a/doc/img/smaller/text.png b/doc/img/smaller/text.png
new file mode 100644
index 0000000..6819889
--- /dev/null
+++ b/doc/img/smaller/text.png
Binary files differ
diff --git a/doc/img/text.png b/doc/img/text.png
new file mode 100644
index 0000000..b47e9c6
--- /dev/null
+++ b/doc/img/text.png
Binary files differ
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..15ebda9
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,16 @@
+Welcome to Tipboard's documentation!
+====================================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ overview
+ installation
+ configuration
+ tiles
+ api
+ extras
+ changelog
+ license
diff --git a/doc/installation.rst b/doc/installation.rst
new file mode 100644
index 0000000..9f16ff0
--- /dev/null
+++ b/doc/installation.rst
@@ -0,0 +1,98 @@
+.. _installation:
+
+==============
+How to install
+==============
+
+You can install Tipboard on a variety of sensible operating systems. This guide
+assumes **Ubuntu Server 12.04 LTS** and presents shell command examples
+accordingly.
+
+Prerequisites
+-------------
+
+Tipboard requires Python 2.7 which can be installed with this command::
+
+ $ sudo apt-get install python-dev python-virtualenv
+
+Another dependency which needs to be satisfied before proceeding further is
+`Redis <http://redis.io/>`_ server::
+
+ $ sudo apt-get install redis-server
+
+Optional yet recommended packages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One of such packages is ``supervisor`` - it facilitates program administration
+(e.g. its reboot), especially if there are a few instances launched on the
+machine.
+
+Based on the Tornado framework, Tipboard has a built-in server available, but
+a typical use case assumes communication with the world via reverse proxy (e.g.
+using ``nginx`` or ``apache``).
+
+.. note::
+
+ Although configuration of reverse proxy is out of scope of this manual, we
+ would like to emphasise that Tipboard use Web Sockets – a relatively new
+ mechanism – and thus you should ensure a server in a version that will
+ support it (e.g. ``nginx`` >= 1.3.13 or ``apache2`` >= 2.4.6). By default
+ Ubuntu 12.04 offers older versions – you may then use backports.
+
+.. note::
+
+ It will be useful to have an updated version of ``pip`` (i.e. >= 1.4) and
+ ``virtualenv`` (i.e. >= 1.10).
+
+Preparing environment for installation
+--------------------------------------
+
+Start by creating a user, the privileges of whom will be used by the
+application (for the needs of this manual, let's create the user "pylabs")::
+
+ $ sudo adduser pylabs --home /home/pylabs --shell /bin/bash
+ $ sudo su - pylabs
+
+Virtual environment
+~~~~~~~~~~~~~~~~~~~
+
+Continue by creating a virtual environment that will help you conveniently
+separate your instance from what you already have installed in the system
+(let's say we name it "tb-env")::
+
+ $ cd /home/pylabs
+ $ virtualenv tb-env
+
+Activate the created virtual environment with the following command::
+
+ $ source /home/pylabs/tb-env/bin/activate
+
+.. note::
+
+ It is worth saving the above line in the ``~/.profile`` file. As a result,
+ the virtual environment will be activated automatically whenever you log in
+ on the machine.
+
+.. note::
+
+ Further setup assumes an activated virtual environment, which is denoted by
+ ``(tb-env)`` prefix in your shell prompt.
+
+Installing with pip
+-------------------
+
+After creating and activating virtualenv, install the latest (current) version
+of Tipboard package available on pypi ("Python Package Index") with the
+following command::
+
+ (tb-env)$ pip install tipboard
+
+Verification
+------------
+
+To verify if installation has been successful, launch this command::
+
+ (tb-env)$ tipboard runserver
+
+If you see the message "Listening on port..." instead of errors, it means that
+installation was successful and you may proceed to the next section.
diff --git a/doc/license.rst b/doc/license.rst
new file mode 100644
index 0000000..d8e3824
--- /dev/null
+++ b/doc/license.rst
@@ -0,0 +1,5 @@
+License
+_______
+
+.. include:: ../LICENSE.rst
+ :literal:
diff --git a/doc/overview.rst b/doc/overview.rst
new file mode 100644
index 0000000..dc3f75f
--- /dev/null
+++ b/doc/overview.rst
@@ -0,0 +1,21 @@
+Overview
+--------
+
+Tipboard is a system for creating dashboards, written in JavaScript and Python.
+Its widgets ('tiles' in Tipboard's terminology) are completely separated from
+data sources, which provides great flexibility and relatively high degree of
+possible customizations.
+
+Because of its intended target (displaying various data and statistics in your
+office), it is optimized for larger screens.
+
+Similar projects: `Geckoboard <http://www.geckoboard.com/>`_,
+`Dashing <http://shopify.github.io/dashing/>`_.
+
+Project assumptions
+~~~~~~~~~~~~~~~~~~~
+
+#. Defining a dashboard layout (number of lines, columns, types of tiles etc.).
+#. Clear separation between tiles and data sources.
+#. Ability to create own tiles and scripts querying data sources (e.g. Jira, Bamboo, Confluence etc.).
+#. Feeding tiles via REST API.
diff --git a/doc/tile__advanced_plot.rst b/doc/tile__advanced_plot.rst
new file mode 100644
index 0000000..afeabab
--- /dev/null
+++ b/doc/tile__advanced_plot.rst
@@ -0,0 +1,140 @@
+=================
+``advanced_plot``
+=================
+
+.. image:: img/smaller/advanced-plot.png
+
+**Description**
+
+This tile is for more demanding users. It basically allows to display arbitrary
+type of chart/plot from the `jqPlot <http://www.jqplot.com/>`_ library, along
+with the title and description (both are optional).
+
+Before you start experimenting with jqPlot library, we suggest to familiarize
+yourself with `this manual
+<http://www.jqplot.com/docs/files/usage-txt.html#jqPlot_Usage>`_. After that
+you should check out `options tutorial
+<http://www.jqplot.com/docs/files/optionsTutorial-txt.html#Options_Tutorial>`_
+and `options summary <http://www.jqplot.com/docs/files/jqplot-core-js.html>`_.
+
+Here you will find `some examples <http://www.jqplot.com/deploy/dist/examples/>`_.
+
+**Content**
+
+::
+
+ "data" = {
+ "title": "<tile>",
+ "description": "<description>",
+ "plot_data": "<data>"
+ }
+
+where:
+
+.. describe:: title, description
+
+ Title and description (subtitle) for the tile.
+
+.. describe:: plot_data
+
+ Data that will be fed directly to your plot. Its form depends on the
+ specific type of plot that you are going to use - see jqPlot's documentation
+ for the details.
+
+Example (using horizontal `Bar Chart
+<http://www.jqplot.com/deploy/dist/examples/barTest.html>`_ - third example
+from the top)::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=advanced_plot"
+ -d "key=<tile_id>"
+ -d 'data={"title": "Metric Tons per Year", "description": "",
+ "plot_data": [[[2,1], [4,2], [6,3], [3,4]],
+ [[5,1], [1,2], [3,3], [4,4]],
+ [[4,1], [7,2], [1,3], [2,4]]]}'
+
+.. note::
+
+ Keep in mind that ``advanced_plot`` can display arbitrary charts from jqPlot
+ library, and more than often they are quite different when it comes to the
+ parameters required etc.
+
+**Configuration**
+
+::
+
+ value = {
+ "value": "<jqplot_config>"
+ }
+
+where:
+
+.. describe:: value
+
+ Raw configuration that will be passed directly to jqPlot and which should
+ obey the rules defined by the jqPlot library. Internally, this config will
+ be passed as ``$.jqplot(some-container, some-data, our-config)``.
+
+ If such configuration contains one of jqPlot's renderers, its name should be
+ passed as a string, according to the table below:
+
+ +--------------------------------------+-------------------------------+
+ | jqPlot's renderer | string to send |
+ +======================================+===============================+
+ | ``$.jqplot.BarRenderer`` | ``"BarRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.BlockRenderer`` | ``"BlockRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.BubbleRenderer`` | ``"BubbleRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.CanvasAxisLabelRenderer`` | ``"CanvasAxisLabelRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.CanvasAxisTickRenderer`` | ``"CanvasAxisTickRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.CanvasTextRenderer`` | ``"CanvasTextRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.CategoryAxisRenderer`` | ``"CategoryAxisRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.DateAxisRenderer`` | ``"DateAxisRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.DonutRenderer`` | ``"DonutRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.EnhancedLegendRenderer`` | ``"EnhancedLegendRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.FunnelRenderer`` | ``"FunnelRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.LogAxisRenderer`` | ``"LogAxisRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.MekkoAxisRenderer`` | ``"MekkoAxisRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.MekkoRenderer`` | ``"MekkoRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.MeterGaugeRenderer`` | ``"MeterGaugeRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.OhlcRenderer`` | ``"OhlcRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.PieRenderer`` | ``"PieRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.PyramidAxisRenderer`` | ``"PyramidAxisRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.PyramidGridRenderer`` | ``"PyramidGridRenderer"`` |
+ +--------------------------------------+-------------------------------+
+ | ``$.jqplot.PyramidRenderer`` | ``"PyramidRenderer"`` |
+ +--------------------------------------+-------------------------------+
+
+Example (using horizontal `Bar Chart
+<http://www.jqplot.com/deploy/dist/examples/barTest.html>`_ - third example
+from the top)::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={
+ "seriesDefaults": {
+ "trendline": {"show": false},
+ "renderer":"BarRenderer",
+ "pointLabels": {"show": true, "location": "e", "edgeTolerance": -15},
+ "shadowAngle": 135,
+ "rendererOptions": {"barDirection": "horizontal"}
+ },
+ "axes": {"yaxis": { "renderer": "CategoryAxisRenderer"}}}'
diff --git a/doc/tile__bar_chart.rst b/doc/tile__bar_chart.rst
new file mode 100644
index 0000000..47dfd32
--- /dev/null
+++ b/doc/tile__bar_chart.rst
@@ -0,0 +1,61 @@
+=============
+``bar_chart``
+=============
+
+.. image:: img/smaller/bar-chart.png
+
+**Description**
+
+Tile displaying data in the form of horizontal bar charts. Each cart may
+consist of one or more series of data - in the latter case, such bars are
+grouped respectively. There are no "hard" limits when it comes to the number of
+charts/series, although we suggest to keep them down to 3 charts of 2 series
+each.
+
+**Content**
+
+::
+
+ data = {
+ "title": "<title>",
+ "subtitle": "<subtitle>",
+ "ticks": ["<label1>", "<label2>", "<label3>" ...],
+ "series_list": [[<val1>, <val2>, <val3>, ...],
+ [<val1>, <val2>, <val3>, ...]]
+ }
+
+where:
+
+.. describe:: title, subtitle
+
+ Title and subtitle displayed on the top of the tile.
+
+.. describe:: ticks
+
+ Labels to be displayed on the left side of the charts.
+
+.. describe:: series_list
+
+ List of series, as name suggests. ``[[1, 2]]`` will give one chart of two
+ bars, ``[[3, 4, 5], [6, 7, 8]]`` will give two charts of three bars each.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=bar_chart"
+ -d "key=<tile_id>"
+ -d 'data={"title": "The A-Team",
+ "subtitle": "Velocity (Last tree sprints)",
+ "ticks": ["n-2", "n-1", "Last (n)"],
+ "series_list": [[49, 50, 35], [13, 45, 9]]}'
+
+**Configuration**
+
+This tile does not offer any configuration options.
+
+.. note::
+
+ In case of displaying more than one charts on the same tile, the number of
+ values in ``series_list`` for every chart should be the same (and they
+ should be equal to the number of ``ticks``.
diff --git a/doc/tile__big_value.rst b/doc/tile__big_value.rst
new file mode 100644
index 0000000..dbdcad2
--- /dev/null
+++ b/doc/tile__big_value.rst
@@ -0,0 +1,97 @@
+=============
+``big_value``
+=============
+
+.. image:: img/smaller/big-value.png
+
+**Description**
+
+This is a variation of ``simple_percentage`` tile. It has slightly different
+footer (four possible values instead of just two; values are displayed on the
+right side of the label instead of below), and the main value (``big_value``)
+is little bit bigger.
+
+**Content**
+
+::
+
+ data = {
+ "title": "<title>",
+ "description": "<description>",
+ "big-value": "<value>",
+ "upper-left-label": "<label>",
+ "upper-left-value": "<value>",
+ "lower-left-label": "<label>",
+ "lower-left-value": "<value>",
+ "upper-right-label": "<label>",
+ "upper-right-value": "<value>",
+ "lower-right-label": "<label>",
+ "lower-right-value": "<value>"
+ }
+
+where:
+
+.. describe:: title, description
+
+ Title and description (subtitle) for the tile.
+
+.. describe:: big_value
+
+ Main value, which treated as a string, so it can contain symbols like ``%`` etc.
+
+.. describe:: upper-left-value, lower-left-value, upper-right-value, lower-right-value
+
+ Smaller, bottom-left and bottom-right values.
+
+.. describe:: upper-left-label, lower-left-label, upper-right-label, lower-right-label
+
+ Labels for above values.
+
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=big_value"
+ -d "key=<tile_id>"
+ -d 'data={"title": "Tickets",
+ "description": "number of blockers",
+ "big-value": "314",
+ "upper-left-label": "critical:",
+ "upper-left-value": "1020",
+ "lower-left-label": "major:",
+ "lower-left-value": "8609",
+ "upper-right-label": "minor:",
+ "upper-right-value": "7532",
+ "lower-right-label": "all:",
+ "lower-right-value": "19 853"}'
+
+**Configuration**
+
+::
+
+ value = {
+ "big_value_color": "<color>",
+ "fading_background": <BOOLEAN>
+ }
+
+where:
+
+.. describe:: big_value_color
+
+ Background color for ``big_value`` in a hexadecimal form or color name (e.g.
+ ``#94C140`` or ``green``).
+
+.. describe:: fading_background
+
+ Turns on/off background pulsation for ``big_value`` (may be useful for
+ alerts etc.).
+
+ .. versionadded:: 1.3.0
+
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={"big_value_color": "green", "fading_background": true}'
diff --git a/doc/tile__cumulative_flow.rst b/doc/tile__cumulative_flow.rst
new file mode 100644
index 0000000..c26d46a
--- /dev/null
+++ b/doc/tile__cumulative_flow.rst
@@ -0,0 +1,68 @@
+===================
+``cumulative_flow``
+===================
+
+.. image:: img/smaller/cumulative-flow.png
+
+**Description**
+
+Cumulative chart using `jqPlot <http://www.jqplot.com/>`_ library. Allows to
+display up to seven plots on a single chart.
+
+**Content**
+
+::
+
+ data = {
+ "title": "<title>",
+ "series_list": [{"label": "<label1>", "series": [<val1>, <val2>, ...]},
+ {"label": "<label2>", "series": [<val1>, <val2>, ...]}]
+ }
+
+where:
+
+.. describe:: title
+
+ Title to be displayed above the labels.
+
+.. describe:: series_list
+
+ A container (i.e. list of objects) for the data; each such object
+ corresponding to a single plot consists of two keys: ``label`` and
+ ``series``, where the latter is a list of values constructing the plot.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=cumulative_flow"
+ -d "key=<tile_id>"
+ -d 'data={"title": "My title:",
+ "series_list": [{"label": "label 1", "series": [ 0, 0, 0, 0, 1, 1, 2, 2, 1, 1, 1, 0, 0, 2, 0 ]},
+ {"label": "label 2", "series": [ 0, 5, 0, 0, 1, 0, 0, 3, 0, 0, 0, 7, 8, 9, 1 ]}]}'
+
+**Configuration**
+
+::
+
+ value = {"ticks": [[<key>, "<value>"], [<key>, "<value>"], ... ]}
+
+where:
+
+.. describe:: ticks
+
+ List of elements defining x-axis; each such element is a list of form ``[k,
+ v]`` where ``k`` is an oridinal number designating position of such tick and
+ ``v`` is a string which will be displayed in that place.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={"ticks": [[1, "mon"], [2, "tue"], [3, "wed"], [4, "thu"], [5, "fri"], [6, "sat"], [7, "sun"]]}'
+
+.. note::
+
+ If ``series_list`` contains more than one object (which is the case 99% of
+ the time), each one of them should have ``series`` list of the same length -
+ and this lenght should be equal to the number of ``ticks``.
diff --git a/doc/tile__fancy_listing.rst b/doc/tile__fancy_listing.rst
new file mode 100644
index 0000000..902bdc9
--- /dev/null
+++ b/doc/tile__fancy_listing.rst
@@ -0,0 +1,91 @@
+=================
+``fancy_listing``
+=================
+
+.. image:: img/smaller/fancy-listing.png
+
+**Description**
+
+This tile is a more sophisticated version of ``listing`` tile offering colored
+labels and centering options. Each entry is an object containing three keys:
+``label``, ``text`` and ``description``. Threrefore, ``data`` (i.e. content) is
+just a list of such objects.
+
+**Content**
+
+::
+
+ "data" = [
+ {"label": "<label1>", "text": "<entry1>", "description": "<desc1>" },
+ {"label": "<label2>", "text": "<entry2>", "description": "<desc2>" }
+ ]
+
+where:
+
+.. describe:: label
+
+ Smaller label displayed on the left which can be colored.
+
+.. describe:: text
+
+ A textual entry to be displayed next to the label.
+
+.. describe:: description
+
+ Subtitle displayed below ``text`` element.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=fancy_listing"
+ -d "key=<tile_id>"
+ -d 'data=[{"label": "My label 1", "text": "Lorem ipsum", "description": "such description" },
+ {"label": "My label 2", "text": "Dolor sit", "description": "yet another" },
+ {"label": "My label 3", "text": "Amet", "description": "" }]'
+
+**Configuration**
+
+::
+
+ value = {
+ "vertical_center": <BOOLEAN>,
+ "<position>": {
+ "label_color": "<color>",
+ "center": <BOOLEAN>
+ },
+ }
+
+where:
+
+.. describe:: vertical_center
+
+ Centers vertically all the entries (along with their labels).
+
+ .. versionadded:: 1.3.0
+
+.. describe:: position
+
+ Tells which entry (starting from 1) should be a subject to ``label_color``
+ and ``center`` (specified as subkeys of ``position``).
+
+ .. describe:: label_color
+
+ Sets the color of label for the entry given with ``position``. Color can
+ be specified in a hexadecimal form (``#RRGGBB``) or by name (e.g.
+ ``green``).
+
+ .. describe:: center
+
+ Centers horizontally entry's ``text`` and ``description`` (it does not
+ affect label's position).
+
+ .. versionadded:: 1.3.0
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={"vertical_center": true,
+ "1": {"label_color": "red", "center": true},
+ "3": {"label_color": "green", "center": true }}'
diff --git a/doc/tile__just_value.rst b/doc/tile__just_value.rst
new file mode 100644
index 0000000..0d76082
--- /dev/null
+++ b/doc/tile__just_value.rst
@@ -0,0 +1,67 @@
+==============
+``just_value``
+==============
+
+.. image:: img/smaller/just-value.png
+
+**Description**
+
+Tile for displaying single, short information with a possibility to change its
+background color.
+
+**Content**
+
+::
+
+ "data" = {
+ "title": "<title>",
+ "description": "<description>",
+ "just-value": "<value>"
+ }
+
+where:
+
+.. describe:: title, description
+
+ Title and description (subtitle) for the tile.
+
+.. describe:: just-value
+
+ Value to be displayed on the tile, with optionally colored background.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=just_value"
+ -d "key=<tile_id>"
+ -d 'data={"title": "Next release:", "description": "(days remaining)", "just-value": "23"}'
+
+**Configuration**
+
+::
+
+ value = {
+ "just-value-color": "<color>",
+ "fading_background": <BOOLEAN>
+ }
+
+where:
+
+.. describe:: just-value-color
+
+ Background color for ``just-value`` in a hexadecimal form or color name (e.g.
+ ``#94C140`` or ``green``).
+
+.. describe:: fading_background
+
+ Turns on/off background pulsation for ``just-value`` (may be useful for
+ alerts etc.).
+
+ .. versionadded:: 1.3.0
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={"just-value-color": "green", "fading_background": true}'
diff --git a/doc/tile__line_chart.rst b/doc/tile__line_chart.rst
new file mode 100644
index 0000000..a7435b1
--- /dev/null
+++ b/doc/tile__line_chart.rst
@@ -0,0 +1,81 @@
+==============
+``line_chart``
+==============
+
+.. image:: img/smaller/line-chart.png
+
+**Description**
+
+Line-chart using `jqPlot <http://www.jqplot.com/>`_ library. Allows to display
+arbitrary number of plots on single chart, with automatical generation of trend
+lines for them (which is turned on by default).
+
+**Content**
+
+::
+
+ data = {
+ "subtitle": "<subtitle_text">,
+ "description": "<description_text>",
+ "series_list": [[<series1>], [<series2>], [<series3>], ...]
+ }
+
+where:
+
+.. describe:: subtitle, description
+
+ Additional text fields for charts descriptions (optional - you can pass
+ empty strings here).
+
+.. describe:: series_list
+
+ Data for line-charts in a form of list of series, where each series
+ designates single chart; each element of a given series is a pair
+ ``[x_axis_value, y_axis_value]``.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=line_chart"
+ -d "key=example_line"
+ -d 'data={"subtitle": "averages from last week",
+ "description": "Sales in our dept",
+ "series_list": [[["23.09", 8326], ["24.09", 260630], ["25.09", 240933], ["26.09", 229639], ["27.09", 190240], ["28.09", 125272], ["29.09", 3685]],
+ [["23.09", 3685], ["24.09", 125272], ["25.09", 190240], ["26.09", 229639], ["27.09", 240933], ["28.09", 260630], ["29.09", 108326]]]}'
+
+-- this will give two plots on a single chart (on x-axis there will be "23.09",
+"24.09", "25.09" and so on) with heading "Sales in our dept" and subtitle
+"averages from last week".
+
+**Configuration**
+
+::
+
+ value = {<jqplot_config>}
+
+where:
+
+.. describe:: jqplot_config
+
+ Configuration params in the form described by `jqPlot documentation
+ <http://www.jqplot.com/tests/line-charts.php>`_.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/example_line
+ -X POST
+ -d 'value={"grid": {"drawGridLines": true,
+ "gridLineColor": "#FFFFFF",
+ "shadow": false,
+ "background": "#000000",
+ "borderWidth": 0}}'
+
+-- this will set up the grid (in white color), black background and will turn
+off shadow effects as well as borders.
+
+.. note::
+
+ In case of displaying multiple plots on a single chart (e.g. for more than
+ one data series) you have to keep in mind that the ``x_axis_value`` values
+ should be the same for all of those plots.
diff --git a/doc/tile__listing.rst b/doc/tile__listing.rst
new file mode 100644
index 0000000..dbe2516
--- /dev/null
+++ b/doc/tile__listing.rst
@@ -0,0 +1,34 @@
+===========
+``listing``
+===========
+
+.. image:: img/smaller/listing.png
+
+**Description**
+
+Very simple tile for displaying list of entries (up to seven) of arbitrary
+content. For more sophisticated needs there is a ``fancy_listing`` tile.
+
+**Content**
+
+::
+
+ data = {"items": ["<entry1>", "<entry2>", ..., "<entry7>"]}
+
+where:
+
+.. describe:: items
+
+ List of items (entries) to display.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=listing"
+ -d "key=<tile_id>"
+ -d "data={"items": ["Leader: 5", "Product Owner: 0", "Scrum Master: 3", "Developer: 0"]}"
+
+**Configuration**
+
+This tile does not offer any configuration options.
diff --git a/doc/tile__norm_chart.rst b/doc/tile__norm_chart.rst
new file mode 100644
index 0000000..6cfd8c0
--- /dev/null
+++ b/doc/tile__norm_chart.rst
@@ -0,0 +1,79 @@
+==============
+``norm_chart``
+==============
+
+.. image:: img/smaller/norm-chart.png
+
+.. versionadded:: 1.3.0
+
+**Description**
+
+"Curve vs norm" style chart. Suitable for situations, when you want to
+compare some data with expected value ("norm") or put an emphasis on y-axis
+values.
+
+**Content**
+
+::
+
+ "data" = {
+ "title": "<title>",
+ "description": "<description>",
+ "plot_data": [ [<series1>], [<series2>], [<series3>], ... ]
+ }
+
+where:
+
+.. describe:: title, description
+
+ Title and description (subtitle) for the tile.
+
+.. describe:: plot_data
+
+ Data for charts in a form of list of series, where each series designates
+ single chart; each element of a given series is a pair ``[x_axis_value,
+ y_axis_value]``.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=norm_chart"
+ -d "key=<tile_id>"
+ -d 'data={"title": "My title",
+ "description": "Some description",
+ "plot_data": [[[1, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]],
+ [[6, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]]]}'
+
+**Configuration**
+
+::
+
+ value = {
+ "easyNorms": [["<color>", <y-value>, <line_width>], ...]
+ }
+
+where:
+
+.. describe:: easyNorms
+
+ List of norms to be displayed. Each norm consists of three elements:
+
+ .. describe:: color
+
+ Color which given norm should use - in a hexadecimal form or color name
+ (e.g. ``#94C140`` or ``green``).
+
+ .. describe:: y-value
+
+ Value for the norm.
+
+ .. describe:: line_width
+
+ Line thickness for the norm (in pixels).
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={"easyNorms": [["yellow", 200, 2], ["green", 100, 2]]}'
diff --git a/doc/tile__pie_chart.rst b/doc/tile__pie_chart.rst
new file mode 100644
index 0000000..afd8f84
--- /dev/null
+++ b/doc/tile__pie_chart.rst
@@ -0,0 +1,64 @@
+=============
+``pie_chart``
+=============
+
+.. image:: img/smaller/pie-chart.png
+
+**Description**
+
+"Pie-chart" style chart using `jqPlot <http://www.jqplot.com/>`_ library, with
+optional legend.
+
+**Content**
+
+::
+
+ data = {
+ "title": "<optional_title>",
+ "pie_data": [[identifier1, value1], [identifier2, value2], ...]
+ }
+
+where:
+
+.. describe:: title
+
+ Chart's title (optional).
+
+.. describe:: pie_data
+
+ Data for pie-chart in a form of list of lists, where each sub-list is an
+ identifier-value pair. Percentage of the whole chart shared by given part is
+ calculated automatically by jqPlot - relatively to the sum of values of all
+ parts.
+
+Example::
+
+ curl http//localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=pie_chart"
+ -d "key=example_pie"
+ -d 'data={"title": "My title", "pie_data": [["Pie 1", 25], ["Pie 2", 25], ["Pie 3", 50]]}'
+
+-- this will result in a pie-chart with title "My title", divided by three
+parts "Pie 1", "Pie 2" and "Pie 3".
+
+**Configuration**
+
+::
+
+ value = {<jqplot_config>}
+
+where:
+
+.. describe:: jqplot_config
+
+ Configuration params in the form described by `jqPlot documentation
+ <http://www.jqplot.com/tests/pie-donut-charts.php>`_.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tild_id>
+ -X POST
+ -d 'value={"title": true, "legend": {"show": true, "location": "s"}}'
+
+-- this will result in a pie-chart with legend turned on at the bottom of the tile (``s`` stands for "south") - its title will be turned on as well.
diff --git a/doc/tile__simple_percentage.rst b/doc/tile__simple_percentage.rst
new file mode 100644
index 0000000..d0fbdc7
--- /dev/null
+++ b/doc/tile__simple_percentage.rst
@@ -0,0 +1,81 @@
+=====================
+``simple_percentage``
+=====================
+
+.. image:: img/smaller/simple-percentage.png
+
+**Description**
+
+Tile displaying three arbitrary values (with labels) - one bigger ("main"
+value) and two smaller (at the bottom-left and bottom-right of the tile). It is
+possible to change background color for the main value.
+
+**Content**
+
+::
+
+ data = {
+ "title": "<title>",
+ "subtitle": "<subtitle>",
+ "big_value": "<value1>",
+ "left_value": "<value2>",
+ "right_value": "<value3>"
+ "left_label": "<label1>",
+ "right_label": "<label2>"
+ }
+
+where:
+
+.. describe:: title, subtitle
+
+ They serve as a label for the ``big_value`` ("main" value).
+
+.. describe:: big_value
+
+ Main value, which treated as a string, so it can contain symbols like ``%`` etc.
+
+.. describe:: left_value, right_value
+
+ Smaller, bottom-left and bottom-right values.
+
+.. describe:: left_label, right_label
+
+ Labels for above values.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=simple_percentage"
+ -d "key=<tile_id>"
+ -d 'data={"title": "My title", "subtitle": "My subtitle", "big_value": "100%",
+ "left_value": "50%", "left_label": "smaller label 1",
+ "right_value": "25%", "right_label": "smaller label 2"}'
+
+**Configuration**
+
+::
+
+ value = {
+ "big_value_color": "<color>",
+ "fading_background": <BOOLEAN>
+ }
+
+where:
+
+.. describe:: big_value_color
+
+ Background color for ``big_value`` in a hexadecimal form or color name (e.g.
+ ``#94C140`` or ``green``).
+
+.. describe:: fading_background
+
+ Turns on/off background pulsation for ``big_value`` (may be useful for alerts etc.).
+
+ .. versionadded:: 1.3.0
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/<tile_id>
+ -X POST
+ -d 'value={"big_value_color": "green", "fading_background": true}'
diff --git a/doc/tile__text.rst b/doc/tile__text.rst
new file mode 100644
index 0000000..e9318a7
--- /dev/null
+++ b/doc/tile__text.rst
@@ -0,0 +1,61 @@
+========
+``text``
+========
+
+.. image:: img/smaller/text.png
+
+**Description**
+
+Simple text-tile designated to display... (surprise!) text.
+
+**Content**
+
+::
+
+ data = {"text": "<text_content>"}
+
+where:
+
+.. describe:: text_content
+
+ A textual content to be displayed.
+
+Example::
+
+ curl http://localhot:7272/api/v0.1/<api_key>/push
+ -X POST
+ -d "tile=text"
+ -d "key=mytext"
+ -d 'data={"text": "Hello world!"}'
+
+**Configuration**
+
+::
+
+ value = {"<config_element>": "<config_value>"}
+
+where:
+
+.. describe:: config_element
+
+ One of three attributes of displayed text (i.e. ``font_size``, ``color`` and
+ ``font_weight``).
+
+.. describe:: config_value
+
+ Value matching above.
+
+Example::
+
+ curl http://localhost:7272/api/v0.1/<api_key>/tileconfig/mytext
+ -X POST
+ -d 'value={"font_color": "#00FF00"}'
+
+.. note::
+
+ Parameter ``font_size`` can be specified in two ways - as a number (e.g.
+ ``"font_size": 10``) or as a string (e.g. ``"font_size": "10px"``) - both of
+ them have the same effect.
+
+ Keys ``font_size``, ``color``, ``font_weight`` with empty ``config_value``
+ are ignored (in such case, they will inherit those values from parent CSS).
diff --git a/doc/tiles.rst b/doc/tiles.rst
new file mode 100644
index 0000000..0855a2f
--- /dev/null
+++ b/doc/tiles.rst
@@ -0,0 +1,99 @@
+=====
+Tiles
+=====
+
+Every tile consists of an obligatory ``.html`` file and two optional ``.css``
+and ``.js`` files. All three files belonging to a tile should have the same
+name that corresponds with the tile name – e.g. with the ``pie_chart`` tile
+these are ``pie_chart.html``, ``pie_chart.css`` and ``pie_chart.js`` files
+respectively.
+
+Customising tiles
+-----------------
+
+If you want to modify a tile (e.g. change a CSS attribute, which obviously
+cannot be done via API), copy a desired file in the folder of tiles delivered
+with the application (i.e.
+``<path_to_your_virtualenv>/lib/python2.7/site-packages/tipboard/tiles``),
+paste it in your tile folder (i.e. ``~/.tipboard/custom_tiles``) and edit
+according to your needs.
+
+Files in your ``custom_tiles`` folder take precedence over those shipped by
+default with the application and thus you can easily replace desired elements
+(e.g. if you want to change the text colour, just copy and edit the ``.css``
+file – without touching ``.html`` and ``.js`` files). We plan to introduce a
+command simplifying this process in the future.
+
+Color palette
+-------------
+
+Color palette used by Tipboard's tiles is defined as shown in the table below.
+To retain consistency, we strongly suggest sticking to them while customising
+tiles.
+
++-------------+---------------------+
+| Value | Name |
++=============+=====================+
+| ``#000000`` | ``black`` |
++-------------+---------------------+
+| ``#FFFFFF`` | ``white`` |
++-------------+---------------------+
+| ``#25282D`` | ``tile_background`` |
++-------------+---------------------+
+| ``#DC5945`` | ``red`` |
++-------------+---------------------+
+| ``#FF9618`` | ``yellow`` |
++-------------+---------------------+
+| ``#94C140`` | ``green`` |
++-------------+---------------------+
+| ``#12B0C5`` | ``blue`` |
++-------------+---------------------+
+| ``#9C4274`` | ``violet`` |
++-------------+---------------------+
+| ``#EC663C`` | ``orange`` |
++-------------+---------------------+
+| ``#54C5C0`` | ``naval`` |
++-------------+---------------------+
+
+Common elements
+---------------
+
+* tile's content (``data`` key) and its configuration (``value`` key) should be
+ send as two separate requests - once you have established desired
+ configuration it does not make much sense to send it over and over again;
+* in order to reset tile's config, you just send an empty ``value`` key (e.g.
+ ``value = {}``).
+
+.. _tiles_library:
+
+Library of available tiles
+--------------------------
+
+In the following pages we present a "library" of available tiles (i.e. those
+bundled with the application), which should serve as a specification how to
+send data to them and how to set up its configuration options.
+
+.. toctree::
+ :maxdepth: 2
+
+ tile__text
+ tile__pie_chart
+ tile__line_chart
+ tile__cumulative_flow
+ tile__simple_percentage
+ tile__listing
+ tile__bar_chart
+ tile__fancy_listing
+ tile__big_value
+ tile__just_value
+ tile__advanced_plot
+ tile__norm_chart
+
+.. note::
+
+ In order to keep brevity, all examples presented in specifications above do
+ not include any escape characters. Therefore, it's up to you to insert them
+ where necessary.
+
+ And also, remember to set all the elements in angle brackets (e.g.
+ ``<api_key>``, ``<tile_id>`` etc.) to reflect your configuration.
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ad8ff7b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,11 @@
+mock==1.0.1,
+PyYAML==3.10,
+redis==2.7.5,
+requests==1.2.3,
+six==1.4.1,
+tornado-redis==2.4.2,
+tornado==3.0.1,
+docopt==0.6.1,
+raven==3.5.2,
+Sphinx==1.2.2,
+sphinxcontrib-httpdomain==1.3.0,
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..8463d33
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+ # -*- encoding: utf-8 -*-
+
+import os
+import sys
+from setuptools import setup, find_packages
+
+assert sys.version_info >= (2, 7), "Python 2.7+ required."
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(current_dir, 'README.rst')) as readme_file:
+ with open(os.path.join(current_dir, 'CHANGES.rst')) as changes_file:
+ long_description = readme_file.read() + '\n' + changes_file.read()
+
+from tipboard import __version__
+
+with open('requirements.txt') as requirements:
+ required = requirements.read().splitlines()
+
+setup(
+ name='tipboard',
+ version=__version__,
+ description='Tipboard - a flexible solution for creating your dashboards.',
+ long_description = long_description,
+ url='http://tipboard.allegrogroup.com',
+ license='Apache Software License v2.0',
+ author='Allegro Group and Contributors',
+ author_email='pylabs@allegro.pl',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ platforms=['any'],
+ install_requires=required,
+ entry_points={
+ 'console_scripts': [
+ 'tipboard = tipboard.console:main',
+ ],
+ },
+)
diff --git a/tipboard/__init__.py b/tipboard/__init__.py
new file mode 100644
index 0000000..99c8ed0
--- /dev/null
+++ b/tipboard/__init__.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+
+VERSION = (1, 4, 0)
+__version__ = '.'.join(str(num) for num in VERSION)
diff --git a/tipboard/api.py b/tipboard/api.py
new file mode 100644
index 0000000..3fb286c
--- /dev/null
+++ b/tipboard/api.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import logging
+import json
+import urlparse
+
+import tornado.escape
+import tornado.ioloop
+import tornado.web
+
+import tipboard
+
+from tipboard import redis_utils, settings
+from tipboard.parser import (
+ get_config_files_names,
+ get_tiles_configs,
+ process_layout_config,
+)
+
+logger = logging.getLogger(__name__)
+
+api_version = 'v0.1'
+
+
+class ValidatorMixin(object):
+ """
+ Validators returning None if everything is OK - otherwise suitable HTTP
+ status and message is set.
+ """
+ def validate_post_request(self, post_field, allowed_fields):
+ error_msg = None
+ dict_diff = list(set(post_field) - set(allowed_fields))
+ if dict_diff:
+ self.set_status(400)
+ error_msg = 'There are fields not allowed in your request.'
+ for field in allowed_fields:
+ if field not in post_field.keys():
+ self.set_status(400)
+ error_msg = "There is no '{}' field in your request.".format(
+ field,
+ )
+ break
+ return error_msg
+
+ def validate_with_config_file(self, post, parsed_config):
+ error_msg = None
+ if post['tile'][0] not in parsed_config['tiles_names']:
+ self.set_status(404)
+ error_msg = "Tile's name not found in the configuration file.\n"
+ elif post['key'][0] not in parsed_config['tiles_keys']:
+ self.set_status(404)
+ error_msg = "Tile's key/id not found in the configuration file.\n"
+ return error_msg
+
+ def validate_with_config_files(self, post):
+ tiles_configs = get_tiles_configs()
+ error_msg = self.validate_with_config_file(post, tiles_configs)
+ return error_msg
+
+ def validate_with_json(self, jsoned_data):
+ error_msg = None
+ try:
+ json.loads(jsoned_data)
+ except ValueError:
+ self.set_status(400)
+ logger.debug('invalid json data: {}'.format(repr(jsoned_data)))
+ error_msg = 'Invalid JSON data.\n'
+ return error_msg
+
+
+class ProjectInfo(tornado.web.RequestHandler):
+ """Handles project info requests (for debugging/diagnostics)."""
+ def get(self):
+ # TODO: add info regarding custom/default tiles being used
+ response = {
+ 'tipboard_version': tipboard.__version__,
+ 'project_name': settings.PROJECT_NAME,
+ 'project_layout_config': settings.LAYOUT_CONFIG,
+ 'redis_db': settings.REDIS,
+ }
+ self.write(response)
+
+
+class Push(tornado.web.RequestHandler, ValidatorMixin):
+ """
+ Handles pushing tile's data.
+ For pushing tile's config see MetaProperty class.
+ """
+ def post(self):
+ validation_error = None
+ post_field = urlparse.parse_qs(self.request.body)
+ validation_error = self.validate_post_request(
+ post_field,
+ ['tile', 'key', 'data'],
+ )
+ if validation_error:
+ self.write(validation_error)
+ return
+ validation_error = self.validate_with_config_files(post_field)
+ if validation_error:
+ self.write(validation_error)
+ return
+ validation_error = self.validate_with_json(post_field['data'][0])
+ if validation_error:
+ self.write(validation_error)
+ return
+ try:
+ redis_utils.redis_actions(
+ method='post',
+ tile_id=post_field['key'][0],
+ value=post_field['data'][0],
+ tile=post_field['tile'][0],
+ )
+ except Exception as e:
+ self.set_status(500)
+ self.write(e.message)
+ else:
+ self.write("Tile's data pushed successfully.\n")
+
+
+class TileData(tornado.web.RequestHandler):
+ """Handles reading and deleting of tile's data."""
+ # TODO: "it's better to ask forgiveness than permission" ;)
+ def get(self, tile_key):
+ if redis_utils.key_exist(tile_key):
+ self.write(redis_utils.redis_actions('get', tile_key))
+ else:
+ self.set_status(404)
+ self.write('%s key does not exist.\n' % tile_key)
+
+ def delete(self, tile_key):
+ if redis_utils.key_exist(tile_key):
+ redis_utils.redis_actions('delete', tile_key)
+ self.write("Tile's data deleted.\n")
+ else:
+ self.set_status(404)
+ self.write('%s key does not exist.\n' % tile_key)
+
+
+class MetaProperty(tornado.web.RequestHandler, ValidatorMixin):
+ """Handles requests related to tile config changes."""
+ def post(self, tile_key):
+ post_field = urlparse.parse_qs(self.request.body)
+ validation_error = self.validate_post_request(
+ post_field,
+ ['value'],
+ )
+ if validation_error:
+ self.write(validation_error)
+ return
+ try:
+ tile_data = redis_utils.get_redis_value(tile_key)
+ except Exception:
+ self.set_status(404)
+ self.write("Can't find key %s.\n" % tile_key)
+ return
+ validation_error = self.validate_with_json(post_field['value'][0])
+ if validation_error:
+ self.write(validation_error)
+ return
+ tile_data['meta'] = json.loads(post_field['value'][0])
+ redis_utils.set_redis_value(tile_key, tile_data)
+ self.write("Tile's config updated.\n")
+
+ def delete(self, tile_key):
+ try:
+ tile_data = redis_utils.get_redis_value(tile_key)
+ except Exception:
+ self.set_status(404)
+ self.write('Cant find %s.\n' % tile_key)
+ else:
+ tile_data['meta'] = {}
+ redis_utils.set_redis_value(tile_key, tile_data)
+ self.write("Tile's config deleted.\n")
diff --git a/tipboard/app.py b/tipboard/app.py
new file mode 100644
index 0000000..59f93b4
--- /dev/null
+++ b/tipboard/app.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import json
+import logging
+
+from raven.contrib.tornado import SentryMixin, AsyncSentryClient
+import tornado.gen
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import tornado.websocket
+import tornadoredis
+
+from tipboard import settings
+from tipboard.api import api_version, MetaProperty, TileData, ProjectInfo, Push
+from tipboard.parser import process_layout_config, get_config_files_names
+from tipboard.redis_utils import db_events_path
+
+log = logging.getLogger(__name__)
+
+
+class Flipboard(object):
+
+ def __init__(self):
+ self.last_found_configs_number = -1
+ self.paths = []
+
+ def get_paths(self):
+ """
+ Returns url paths to dashboards (created from .yaml config from
+ userspace).
+ """
+ config_names = self._get_config_names()
+ if len(config_names) != self.last_found_configs_number:
+ self.paths = self._config_names2paths(config_names)
+ self.last_found_configs_number = len(config_names)
+ return self.paths
+
+ def get_flipboard_title(self):
+ """
+ Returns title to display as a html title.
+ """
+ title = ''
+ config_names = self._get_config_names()
+ if len(config_names) == 1:
+ config = process_layout_config(config_names[0])
+ try:
+ title = config['details']['page_title']
+ except KeyError:
+ msg = 'config {} has no key: details/page_title'.format(
+ config_names[0]
+ )
+ log.error(msg)
+ elif len(config_names) > 1:
+ # TODO: put here more suitable title?
+ title = 'Flipboard Mode'
+ return title
+
+ def _get_config_names(self):
+ config_names = settings.FLIPBOARD_SEQUENCE
+ if not any(config_names):
+ config_names = get_config_files_names()
+ if not any(config_names):
+ raise Exception('No config (.yaml) file found in ~/.tipboard/')
+ return config_names
+
+ def _config_names2paths(self, config_names):
+ paths = []
+ for name in config_names:
+ path = '/' + name
+ paths.append(path)
+ return paths
+
+
+class RedisMixin(object):
+ """Trivial connection mixin."""
+
+ def setup_redis(self):
+ # We don't have to manage reconnections, tornadoredis does that
+ # automatically.
+ client = tornadoredis.Client(**settings.REDIS_ASYNC)
+ client.connect()
+ return client
+
+
+class DashboardSocketHandler(tornado.websocket.WebSocketHandler, RedisMixin,
+ SentryMixin):
+ """Handles client connections on web sockets and listens on a Redis
+ subscription."""
+
+ cache = set() # Only cache keys. Values will be retrieved from Redis
+ # every time. This ensures users don't get stale data.
+ tipboard_helpers = {
+ 'color': settings.COLORS,
+ 'log_level': settings.JS_LOG_LEVEL,
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(DashboardSocketHandler, self).__init__(*args, **kwargs)
+ self.listen()
+ self.getter = self.setup_redis()
+
+ def allow_draft76(self):
+ # for iOS 5.0 Safari
+ return True
+
+ def on_close(self):
+ log.info('Web socket closed.')
+ if self.pubsub.subscribed:
+ self.pubsub.unsubscribe(db_events_path())
+ self.pubsub.disconnect()
+ self.getter.disconnect()
+
+ @tornado.gen.engine
+ def on_message(self, message):
+ log.info('Message received: %s.', message)
+ if message != 'update':
+ return
+ for tile_id in self.cache:
+ log.debug('Putting data for tile: {}'.format(tile_id))
+ raw = yield tornado.gen.Task(self.getter.get, tile_id)
+ if not raw:
+ log.warn('No data in key %s on Redis.', tile_id)
+ del self.cache[tile_id]
+ continue
+ data = json.loads(raw)
+ data['tipboard'] = self.tipboard_helpers
+ self.write_message(data)
+
+ @tornado.gen.engine
+ def on_publish(self, msg):
+ if msg.kind == 'disconnect':
+ log.warn('Redis disconnected, closing Web socket.')
+ self.write_message(
+ 'The connection terminated due to a Redis server error.'
+ )
+ self.close()
+ elif msg.kind == 'message':
+ tile_id = str(msg.body)
+ log.info('Updating %s...', tile_id)
+ raw = yield tornado.gen.Task(self.getter.get, tile_id)
+ if not raw:
+ log.warn('No data in key %s on Redis.', tile_id)
+ return
+ data = json.loads(raw)
+ data['tipboard'] = self.tipboard_helpers
+ self.cache.add(tile_id)
+ self.write_message(data)
+ log.info('Sent new data for %s through the Web socket.', tile_id)
+
+ @tornado.gen.engine
+ def listen(self):
+ log.info('Web socket opened.')
+ self.pubsub = self.setup_redis()
+ yield tornado.gen.Task(
+ self.pubsub.subscribe, db_events_path()
+ )
+ self.pubsub.listen(self.on_publish)
+ log.info('Subscribed to %s on Redis.' % db_events_path())
+
+
+class DashboardRendererHandler(tornado.web.RequestHandler, SentryMixin):
+ def get(self, layout_name):
+ def _verify_statics(static_file):
+ user_tiles_path = os.path.join(
+ os.path.expanduser('~'), '.tipboard/custom_tiles'
+ )
+ tipboard_tiles_path = os.path.join(settings.TIPBOARD_PATH, 'tiles')
+ found = False
+ for path in user_tiles_path, tipboard_tiles_path:
+ if os.path.exists(os.path.join(path, static_file)):
+ found = True
+ break
+ return found
+
+ def _tile_path(tile_name):
+ """
+ Searches for tile's html file (in user's 'custom_tiles' folder,
+ and then in app's 'tiles' folder) and returns full path of
+ the tile, or raises exception if html file is not present in none
+ of those locations.
+ """
+ user_tiles_path = os.path.join(
+ os.path.expanduser('~'), '.tipboard/custom_tiles'
+ )
+ tipboard_tiles_path = os.path.join(settings.TIPBOARD_PATH, 'tiles')
+ tile_html = '.'.join((tile_name, 'html'))
+ for path in user_tiles_path, tipboard_tiles_path:
+ tile_path = os.path.join(path, tile_html)
+ if os.path.exists(tile_path):
+ return tile_path
+ raise UserWarning('No such tile: %s' % tile_name)
+
+ try:
+ config = process_layout_config(layout_name or 'layout_config')
+ except IOError as e:
+ msg = '<br>'.join([
+ '<div style="color: red">',
+ 'No config file found for dashboard: {}'.format(layout_name),
+ 'Make sure that file: "{}" exists.'.format(e.filename),
+ '</div>',
+ ])
+ self.write(msg)
+ return
+
+ tiles_js = ['.'.join((name, 'js')) for name in config['tiles_names']]
+ tiles_js = filter(_verify_statics, tiles_js)
+ tiles_css = ['.'.join((name, 'css')) for name in config['tiles_names']]
+ tiles_css = filter(_verify_statics, tiles_css)
+ self.render(
+ 'layout.html',
+ details=config['details'],
+ layout=config['layout'],
+ tipboard_css=settings.TIPBOARD_CSS_STYLES,
+ tipboard_js=settings.TIPBOARD_JAVASCRIPTS,
+ tiles_css=tiles_css,
+ tiles_js=tiles_js,
+ tile_path=_tile_path,
+ )
+
+ def head(self):
+ # muted
+ pass
+
+
+class GetDashboardsPaths(tornado.web.RequestHandler):
+ def post(self):
+ paths = flipboard.get_paths()
+ jsoned = json.dumps({'paths': paths})
+ self.set_header("Content-Type", 'application/json')
+ self.write(jsoned)
+
+
+class FlipboardHandler(tornado.web.RequestHandler, SentryMixin):
+ def get(self):
+ self.render(
+ 'flipboard.html',
+ page_title=flipboard.get_flipboard_title(),
+ tipboard_css=settings.TIPBOARD_CSS_STYLES,
+ tipboard_js=['js/lib/jquery.js', 'js/flipboard.js'],
+ flipboard_interval=settings.FLIPBOARD_INTERVAL,
+ )
+
+
+class MultiStaticFileHandler(tornado.web.StaticFileHandler, SentryMixin):
+ def initialize(self, path):
+ self.static_paths = []
+ self.static_paths.extend(self.settings.get('tiles_paths'))
+ self.static_paths.append(path)
+
+ def get(self, path):
+ # static paths are examined in following order:
+ # custom_tiles --> tipboard/tiles --> tipboard/static
+ for p in self.static_paths:
+ try:
+ super(MultiStaticFileHandler, self).initialize(p)
+ return super(MultiStaticFileHandler, self).get(path)
+ except tornado.web.HTTPError as exc:
+ if exc.status_code == 404:
+ continue
+ raise
+ raise tornado.web.HTTPError(404)
+
+ @classmethod
+ def get_version(cls, settings, path):
+ # temporarily muted
+ return None
+
+
+urls = [
+ (r"/", FlipboardHandler),
+ (r"/flipboard/getDashboardsPaths", GetDashboardsPaths),
+ (r"/communication/websocket", DashboardSocketHandler),
+ (r"/([a-zA-Z0-9_-]*)", DashboardRendererHandler),
+ (r"/api/{}/{}/tileconfig/([a-zA-Z0-9_-]+)".format(
+ api_version, settings.API_KEY), MetaProperty),
+ (r"/api/{}/{}/tiledata/([a-zA-Z0-9_-]+)".format(
+ api_version, settings.API_KEY), TileData),
+ (r"/api/{}/{}/info".format(api_version, settings.API_KEY), ProjectInfo),
+ (r"/api/{}/{}/push".format(api_version, settings.API_KEY), Push),
+]
+
+flipboard = Flipboard()
+app = tornado.web.Application(
+ urls,
+ template_path=os.path.join(settings.TIPBOARD_PATH, 'templates'),
+ static_path=os.path.join(settings.TIPBOARD_PATH, "static"),
+ static_handler_class=MultiStaticFileHandler,
+ debug=settings.DEBUG,
+ tiles_paths=settings.TILES_PATHS,
+ sentry_client=AsyncSentryClient(settings.SENTRY_DSN),
+)
diff --git a/tipboard/console.py b/tipboard/console.py
new file mode 100644
index 0000000..0a77a31
--- /dev/null
+++ b/tipboard/console.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Tipboard - flexible framework for creating dashboards.
+
+Usage:
+ tipboard runserver
+ tipboard runserver [<port>]
+ tipboard runserver [<host>] [<port>]
+ tipboard create_project <name>
+ tipboard test --list | -l
+ tipboard test [<test_selector>]...
+ tipboard (-h | --help)
+ tipboard --version
+
+Arguments:
+ <test_selector> name of a test. Run "tipboard test --list" command to see
+ names.
+
+Options:
+ -l --list Show list of tests.
+ -h --help Show this screen.
+ --version Show version.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import logging
+import logging.handlers
+import os
+import errno
+import shutil
+import sys
+import unittest
+import uuid
+
+from docopt import docopt
+import json
+import redis
+import tornado.ioloop
+import tornado.options
+import tornado.web
+
+from tipboard import __version__, settings
+from tipboard.app import app, DashboardSocketHandler
+
+
+def _setup_logger():
+ log_file_path = os.path.join(os.path.expanduser("~"), 'tipboard.log')
+ # for the explaination of the options below see method
+ # 'define_logging_options' in Tornado's sources (file 'log.py')
+ args = [
+ # first element is skipped (parse_command_line emulates sys.argv)
+ 'tipboard',
+ # logging to STDERR
+ '--logging=debug',
+ '--log_to_stderr=true',
+ # logging to file with Tornado's logrotate
+ # (10 MB for the current log + 1 backup)
+ '--log_file_prefix={}'.format(log_file_path),
+ '--log_file_max_size={}'.format(10 * 1000 * 1000),
+ '--log_file_num_backups={}'.format(1),
+ ]
+ tornado.options.parse_command_line(args=args)
+
+
+_setup_logger()
+log = logging.getLogger(__name__)
+
+
+def _create_project_dir(project_name):
+ user_config_dir = os.path.join(os.path.expanduser("~"), '.tipboard')
+ try:
+ os.mkdir(user_config_dir)
+ except OSError as e:
+ if all((e.errno == errno.EEXIST, os.path.isdir(user_config_dir))):
+ pass
+ custom_tiles_dir = os.path.join(user_config_dir, 'custom_tiles')
+ if not os.path.exists(custom_tiles_dir):
+ os.mkdir(custom_tiles_dir)
+
+
+def _create_local_settings(project_name):
+ user_config_dir = os.path.join(os.path.expanduser("~"), '.tipboard')
+ user_layout_config = os.path.join(user_config_dir, 'layout_config.yaml')
+ settings_local = os.path.join(user_config_dir, 'settings-local.py')
+ if not os.path.exists(user_layout_config):
+ shutil.copy2(
+ os.path.join(
+ settings.TIPBOARD_PATH, 'defaults/layout_config.yaml' # noqa
+ ),
+ user_layout_config,
+ )
+ if not os.path.exists(settings_local):
+ shutil.copy2(
+ os.path.join(
+ settings.TIPBOARD_PATH, 'defaults/settings-local.py'
+ ),
+ settings_local
+ )
+ api_key = str(uuid.uuid4()).replace('-', '')
+ to_settings = (
+ "PROJECT_NAME = '%s'" % project_name,
+ "API_KEY = '%s'" % api_key,
+ )
+ with open(settings_local, 'a') as file_:
+ for item in to_settings:
+ file_.write('%s\n' % item)
+ file_.write('\n')
+ else:
+ error_msg = "Can't override file: %s" % settings_local
+ log.error(error_msg)
+ print(error_msg)
+
+
+def create_project(project_name):
+ user_config_dir = os.path.join(os.path.expanduser("~"), '.tipboard')
+ if not os.access(os.path.expanduser("~"), os.W_OK):
+ print('Your home directory is not writable. Aborting.')
+ return
+ if not os.path.isdir(user_config_dir):
+ _create_project_dir(project_name)
+ _create_local_settings(project_name)
+ print('Configuration files created in: %s' % user_config_dir)
+ else:
+ print('Configuration directory ~/.tipboard already exist. Aborting.')
+
+
+def runserver(address=None, port=None):
+ def populate_key_cache():
+ client = redis.StrictRedis(**settings.REDIS)
+ keys = ':'.join([settings.PROJECT_NAME, 'tile', '*'])
+ for key in client.keys(keys):
+ try:
+ json.loads(client.get(key))
+ except Exception:
+ log.warn('Key %s in Redis holds invalid data.', key)
+ continue
+ DashboardSocketHandler.cache.add(key)
+ log.info('Following keys already in Redis:\n{}'.format(
+ '\n'.join(sorted(DashboardSocketHandler.cache)),
+ ))
+
+ if not address:
+ address = settings.HOST
+ if not port:
+ port = settings.PORT
+ else:
+ try:
+ int(port)
+ except ValueError:
+ print(u'Passed port {} is not integer'.format(port))
+ return False
+ populate_key_cache()
+ app.listen(port, address=address)
+ log.info("Listening on port {}:{}...".format(address, port))
+ tornado.ioloop.IOLoop.instance().start()
+
+
+def run_tests(test_names_list):
+ loader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+ if test_names_list:
+ for test_name in test_names_list:
+ full_name = '.'.join(['tipboard.tests', test_name])
+ try:
+ found = loader.loadTestsFromName(full_name)
+ suite.addTests(found)
+ except (AttributeError, ImportError) as e:
+ print("No such test: {}".format(test_name))
+ print("Running tests - STOPPED.")
+ print("Run:\n {}\nto see list of tests.".format(
+ "tipboard test --list"
+ ))
+ return False
+ else:
+ tests_path = os.path.join(settings.TIPBOARD_PATH, 'tests')
+ suite = unittest.TestLoader().discover(tests_path, pattern='test*.py')
+ if suite.countTestCases() == 0:
+ print("Could not find any tests.")
+ return False
+ # mute useless communicates
+ out_hdlr = open(os.devnull, "w")
+ sys.stdout = out_hdlr
+ logging.disable(logging.CRITICAL)
+ unittest.TextTestRunner(verbosity=2).run(suite)
+
+
+def show_test_names():
+ """
+ Prints available tests list, easy to be copied and run.
+ """
+ def _found_tests():
+ tests_path = os.path.join(settings.TIPBOARD_PATH, 'tests')
+ suite = loader.discover(tests_path, pattern='test*.py')
+ for file_tests in suite._tests:
+ for class_tests in file_tests._tests:
+ for test in class_tests._tests:
+ yield test
+
+ print('Available test names:')
+ loader = unittest.TestLoader()
+ last_test_file_name, last_test_case_name = '', ''
+ for test in _found_tests():
+ test_file_name, test_case_name = (
+ str(test).split(' ')[1][1:-1].split('.')
+ )
+ test_name = test._testMethodName
+ if last_test_file_name != test_file_name:
+ to_print = ' ' + test_file_name
+ print(to_print)
+ last_test_file_name = test_file_name
+ if last_test_case_name != test_case_name:
+ to_print = ' {}'.format(
+ '.'.join([to_print, test_case_name])
+ )
+ print(to_print)
+ last_test_case_name = test_case_name
+ print(' {}'.format('.'.join([to_print, test_name])))
+
+
+def main():
+ arguments = docopt(
+ __doc__,
+ version='Tipboard {}'.format(__version__)
+ )
+ if arguments.get('runserver'):
+ runserver(
+ address=arguments.get('<host>'), port=arguments.get('<port>')
+ )
+ if arguments.get('create_project'):
+ create_project(arguments.get('<name>'))
+
+ if arguments.get('test'):
+ if arguments.get('--list'):
+ show_test_names()
+ else:
+ run_tests(arguments.get('<test_selector>'))
+
+if __name__ == '__main__':
+ main()
diff --git a/tipboard/defaults/layout_config.yaml b/tipboard/defaults/layout_config.yaml
new file mode 100644
index 0000000..eb12ec0
--- /dev/null
+++ b/tipboard/defaults/layout_config.yaml
@@ -0,0 +1,52 @@
+details:
+ page_title: Empty Dashboard
+layout:
+ - row_1_of_2:
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - row_1_of_2:
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
+
+ - col_1_of_4:
+ - tile_template: empty
+ tile_id: empty
+ title: Empty Tile
+ classes:
diff --git a/tipboard/defaults/settings-local.py b/tipboard/defaults/settings-local.py
new file mode 100644
index 0000000..afccd95
--- /dev/null
+++ b/tipboard/defaults/settings-local.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
diff --git a/tipboard/extras/README.rst b/tipboard/extras/README.rst
new file mode 100644
index 0000000..77f1d70
--- /dev/null
+++ b/tipboard/extras/README.rst
@@ -0,0 +1,5 @@
+Extras
+------
+
+For the detailed description of the contents of this dir, see relevant section
+in our documentation.
diff --git a/tipboard/extras/client_code_example.py b/tipboard/extras/client_code_example.py
new file mode 100644
index 0000000..cf2520b
--- /dev/null
+++ b/tipboard/extras/client_code_example.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+
+# This is an example 'client_code' script which demonstrates how to 'glue'
+# together a tile (or collection of tiles) with an external script fetching
+# json'ed data from data source.
+#
+# Script like this one is meant to be launched periodically from crontab.
+#
+# The main idea here is as follows: you're querying the data source(s) you want,
+# transform resulting data according to your needs, and finally you're pushing
+# it to your tile, along with optional config data. Really simple, isn't it?
+
+import json
+import requests
+
+from subprocess import check_output
+
+# Get your API_KEY from your settings file ('~/.tipboard/settings-local.py').
+API_KEY = ''
+# Change '127.0.0.1:7272' to the address of your Tipboard instance.
+API_URL = 'http://127.0.0.1:7272/api/v0.1/{}'.format(API_KEY)
+API_URL_PUSH = '/'.join((API_URL, 'push'))
+API_URL_TILECONFIG = '/'.join((API_URL, 'tileconfig'))
+
+
+#### Helper functions for data transformations (data sources --> tiles).
+
+# We have only one tile here, hence 'prepare_for_pie_chart' is all we need,
+# but in real-life scenario you may want some additional functions like
+# 'prepare_for_line_chart', 'prepare_for_velocity_chart' and so on.
+
+def prepare_for_pie_chart(data):
+ # Pie chart needs data as a list of lists (whose elements are pairs
+ # component-percentage), so we have to prepare it.
+ data_prepared = []
+ for k, v in data.items():
+ data_prepared.append([k, v[0]])
+ data_prepared = {'title': 'my title', 'pie_data': data_prepared}
+ return data_prepared
+
+
+#### Putting it all together.
+
+def main():
+ # Tile 'pie001' (pie chart)
+ # (let's say we want to show issues count for project 'Tipboard' grouped by
+ # issue status i.e. 'Resolved', 'In Progress', 'Open', 'Closed' etc.)
+ TILE_NAME = 'pie_chart'
+ TILE_KEY = 'pie001'
+ # Now we are launching our script asking data source for the stuff we want:
+ # (see its documentation for available options/switches)
+ ds_output = check_output(["python", "jira-ds.py", "--project=Tipboard",
+ "--issues-counts"])
+ data = json.loads(ds_output)
+ data_selected = data.get('status_summary')
+ data_prepared = prepare_for_pie_chart(data_selected)
+ data_jsoned = json.dumps(data_prepared)
+ data_to_push = {
+ 'tile': TILE_NAME,
+ 'key': TILE_KEY,
+ 'data': data_jsoned,
+ }
+ resp = requests.post(API_URL_PUSH, data=data_to_push)
+ if resp.status_code != 200:
+ print(resp.text)
+ return
+
+ # (optional) config for our pie chart, which is pushed in a separate request
+ tile_config = {
+ 'legend': {
+ 'show': True,
+ 'location': 's',
+ }
+ }
+ tile_config_jsoned = json.dumps(tile_config)
+ data_to_push = {
+ 'value': tile_config_jsoned,
+ }
+ resp = requests.post(
+ '/'.join((API_URL_TILECONFIG, TILE_KEY)),
+ data=data_to_push,
+ )
+ if resp.status_code != 200:
+ print(resp.text)
+ return
+
+
+ # Tile 'pie002'
+ # TILE_NAME = 'pie_chart'
+ # TILE_KEY = 'pie002'
+ # output = check_output(...
+ # ...and the rest in the same manner as in above example, i.e.:
+ # - query the data source for data
+ # - select what you want from that data
+ # - transform selected data according to tile's specs (if needed)
+ # - push transformed data to API_URL_PUSH
+ # - push config data for your tile to API_URL_CONFIG (let's say you want
+ # to override default behavior and show pie chart's legend at
+ # the bottom of the tile, or change some colors etc.)
+ # That's all!
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tipboard/extras/fabfile.py b/tipboard/extras/fabfile.py
new file mode 100644
index 0000000..079d1c4
--- /dev/null
+++ b/tipboard/extras/fabfile.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from fabric.api import run
+
+from fabtools import require
+from fabtools.python import virtualenv
+
+
+_TIPBOARD_USER = 'tipboard'
+_SOURCE_CODE_URL = 'https://github.com/allegro/tipboard.git'
+
+
+def install():
+ configure_os()
+
+ install_tipboard()
+
+ run('chown -R %(user)s:%(user)s /home/%(user)s' % {'user': _TIPBOARD_USER})
+
+
+def configure_os():
+ require.deb.packages([
+ 'python',
+ 'python-dev',
+ 'python-virtualenv',
+ 'redis-server',
+ 'libmysqlclient-dev',
+ 'supervisor',
+ 'git',
+ ])
+
+ require.user(_TIPBOARD_USER, home='/home/' + _TIPBOARD_USER,
+ shell='/bin/bash')
+
+
+def install_tipboard():
+ venv_path = '/home/%s/tipboard-current' % _TIPBOARD_USER
+
+ require.python.virtualenv(venv_path)
+
+ with virtualenv(venv_path):
+ require.python.package('distribute==0.6.28')
+ require.python.package('tipboard',
+ url=_SOURCE_CODE_URL + '@master#egg=tipboard')
+
+ require.supervisor.process('tipboard',
+ command='%s/bin/tipboard runserver' % venv_path,
+ directory='/home/%s' % _TIPBOARD_USER,
+ user=_TIPBOARD_USER,
+ stdout_logfile='/home/%s/out.log' %
+ _TIPBOARD_USER)
diff --git a/tipboard/extras/jira-ds.js b/tipboard/extras/jira-ds.js
new file mode 100644
index 0000000..f7ab479
--- /dev/null
+++ b/tipboard/extras/jira-ds.js
@@ -0,0 +1,191 @@
+// If you want to use this script directly (i.e. without jira-ds.py), you can
+// do it like this (remember to fill 'username' & 'password' in casper.start):
+//
+// casperjs jira-ds.js \
+// (--project-key=P_KEY | --project-id=P_ID | --board-id=B_ID) --step=STEP
+
+var utils = require('utils');
+var casper = require('casper').create({
+ verbose: false,
+ logLevel: 'info',
+ pageSettings: {
+ loadImages: false,
+ loadPlugins: false,
+ },
+ timeout: 10000, // in milliseconds
+ onTimeout: function () {
+ this.echo('Timeout of 10 seconds has been reached');
+ this.exit(1);
+ },
+});
+
+// Uncomment if you need some additional diagnostic info.
+// casper.on('remote.message', function (msg) {
+// this.echo('=== Remote message caught: ' + msg);
+// });
+// casper.on("page.error", function (msg, trace) {
+// this.echo("=== Page error: " + msg, "ERROR");
+// });
+
+// available steps: 'control-chart', 'summary', 'cvsr','velocity', 'issues-counts'
+var step = casper.cli.options.step;
+var project_key = casper.cli.options['project-key'];
+var project_id = casper.cli.options['project-id'];
+var board_id = casper.cli.options['board-id'];
+
+var url_jira_login = ''; // needs to be filled in
+var url_jira = ''; // needs to be filled in
+var url_board = url_jira + '/secure/RapidBoard.jspa?rapidView=' + board_id;
+var url_control = url_board + '&view=reporting&chart=controlChart&days=30';
+var url_cumulative = url_jira + '/secure/ConfigureReport.jspa' +
+ '?projectOrFilterId=project-' + project_id +
+ '&periodName=daily&daysprevious=30&cumulative=true&versionLabels=major' +
+ '&selectedProjectId=' + project_id + '&reportKey=' +
+ 'com.atlassian.jira.plugin.system.reports:createdvsresolved-report' +
+ '&Next=Next';
+var url_velocity = url_board + '&view=reporting&chart=velocityChart';
+var url_summary = url_jira + '/browse/' + project_key;
+var url_issues = url_summary + '?selectedTab=' +
+ 'com.atlassian.jira.plugin.system.project%3Aissues-panel';
+
+casper.start(url_jira_login, function login() {
+ "use strict";
+ var form_selector = '#login-form',
+ credentials = {};
+ (function () {
+ var system = require('system');
+ credentials.user = system.env.JIRA_USER;
+ credentials.password = system.env.JIRA_PASSWORD;
+ }());
+ this.fill(form_selector, {
+ // 'os_username' and 'os_password' - names of the fields in login form
+ 'os_username': credentials.user,
+ 'os_password': credentials.password
+ }, true);
+});
+
+casper.thenBypassUnless(step === 'control-chart', 2);
+casper.thenOpen(url_control, function control_chart() {
+ "use strict";
+
+ var selector_data = '#ghx-chart-snapshot dt',
+ selector_timeframe = '#ghx-chart-selector .js-chart-time-window',
+ data = {};
+ data = this.evaluate(function (selector_data, selector_timeframe) {
+ var elems = document.querySelectorAll(selector_data),
+ data = {},
+ idxs = ['mean', 'median', 'min-time', 'max-time', 'issue-count'];
+ for (i = 0; i < idxs.length; i += 1) {
+ data[idxs[i]] = elems[i].textContent;
+ };
+ data.timeframe = document.querySelector(selector_timeframe).textContent;
+ return data;
+ }, selector_data, selector_timeframe);
+ utils.dump(data);
+});
+
+casper.thenBypassUnless(step === 'summary', 2);
+casper.thenOpen(url_summary, function summary() {
+ "use strict";
+ var selector_created = '#fragcreatedvsresolved span.created-issue-count',
+ selector_resolved = '#fragcreatedvsresolved span.resolved-issue-count',
+ data = {};
+ data = this.evaluate(function (selector_created, selector_resolved) {
+ var issues_created, issues_resolved, data = {};
+ issues_created = document.querySelector(selector_created).textContent;
+ issues_resolved = document.querySelector(selector_resolved).textContent;
+ data = {
+ 'issues_created': issues_created,
+ 'issues_resolved': issues_resolved,
+ };
+ return data;
+ }, selector_created, selector_resolved);
+ utils.dump(data);
+});
+
+casper.thenBypassUnless(step === 'cvsr', 2);
+casper.thenOpen(url_cumulative, function cvsr() {
+ "use strict";
+ var selector = '#createdvsresolved-report-datatable > tbody > tr',
+ data = {};
+ data = this.evaluate(function (selector) {
+ var i, rows, data = {};
+ rows = document.querySelectorAll(selector);
+ for (i = 1; i < rows.length; i += 1) {
+ data[rows[i].cells[0].textContent] = {
+ 'created': parseInt(rows[i].cells[1].textContent, 10),
+ 'resolved': parseInt(rows[i].cells[2].textContent, 10),
+ };
+ }
+ return data;
+ }, selector);
+ utils.dump(data);
+});
+
+casper.thenBypassUnless(step === 'velocity', 2);
+casper.thenOpen(url_velocity, function velocity() {
+ "use strict";
+ var selector = '#ghx-chart-data > table > tbody tr',
+ data = {};
+ data = this.evaluate(function (selector) {
+ var i, rows = {}, data = {};
+ rows = document.querySelectorAll(selector);
+ for (i = 0; i < rows.length; i += 1) {
+ data[rows[i].cells[0].textContent] = {
+ 'commitment': parseInt(rows[i].cells[1].textContent, 10),
+ 'completed': parseInt(rows[i].cells[2].textContent, 10),
+ };
+ }
+ return data;
+ }, selector);
+ utils.dump(data);
+});
+
+casper.thenBypassUnless(step === 'issues-counts', 2);
+casper.thenOpen(url_issues, function issues_counts() {
+ // function that we gonna use in a loop to retrieve data from selectors
+ "use strict";
+ function get_data(selector) {
+ var data = {};
+ data = this.evaluate(function (selector) {
+ var i, idx, rows = {}, data = {};
+ rows = document.querySelectorAll(selector);
+ // pack tr cells contents into more convenient structures
+ for (i = 0; i < rows.length; i += 1) {
+ idx = rows[i].cells[0].textContent.trim();
+ if (rows[i].cells.length === 3) {
+ data[idx] = [
+ parseInt(rows[i].cells[1].textContent, 10),
+ rows[i].cells[2].textContent.trim(),
+ ];
+ } else {
+ data[idx] = parseInt(rows[i].cells[1].textContent, 10);
+ }
+ }
+ return data;
+ }, selector);
+ return data;
+ }
+ var i, issues = {}, selectors = [
+ ['by_priority', '#fragunresolvedissuesbypriority div.mod-content table.aui > tbody > tr'],
+ ['status_summary', '#fragstatussummary div.mod-content table.aui > tbody > tr'],
+ ['by_issuetype', '#fragunresolvedissuesbyissuetype div.mod-content table.aui > tbody > tr'],
+ ['by_assignee', '#fragunresolvedissuesbyassignee div.mod-content table.aui > tbody > tr'],
+ ['by_component', '#fragunresolvedissuesbycomponent div.mod-content table.aui > tbody > tr'],
+ //['by_version', '#fragunresolvedissuesbyfixversion div.mod-content table.aui > tbody > tr'], // doesn't work (yet)
+ ];
+ for (i = 0; i < selectors.length; i += 1) {
+ issues[selectors[i][0]] = get_data.call(this, selectors[i][1]);
+ }
+ utils.dump(issues);
+});
+
+// Prints all defined steps (useful for diagnostics). Please notice that
+// both 'casper.thenOpen' and 'casper.start' contain 'casper.open', hence
+// every on of these counts as two steps.
+// Also, every 'casper.thenBypass' (and friends) counts as a separate step.
+// utils.dump(casper.steps.map(function (step) {
+// return step.toString();
+// }));
+
+casper.run();
diff --git a/tipboard/extras/jira-ds.py b/tipboard/extras/jira-ds.py
new file mode 100755
index 0000000..8a95eb0
--- /dev/null
+++ b/tipboard/extras/jira-ds.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+
+"""
+Jira-ds - a command line utility for fetching various data from Jira.
+
+Usage:
+ jira-ds.py --project=NAME (--summary|--cvsr|--issues-counts)
+ jira-ds.py --board=NAME (--control-chart|--velocity)
+ jira-ds.py --jql=QUERY [-m <limit>]
+ jira-ds.py -h | --help
+
+Options:
+ --project=NAME Project name (case sensitive).
+ --board=NAME Board name (case sensitive).
+ --jql=QUERY Return raw data using JQL (Jira Query Language) query.
+ Beware that some queries may be quite heavy on resources!
+ -m <limit> Set 'maxResults' for '--jql' option [default: 1000].
+ (hint: setting it to '0' gives you total number of results
+ without fetching the data)
+ --summary Get issues counts from 'summary' tab.
+ --cvsr Get data table from 'created vs resolved' report
+ (past month).
+ --issues-counts Get issues counts grouped by priority, status, type etc.
+ --control-chart Get data table from 'control chart' report (past month).
+ --velocity Get data table from 'velocity chart' report.
+ -h --help Show this screen.
+
+"""
+
+import json
+import os
+import requests
+import subprocess
+
+from docopt import docopt
+
+JIRA_BASE_URL = '' # needs to be filled in
+JIRA_API_URL = JIRA_BASE_URL + '/rest/api/2/'
+GREENHOPPER_API_URL = JIRA_BASE_URL + '/rest/greenhopper/1.0/'
+JIRA_CREDENTIALS = { # needs to be filled in
+ 'user': '',
+ 'password': '',
+}
+SUBP_ENV = os.environ.copy()
+SUBP_ENV['JIRA_USER'] = JIRA_CREDENTIALS.get('user')
+SUBP_ENV['JIRA_PASSWORD'] = JIRA_CREDENTIALS.get('password')
+SUBP_PARAMS = ["casperjs", "jira-ds.js"]
+REQUESTS_TIMEOUT = 10 # in seconds
+
+
+##### 'Internal' stuff.
+
+def _get_identifiers(name, url):
+ """
+ Gets project_id and project_key (when 'url' points to project) or board_id
+ (when 'url' points to board).
+ """
+ session = requests.Session()
+ resp = session.get(url, timeout=REQUESTS_TIMEOUT, params={
+ 'os_authType': 'basic',
+ 'os_username': JIRA_CREDENTIALS.get('user'),
+ 'os_password': JIRA_CREDENTIALS.get('password'),
+ })
+ resp.raise_for_status()
+ rest_data = resp.json()
+ try:
+ data = rest_data.get('views') # for boards
+ except AttributeError:
+ data = rest_data # for projects
+ for d in data:
+ if d.get('name') == name:
+ return (d.get('id'), d.get('key'))
+ return (None, None) # board/project doesn't exist
+
+
+def _get_data_from_subprocess(subp_params):
+ try:
+ data = subprocess.check_output(subp_params, env=SUBP_ENV)
+ except subprocess.CalledProcessError as e:
+ print("Error in jira-ds.js script: '{}'.".format(e.output.strip()))
+ return
+ else:
+ return json.loads(data)
+
+
+##### Command-line options.
+
+def get_summary(project_key):
+ subp_params = SUBP_PARAMS + ["--project-key=" + project_key,
+ "--step=summary"]
+ return _get_data_from_subprocess(subp_params)
+
+
+def get_cvsr(project_id):
+ subp_params = SUBP_PARAMS + ["--project-id=" + project_id, "--step=cvsr"]
+ return _get_data_from_subprocess(subp_params)
+
+
+def get_issues_counts(project_key):
+ subp_params = SUBP_PARAMS + ["--project-key=" + project_key,
+ "--step=issues-counts"]
+ return _get_data_from_subprocess(subp_params)
+
+
+def get_control_chart(board_id):
+ subp_params = SUBP_PARAMS + ["--board-id=" + str(board_id),
+ "--step=control-chart"]
+ return _get_data_from_subprocess(subp_params)
+
+
+def get_velocity(board_id):
+ subp_params = SUBP_PARAMS + ["--board-id=" + str(board_id),
+ "--step=velocity"]
+ return _get_data_from_subprocess(subp_params)
+
+
+def get_jql(query, max_results):
+ session = requests.Session()
+ query_url = JIRA_API_URL + 'search?'
+ resp = session.get(query_url, timeout=REQUESTS_TIMEOUT, params={
+ 'os_authType': 'basic',
+ 'os_username': JIRA_CREDENTIALS.get('user'),
+ 'os_password': JIRA_CREDENTIALS.get('password'),
+ 'start-index': 0,
+ 'maxResults': max_results,
+ 'jql': query,
+ })
+ resp.raise_for_status()
+ rest_data = resp.json()
+ return(rest_data)
+
+
+##### Putting it all together.
+
+def main():
+ args = docopt(__doc__)
+ project_name = args.get('--project')
+ board_name = args.get('--board')
+ jql = args.get('--jql')
+ if project_name:
+ url = JIRA_API_URL + 'project'
+ try:
+ project_id, project_key = _get_identifiers(project_name, url)
+ except requests.exceptions.RequestException as e:
+ print("Your query has raised an error: '{}'.".format(e.message))
+ return
+ else:
+ if not project_id:
+ print("Project '{}' does not exist.").format(project_name)
+ return
+ if args.get('--summary'):
+ result = get_summary(project_key)
+ elif args.get('--cvsr'):
+ result = get_cvsr(project_id)
+ elif args.get('--issues-counts'):
+ result = get_issues_counts(project_key)
+ elif board_name:
+ url = GREENHOPPER_API_URL + 'rapidview'
+ try:
+ board_id, _ = _get_identifiers(board_name, url)
+ except requests.exceptions.RequestException as e:
+ print("Your query has raised an error: '{}'.".format(e.message))
+ return
+ else:
+ if not board_id:
+ print("Board '{}' does not exist.").format(board_name)
+ return
+ if args.get('--control-chart'):
+ result = get_control_chart(board_id)
+ elif args.get('--velocity'):
+ result = get_velocity(board_id)
+ elif jql:
+ max_results = args.get('-m')
+ try:
+ result = get_jql(jql, max_results)
+ except requests.exceptions.RequestException as e:
+ print("Your query has raised an error: '{}'.".format(e.message))
+ return
+ print(json.dumps(result, ensure_ascii=False))
+
+if __name__ == '__main__':
+ main()
diff --git a/tipboard/parser.py b/tipboard/parser.py
new file mode 100644
index 0000000..05d23c3
--- /dev/null
+++ b/tipboard/parser.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import glob
+import os
+import yaml
+
+from tipboard import settings
+
+
+class WrongSumOfRows(Exception):
+ pass
+
+
+def _get_tiles_dict(col):
+ return col.values().pop()
+
+
+def _get_cols(rows):
+ #TODO: validation col_1_of_4
+ cols = []
+ for col in rows.values()[0]:
+ cols.append(col)
+ return cols
+
+
+def _get_rows(layout):
+ """Validates and returns number of rows."""
+ rows_data = []
+ rows_class = []
+ for row in layout:
+ rows_data.append(row)
+ rows_class.append(row.keys().pop())
+ rows_count = 0
+ sum_of_rows = []
+ for row_class in rows_class:
+ splited_class = row_class.split('_') # ex: row_1_of_2
+ row = splited_class[1]
+ of_rows = int(splited_class[3])
+ if rows_count == 0:
+ rows_count = int(of_rows)
+ sum_of_rows.append(int(row))
+ elif not rows_count == of_rows:
+ raise WrongSumOfRows('The sum of the lines is incorrect.')
+ else:
+ sum_of_rows.append(int(row))
+ if not sum(sum_of_rows) == rows_count:
+ raise WrongSumOfRows('The sum of the lines is incorrect.')
+ return rows_data
+
+
+def _find_tiles_names(layout):
+ name_list, key_list = [], []
+ for row in _get_rows(layout):
+ for col in _get_cols(row):
+ for tile_dict in _get_tiles_dict(col):
+ name = tile_dict['tile_template']
+ key = tile_dict['tile_id']
+ if key not in key_list:
+ key_list.append(key)
+ if name not in name_list:
+ name_list.append(name)
+ return name_list, key_list
+
+
+def get_config_files_names():
+ """
+ Return all configs files' names (without '.yaml' ext.) from user space
+ (~/.tipboard/)
+ """
+ configs_names = []
+ configs_dir = os.path.join(settings._user_config_dir, '*.yaml')
+ for config_path in glob.glob(configs_dir):
+ filename = os.path.basename(config_path)
+ head, ext = os.path.splitext(filename)
+ configs_names.append(head)
+ return configs_names
+
+
+def config_file_name2path(config_name):
+ """
+ Return file path to *config_name* (eg. 'layout_config')
+ """
+ path = os.path.join(
+ settings._user_config_dir, '.'.join([config_name, 'yaml'])
+ )
+ return path
+
+
+def get_tiles_configs():
+ """
+ Return dict with both tiles' keys and ids from all available configs
+ """
+ tiles_configs = {
+ 'tiles_keys': set(),
+ 'tiles_names': set()
+ }
+ configs_names = get_config_files_names()
+ for config_name in configs_names:
+ parsed_config = process_layout_config(config_name)
+ tiles_configs['tiles_keys'].update(set(parsed_config['tiles_keys']))
+ tiles_configs['tiles_names'].update(set(parsed_config['tiles_names']))
+ return tiles_configs
+
+
+def process_layout_config(layout_name):
+ config_path = config_file_name2path(layout_name)
+ with open(config_path, 'r') as layout_config:
+ config = yaml.load(layout_config)
+ layout = config['layout']
+ config['tiles_names'], config['tiles_keys'] = _find_tiles_names(layout)
+ return config
diff --git a/tipboard/redis_utils.py b/tipboard/redis_utils.py
new file mode 100644
index 0000000..6492ed3
--- /dev/null
+++ b/tipboard/redis_utils.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from datetime import datetime
+import json
+import logging
+import time
+
+import redis
+
+from tipboard import settings
+
+log = logging.getLogger(__name__)
+
+db_events_path = lambda: '{}:events'.format(settings.PROJECT_NAME)
+
+
+class KeyNotFound(Exception):
+ def __init__(self, key):
+ self.key = key
+ self.message = "Key '{}' was not found in db".format(self.key)
+
+
+def redis_actions(method, tile_id, value=None, tile=None):
+ key = '%s:tile:%s' % (settings.PROJECT_NAME, tile_id)
+ try:
+ redis_db = redis.Redis(**settings.REDIS_SYNC)
+ except Exception as e:
+ raise TypeError('Redis: %s' % e)
+ else:
+ if method.lower() == 'post':
+ try:
+ last_tile_data = get_redis_value(tile_id)
+ except KeyNotFound as e:
+ meta = {}
+ else:
+ meta = last_tile_data.get('meta')
+ localtime = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
+ tz = '{0:+06.2f}'.format(-float(time.altzone) / 3600)
+ iso_time = localtime + tz.replace('.', ':') # ISO-8601
+ required_structure = dict(
+ id=tile_id,
+ tile_template=tile,
+ data=json.loads(value),
+ meta=meta,
+ modified=iso_time,
+ )
+ dumped_value = json.dumps(required_structure)
+ redis_db.set(key, dumped_value)
+ log.debug(
+ 'db key: {} set to value: {}'.format(
+ repr(key), repr(dumped_value)
+ )
+ )
+ redis_db.publish(db_events_path(), key)
+ return 'Push success.'
+
+ elif method.lower() == 'get':
+ db_value = redis_db.get(key)
+ log.debug('got db value: {}'.format(db_value))
+ return db_value
+ elif method.lower() == 'delete':
+ redis_db.delete(key)
+ log.debug('deleted db key: {} '.format(key))
+ return 'Deleted.'
+ else:
+ return 'Unkown method.'
+
+
+def get_redis_value(tile_id):
+ key = '%s:tile:%s' % (settings.PROJECT_NAME, tile_id)
+ try:
+ redis_db = redis.Redis(**settings.REDIS_SYNC)
+ except Exception as e:
+ raise TypeError('Redis: %s' % e)
+ else:
+ jsoned = redis_db.get(key)
+ if not jsoned:
+ raise KeyNotFound(key)
+ db_data = json.loads(jsoned)
+ return db_data
+
+
+def set_redis_value(tile_id, data):
+ key = '%s:tile:%s' % (settings.PROJECT_NAME, tile_id)
+ try:
+ redis_db = redis.Redis(**settings.REDIS)
+ except Exception as e:
+ raise TypeError('Redis: %s' % e)
+ else:
+ dumped_value = json.dumps(data)
+ redis_db.set(key, dumped_value)
+ redis_db.publish(db_events_path(), key)
+
+
+def key_exist(key_id):
+ key = '%s:tile:%s' % (settings.PROJECT_NAME, key_id)
+ redis_db = redis.Redis(**settings.REDIS)
+ return True if redis_db.get(key) else False
diff --git a/tipboard/settings.py b/tipboard/settings.py
new file mode 100644
index 0000000..83d344d
--- /dev/null
+++ b/tipboard/settings.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+
+##############################################################################
+# Stuff that can be / should be overridden in local settings
+
+# Redis serwer configuration
+REDIS_HOST = 'localhost'
+REDIS_PORT = 6379
+REDIS_DB = 4
+
+DEBUG = False
+API_KEY = 'default api key'
+HOST = 'localhost'
+PORT = 7272
+PROJECT_NAME = 'example'
+
+# Javascript log level ('1' for 'standard', '2' for 'debug')
+JS_LOG_LEVEL = 1
+
+# Our default color palette
+COLORS = {
+ 'black': '#000000',
+ 'white': '#FFFFFF',
+ 'tile_background': '#25282d',
+ 'red': '#DC5945',
+ 'yellow': '#FF9618',
+ 'green': '#94C140',
+ 'blue': '#12B0C5',
+ 'violet': '#9C4274',
+ 'orange': '#EC663C',
+ 'naval': '#54C5C0',
+}
+
+# how many seconds dashboard is displayed before is flipped
+FLIPBOARD_INTERVAL = 0
+# file name(s) of EXISTING layouts without extension, eg. ['layout_config']
+FLIPBOARD_SEQUENCE = []
+
+# We are using Sentry for catching/aggregating errors
+SENTRY_DSN = ''
+
+##############################################################################
+# Settings below should not be changed directly by user
+
+# Load local settings (~/.tipboard/local_settings.py)
+try:
+ execfile(
+ os.path.join(os.path.expanduser("~"), '.tipboard/settings-local.py')
+ )
+except IOError:
+ pass
+
+# Redis config that should be processed after local settings
+REDIS = dict(
+ host=REDIS_HOST,
+ port=REDIS_PORT,
+ db=REDIS_DB,
+)
+REDIS_ASYNC = dict(
+ host=REDIS_HOST,
+ port=REDIS_PORT,
+ selected_db=REDIS_DB,
+)
+REDIS_SYNC = dict(
+ host=REDIS_HOST,
+ port=REDIS_PORT,
+ db=REDIS_DB,
+)
+
+# Location of Tipboard sources
+TIPBOARD_PATH = os.path.dirname(__file__)
+
+# Tiles' paths which should be examined in given order (i.e. user's --> app's)
+TILES_PATHS = [
+ os.path.join(os.path.expanduser("~"), '.tipboard/custom_tiles'),
+ os.path.join(TIPBOARD_PATH, 'tiles'),
+]
+
+# Determine which layout config should be used
+_user_config_dir = os.path.join(os.path.expanduser("~"), '.tipboard')
+_user_layout_config = os.path.join(_user_config_dir, 'layout_config.yaml')
+_fallback_layout_config = os.path.join(
+ TIPBOARD_PATH, 'defaults/layout_config.yaml'
+)
+if not os.path.exists(_user_layout_config):
+ LAYOUT_CONFIG = _fallback_layout_config
+else:
+ LAYOUT_CONFIG = _user_layout_config
+
+# CSS/JS files required by Tipboard
+# TODO: do we really need to put this stuff in settings..??
+TIPBOARD_CSS_STYLES = [
+ 'css/reset.css',
+ 'css/jquery.jqplot.css',
+ 'css/layout.css',
+]
+TIPBOARD_JAVASCRIPTS = [
+ 'js/lib/jquery.js',
+ 'js/lib/jquery.fullscreen.js',
+ 'js/lib/jqplot/jquery.jqplot.js',
+ 'js/lib/jqplot/plugins/jqplot.trendline.js',
+ 'js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.barRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.pointLabels.js',
+ 'js/lib/jqplot/plugins/jqplot.highlighter.js',
+ 'js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.pieRenderer.js',
+ # XXX: importing ..
+ #'js/lib/jqplot/plugins/jqplot.BezierCurveRenderer.js',
+ # .. spoils rendering. Try: first plot from:
+ # http://www.jqplot.com/deploy/dist/examples/pieTest4.html
+ 'js/lib/jqplot/plugins/jqplot.blockRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.bubbleRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.canvasOverlay.js',
+ 'js/lib/jqplot/plugins/jqplot.ciParser.js',
+ 'js/lib/jqplot/plugins/jqplot.cursor.js',
+ 'js/lib/jqplot/plugins/jqplot.donutRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.dragable.js',
+ 'js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.funnelRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.json2.js',
+ 'js/lib/jqplot/plugins/jqplot.logAxisRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.mekkoRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.mobile.js',
+ 'js/lib/jqplot/plugins/jqplot.ohlcRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js',
+ 'js/lib/jqplot/plugins/jqplot.pyramidRenderer.js',
+ 'js/tipboard.js',
+]
diff --git a/tipboard/static/css/jquery.jqplot.css b/tipboard/static/css/jquery.jqplot.css
new file mode 100644
index 0000000..f6768a6
--- /dev/null
+++ b/tipboard/static/css/jquery.jqplot.css
@@ -0,0 +1,259 @@
+/*rules for the plot target div. These will be cascaded down to all plot elements according to css rules*/
+.jqplot-target {
+ position: relative;
+ color: #666666;
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
+ font-size: 1em;
+/* height: 300px;
+ width: 400px;*/
+}
+
+/*rules applied to all axes*/
+.jqplot-axis {
+ font-size: 0.75em;
+}
+
+.jqplot-xaxis {
+ margin-top: 10px;
+}
+
+.jqplot-x2axis {
+ margin-bottom: 10px;
+}
+
+.jqplot-yaxis {
+ margin-right: 10px;
+}
+
+.jqplot-y2axis, .jqplot-y3axis, .jqplot-y4axis, .jqplot-y5axis, .jqplot-y6axis, .jqplot-y7axis, .jqplot-y8axis, .jqplot-y9axis, .jqplot-yMidAxis {
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+/*rules applied to all axis tick divs*/
+.jqplot-axis-tick, .jqplot-xaxis-tick, .jqplot-yaxis-tick, .jqplot-x2axis-tick, .jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick, .jqplot-yMidAxis-tick {
+ position: absolute;
+ white-space: pre;
+}
+
+
+.jqplot-xaxis-tick {
+ top: 0px;
+ /* initial position untill tick is drawn in proper place */
+ left: 15px;
+/* padding-top: 10px;*/
+ vertical-align: top;
+}
+
+.jqplot-x2axis-tick {
+ bottom: 0px;
+ /* initial position untill tick is drawn in proper place */
+ left: 15px;
+/* padding-bottom: 10px;*/
+ vertical-align: bottom;
+}
+
+.jqplot-yaxis-tick {
+ right: 0px;
+ /* initial position untill tick is drawn in proper place */
+ top: 15px;
+/* padding-right: 10px;*/
+ text-align: right;
+}
+
+.jqplot-yaxis-tick.jqplot-breakTick {
+ right: -20px;
+ margin-right: 0px;
+ padding:1px 5px 1px 5px;
+ /*background-color: white;*/
+ z-index: 2;
+ font-size: 1.5em;
+}
+
+.jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick {
+ left: 0px;
+ /* initial position untill tick is drawn in proper place */
+ top: 15px;
+/* padding-left: 10px;*/
+/* padding-right: 15px;*/
+ text-align: left;
+}
+
+.jqplot-yMidAxis-tick {
+ text-align: center;
+ white-space: nowrap;
+}
+
+.jqplot-xaxis-label {
+ margin-top: 10px;
+ font-size: 11pt;
+ position: absolute;
+}
+
+.jqplot-x2axis-label {
+ margin-bottom: 10px;
+ font-size: 11pt;
+ position: absolute;
+}
+
+.jqplot-yaxis-label {
+ margin-right: 10px;
+/* text-align: center;*/
+ font-size: 11pt;
+ position: absolute;
+}
+
+.jqplot-yMidAxis-label {
+ font-size: 11pt;
+ position: absolute;
+}
+
+.jqplot-y2axis-label, .jqplot-y3axis-label, .jqplot-y4axis-label, .jqplot-y5axis-label, .jqplot-y6axis-label, .jqplot-y7axis-label, .jqplot-y8axis-label, .jqplot-y9axis-label {
+/* text-align: center;*/
+ font-size: 11pt;
+ margin-left: 10px;
+ position: absolute;
+}
+
+.jqplot-meterGauge-tick {
+ font-size: 0.75em;
+ color: #999999;
+}
+
+.jqplot-meterGauge-label {
+ font-size: 1em;
+ color: #999999;
+}
+
+table.jqplot-table-legend {
+ margin-top: 12px;
+ margin-bottom: 12px;
+ margin-left: 12px;
+ margin-right: 12px;
+}
+
+table.jqplot-table-legend, table.jqplot-cursor-legend {
+ background-color: rgba(255,255,255,0.6);
+ border: 1px solid #cccccc;
+ position: absolute;
+ font-size: 0.75em;
+}
+
+td.jqplot-table-legend {
+ vertical-align:middle;
+}
+
+/*
+These rules could be used instead of assigning
+element styles and relying on js object properties.
+*/
+
+/*
+td.jqplot-table-legend-swatch {
+ padding-top: 0.5em;
+ text-align: center;
+}
+
+tr.jqplot-table-legend:first td.jqplot-table-legend-swatch {
+ padding-top: 0px;
+}
+*/
+
+td.jqplot-seriesToggle:hover, td.jqplot-seriesToggle:active {
+ cursor: pointer;
+}
+
+.jqplot-table-legend .jqplot-series-hidden {
+ text-decoration: line-through;
+}
+
+div.jqplot-table-legend-swatch-outline {
+ border: 1px solid #cccccc;
+ padding:1px;
+}
+
+div.jqplot-table-legend-swatch {
+ width:0px;
+ height:0px;
+ border-top-width: 5px;
+ border-bottom-width: 5px;
+ border-left-width: 6px;
+ border-right-width: 6px;
+ border-top-style: solid;
+ border-bottom-style: solid;
+ border-left-style: solid;
+ border-right-style: solid;
+}
+
+.jqplot-title {
+ top: 0px;
+ left: 0px;
+ padding-bottom: 0.5em;
+ font-size: 1.2em;
+}
+
+table.jqplot-cursor-tooltip {
+ border: 1px solid #cccccc;
+ font-size: 0.75em;
+}
+
+
+.jqplot-cursor-tooltip {
+ border: 1px solid #cccccc;
+ font-size: 0.75em;
+ white-space: nowrap;
+ background: rgba(208,208,208,0.5);
+ padding: 1px;
+}
+
+.jqplot-highlighter-tooltip, .jqplot-canvasOverlay-tooltip {
+ border: 1px solid #cccccc;
+ font-size: 0.75em;
+ white-space: nowrap;
+ background: rgba(208,208,208,0.5);
+ padding: 1px;
+}
+
+.jqplot-point-label {
+ font-size: 0.75em;
+ z-index: 2;
+}
+
+td.jqplot-cursor-legend-swatch {
+ vertical-align: middle;
+ text-align: center;
+}
+
+div.jqplot-cursor-legend-swatch {
+ width: 1.2em;
+ height: 0.7em;
+}
+
+.jqplot-error {
+/* Styles added to the plot target container when there is an error go here.*/
+ text-align: center;
+}
+
+.jqplot-error-message {
+/* Styling of the custom error message div goes here.*/
+ position: relative;
+ top: 46%;
+ display: inline-block;
+}
+
+div.jqplot-bubble-label {
+ font-size: 0.8em;
+/* background: rgba(90%, 90%, 90%, 0.15);*/
+ padding-left: 2px;
+ padding-right: 2px;
+ color: rgb(20%, 20%, 20%);
+}
+
+div.jqplot-bubble-label.jqplot-bubble-label-highlight {
+ background: rgba(90%, 90%, 90%, 0.7);
+}
+
+div.jqplot-noData-container {
+ text-align: center;
+ background-color: rgba(96%, 96%, 96%, 0.3);
+}
diff --git a/tipboard/static/css/layout.css b/tipboard/static/css/layout.css
new file mode 100644
index 0000000..511a64a
--- /dev/null
+++ b/tipboard/static/css/layout.css
@@ -0,0 +1,544 @@
+/* ---------------------------------------------------------------------------------------------------------
+
+ Project: Tipboard
+ Author: ...
+ Date: 2013/11/28
+
+------------------------------------------------------------------------------------------------------------- */
+
+html, body {
+ height: 100%;
+ font-size: 62.5%;
+}
+
+body {
+ background: #1c1d1f;
+ color: #c7cbd2;
+ font-family:'misolight',"Trebuchet MS", Arial, sans-serif;
+ min-width: 1024px;
+ width: 100%;
+ min-height: 768px;
+ -webkit-font-smoothing: antialiased;
+}
+
+h2 {
+ font-size: 2.4rem;
+ font-weight: normal;
+ line-height: 1.2em;
+ margin-bottom: 0.5em;
+ color: #fff;
+}
+
+h3 {
+ font-size: 1.6rem;
+}
+
+table {
+ font-size: 1.6rem;
+}
+
+/* ---------------------------------------------------------------------------------------------------------
+ LAYOUT
+------------------------------------------------------------------------------------------------------------- */
+
+.row {
+ clear: both;
+}
+
+.row:before, .row:after {
+ content:"";
+ display: table;
+}
+
+.row:after {
+ clear: both;
+}
+
+.col {
+ display: block;
+ float: left;
+ height: 100%;
+ position: relative;
+ /* 3d animation broken in firefox
+ perspective: 600px;
+ -webkit-perspective: 600px;
+ */
+}
+
+.col_5_of_5 { width: 100%; }
+.col_4_of_5 { width: 80%; }
+.col_3_of_5 { width: 60%; }
+.col_2_of_5 { width: 40%; }
+.col_1_of_5 { width: 20%; }
+.col_4_of_4 { width: 100%; }
+.col_3_of_4 { width: 75%; }
+.col_2_of_4 { width: 50%; }
+.col_1_of_4 { width: 25%; }
+.col_3_of_3 { width: 100%; }
+.col_2_of_3 { width: 66.6666%; }
+.col_1_of_3 { width: 33.3333%; }
+.col_1_of_2 { width: 50%; }
+.col_1_of_1 { width: 100%; }
+.row_3_of_3 { height: 100%; }
+.row_2_of_3 { height: 66.6666%; }
+.row_1_of_3 { height: 33.3333%; }
+.row_2_of_2 { height: 100%; }
+.row_1_of_2 { height: 50%; }
+.col_15_of_5 { width: 30%; }
+.w80 { width: 80%; }
+
+/* ---------------------------------------------------------------------------------------------------------
+ COMPONENTS
+------------------------------------------------------------------------------------------------------------- */
+
+.tile {
+ overflow: hidden;
+ background-color: #25282d;
+ width: 96%;
+ height: 96%;
+ position: absolute;
+ top: 2%;
+ left: 2%;
+}
+
+.tile > .tile-header {
+ height: 36px;
+ line-height: 36px;
+ text-transform: uppercase;
+ border-bottom: 1px solid #4a4d52;
+ padding: 0 10px;
+ margin: 0 0 10px 0;
+}
+
+.tile > .tile-header h3 {
+ overflow: hidden;
+ width: 90%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 1.6rem;
+}
+
+.tile > .tile-header > .flip-tile-counter {
+ float: left;
+}
+
+.tile > .tile-header > .tile-counter {
+ font-size: 1.6rem;
+ float: right;
+ color:#666;
+}
+
+.tile > .tile-content {
+ padding: 0 10px;
+}
+
+.row_1_of_3 .tile > .tile-content > h2 {
+ font-size: 2.5rem;
+ margin-bottom: 0;
+ padding-bottom: 1%;
+}
+.tile > .tile-content > h2.fixed-height{
+ height: 12%;
+ margin-bottom: 0;
+ padding-bottom: 6%;
+}
+.row_1_of_3 .tile > .tile-content > h2.fixed-height{
+ font-size: 2.5rem;
+ height: 10%;
+ margin-bottom: 0;
+ padding-bottom: 2%;
+}
+
+.tile > .tile-content > h3.fixed-height{
+ height: 8%;
+ margin-bottom: 0;
+}
+
+.row_1_of_3 .tile > .tile-content > h3.fixed-height{
+ height: 7%;
+ margin-bottom: 0;
+}
+
+.tile > .tile-content .result {
+ font-size: 1.6rem;
+ margin-bottom: 0.25em;
+ color: #fff;
+}
+
+.tile > .tile-content .label {
+ text-transform: uppercase;
+ color: #B7B7B7;
+}
+
+.tile > .tile-content .with-arrow-result {
+ line-height: 1.6em;
+}
+
+.tile > .tile-content .big-result {
+ font-size: 5rem;
+ font-weight: normal;
+}
+
+.tile > .tile-content .small-padding {
+ margin-bottom: 0;
+ line-height: 1em;
+}
+
+.tile > .tile-content .medium-result {
+ font-size: 2.5rem;
+}
+
+.tile > .tile-content .highlighted-result.big-result {
+ font-size: 10rem;
+ margin-bottom: 20px;
+}
+
+.tile > .tile-content .highlighted-result {
+ display: block;
+ width: 100%;
+ text-align: center;
+}
+
+.tile > .tile-content .highlighted-red {
+ background-color: #8a1f11;
+}
+
+.tile > .tile-content .highlighted-green {
+ background-color: #66c95d;
+}
+
+.tile > .tile-content .float-right {
+ float: right;
+}
+
+.tile > .tile-content .mb-1em {
+ margin-bottom: 1em;
+}
+
+.tile > .tile-content > .result-row {
+ clear: both;
+ width: 100%;
+}
+
+.tile > .tile-content > .result-row > .result-col {
+ float: left;
+ width: 45%;
+}
+
+.tile > .tile-content > .result-row > .with-right-border {
+ border-right: 2px solid #313232;
+ margin-right: 10px;
+}
+
+.text-container {
+ font-size: 2.6rem;
+}
+
+.arrow {
+ display: block;
+ width: 0;
+ height: 0;
+ float: left;
+ margin-right: 10px;
+ border-style: solid;
+}
+
+.arrow-up {
+ border-width: 0 22.5px 35px 22.5px;
+ border-color: transparent transparent #5da400 transparent;
+}
+
+.arrow-down {
+ border-width: 35px 22.5px 0 22.5px;
+ border-color: #8a1f11 transparent transparent transparent;
+}
+.centerbox {
+ /* basic styling */
+ height: 100%;
+ width: 100%;
+
+ /* flexbox, por favor */
+ display: -webkit-box;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ -webkit-box-align: center;
+
+ display: -moz-box;
+ -moz-box-orient: horizontal;
+ -moz-box-pack: center;
+ -moz-box-align: center;
+
+ display: box;
+ box-orient: horizontal;
+ box-pack: center;
+ box-align: center;
+}
+
+.highlighter-tile > .tile-content > h3 {
+ padding-bottom: 4%;
+}
+
+.highlighter-tile > .tile-content > .big-value-container {
+ margin-bottom: 0;
+ height: 40%;
+ width: 100%;
+
+ /* Internet Explorer 10 */
+ display:-ms-flexbox;
+ -ms-flex-pack:center;
+ -ms-flex-align:center;
+
+ /* Firefox */
+ display:-moz-box;
+ -moz-box-pack:center;
+ -moz-box-align:center;
+
+ /* Safari, Opera, and Chrome */
+ display:-webkit-box;
+ -webkit-box-pack:center;
+ -webkit-box-align:center;
+
+ /* W3C */
+ display:box;
+ box-pack:center;
+ box-align:center;
+}
+
+.highlighter-tile .big-value-container > span {
+ font-size: 13.0rem;
+}
+
+.error-wrapper {
+ display: -webkit-flex;
+ display: -moz-flex;
+ display: -ms-flex;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ -webkit-flex-direction: column;
+ -moz-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ justify-content: center;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.4);
+}
+
+.error-message {
+ padding: 1em;
+ text-align: center;
+ color: #fff;
+ font-family: 'Roboto Slab',serif;
+ font-size: 20px;
+}
+
+.exception-message {
+ white-space: normal !important;
+ font-size: 3.2rem;
+ color: #DC5945;
+}
+
+.row_1_of_3 .highlighter-tile .big-value-container > span {
+ font-size: 7.9rem;
+}
+
+.round-legend .jqplot-table-legend {
+ padding-right: 5px;
+}
+
+.round-legend span.jqplot-data-label {
+ font-size: 1.7em;
+}
+
+.round-legend .jqplot-table-legend-swatch {
+ -moz-border-radius: 50%;
+ border-radius: 50%;
+}
+/* ---------------------------------------------------------------------------------------------------------
+ TILES
+------------------------------------------------------------------------------------------------------------- */
+
+
+
+@keyframes fading-background-color {
+ 100% {
+ background-color: transparent;
+ }
+}
+
+@-webkit-keyframes fading-background-color {
+ 100% {
+ background-color: transparent;
+ }
+}
+
+.fading-background-color {
+ animation-name: fading-background-color;
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+
+ -webkit-animation-name: fading-background-color;
+ -webkit-animation-duration: 1s;
+ -webkit-animation-iteration-count: infinite;
+ -webkit-animation-direction: alternate;
+}
+
+
+
+
+/* ---------------------------------------------------------------------------------------------------------
+ FLIP
+------------------------------------------------------------------------------------------------------------- */
+
+@keyframes flip {
+ 0% {
+ transform: rotateX(90deg);
+ }
+ 100% {
+ transform: rotateX(0deg);
+ }
+}
+
+@-webkit-keyframes flip {
+ 0% {
+ -webkit-transform: rotateX(90deg);
+ }
+ 100% {
+ -webkit-transform: rotateX(0deg);
+ }
+}
+
+.flippable {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+}
+
+.flippedforward {
+ opacity: 1;
+ display: block;
+ animation-name: flip;
+ animation-duration: 1s;
+ animation-iteration-count: 1;
+ -webkit-animation-name: flip;
+ -webkit-animation-duration: 1s;
+ -webkit-animation-iteration-count: 1;
+}
+
+/* ---------------------------------------------------------------------------------------------------------
+ FLIPBOARD
+------------------------------------------------------------------------------------------------------------- */
+body.flipboard {
+ overflow: hidden;
+}
+
+iframe {
+ height: 100%;
+ width: 100%;
+ border: 0;
+}
+
+@keyframes fadeIn-anim {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@-webkit-keyframes fadeIn-anim {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+
+.fadeIn {
+ opacity: 1;
+ animation-name: fadeIn-anim;
+ animation-duration: 1s;
+ animation-iteration-count: 1;
+ animation-timing-function: linear;
+ -webkit-animation-name: fadeIn-anim;
+ -webkit-animation-duration: 1s;
+ -webkit-animation-iteration-count: 1;
+ -webkit-animation-timing-function: linear;
+}
+
+
+/* ---------------------------------------------------------------------------------------------------------
+ MEDIA QUERIES
+------------------------------------------------------------------------------------------------------------- */
+
+@media only screen and (max-width: 1024px) {
+ body {
+ }
+ .tile > .tile-header h3 {}
+ .tile > .tile-content .title {}
+ .tile > .tile-content .subtitle {}
+ .tile > .tile-content .big-result {}
+
+}
+@media only screen and (min-width: 1025px) and (max-width: 1260px) {
+ body {
+ }
+}
+
+/* ---------------------------------------------------------------------------------------------------------
+ ASSETS
+------------------------------------------------------------------------------------------------------------- */
+
+@font-face {
+ font-family: 'misolight';
+ src: url('/static/fonts/miso-light-webfont.eot');
+ src: url('/static/fonts/miso-light-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/static/fonts/miso-light-webfont.woff') format('woff'),
+ url('/static/fonts/miso-light-webfont.ttf') format('truetype'),
+ url('/static/fonts/miso-light-webfont.svg#misolight') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'misoregular';
+ src: url('/static/fonts/miso-webfont.eot');
+ src: url('/static/fonts/miso-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/static/fonts/miso-webfont.woff') format('woff'),
+ url('/static/fonts/miso-webfont.ttf') format('truetype'),
+ url('/static/fonts/miso-webfont.svg#misoregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+.jqplot-point-label {
+ color: white;
+ font: 14px/1 'misolight',"Trebuchet MS", Arial, sans-serif;
+}
+
+.jqplot-data-label {
+ color: white;
+ font: 14px/1 'misolight',"Trebuchet MS", Arial, sans-serif;
+}
+
+.jqplot-table-legend {
+ font: 14px/1 'misolight',"Trebuchet MS", Arial, sans-serif;
+ text-transform: uppercase;
+ background: #25282D;
+}
+
+div.jqplot-table-legend-swatch-outline {
+ border: 0 none;
+}
+
+.jqplot-axis {
+ font: 14px/1 'misolight',"Trebuchet MS", Arial, sans-serif;
+}
diff --git a/tipboard/static/css/reset.css b/tipboard/static/css/reset.css
new file mode 100644
index 0000000..3c3fa39
--- /dev/null
+++ b/tipboard/static/css/reset.css
@@ -0,0 +1,2 @@
+/* Eric Meyer's Reset CSS v2.0 - http://cssreset.com */
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
diff --git a/tipboard/static/fonts/MISO_LICENSE b/tipboard/static/fonts/MISO_LICENSE
new file mode 100644
index 0000000..918a01b
--- /dev/null
+++ b/tipboard/static/fonts/MISO_LICENSE
@@ -0,0 +1,41 @@
+Mårten Nettelbladt - Miso License
+
+M M I SSS OOO
+ MM MM I S S O O
+ M M M M I S O O
+ M M M I S O O
+ M M I S O O
+ M M I S S O O
+ M M I SSS OOO
+———————————————————-
+MISO is an architectural lettering font
+completed in 2006 by Mårten Nettelbladt.
+———————————————————-
+MISO is available in three weights
+(Light, Regular, Bold)
+in TrueType format for Windows.
+———————————————————-
+
+L I C E N S E I N F O R M A T I O N
+———————————————————-
+You can use MISO for free.
+You can share MISO with your friends
+as long as you include this text file.
+You are NOT permitted to sell MISO.
+
+———————————————————-
+If you have any comments about MISO
+please let me know!
+
+miso (a) omkrets.se
+———————————————————-
+October 23rd 2006
+Mårten Nettelbladt
+Omkrets arkitektur
+
+June 24th 2007
+Some small adjustments
+
+November 27th 2008
+Open Type format added.
+Thanks to Torin Hill!
diff --git a/tipboard/static/fonts/miso-light-webfont.eot b/tipboard/static/fonts/miso-light-webfont.eot
new file mode 100644
index 0000000..ab99d57
--- /dev/null
+++ b/tipboard/static/fonts/miso-light-webfont.eot
Binary files differ
diff --git a/tipboard/static/fonts/miso-light-webfont.svg b/tipboard/static/fonts/miso-light-webfont.svg
new file mode 100644
index 0000000..9394086
--- /dev/null
+++ b/tipboard/static/fonts/miso-light-webfont.svg
@@ -0,0 +1,420 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="misolight" horiz-adv-x="851" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="393" />
+<glyph horiz-adv-x="2048" />
+<glyph horiz-adv-x="2048" />
+<glyph unicode="&#xd;" horiz-adv-x="2048" />
+<glyph unicode=" " horiz-adv-x="393" />
+<glyph unicode="&#x09;" horiz-adv-x="393" />
+<glyph unicode="&#xa0;" horiz-adv-x="393" />
+<glyph unicode="!" horiz-adv-x="409" d="M145 78q0 25 17.5 42t42.5 17t43 -17t18 -42t-18 -42.5t-43 -17.5t-42.5 17.5t-17.5 42.5zM166 385v819q0 17 11 28t28 11t29 -11t12 -28v-819q0 -17 -12 -28t-29 -11t-28 11t-11 28z" />
+<glyph unicode="&#x22;" horiz-adv-x="720" d="M158 1040v205q0 39 39 39t39 -39v-205q0 -39 -39 -39t-39 39zM485 1040v205q0 39 39 39t39 -39v-205q0 -39 -39 -39t-39 39z" />
+<glyph unicode="#" horiz-adv-x="937" d="M61 508q0 17 11 28t28 11h166v250h-166q-17 0 -28 11t-11 28t11 27.5t28 10.5h166v371q0 17 11 28t28 11q39 0 39 -39v-371h250v371q0 17 11 28t28 11q39 0 39 -39v-371h166q39 0 39 -38q0 -39 -39 -39h-166v-250h166q39 0 39 -39t-39 -39h-166v-412q0 -39 -39 -39 q-17 0 -28 11t-11 28v412h-250v-412q0 -39 -39 -39q-17 0 -28 11t-11 28v412h-166q-17 0 -28 11t-11 28zM344 547h250v250h-250v-250z" />
+<glyph unicode="$" d="M174 236q-15 35 14 55q13 8 30 3t24 -18q50 -82 135 -94q28 -4 33 -4h2q72 0 125 47q70 58 67 150q0 93 -72 147l-256 211q-95 74 -98 203q0 122 94 203q57 48 131 61v127q0 17 12 28t29 11t28 -11t11 -28v-127q1 0 3 -1t3 -1q123 -23 183 -127q20 -33 -15 -55 q-29 -16 -53 14q-41 78 -129 90q-28 4 -31 4h-2q-67 0 -118 -45q-69 -56 -66 -143q0 -85 68 -141l258 -211q97 -78 100 -209q0 -131 -96 -209q-57 -51 -137 -64v-127q0 -17 -11 -27.5t-28 -10.5q-18 0 -29.5 10.5t-11.5 27.5v127q-4 0 -6 2q-129 23 -191 132z" />
+<glyph unicode="%" horiz-adv-x="1214" d="M100 1061q0 92 65 156.5t157 64.5t156.5 -64.5t64.5 -156.5t-64.5 -156.5t-156.5 -64.5t-157 64.5t-65 156.5zM174 1061q0 -61 43 -104.5t105 -43.5q61 0 104 43t43 105q0 61 -42.5 104t-104.5 43t-105 -43t-43 -104zM283.5 45q-4.5 16 3.5 29l573 1188q6 17 22 21.5 t29 -3.5q17 -6 22 -22t-3 -29l-574 -1188q-6 -17 -22 -21.5t-29 3.5q-17 6 -21.5 22zM674 242q0 92 64.5 156.5t156.5 64.5t156.5 -64.5t64.5 -156.5t-64.5 -157t-156.5 -65t-156.5 65t-64.5 157zM748 242q0 -62 43 -105t104 -43t104 43t43 105t-43 104.5t-104 42.5 t-104 -42.5t-43 -104.5z" />
+<glyph unicode="&#x26;" horiz-adv-x="1009" d="M129 258q-21 118 55 219l176 230l-151 196l-2 2q-38 59 -40.5 127t30.5 127q36 61 95.5 95t127.5 30q67 -1 124.5 -39t86.5 -100q29 -63 21.5 -130.5t-48.5 -123.5l-143 -184l168 -218l76 -98l81 137q8 14 24 18.5t30 -5.5q13 -7 17.5 -24t-3.5 -30l-98 -163l170 -222 q10 -14 8 -29t-14 -26q-12 -10 -28.5 -8t-27.5 14l-151 199l-58 -96q-64 -107 -184 -131t-221 43q-103 71 -121 190zM207 270q12 -85 88 -137q72 -52 160 -33q82 18 133 97l71 120l-94 123l-153 201l-164 -211q-56 -75 -41 -160zM244.5 1033.5q1.5 -45.5 27.5 -83.5 l140 -180l129 170q29 37 34.5 83t-14.5 87q-20 43 -59 69t-86 27t-87 -22t-63 -64t-21.5 -86.5z" />
+<glyph unicode="'" horiz-adv-x="393" d="M156 1040v205q0 17 12 28t29 11t28 -11t11 -28v-205q0 -17 -11 -28t-28 -11t-29 11t-12 28z" />
+<glyph unicode="(" horiz-adv-x="548" d="M141 651q0 210 68 408q66 202 201 375q11 12 27 14t28 -8q14 -11 16 -27.5t-8 -28.5q-125 -158 -190 -350q-64 -180 -64 -383t64 -383q65 -195 190 -350q10 -14 8 -29t-16 -26q-12 -10 -28 -8t-27 14q-135 173 -201 375q-68 198 -68 407z" />
+<glyph unicode=")" horiz-adv-x="550" d="M68 -90.5q-2 15.5 8 29.5q125 155 190 350q64 192 64 383t-64 383q-65 192 -190 350q-10 12 -8 28t14 27q14 10 29 8t26 -14q135 -176 203 -375q68 -198 68 -407q0 -210 -68 -408q-68 -199 -203 -375q-11 -12 -26 -14t-29 8q-12 11 -14 26.5z" />
+<glyph unicode="*" d="M123.5 794.5q4.5 15.5 17.5 22.5l211 121l-211 121q-13 7 -17.5 22.5t3.5 28.5q7 13 23 17.5t28 -3.5l211 -120v241q0 37 37 37t37 -37v-241l211 120q13 8 28.5 3.5t22.5 -17.5q8 -13 3.5 -28.5t-17.5 -22.5l-209 -121l209 -121q13 -7 17.5 -22.5t-3.5 -28.5 q-7 -13 -22.5 -17.5t-28.5 3.5l-211 120v-241q0 -37 -37 -37t-37 37v241l-211 -120q-12 -8 -28 -3.5t-23 17.5q-8 13 -3.5 28.5z" />
+<glyph unicode="+" d="M141 590q0 39 39 39h207v207q0 38 39 38q17 0 28 -10.5t11 -27.5v-207h207q17 0 28 -11t11 -28t-11 -28t-28 -11h-207v-207q0 -17 -11 -28t-28 -11q-39 0 -39 39v207h-207q-39 0 -39 39z" />
+<glyph unicode="," horiz-adv-x="393" d="M127 98q0 37 37 37h65q37 0 37 -37v-98q0 -44 -23.5 -80t-64.5 -53q-13 -6 -28 0t-21 20q-6 13 0.5 28.5t20.5 21.5q43 15 43 63v61h-29q-37 0 -37 37z" />
+<glyph unicode="-" d="M139 590q0 17 12 28t29 11h492q17 0 28 -11t11 -28t-11 -28t-28 -11h-492q-17 0 -29 11t-12 28z" />
+<glyph unicode="." horiz-adv-x="393" d="M137 78q0 25 17.5 42t42.5 17t42 -17t17 -42t-17 -42.5t-42 -17.5t-42.5 17.5t-17.5 42.5z" />
+<glyph unicode="/" d="M143 -10l492 1351q6 14 20.5 21.5t30.5 1.5q36 -13 23 -51l-492 -1352q-4 -17 -18 -22.5t-31 0.5q-35 12 -25 51z" />
+<glyph unicode="0" d="M139 303v696q0 118 84 201.5t203 83.5q117 0 201 -83.5t84 -201.5v-696q0 -118 -84 -201.5t-201 -83.5q-119 0 -203 83.5t-84 201.5zM219 303q0 -86 60.5 -146.5t146.5 -60.5q85 0 145 60.5t60 146.5v696q0 86 -60 146.5t-145 60.5q-86 0 -146.5 -60.5t-60.5 -146.5v-696 z" />
+<glyph unicode="1" d="M201 971q-25 28 0 57l245 246q21 18 43 8q25 -9 25 -37v-1188q0 -17 -11 -28t-28 -11q-39 0 -39 39v1094l-180 -180q-10 -10 -27 -10q-15 0 -28 10z" />
+<glyph unicode="2" d="M154 45q-9 18 0 39l430 819q40 75 14 156q-21 80 -98 121q-69 40 -154 18q-78 -21 -123 -96q-11 -14 -26 -18t-29 3q-29 21 -14 56q59 96 170 131q112 34 215 -25q100 -59 135 -168q34 -107 -21 -215q-34 -64 -93.5 -178.5t-86.5 -165.5q-38 -74 -89.5 -171.5 t-85.5 -161.5t-44 -85h393q17 0 28 -10.5t11 -27.5t-11 -28t-28 -11h-459q-22 0 -34 18z" />
+<glyph unicode="3" d="M125 160q-15 35 14 55q35 15 56 -14q35 -62 99.5 -89t131.5 -8q68 17 111 72t43 127v123q0 86 -60.5 146.5t-146.5 60.5q-39 0 -39 39t39 39q86 0 146.5 60.5t60.5 146.5v81q0 69 -43.5 125.5t-110.5 73.5q-68 19 -132 -8t-99 -88q-21 -30 -56 -15q-29 21 -14 56 q49 85 138 121t183 10q94 -25 152.5 -101t58.5 -174v-81q0 -77 -38.5 -143t-102.5 -103q64 -37 102.5 -103t38.5 -143v-123q0 -98 -58.5 -174t-152.5 -102q-94 -25 -183 11.5t-138 121.5z" />
+<glyph unicode="4" d="M115 356l286 901q4 17 18 23.5t32 1.5q26 -10 26 -37q0 -10 -2 -12q-36 -109 -125 -391q-28 -86 -61.5 -193.5t-55.5 -177.5t-28 -88h317v248q0 39 39 39t39 -39v-248h125q39 0 39 -39t-39 -39h-125v-248q0 -39 -39 -39t-39 39v248h-370q-19 0 -33 17q-10 16 -4 34z" />
+<glyph unicode="5" d="M172 651v586q0 17 11 28t28 11h426q39 0 39 -39t-39 -39h-387v-444q114 87 248 49q94 -25 152.5 -101t58.5 -174v-225q0 -98 -58.5 -174t-152.5 -102q-94 -25 -183.5 12t-136.5 121q-20 33 15 55q29 16 53 -14q35 -62 99.5 -89t131.5 -8q68 17 111 72t43 127v225 q0 69 -43.5 125.5t-110.5 73.5q-68 19 -132 -8t-99 -88q-19 -25 -45 -19q-29 10 -29 39z" />
+<glyph unicode="6" d="M160 303v696q0 98 58.5 174t152.5 101q94 26 183 -10t138 -121q15 -35 -14 -56q-14 -7 -29 -3t-26 18q-35 61 -99.5 88t-132.5 8q-67 -17 -110 -73.5t-43 -125.5v-294q88 88 206 88t201.5 -83.5t83.5 -201.5v-205q0 -119 -83.5 -202t-201.5 -83t-201 83t-83 202zM238 303 q0 -86 60.5 -146.5t145.5 -60.5q86 0 146.5 60.5t60.5 146.5v205q0 86 -60.5 146.5t-146.5 60.5q-85 0 -145.5 -60.5t-60.5 -146.5v-205z" />
+<glyph unicode="7" d="M135 1073v164q0 17 11 28t28 11h492q22 0 30 -14q12 -15 9 -35l-287 -1180q-5 -17 -17.5 -25t-29.5 -4q-31 6 -31 39q0 7 2 9q60 248 129 532q131 542 145 600h-403v-125q0 -39 -39 -39q-17 0 -28 11t-11 28z" />
+<glyph unicode="8" d="M141 303v123q0 77 39 143t103 103q-64 37 -103 103t-39 143v81q0 118 84 201.5t201 83.5q119 0 203 -83.5t84 -201.5v-81q0 -77 -39 -143t-103 -103q64 -37 103 -103t39 -143v-123q0 -118 -84 -201.5t-203 -83.5q-117 0 -201 83.5t-84 201.5zM221 303q0 -86 60 -146.5 t145 -60.5q86 0 146.5 60.5t60.5 146.5v123q0 86 -60.5 146.5t-146.5 60.5q-85 0 -145 -60.5t-60 -146.5v-123zM221 918q0 -86 60 -146.5t145 -60.5q86 0 146.5 60.5t60.5 146.5v81q0 86 -60.5 146.5t-146.5 60.5q-85 0 -145 -60.5t-60 -146.5v-81z" />
+<glyph unicode="9" d="M123 795v204q0 118 84 201.5t201 83.5q119 0 202.5 -83.5t83.5 -201.5v-696q0 -98 -59.5 -174t-153.5 -102q-93 -25 -182.5 12t-136.5 121q-15 35 14 55q13 8 29.5 3.5t23.5 -17.5q35 -62 100 -89t132 -8q68 17 110.5 72t42.5 127v295q-82 -88 -206 -88q-117 0 -201 83.5 t-84 201.5zM203 795q0 -86 60 -146.5t145 -60.5q86 0 146 60.5t60 146.5v204q0 86 -60 146.5t-146 60.5q-85 0 -145 -60.5t-60 -146.5v-204z" />
+<glyph unicode=":" horiz-adv-x="393" d="M137 78q0 25 17.5 42t42.5 17q26 0 43.5 -17t17.5 -42t-17.5 -42.5t-43.5 -17.5q-25 0 -42.5 17.5t-17.5 42.5zM137 856q0 25 17.5 42t42.5 17q26 0 43.5 -17t17.5 -42t-17.5 -42t-43.5 -17q-25 0 -42.5 17t-17.5 42z" />
+<glyph unicode=";" horiz-adv-x="393" d="M127 98q0 37 37 37h65q37 0 37 -37v-98q0 -44 -23.5 -80t-64.5 -53q-13 -6 -28 0t-21 20q-6 13 0.5 28.5t20.5 21.5q43 15 43 63v61h-29q-37 0 -37 37zM137 856q0 25 17.5 42t42.5 17t42 -17t17 -42t-17 -42t-42 -17t-42.5 17t-17.5 42z" />
+<glyph unicode="&#x3c;" d="M125 590q0 19 12 31l492 409q14 10 29 8t26 -14q10 -10 10 -25q0 -19 -14 -30l-455 -379l455 -379q14 -11 14 -31q0 -14 -10 -24q-11 -12 -26 -14t-29 8l-492 409q-12 12 -12 31z" />
+<glyph unicode="=" d="M141 426q0 17 11 28t28 11h492q39 0 39 -39t-39 -39h-492q-17 0 -28 11t-11 28zM141 754q0 17 11 28t28 11h492q39 0 39 -39t-39 -39h-492q-17 0 -28 11t-11 28z" />
+<glyph unicode="&#x3e;" d="M158 180q0 20 14 31l455 379l-455 379q-14 11 -14 30q0 17 8 25q11 12 27 14t28 -8l492 -409q14 -11 14 -31t-14 -31l-492 -409q-12 -10 -28 -8t-27 14q-8 8 -8 24z" />
+<glyph unicode="?" horiz-adv-x="731" d="M90 1143q31 54 81 89t110 46q126 25 229 -51q102 -79 111 -207q9 -127 -82 -221l-103 -105q-59 -59 -59 -145v-164q0 -17 -12 -28t-29 -11t-28 11t-11 28v164q0 119 82 201l104 104q69 72 60 160q-10 95 -82 149q-67 56 -164 39q-91 -22 -139 -100q-7 -13 -24 -18t-30 3 q-33 25 -14 56zM276 78q0 25 17.5 42t42.5 17t43 -17t18 -42t-18 -42.5t-43 -17.5t-42.5 17.5t-17.5 42.5z" />
+<glyph unicode="@" horiz-adv-x="1564" d="M152 283v368q0 262 186 445q183 186 444 186q264 0 447 -186q184 -184 184 -445v-381q0 -102 -71.5 -171.5t-170.5 -69.5h-409q-99 0 -170.5 69.5t-71.5 171.5q0 98 72 170t170 72h168v164q0 49 -28.5 90t-76.5 61q-47 19 -96 9t-86 -45q-11 -11 -27 -11q-13 0 -24 11 q-10 10 -10 26q0 17 10 27q55 52 124.5 66t137.5 -15q68 -30 109 -89t41 -130v-574h167q72 0 120 48t48 120v381q0 111 -41.5 213t-119.5 180q-80 79 -182 121.5t-214 42.5q-110 0 -212.5 -43t-180.5 -121t-121 -180.5t-43 -212.5v-368q0 -112 42.5 -214t121.5 -182 q78 -78 180 -119.5t213 -41.5h226q16 0 26 -11t10 -26q0 -37 -36 -37h-226q-260 0 -444 184q-186 183 -186 447zM594 270q0 -71 49 -119.5t119 -48.5h168v336h-168q-69 0 -118.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="A" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q2 -7 2 -14q-1 -10 -5 -18q-8 -14 -24 -18q-6 -1 -11 -1q-11 0 -19 5q-14 8 -19 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25q-9 -5 -19 -5 q-5 0 -11 1q-17 4 -25 18q-5 9 -5 19q0 6 2 13zM246 383h393q-153 547 -197 715q-62 -235 -196 -715z" />
+<glyph unicode="B" horiz-adv-x="876" d="M166 66v1171q0 39 39 39h246q118 0 201 -83t83 -202v-73q0 -77 -38.5 -143t-102.5 -103q64 -37 102.5 -103t38.5 -143v-115q0 -119 -82.5 -201.5t-201.5 -82.5h-246q-39 0 -39 39zM244 104h207q85 0 145.5 60.5t60.5 146.5v115q0 86 -60.5 146.5t-145.5 60.5h-207v-529z M244 711h207q85 0 145.5 60.5t60.5 146.5v73q0 86 -60.5 146.5t-145.5 60.5h-207v-487z" />
+<glyph unicode="C" horiz-adv-x="788" d="M141 303v696q0 98 58.5 174t152.5 101q39 11 78 11q54 0 106 -21q90 -36 136 -121q8 -13 8 -23q0 -19 -23 -33q-13 -8 -29.5 -3t-23.5 18q-35 61 -99 88q-38 16 -77 16q-27 0 -55 -8q-67 -17 -110.5 -73.5t-43.5 -125.5v-696q0 -72 43 -127t111 -72q28 -8 55 -8 q38 0 76 16q64 27 100 89q7 13 23.5 17.5t29.5 -3.5q22 -14 22 -32q0 -11 -7 -23q-46 -84 -136 -121q-53 -22 -109 -22q-37 0 -75 10q-94 26 -152.5 102t-58.5 174z" />
+<glyph unicode="D" horiz-adv-x="876" d="M166 66v1171q0 39 39 39h246q118 0 201 -83t83 -202v-680q0 -119 -82.5 -201.5t-201.5 -82.5h-246q-39 0 -39 39zM244 104h207q86 0 146 60.5t60 146.5v680q0 86 -60 146.5t-146 60.5h-207v-1094z" />
+<glyph unicode="E" horiz-adv-x="753" d="M166 66v1171q0 39 39 39h409q39 0 39 -39t-39 -39h-370v-487h329q39 0 39 -39t-39 -39h-329v-529h370q39 0 39 -38q0 -39 -39 -39h-409q-39 0 -39 39z" />
+<glyph unicode="F" horiz-adv-x="737" d="M166 66v1171q0 17 11 28t28 11h409q39 0 39 -39t-39 -39h-370v-487h329q39 0 39 -39t-39 -39h-329v-567q0 -39 -39 -39q-17 0 -28 11t-11 28z" />
+<glyph unicode="G" horiz-adv-x="843" d="M141 303v696q0 98 59 174t152 101q39 11 78 11q54 0 106 -21q89 -36 138 -121q15 -35 -15 -56q-13 -8 -29.5 -3t-23.5 18q-35 61 -100 88q-38 16 -77 16q-28 0 -56 -8q-67 -17 -110.5 -73.5t-43.5 -125.5v-696q0 -86 61 -146.5t146 -60.5q86 0 146.5 60.5t60.5 146.5v289 h-166q-17 0 -28 11t-11 28t11 28t28 11h205q39 0 39 -39v-328q0 -119 -83.5 -202t-201.5 -83q-117 0 -201 83.5t-84 201.5z" />
+<glyph unicode="H" horiz-adv-x="901" d="M166 57v1188q0 17 11 28t28 11t29 -11t12 -28v-534h411v534q0 17 11 28t28 11t29 -11t12 -28v-1188q0 -17 -12 -28t-29 -11t-28 11t-11 28v576h-411v-576q0 -17 -12 -28t-29 -11t-28 11t-11 28z" />
+<glyph unicode="I" horiz-adv-x="409" d="M166 57v1188q0 17 11 28t28 11t29 -11t12 -28v-1188q0 -17 -12 -28t-29 -11t-28 11t-11 28z" />
+<glyph unicode="J" horiz-adv-x="681" d="M82 111q7 13 23.5 17.5t29.5 -3.5q50 -29 103 -29q54 0 104 29q48 30 75 76.5t27 101.5v942q0 17 12 28t29 11t28 -11t11 -28v-942q0 -78 -38.5 -144t-104.5 -104q-64 -37 -143 -37q-78 0 -142 37q-19 14 -19 34q0 10 5 22z" />
+<glyph unicode="K" horiz-adv-x="862" d="M166 57v1188q0 17 11 28t28 11t29 -11t12 -28v-624l461 647q11 14 26 17q3 1 7 1q11 0 22 -8q12 -11 15 -27q1 -4 1 -7q0 -12 -8 -21l-328 -461l332 -688q3 -7 3 -14q-1 -7 -3 -15q-6 -16 -18 -22q-13 -6 -23 -6q-21 0 -31 24l-311 647l-145 -203v-428q0 -17 -12 -28 t-29 -11t-28 11t-11 28z" />
+<glyph unicode="L" horiz-adv-x="712" d="M164 66v1179q0 17 12 28t29 11t28 -11t11 -28v-1141h370q17 0 28 -10.5t11 -27.5t-11 -28t-28 -11h-409q-17 0 -29 11t-12 28z" />
+<glyph unicode="M" horiz-adv-x="1064" d="M166 57v1188q0 33 31 39q5 1 9 1q21 0 34 -23q25 -52 67 -141.5t58 -123.5q24 -52 63.5 -135.5t65.5 -138.5t38 -82q28 61 166 356q16 34 58 123.5t67 141.5q12 23 35 23q5 0 10 -1q31 -6 31 -39v-1188q0 -17 -11 -28t-28 -11q-39 0 -39 39v1012q-109 -223 -162 -340 l-92 -197q-9 -22 -35 -22q-23 0 -36 22l-93 197q-88 195 -159 340v-1012q0 -17 -11 -28t-28 -11q-39 0 -39 39z" />
+<glyph unicode="N" horiz-adv-x="983" d="M166 57v1188q0 33 31 39q5 1 9 1q21 0 34 -23q175 -365 206 -428q223 -469 293 -605v1016q0 39 39 39t39 -39v-1188q0 -33 -31 -39q-5 -1 -9 -1q-21 0 -34 24q-175 365 -206 428q-63 131 -169 349t-124 255v-1016q0 -39 -39 -39t-39 39z" />
+<glyph unicode="O" d="M141 303v696q0 119 83.5 202t201.5 83t201.5 -83t83.5 -202v-696q0 -119 -83.5 -202t-201.5 -83t-201.5 83t-83.5 202zM219 303q0 -86 60.5 -146.5t146.5 -60.5t146.5 60.5t60.5 146.5v696q0 86 -60.5 146.5t-146.5 60.5t-146.5 -60.5t-60.5 -146.5v-696z" />
+<glyph unicode="P" horiz-adv-x="819" d="M166 57v1180q0 39 39 39h246q118 0 201 -83t83 -202v-114q0 -119 -83 -202t-201 -83h-207v-535q0 -39 -39 -39t-39 39zM244 670h207q86 0 146 60.5t60 146.5v114q0 86 -60 146.5t-146 60.5h-207v-528z" />
+<glyph unicode="Q" d="M141 303v696q0 119 83.5 202t201.5 83t201.5 -83t83.5 -202v-696q0 -134 -109 -225l105 -180q4 -9 4 -18q0 -6 -1 -12q-4 -15 -18 -26q-8 -5 -18 -5q-6 0 -12 1q-16 5 -23 21l-107 180q-46 -21 -106 -21q-118 0 -201.5 83t-83.5 202zM219 303q0 -86 61 -146.5t146 -60.5 q86 0 146.5 60.5t60.5 146.5v696q0 86 -60.5 146.5t-146.5 60.5q-85 0 -146 -60.5t-61 -146.5v-696z" />
+<glyph unicode="R" d="M166 57v1180q0 17 11 28t28 11h246q118 0 201 -83t83 -202v-114q0 -97 -55.5 -171.5t-144.5 -101.5l198 -532q3 -9 3 -17q0 -9 -2 -15q-6 -14 -23 -20q-8 -3 -16 -3q-7 0 -15 3q-14 6 -21 23l-204 549h-4h-207v-535q0 -39 -39 -39q-17 0 -28 11t-11 28zM244 670h207 q86 0 146 60.5t60 146.5v114q0 86 -60 146.5t-146 60.5h-207v-528z" />
+<glyph unicode="S" horiz-adv-x="782" d="M117 196q4 16 20 23q29 16 53 -14q51 -84 144 -105q24 -5 47 -5q68 0 127 46q77 61 86 156v16q0 87 -62 152l-325 336q-81 84 -81 198q0 11 1 23q4 60 33.5 114t77.5 91q78 57 168 57q30 0 61 -6q128 -26 190 -135q15 -35 -14 -56q-13 -8 -29.5 -3t-23.5 18 q-48 78 -139 100q-23 4 -44 4q-69 0 -120 -43q-72 -54 -82 -147q-1 -9 -1 -18q0 -34 12 -68q15 -42 46 -74l328 -338q44 -44 66 -104q17 -48 16 -99v-24q-4 -63 -35 -119t-82 -94q-80 -60 -172 -60q-32 0 -64 7q-131 25 -198 141q-5 9 -5 19q0 5 1 11z" />
+<glyph unicode="T" horiz-adv-x="778" d="M84 1237q0 17 11 28t28 11h532q17 0 29 -11t12 -28t-12 -28t-29 -11h-225v-1141q0 -17 -12 -28t-29 -11t-28 11t-11 28v1141h-227q-17 0 -28 11t-11 28z" />
+<glyph unicode="U" horiz-adv-x="868" d="M147 303v942q0 17 12 28t29 11t28 -11t11 -28v-942q0 -86 60.5 -146.5t146.5 -60.5q85 0 145 60.5t60 146.5v942q0 17 12 28t29 11t28 -11t11 -28v-942q0 -118 -84 -201.5t-201 -83.5q-119 0 -203 83.5t-84 201.5z" />
+<glyph unicode="V" horiz-adv-x="917" d="M94 1235q-1 6 -1 11q0 10 5 18q8 12 25 18q8 2 15 2q25 0 32 -29q74 -271 123 -444q25 -90 63 -229t64.5 -235t40.5 -142q156 572 166 606q49 173 123 444q8 29 34 29q7 0 15 -2q28 -7 28 -32q0 -7 -2 -15l-327 -1188q-10 -29 -37 -29q-33 0 -39 29z" />
+<glyph unicode="W" horiz-adv-x="1429" d="M102 1235q-1 6 -1 11q0 30 30 38q5 1 10 1q11 1 19 -5q12 -8 18 -27q43 -183 103 -426q23 -96 77.5 -316t69.5 -286q12 53 35 148.5t56.5 233t53.5 220.5q60 243 103 426q6 31 39 31q31 0 37 -31q58 -250 102 -426q23 -96 77.5 -316t69.5 -286q15 68 70.5 293.5 t75.5 308.5q44 176 102 426q5 19 17 27q8 6 20 5q5 0 10 -1q31 -6 31 -35q0 -7 -2 -14l-287 -1188q-10 -29 -37 -29q-32 0 -38 29q-43 183 -103 426q-144 599 -145 604q-14 -63 -68.5 -281t-79.5 -323q-44 -176 -102 -426q-10 -29 -37 -29q-33 0 -39 29z" />
+<glyph unicode="X" horiz-adv-x="864" d="M90 76l297 596l-276 555q-15 36 16 53q12 5 21 5q20 0 32 -21l252 -504l252 504q6 14 22 18q6 2 11 2q9 0 18 -4q24 -10 24 -31q0 -10 -5 -22l-279 -555l299 -596q6 -12 6 -22q0 -21 -24 -31q-9 -4 -18 -4q-6 -1 -12 1q-16 4 -21 19l-273 545l-272 -545q-12 -21 -32 -21 q-10 0 -22 5q-21 11 -21 32q0 9 5 21z" />
+<glyph unicode="Y" horiz-adv-x="823" d="M90 1227q-5 12 -5 21q0 21 21 32q12 5 22 5q20 0 32 -21q66 -136 106 -211q33 -68 88 -177t58 -116q23 48 145 293q48 97 107 211q11 21 32 21q9 0 21 -5q21 -11 21 -32q0 -9 -5 -21l-282 -565v-605q0 -39 -39 -39t-39 39v605z" />
+<glyph unicode="Z" horiz-adv-x="786" d="M109 66q0 12 2 14q89 215 219 522q49 114 107.5 254.5t96 231.5t46.5 110h-433q-38 0 -38 39t38 39h492q20 0 33 -16q6 -12 6 -23q0 -12 -2 -14q-110 -265 -219 -523q-49 -114 -107.5 -254.5t-96 -231.5t-46.5 -110h432q39 0 39 -38q0 -39 -39 -39h-492q-19 0 -32 16 q-6 12 -6 23z" />
+<glyph unicode="[" horiz-adv-x="524" d="M166 -98v1499q0 39 39 39h205q39 0 39 -39t-39 -39h-166v-1421h166q39 0 39 -39t-39 -39h-205q-39 0 -39 39z" />
+<glyph unicode="\" d="M143 1313q-10 39 25 51q17 6 30.5 -1.5t18.5 -21.5l492 -1351q13 -38 -23 -51q-39 -13 -51 22z" />
+<glyph unicode="]" horiz-adv-x="524" d="M76 -98q0 39 39 39h166v1421h-166q-39 0 -39 39t39 39h204q17 0 28 -11t11 -28v-1499q0 -17 -11 -28t-28 -11h-204q-39 0 -39 39z" />
+<glyph unicode="^" horiz-adv-x="1024" d="M151 875q-1 16 9 28l323 369q9 12 29 12t29 -12l323 -369q10 -12 9 -28t-13 -27q-13 -10 -29 -10q-17 0 -26 12q-71 80 -129 147q-34 41 -93 107.5t-71 81.5q-12 -14 -71 -80.5t-95 -108.5q-108 -124 -127 -147q-9 -12 -26 -12q-16 0 -29 10q-12 11 -13 27z" />
+<glyph unicode="_" horiz-adv-x="1024" d="M-41 -229q0 17 12 28t29 11h1024q17 0 28 -11t11 -28t-11 -28t-28 -11h-1024q-17 0 -29 11t-12 28z" />
+<glyph unicode="`" horiz-adv-x="1024" d="M373.5 1285.5q-0.5 14.5 9.5 25.5q11 11 25.5 11.5t25.5 -9.5l205 -185q12 -11 13 -25.5t-9 -25.5q-11 -11 -26 -11.5t-27 9.5l-205 185q-11 11 -11.5 25.5z" />
+<glyph unicode="a" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 49 -28 90t-75 61q-32 13 -64 13q-15 0 -31 -3q-48 -9 -83 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q52 52 122 66q24 5 46 4q46 0 90 -19q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5 t-72 169.5zM160 270q0 -70 48.5 -119t119.5 -49h168v336h-168q-71 0 -119.5 -49t-48.5 -119z" />
+<glyph unicode="b" horiz-adv-x="794" d="M168 66v1220q0 37 37 37t37 -37v-381h168q98 0 169.5 -71.5t71.5 -169.5v-394q0 -98 -71.5 -169.5t-169.5 -71.5h-205q-37 0 -37 37zM242 102h168q69 0 118.5 49.5t49.5 118.5v394q0 69 -49.5 118t-118.5 49h-168v-729z" />
+<glyph unicode="c" horiz-adv-x="671" d="M143 262v410q0 73 40.5 133.5t109.5 89.5q45 20 92 19q23 0 45 -4q70 -14 125 -68q10 -10 10 -27q0 -14 -10 -24q-11 -11 -27 -11q-13 0 -24 11q-38 35 -88 45q-17 3 -34 3q-32 0 -63 -12q-47 -20 -74.5 -62t-27.5 -93v-410q0 -51 27.5 -93.5t74.5 -62.5q31 -13 64 -14 q15 0 33 4q50 10 88 47q11 11 24 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-55 -54 -125 -68q-23 -5 -45 -5q-47 0 -92 20q-69 29 -109.5 89.5t-40.5 133.5z" />
+<glyph unicode="d" horiz-adv-x="794" d="M143 270v394q0 98 72 169.5t170 71.5h168v381q0 37 37 37t37 -37v-1220q0 -37 -37 -37h-205q-98 0 -170 71.5t-72 169.5zM217 270q0 -69 49.5 -118.5t118.5 -49.5h168v729h-168q-69 0 -118.5 -49t-49.5 -118v-394z" />
+<glyph unicode="e" horiz-adv-x="753" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-205q0 -37 -37 -37h-373v-168q0 -51 29 -93.5t76 -62.5q31 -13 64 -14q16 0 32 4q49 10 86 47q11 11 24 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-51 -54 -123 -68q-23 -5 -46 -5q-47 0 -93 20 q-68 29 -109 89.5t-41 133.5zM217 504h336v168q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-168z" />
+<glyph unicode="f" horiz-adv-x="606" d="M78 868q0 37 37 37h127v176q0 98 71.5 170t169.5 72q37 0 37 -37t-37 -37q-69 0 -118.5 -49.5t-49.5 -118.5v-176h168q37 0 37 -37t-37 -37h-168v-774q0 -37 -36 -37q-37 0 -37 37v774h-127q-37 0 -37 37z" />
+<glyph unicode="g" horiz-adv-x="786" d="M143 270v394q0 98 72 169.5t170 71.5h205q37 0 37 -37v-1015q0 -74 -40.5 -134.5t-109.5 -89.5q-45 -19 -92 -19q-23 0 -46 4q-70 15 -124 69q-10 10 -10 26q0 15 10 25t25 10q16 0 26 -10q37 -37 88 -47q16 -3 33 -3q33 0 64 13q47 20 74.5 62.5t27.5 93.5v176h-168 q-98 0 -170 71.5t-72 169.5zM217 270q0 -69 49.5 -118.5t118.5 -49.5h168v729h-168q-69 0 -118.5 -49t-49.5 -118v-394z" />
+<glyph unicode="h" horiz-adv-x="802" d="M168 57v1229q0 37 37 37t37 -37v-381h168q98 0 169.5 -71.5t71.5 -169.5v-607q0 -37 -37 -37q-36 0 -36 37v607q0 69 -49.5 118t-118.5 49h-168v-774q0 -37 -37 -37t-37 37z" />
+<glyph unicode="i" horiz-adv-x="409" d="M147 1184q0 23 17.5 40t40.5 17q24 0 40.5 -16.5t16.5 -40.5t-16.5 -41t-40.5 -17q-23 0 -40.5 17.5t-17.5 40.5zM168 57v820q0 36 37 36t37 -36v-820q0 -37 -37 -37t-37 37z" />
+<glyph unicode="j" horiz-adv-x="425" d="M-20 -352q0 37 36 37q71 0 119.5 49t48.5 119v1024q0 36 37 36t37 -36v-1024q0 -98 -72 -170t-170 -72q-36 0 -36 37zM164 1184q0 23 17 40t40 17q25 0 41.5 -16.5t16.5 -40.5t-17 -41t-41 -17q-23 0 -40 17.5t-17 40.5z" />
+<glyph unicode="k" horiz-adv-x="778" d="M168 57v1229q0 37 37 37t37 -37v-817l385 432q11 11 26 11.5t27 -9.5q12 -12 12 -27q0 -14 -10 -24l-262 -293l266 -483q8 -13 3.5 -28.5t-17.5 -22.5q-12 -7 -26.5 -2.5t-22.5 16.5l-254 463l-127 -144v-301q0 -37 -37 -37t-37 37z" />
+<glyph unicode="l" horiz-adv-x="409" d="M162 199v1087q0 16 11.5 26.5t25.5 10.5q37 0 37 -37v-1087q0 -72 63 -109q13 -6 17 -22q2 -6 2 -11q0 -9 -5 -18q-7 -13 -23 -17q-6 -2 -10 -2q-9 0 -18 5q-100 62 -100 174z" />
+<glyph unicode="m" horiz-adv-x="1212" d="M168 57v811q0 37 37 37h614q98 0 170 -71.5t72 -169.5v-607q0 -16 -11.5 -26.5t-25.5 -10.5q-37 0 -37 37v607q0 69 -49.5 118t-118.5 49h-168v-774q0 -16 -11.5 -26.5t-25.5 -10.5q-36 0 -36 37v774h-336v-774q0 -16 -11.5 -26.5t-25.5 -10.5q-37 0 -37 37z" />
+<glyph unicode="n" horiz-adv-x="802" d="M168 57v811q0 37 37 37h205q98 0 169.5 -71.5t71.5 -169.5v-607q0 -37 -37 -37q-36 0 -36 37v607q0 69 -49.5 118t-118.5 49h-168v-774q0 -37 -37 -37t-37 37z" />
+<glyph unicode="o" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410z" />
+<glyph unicode="p" horiz-adv-x="794" d="M168 -352v1220q0 37 37 37h205q98 0 169.5 -71.5t71.5 -169.5v-394q0 -98 -71.5 -169.5t-169.5 -71.5h-168v-381q0 -37 -37 -37t-37 37zM242 102h168q69 0 118.5 49.5t49.5 118.5v394q0 69 -49.5 118t-118.5 49h-168v-729z" />
+<glyph unicode="q" horiz-adv-x="794" d="M143 270v394q0 98 72 169.5t170 71.5h205q37 0 37 -37v-1220q0 -37 -37 -37t-37 37v381h-168q-98 0 -170 71.5t-72 169.5zM217 270q0 -69 49.5 -118.5t118.5 -49.5h168v729h-168q-69 0 -118.5 -49t-49.5 -118v-394z" />
+<glyph unicode="r" horiz-adv-x="612" d="M168 57v811q0 37 37 37h205q64 0 120 -33q13 -7 18 -22q2 -6 2 -11q0 -9 -5 -16q-7 -13 -22.5 -17.5t-28.5 3.5q-42 22 -84 22h-168v-774q0 -37 -37 -37t-37 37z" />
+<glyph unicode="s" horiz-adv-x="673" d="M122 164q4 16 17 22q13 8 28.5 3.5t22.5 -17.5q39 -67 111 -76q14 -2 28 -3q57 0 101 42q50 47 50 111q0 7 -1 14q-3 73 -65 117l-191 135q-85 60 -94 166v15q0 94 72 163q60 58 143 57q19 0 39 -2q104 -15 158 -108q8 -13 4 -28.5t-17 -22.5q-13 -8 -28.5 -3.5 t-22.5 17.5q-39 63 -104 72q-12 1 -23 1q-57 0 -100 -38q-47 -42 -47 -111v-8q6 -72 63 -111l191 -133q87 -62 96 -174q1 -11 1 -21q0 -95 -75 -163q-65 -60 -153 -60q-18 0 -37 3q-115 24 -164 112q-5 8 -5 18q0 5 2 11z" />
+<glyph unicode="t" horiz-adv-x="606" d="M78 868q0 37 37 37h127v381q0 37 37 37q36 0 36 -37v-381h168q37 0 37 -37t-37 -37h-168v-569q0 -69 49.5 -118.5t118.5 -49.5q37 0 37 -37t-37 -37q-98 0 -169.5 72t-71.5 170v569h-127q-37 0 -37 37z" />
+<glyph unicode="u" horiz-adv-x="802" d="M152 270v607q0 36 36 36q37 0 37 -36v-607q0 -69 49.5 -118.5t118.5 -49.5h168v775q0 36 37 36t37 -36v-811q0 -37 -37 -37h-205q-98 0 -169.5 71.5t-71.5 169.5z" />
+<glyph unicode="v" horiz-adv-x="757" d="M98 866q-2 6 -2 12q0 8 4 16q6 14 23 17q6 2 12 2q7 1 15 -3q14 -6 18 -23q14 -44 28.5 -93.5t31.5 -106.5t26 -87q29 -90 75.5 -247.5t49.5 -166.5q41 154 125 414q9 30 26 87t31.5 106.5t28.5 93.5q4 17 18 23q8 4 15 3q6 0 12 -2q17 -3 23 -17q4 -8 3 -16q0 -6 -2 -12 l-245 -819q-6 -27 -35 -27t-35 27z" />
+<glyph unicode="w" horiz-adv-x="1101" d="M104 868q-2 6 -2 11q0 7 4 15q8 12 25 17q36 9 45 -26l66 -264q55 -244 102 -412q56 239 102 412q23 84 68 264q6 28 35 28q28 0 37 -28q6 -25 29 -120t36 -144q51 -227 103 -412q56 239 102 412q23 84 68 264q9 35 45 26q14 -5 22 -17q5 -8 5 -17q0 -4 -1 -9l-204 -819 q-10 -29 -37 -29q-29 0 -35 29q-45 180 -68 264q-46 173 -102 412q-52 -185 -103 -412q-13 -49 -36 -144t-29 -120q-10 -29 -37 -29q-29 0 -35 29z" />
+<glyph unicode="x" horiz-adv-x="737" d="M106 48q-2 6 -2 10q0 9 5 18l219 399l-211 383q-8 13 -3.5 28.5t17.5 22.5q7 4 16 4q5 1 11 -1q14 -4 22 -17l189 -344l190 344q7 13 21 17q6 2 11 1q9 0 17 -4q14 -8 18 -22.5t-3 -28.5l-211 -383l219 -399q4 -9 5 -18q0 -4 -2 -10q-4 -14 -18 -23q-9 -4 -17 -4 q-5 -1 -11 1q-14 4 -21 17l-198 358l-197 -358q-8 -13 -22 -17q-6 -2 -11 -1q-8 0 -16 4q-13 7 -17 23z" />
+<glyph unicode="y" horiz-adv-x="755" d="M66 -352q0 37 36 37q58 0 101 32q43 34 61 86l76 254l-244 809q-4 13 3.5 26.5t21.5 18.5q9 2 16 2q24 0 31 -26l209 -701l211 701q7 26 31 26q7 0 16 -2q14 -5 21 -19q4 -8 4 -16q0 -5 -1 -10l-245 -819l-80 -264q-20 -70 -88 -125q-65 -47 -144 -47q-36 0 -36 37z" />
+<glyph unicode="z" horiz-adv-x="704" d="M111 66q0 14 2 16q145 283 174 340q174 349 209 409h-349q-36 0 -36 37t36 37h410q22 0 31 -18q6 -9 6 -19q0 -4 -4 -16q-145 -283 -174 -340q-177 -335 -209 -410h350q15 0 26 -10t11 -26t-11.5 -26.5t-25.5 -10.5h-410q-23 0 -32 18q-4 4 -4 19z" />
+<glyph unicode="{" horiz-adv-x="524" d="M59 672q0 39 39 39q52 0 88.5 36t36.5 89v409q0 86 58.5 144.5t144.5 58.5q39 0 39 -39t-39 -39q-53 0 -89 -36.5t-36 -88.5v-409q0 -100 -84 -164q84 -64 84 -164v-451q0 -54 35.5 -89.5t89.5 -35.5q39 0 39 -38q0 -39 -39 -39q-86 0 -144.5 58t-58.5 144v451 q0 51 -37 88t-88 37q-39 0 -39 39z" />
+<glyph unicode="|" horiz-adv-x="425" d="M184 -352v2048q0 39 39 39t39 -39v-2048q0 -39 -39 -39t-39 39z" />
+<glyph unicode="}" horiz-adv-x="524" d="M57 -106q0 17 11.5 27.5t29.5 10.5q52 0 87.5 36t35.5 89v451q0 100 84 164q-84 64 -84 164v409q0 51 -36 88t-87 37q-17 0 -29 11t-12 28t12 28t29 11q83 0 143 -59t60 -144v-409q0 -54 35.5 -89.5t89.5 -35.5q17 0 28 -11t11 -28t-11 -28t-28 -11q-53 0 -89 -36.5 t-36 -88.5v-451q0 -85 -60 -143.5t-143 -58.5q-17 0 -29 11t-12 28z" />
+<glyph unicode="~" d="M119 516q57 81 147 96q96 15 170 -41l25 -18q50 -39 114 -27q61 9 97 64q23 29 51 10q13 -10 16.5 -24.5t-4.5 -26.5q-57 -81 -147 -96q-98 -16 -172 41l-25 18q-51 39 -112 27q-61 -9 -97 -64q-27 -30 -53 -10q-29 23 -10 51z" />
+<glyph unicode="&#xa1;" horiz-adv-x="409" d="M145 856q0 25 17.5 42t42.5 17t43 -17t18 -42t-18 -42t-43 -17t-42.5 17t-17.5 42zM166 -270v819q0 17 11 28t28 11t29 -11t12 -28v-819q0 -17 -12 -28t-29 -11t-28 11t-11 28z" />
+<glyph unicode="&#xa2;" d="M209 262v410q0 74 41 135t110 90q10 5 29 9.5t25 6.5v127q0 17 12 28t29 11t28 -11t11 -28v-127q76 -12 133 -69q10 -10 10 -27q0 -16 -10 -29q-13 -10 -29 -10q-17 0 -27 10q-53 50 -116 50h-2q-29 0 -62 -13q-47 -20 -74.5 -61t-27.5 -92v-410q0 -51 27.5 -92t74.5 -61 q32 -13 62 -13h2q67 0 116 49q11 11 27 11q14 0 29 -11q10 -13 10 -28q0 -17 -10 -27q-58 -58 -133 -70v-126q0 -17 -11 -28t-28 -11t-29 11t-12 28v126q-4 2 -23.5 7t-30.5 10q-69 29 -110 90t-41 135z" />
+<glyph unicode="&#xa3;" d="M100 59q-3 34 27 43q62 23 99.5 76t37.5 119v336h-125q-17 0 -28 11t-11 28t11 28t28 11h125v329q0 84 50 148t130 88q79 23 157.5 -9t119.5 -104q18 -34 -16 -55q-14 -7 -28.5 -3.5t-25.5 17.5q-29 49 -79.5 70.5t-106.5 7.5q-54 -13 -88.5 -58.5t-34.5 -101.5v-329h207 q39 0 39 -39t-39 -39h-207v-336q0 -113 -74 -193h445q39 0 39 -38q0 -39 -39 -39h-574q-33 0 -39 32z" />
+<glyph unicode="&#xa4;" d="M98 381q0 18 11 29l90 90q-58 73 -58 172q0 96 58 172l-90 90q-11 11 -11 27q0 13 11 28q10 10 26 10q19 0 29 -10l90 -90q72 57 172 57t172 -57l90 90q10 10 27 10q15 0 28 -10q11 -15 11 -28q0 -16 -11 -27l-90 -90q58 -76 58 -172q0 -99 -58 -172l90 -90 q11 -11 11 -29q0 -16 -11 -27q-12 -12 -28 -12q-18 0 -27 12l-90 90q-72 -57 -172 -57t-172 57l-90 -90q-9 -12 -29 -12q-17 0 -26 12q-11 11 -11 27zM219 672q0 -86 60.5 -146.5t146.5 -60.5t146.5 60.5t60.5 146.5t-60.5 146.5t-146.5 60.5t-146.5 -60.5t-60.5 -146.5z " />
+<glyph unicode="&#xa5;" d="M104 1227q-15 35 17 53q36 15 53 -16q55 -113 107 -211q33 -69 89 -181l56 -112q23 48 145 293q48 97 107 211q17 31 53 16q32 -18 17 -53l-259 -516h191q39 0 39 -39t-39 -39h-215v-250h215q39 0 39 -39t-39 -39h-215v-248q0 -39 -39 -39t-39 39v248h-199q-38 0 -38 39 t38 39h199v250h-199q-38 0 -38 39t38 39h174z" />
+<glyph unicode="&#xa6;" horiz-adv-x="425" d="M174 -352v819q0 39 39 39t39 -39v-819q0 -39 -39 -39t-39 39zM174 877v819q0 39 39 39t39 -39v-819q0 -39 -39 -39t-39 39z" />
+<glyph unicode="&#xa7;" d="M195 754q15 89 92 143q-69 66 -72 160q0 102 78 174q79 67 184 51q107 -23 156 -108q15 -35 -15 -56q-13 -8 -29.5 -3t-23.5 18q-35 59 -102 71q-68 9 -119 -33q-52 -46 -49 -112q0 -67 53 -111l230 -184q49 -38 70.5 -95.5t10.5 -119.5q-18 -90 -92 -143q69 -66 72 -160 q0 -102 -78 -174q-80 -68 -184 -52q-106 22 -156 109q-15 34 15 55q13 8 29.5 3.5t23.5 -17.5q35 -60 102 -72q68 -9 119 33q52 46 49 113q0 66 -53 110l-232 183q-47 41 -68 98.5t-11 118.5zM272 739q-15 -85 52 -139l178 -141q69 39 80 104q15 85 -52 139l-178 142 q-69 -39 -80 -105z" />
+<glyph unicode="&#xa8;" horiz-adv-x="1024" d="M291 1184q0 23 17 40t40 17q24 0 41 -16.5t17 -40.5t-17 -41t-41 -17q-23 0 -40 17.5t-17 40.5zM618 1184q0 23 17.5 40t40.5 17q24 0 40.5 -16.5t16.5 -40.5t-16.5 -41t-40.5 -17q-23 0 -40.5 17.5t-17.5 40.5z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1470" d="M102 651q0 262 187 449q184 184 446 184q265 0 449 -184q186 -189 186 -449q0 -259 -186 -448q-185 -185 -449 -185q-261 0 -446 185q-187 187 -187 448zM182 651q0 -110 42 -211.5t120 -181.5q78 -78 179.5 -120t211.5 -42q112 0 213.5 42t179.5 120q78 80 120 181.5 t42 211.5t-42 211.5t-120 179.5q-161 164 -393 164q-111 0 -212 -42.5t-179 -121.5q-78 -78 -120 -179.5t-42 -211.5zM532 508v287q0 69 42 123t110 73q66 17 129 -8.5t98 -85.5q21 -34 -14 -55q-13 -8 -29.5 -3.5t-23.5 17.5q-23 37 -60.5 53t-78.5 6t-67 -44t-26 -76v-287 q0 -43 26 -77t67 -44t78.5 6t60.5 53q7 13 23.5 18t29.5 -3q35 -21 14 -55q-35 -60 -98 -86t-129 -9q-68 19 -110 73.5t-42 123.5z" />
+<glyph unicode="&#xaa;" horiz-adv-x="624" d="M133 815q0 75 52.5 127.5t127.5 52.5h107v107q0 35 -22.5 63t-57.5 39q-35 10 -67.5 -4t-51.5 -45q-7 -13 -22.5 -17.5t-28.5 3.5q-14 8 -18 22.5t6 28.5q29 54 85.5 77.5t116.5 6.5t97 -65t37 -109v-430q0 -37 -37 -37h-144q-75 0 -127.5 52.5t-52.5 127.5zM207 815 q0 -44 31 -75t75 -31h107v213h-107q-44 0 -75 -32t-31 -75z" />
+<glyph unicode="&#xab;" horiz-adv-x="858" d="M131 578q-12 18 0 38l131 238q17 31 53 16q21 -15 21 -34q0 -10 -6 -19q-9 -18 -27.5 -50.5t-26.5 -47.5q-14 -23 -28.5 -49.5t-25.5 -47t-13 -24.5q14 -29 72 -131q27 -49 57 -105q6 -9 6 -18q0 -21 -20 -35q-39 -17 -54 17zM500 578q-12 18 0 38l131 238q17 31 53 16 q21 -15 21 -34q0 -8 -7 -19q-9 -18 -27 -50.5t-26 -47.5q-14 -23 -28.5 -49.5t-25.5 -47t-13 -24.5q22 -45 71 -131q8 -14 27.5 -49.5t30.5 -55.5q6 -9 6 -18q0 -20 -21 -35q-38 -17 -53 17z" />
+<glyph unicode="&#xac;" d="M141 590q0 17 11 28t28 11h492q39 0 39 -39v-246q0 -39 -39 -39q-17 0 -28 11t-11 28v207h-453q-17 0 -28 11t-11 28z" />
+<glyph unicode="&#xad;" d="M139 590q0 17 12 28t29 11h492q17 0 28 -11t11 -28t-11 -28t-28 -11h-492q-17 0 -29 11t-12 28z" />
+<glyph unicode="&#xae;" horiz-adv-x="1470" d="M102 651q0 262 187 449q184 184 446 184q265 0 449 -184q186 -189 186 -449q0 -259 -186 -448q-185 -185 -449 -185q-261 0 -446 185q-187 187 -187 448zM182 651q0 -110 42 -211.5t120 -181.5q78 -78 179.5 -120t211.5 -42q112 0 213.5 42t179.5 120q78 80 120 181.5 t42 211.5t-42 211.5t-120 179.5q-161 164 -393 164q-111 0 -212 -42.5t-179 -121.5q-78 -78 -120 -179.5t-42 -211.5zM553 344v606q0 17 11 28t28 11h164q86 0 145.5 -58.5t59.5 -144.5q0 -65 -38.5 -116.5t-99.5 -75.5l131 -232q16 -29 -14 -53q-13 -8 -29.5 -3t-23.5 18 l-150 260h-104v-240q0 -17 -12 -28t-29 -11t-28 11t-11 28zM633 662h123q54 0 89.5 35t35.5 89q0 52 -36 88.5t-89 36.5h-123v-249z" />
+<glyph unicode="&#xaf;" horiz-adv-x="1024" d="M-41 1532q0 17 12 28t29 11h1024q17 0 28 -11t11 -28t-11 -28t-28 -11h-1024q-17 0 -29 11t-12 28z" />
+<glyph unicode="&#xb0;" horiz-adv-x="641" d="M100 1061q0 92 65 156.5t157 64.5q91 0 156 -64.5t65 -156.5t-65 -156.5t-156 -64.5q-92 0 -157 64.5t-65 156.5zM174 1061q0 -61 43 -104.5t105 -43.5q60 0 103.5 43.5t43.5 104.5t-43.5 104t-103.5 43q-62 0 -105 -43t-43 -104z" />
+<glyph unicode="&#xb1;" d="M141 66q0 17 11 27.5t28 10.5h492q18 0 29.5 -10.5t11.5 -27.5t-12 -28t-29 -11h-492q-17 0 -28 11t-11 28zM141 590q0 17 11 28t28 11h207v207q0 17 11 27.5t28 10.5q18 0 29.5 -10.5t11.5 -27.5v-207h205q17 0 29 -11t12 -28t-12 -28t-29 -11h-205v-207q0 -17 -12 -28 t-29 -11t-28 11t-11 28v207h-207q-17 0 -28 11t-11 28z" />
+<glyph unicode="&#xb2;" horiz-adv-x="608" d="M133.5 1163.5q-4.5 14.5 5.5 28.5q32 61 111 84q73 21 139 -21q62 -38 82 -112q15 -73 -25 -137q-157 -248 -206 -330h198q37 0 37 -37t-37 -37h-268q-20 0 -33 19q-9 21 2 38l246 385q24 41 12 82q-9 45 -47 66q-41 24 -82 12q-44 -9 -65 -49q-7 -13 -22.5 -17.5 t-28.5 3.5q-14 8 -18.5 22.5z" />
+<glyph unicode="&#xb3;" horiz-adv-x="606" d="M131.5 712.5q4.5 14.5 18.5 22.5q13 8 28.5 3.5t22.5 -17.5q19 -31 51 -45t67 -4q35 11 57.5 39t22.5 63v41q0 43 -31.5 75t-74.5 32q-37 0 -37 36q0 37 37 37q29 0 53.5 14.5t38.5 39.5q30 53 0 106q-29 53 -92 53t-92 -53q-7 -13 -22.5 -17.5t-28.5 3.5 q-14 8 -18.5 22.5t5.5 28.5q23 41 66 65.5t90 24.5t90 -24.5t66 -65.5q24 -40 24 -90t-24 -90q-22 -36 -46 -54q70 -54 70 -143v-41q0 -61 -36.5 -109t-96.5 -65t-117 6.5t-86 77.5q-10 14 -5.5 28.5z" />
+<glyph unicode="&#xb4;" horiz-adv-x="1024" d="M373.5 1102.5q0.5 14.5 11.5 25.5l205 185q12 10 27 9.5t26 -11.5q10 -11 9.5 -25.5t-11.5 -25.5l-205 -185q-12 -10 -27 -9.5t-26 11.5q-10 11 -9.5 25.5z" />
+<glyph unicode="&#xb5;" horiz-adv-x="819" d="M168 -352v1229q0 36 37 36t37 -36v-615q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v615q0 36 36 36q37 0 37 -36v-820q0 -37 -37 -37q-36 0 -36 37v31q-71 -68 -168 -68t-168 68v-440q0 -37 -37 -37t-37 37z" />
+<glyph unicode="&#xb6;" horiz-adv-x="983" d="M92 991q0 119 83 202t202 83h512q39 0 39 -39t-39 -39h-105v-1304q0 -39 -39 -39q-38 0 -38 39v1304h-209v-1304q0 -39 -39 -39t-39 39v813h-43q-119 0 -202 83t-83 201zM170 991q0 -86 60.5 -146.5t146.5 -60.5h43v414h-43q-86 0 -146.5 -60.5t-60.5 -146.5z" />
+<glyph unicode="&#xb7;" horiz-adv-x="393" d="M137 795q0 25 17.5 42t42.5 17t42 -17t17 -42t-17 -42.5t-42 -17.5t-42.5 17.5t-17.5 42.5z" />
+<glyph unicode="&#xb8;" horiz-adv-x="1024" d="M395.5 -199.5q4.5 16.5 20.5 23.5q61 36 61 108v125q0 17 11 28t28 11q39 0 39 -39v-125q0 -56 -26.5 -103t-73.5 -73q-14 -9 -29.5 -4.5t-26.5 19.5q-8 13 -3.5 29.5z" />
+<glyph unicode="&#xb9;" horiz-adv-x="479" d="M76 1063q0 17 10 27l182 182q18 18 41 8t23 -35v-614q0 -37 -37 -37t-37 37v524q-14 -12 -28 -26t-29.5 -30.5t-22.5 -23.5l-39 -39q-10 -10 -26 -10q-17 0 -27 10t-10 27z" />
+<glyph unicode="&#xba;" horiz-adv-x="624" d="M133 815v287q0 75 52.5 127.5t127.5 52.5t128 -52.5t53 -127.5v-287q0 -75 -53 -127.5t-128 -52.5t-127.5 52.5t-52.5 127.5zM207 815q0 -44 31 -75t75 -31q43 0 75 31t32 75v287q0 43 -32 74.5t-75 31.5q-44 0 -75 -31.5t-31 -74.5v-287z" />
+<glyph unicode="&#xbb;" horiz-adv-x="858" d="M147 344q0 8 7 18q30 56 57 105q40 71 70 131q-26 46 -66 121q-8 15 -26 47.5t-27 50.5q-6 9 -6 19q0 20 20 34q14 7 29.5 2.5t23.5 -18.5l131 -238q9 -20 0 -38l-139 -252q-7 -16 -23.5 -20.5t-29.5 3.5q-21 15 -21 35zM516 344q0 9 6 18q11 20 30.5 55.5t27.5 49.5 q31 55 69 131q-13 22 -65 121q-8 15 -26.5 47.5t-27.5 50.5q-6 9 -6 19q0 19 21 34q14 7 29.5 2.5t23.5 -18.5l131 -238q9 -20 0 -38l-139 -252q-7 -16 -23.5 -20.5t-29.5 3.5q-21 15 -21 35z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1224" d="M76 1063q0 17 10 27l182 182q18 18 41 8t23 -35v-614q0 -37 -37 -37t-37 37v524q-14 -12 -28 -26t-29.5 -30.5t-22.5 -23.5l-39 -39q-10 -10 -26 -10q-17 0 -27 10t-10 27zM257 45q-5 16 3 29l574 1188q6 17 22 21.5t29 -3.5q17 -6 21.5 -22t-3.5 -29l-573 -1188 q-6 -17 -22 -21.5t-29 3.5q-17 6 -22 22zM692 236l182 450q15 33 50 21q22 -9 22 -35q0 -13 -2 -15q-11 -27 -34.5 -84.5t-35.5 -85.5q-26 -61 -92 -229h172v86q0 37 37 37t37 -37v-86h70q37 0 37 -37t-37 -37h-70v-127q0 -37 -37 -37t-37 37v127h-227q-19 0 -33 17 q-10 21 -2 35z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1220" d="M76 1063q0 17 10 27l182 182q18 18 41 8t23 -35v-614q0 -37 -37 -37t-37 37v524q-14 -12 -28 -26t-29.5 -30.5t-22.5 -23.5l-39 -39q-10 -10 -26 -10q-17 0 -27 10t-10 27zM257 45q-5 16 3 29l574 1188q6 17 22 21.5t29 -3.5q17 -6 21.5 -22t-3.5 -29l-573 -1188 q-6 -17 -22 -21.5t-29 3.5q-17 6 -22 22zM789 589.5q-4 14.5 6 28.5q33 61 110 84q74 21 139 -20q61 -37 82 -113q15 -75 -24 -137q-10 -15 -84 -133q-44 -67 -123 -197h199q36 0 36 -36q0 -37 -36 -37h-269q-20 0 -32 18q-9 22 2 39l245 385q25 42 13 82q-9 44 -47 65 q-42 25 -82 13q-45 -9 -66 -49q-8 -14 -22.5 -18t-28.5 3q-14 8 -18 22.5z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1286" d="M92.5 712.5q4.5 14.5 18.5 22.5q13 8 28.5 3.5t22.5 -17.5q19 -31 51.5 -45t67.5 -4q35 11 57 39t22 63v41q0 43 -31.5 75t-74.5 32q-37 0 -37 36q0 37 37 37q29 0 53.5 14.5t38.5 39.5q30 53 0 106q-29 53 -92 53t-92 -53q-7 -13 -22.5 -17.5t-28.5 3.5 q-14 8 -18.5 22.5t5.5 28.5q23 41 66 65.5t90 24.5t90 -24.5t66 -65.5q24 -40 24 -90t-24 -90q-23 -37 -45 -54q69 -53 69 -143v-41q0 -61 -36.5 -109t-96.5 -65t-117 6.5t-86 77.5q-10 14 -5.5 28.5zM318.5 45q-4.5 16 3.5 29l573 1188q6 17 22 21.5t29 -3.5q17 -6 22 -22 t-3 -29l-574 -1188q-6 -17 -22 -21.5t-29 3.5q-17 6 -21.5 22zM754 236l182 450q15 33 49 21q23 -10 23 -35q0 -13 -2 -15q-11 -27 -34.5 -84.5t-35.5 -85.5q-26 -61 -92 -229h172v86q0 37 37 37t37 -37v-86h69q37 0 37 -37t-37 -37h-69v-127q0 -37 -37 -37t-37 37v127h-228 q-18 0 -32 17q-10 21 -2 35z" />
+<glyph unicode="&#xbf;" horiz-adv-x="731" d="M109 -86q-9 128 81 221l105 105q59 59 59 145v164q0 17 11 28t28 11t29 -11t12 -28v-164q0 -117 -84 -201l-102 -104q-66 -66 -60 -160q10 -97 80 -149q35 -29 79 -39.5t87 0.5q93 18 139 100q7 13 24 17.5t30 -3.5q29 -20 14 -55q-64 -112 -190 -137q-129 -22 -230 53 q-103 80 -112 207zM334 856q0 25 17 42t42 17t43.5 -17.5t18.5 -41.5t-18.5 -41.5t-43.5 -17.5t-42 17t-17 42z" />
+<glyph unicode="&#xc0;" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q5 -18 -2.5 -32t-24.5 -18t-30.5 4t-18.5 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25t-30 -4t-25 18.5t-3 31.5zM246 383h393 q-153 547 -197 715q-62 -235 -196 -715zM301 1694q0 15 10 28t27 13q14 0 29 -11l204 -184q13 -10 13 -27q0 -13 -11 -28q-9 -12 -26 -12q-16 0 -29 10l-205 184q-12 9 -12 27z" />
+<glyph unicode="&#xc1;" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q5 -18 -2.5 -32t-24.5 -18t-30.5 4t-18.5 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25t-30 -4t-25 18.5t-3 31.5zM246 383h393 q-153 547 -197 715q-62 -235 -196 -715zM301 1513q0 18 12 27l205 184q15 11 29 11q16 0 26 -13q11 -15 11 -28q0 -17 -13 -27l-204 -184q-13 -10 -29 -10q-18 0 -27 12q-10 13 -10 28z" />
+<glyph unicode="&#xc2;" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q5 -18 -2.5 -32t-24.5 -18t-30.5 4t-18.5 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25t-30 -4t-25 18.5t-3 31.5zM219 1516q2 15 14 26 l185 144q24 18 49 0l184 -144q12 -11 14 -26t-8 -29q-11 -12 -26 -14t-29 8q-58 45 -65 51q-15 12 -34.5 27t-37.5 29t-23 18q-5 -4 -39.5 -31t-54.5 -43q-7 -6 -65 -51q-12 -10 -28.5 -8t-27.5 14q-10 14 -8 29zM246 383h393q-153 547 -197 715q-62 -235 -196 -715z" />
+<glyph unicode="&#xc3;" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q5 -18 -2.5 -32t-24.5 -18t-30.5 4t-18.5 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25t-30 -4t-25 18.5t-3 31.5zM219.5 1562 q-4.5 15 5.5 29q37 58 109 70q67 12 125 -31l16 -12q29 -20 64 -14q36 6 55 34q11 14 26 18.5t29 -5.5q14 -11 17 -26.5t-7 -29.5q-37 -57 -106 -69q-71 -12 -125 30l-16 13q-29 20 -66 14q-32 -6 -55 -35q-26 -33 -53 -12q-14 11 -18.5 26zM246 383h393q-153 547 -197 715 q-62 -235 -196 -715z" />
+<glyph unicode="&#xc4;" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q5 -18 -2.5 -32t-24.5 -18t-30.5 4t-18.5 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25t-30 -4t-25 18.5t-3 31.5zM219 1593q0 25 17.5 42.5 t42.5 17.5t42 -17.5t17 -42.5t-17 -42t-42 -17t-42.5 17t-17.5 42zM246 383h393q-153 547 -197 715q-62 -235 -196 -715zM547 1593q0 25 17 42.5t42 17.5t42.5 -17.5t17.5 -42.5t-17.5 -42t-42.5 -17t-42 17t-17 42z" />
+<glyph unicode="&#xc5;" horiz-adv-x="884" d="M76 68l327 1187q10 29 39 29q27 0 37 -29l328 -1187q5 -18 -2.5 -32t-24.5 -18t-30.5 4t-18.5 25q-1 6 -9 32t-11 42q-6 19 -21 80.5t-28 103.5h-439q-13 -42 -29 -104t-22 -80q-4 -16 -11 -42t-9 -32q-5 -17 -18 -25t-30 -4t-25 18.5t-3 31.5zM246 383h393 q-153 547 -197 715q-62 -235 -196 -715zM285 1614q0 66 45.5 112t111.5 46t113 -46t47 -112t-47 -112t-113 -46t-111.5 46t-45.5 112zM356 1614q0 -37 25.5 -62.5t60.5 -25.5q37 0 62.5 25.5t25.5 62.5t-25.5 62.5t-62.5 25.5q-35 0 -60.5 -25.5t-25.5 -62.5z" />
+<glyph unicode="&#xc6;" horiz-adv-x="1263" d="M63 74l529 1179q10 23 37 23h493q17 0 29 -11t12 -28t-12 -28t-29 -11h-368v-487h327q17 0 29 -11t12 -28t-12 -28t-29 -11h-327v-529h368q18 0 29.5 -10.5t11.5 -27.5t-12 -28t-29 -11h-409q-17 0 -28 11t-11 28v239h-420q-59 -135 -86 -190q-4 -8 -15.5 -34.5 t-17.5 -39.5q-6 -16 -22 -22t-29 1q-35 15 -21 54zM289 383h385v815h-21q-7 -11 -61 -131.5t-155 -349t-148 -334.5z" />
+<glyph unicode="&#xc7;" horiz-adv-x="806" d="M139 303v696q0 98 59 174t152 101q95 25 183.5 -11.5t138.5 -119.5l20 -37q8 -13 3.5 -29.5t-17.5 -23.5q-13 -8 -29.5 -3.5t-23.5 17.5l-21 35q-35 61 -100 88t-133 8q-67 -17 -109.5 -73t-42.5 -126v-696q0 -72 42 -127t110 -72q67 -19 132.5 8t100.5 89l21 35 q7 13 23.5 17.5t29.5 -3.5q13 -7 17.5 -23.5t-3.5 -29.5l-20 -37q-69 -117 -207 -140v-92q0 -55 -28 -102.5t-75 -73.5q-14 -10 -29.5 -4.5t-23.5 19.5q-8 13 -3 29.5t18 23.5q61 36 61 108v92q-3 1 -9.5 2.5t-13 2.5t-12.5 2q-93 27 -152 103t-59 173z" />
+<glyph unicode="&#xc8;" horiz-adv-x="753" d="M164 66v1171q0 17 12 28t29 11h409q17 0 28 -11t11 -28t-11 -28t-28 -11h-370v-487h329q17 0 28 -11t11 -28t-11 -28t-28 -11h-329v-529h370q17 0 28 -10.5t11 -27.5t-11 -28t-28 -11h-409q-17 0 -29 11t-12 28zM267.5 1694q-1.5 16 8.5 28q10 13 27 13q18 0 29 -11 l205 -184q12 -11 13 -27t-9 -28q-9 -12 -27 -12q-16 0 -29 10l-204 184q-12 11 -13.5 27z" />
+<glyph unicode="&#xc9;" horiz-adv-x="753" d="M164 66v1171q0 17 12 28t29 11h409q17 0 28 -11t11 -28t-11 -28t-28 -11h-370v-487h329q17 0 28 -11t11 -28t-11 -28t-28 -11h-329v-529h370q17 0 28 -10.5t11 -27.5t-11 -28t-28 -11h-409q-17 0 -29 11t-12 28zM267.5 1513q1.5 16 13.5 27l204 184q15 11 29 11 q17 0 27 -13q10 -12 9.5 -28t-11.5 -27l-205 -184q-13 -10 -31 -10t-27 12q-10 12 -8.5 28z" />
+<glyph unicode="&#xca;" horiz-adv-x="753" d="M166 66v1171q0 39 39 39h409q17 0 28 -11t11 -28t-11 -28t-28 -11h-370v-487h329q17 0 28 -11t11 -28t-11 -28t-28 -11h-329v-529h370q17 0 28 -10.5t11 -27.5t-11 -28t-28 -11h-409q-39 0 -39 39zM187 1516q2 15 14 26l184 144q25 18 49 0l184 -144q12 -11 14.5 -26 t-7.5 -29q-11 -12 -27.5 -14t-28.5 8q-57 45 -65 51q-20 16 -54.5 43t-39.5 31q-6 -4 -23.5 -18t-37 -29t-34.5 -27q-8 -6 -65 -51q-14 -10 -29 -8t-26 14q-10 14 -8 29z" />
+<glyph unicode="&#xcb;" horiz-adv-x="753" d="M166 66v1171q0 39 39 39h409q39 0 39 -39t-39 -39h-370v-487h329q39 0 39 -39t-39 -39h-329v-529h370q39 0 39 -38q0 -39 -39 -39h-409q-39 0 -39 39zM186 1593q0 25 17.5 42.5t42.5 17.5t42 -17.5t17 -42.5t-17 -42t-42 -17t-42.5 17t-17.5 42zM514 1593q0 25 17 42.5 t42 17.5t42.5 -17.5t17.5 -42.5t-17.5 -42t-42.5 -17t-42 17t-17 42z" />
+<glyph unicode="&#xcc;" horiz-adv-x="409" d="M63 1694q0 13 11 28q10 13 26 13q14 0 29 -11l205 -184q12 -9 12 -27q0 -15 -10 -28q-9 -12 -27 -12q-15 0 -28 10l-205 184q-13 10 -13 27zM166 57v1188q0 39 39 39t39 -39v-1188q0 -39 -39 -39t-39 39z" />
+<glyph unicode="&#xcd;" horiz-adv-x="409" d="M63 1513q0 17 13 27l205 184q15 11 28 11q17 0 27 -13t10 -28q0 -18 -12 -27l-205 -184q-13 -10 -29 -10q-17 0 -26 12q-11 15 -11 28zM166 57v1188q0 39 39 39t39 -39v-1188q0 -39 -39 -39t-39 39z" />
+<glyph unicode="&#xce;" horiz-adv-x="409" d="M-18 1516q2 15 14 26l184 144q24 18 49 0l185 -144q12 -11 14 -26t-8 -29q-11 -12 -26 -14t-29 8q-10 7 -32.5 24.5t-33.5 26.5q-20 16 -54.5 43t-39.5 31q-5 -4 -39.5 -31t-54.5 -43q-11 -9 -33.5 -26.5t-32.5 -24.5q-14 -10 -29 -8t-26 14q-10 14 -8 29zM166 57v1188 q0 39 39 39t39 -39v-1188q0 -39 -39 -39t-39 39z" />
+<glyph unicode="&#xcf;" horiz-adv-x="409" d="M-18 1593q0 25 17 42.5t42 17.5t42 -17.5t17 -42.5t-17 -42t-42 -17t-42 17t-17 42zM166 57v1188q0 39 39 39t39 -39v-1188q0 -39 -39 -39t-39 39zM309 1593q0 25 17.5 42.5t42.5 17.5t42 -17.5t17 -42.5t-17 -42t-42 -17t-42.5 17t-17.5 42z" />
+<glyph unicode="&#xd0;" horiz-adv-x="903" d="M29 672q0 17 11 28t28 11h125v526q0 17 10.5 28t27.5 11h246q119 0 203 -83.5t84 -201.5v-680q0 -118 -84 -201t-203 -83h-246q-17 0 -27.5 11t-10.5 28v567h-125q-17 0 -28 11t-11 28zM272 104h205q86 0 146.5 60.5t60.5 146.5v680q0 86 -60.5 146.5t-146.5 60.5h-205 v-487h164q17 0 29 -11t12 -28t-12 -28t-29 -11h-164v-529z" />
+<glyph unicode="&#xd1;" horiz-adv-x="983" d="M166 57v1188q0 33 31 39q34 6 43 -22q175 -365 206 -428q223 -469 293 -605v1016q0 17 11 28t28 11q39 0 39 -39v-1188q0 -33 -31 -39q-27 -6 -43 23q-175 365 -206 428q-58 122 -163 336.5t-130 267.5v-1016q0 -39 -39 -39q-17 0 -28 11t-11 28zM268.5 1562 q-4.5 15 5.5 29q37 58 109 70q67 12 125 -31l16 -12q29 -20 64 -14q36 6 55 34q11 14 26 18.5t29 -5.5q14 -11 17.5 -26.5t-6.5 -29.5q-37 -57 -107 -69q-71 -12 -125 30l-16 13q-29 20 -66 14q-35 -6 -55 -35q-11 -14 -26 -18t-27 6q-14 11 -18.5 26z" />
+<glyph unicode="&#xd2;" d="M141 303v696q0 118 84 201.5t201 83.5q118 0 201.5 -83t83.5 -202v-696q0 -119 -83.5 -202t-201.5 -83q-117 0 -201 83.5t-84 201.5zM219 303q0 -86 61 -146.5t146 -60.5q86 0 146.5 60.5t60.5 146.5v696q0 86 -60.5 146.5t-146.5 60.5q-85 0 -146 -60.5t-61 -146.5v-696 zM285.5 1694q-0.5 16 9.5 28q10 13 27 13q13 0 28 -11l205 -184q12 -11 13 -27t-9 -28q-12 -12 -29 -12q-15 0 -28 10l-205 184q-11 11 -11.5 27z" />
+<glyph unicode="&#xd3;" d="M141 303v696q0 118 84 201.5t201 83.5q119 0 203 -83.5t84 -201.5v-696q0 -118 -84 -201.5t-203 -83.5q-117 0 -201 83.5t-84 201.5zM221 303q0 -86 60 -146.5t145 -60.5q86 0 146.5 60.5t60.5 146.5v696q0 86 -60.5 146.5t-146.5 60.5q-85 0 -145 -60.5t-60 -146.5v-696 zM286 1513q1 16 13 27l205 184q11 11 26 11q19 0 29 -13q10 -12 9 -28t-13 -27l-205 -184q-13 -10 -28 -10q-18 0 -27 12q-10 12 -9 28z" />
+<glyph unicode="&#xd4;" d="M141 303v696q0 118 84 201.5t201 83.5q119 0 203 -83.5t84 -201.5v-696q0 -118 -84 -201.5t-203 -83.5q-117 0 -201 83.5t-84 201.5zM203 1516q2 15 16 26l184 144q22 16 48 0l184 -144q14 -11 16 -26t-8 -29q-11 -12 -27 -14t-28 8q-42 31 -68 51q-17 12 -36 27t-36 29 t-22 18q-6 -5 -39 -32t-53 -42q-25 -20 -68 -51q-12 -10 -28 -8t-27 14q-10 14 -8 29zM221 303q0 -86 60 -146.5t145 -60.5q86 0 146.5 60.5t60.5 146.5v696q0 86 -60.5 146.5t-146.5 60.5q-85 0 -145 -60.5t-60 -146.5v-696z" />
+<glyph unicode="&#xd5;" d="M139 303v696q0 118 84 201.5t203 83.5q117 0 201 -83.5t84 -201.5v-696q0 -118 -84 -201.5t-201 -83.5q-119 0 -203 83.5t-84 201.5zM202 1562q-3 15 7 29q37 58 106 70t125 -31l17 -12q29 -20 65 -14q34 6 56 34q26 34 53 13q14 -11 18 -26.5t-6 -29.5q-36 -57 -108 -69 q-68 -13 -125 30l-17 13q-29 20 -63 14q-37 -6 -56 -35q-11 -14 -26 -18t-29 6q-14 11 -17 26zM219 303q0 -86 60.5 -146.5t146.5 -60.5q85 0 145 60.5t60 146.5v696q0 86 -60 146.5t-145 60.5q-86 0 -146.5 -60.5t-60.5 -146.5v-696z" />
+<glyph unicode="&#xd6;" d="M139 303v696q0 118 84 201.5t203 83.5q117 0 201 -83.5t84 -201.5v-696q0 -118 -84 -201.5t-201 -83.5q-119 0 -203 83.5t-84 201.5zM201 1593q0 25 17.5 42.5t43.5 17.5q25 0 42.5 -17.5t17.5 -42.5t-17.5 -42t-42.5 -17q-26 0 -43.5 17t-17.5 42zM219 303 q0 -86 60.5 -146.5t146.5 -60.5q85 0 145 60.5t60 146.5v696q0 86 -60 146.5t-145 60.5q-86 0 -146.5 -60.5t-60.5 -146.5v-696zM528 1593q0 25 18 42.5t44 17.5q25 0 42 -17.5t17 -42.5t-17 -42t-42 -17q-26 0 -44 17t-18 42z" />
+<glyph unicode="&#xd7;" d="M193 356q-26 29 0 58l178 176l-178 176q-25 28 0 57q28 25 57 0l176 -178l176 178q28 25 57 0q25 -29 0 -57l-178 -176l178 -176q26 -29 0 -58q-29 -25 -57 0l-176 179l-176 -179q-29 -25 -57 0z" />
+<glyph unicode="&#xd8;" horiz-adv-x="872" d="M29 -35.5q-4 16.5 4 29.5l127 235q-8 27 -8 74v696q0 119 83 202t201 83q72 0 134 -33t100 -90l100 185q8 14 23.5 18.5t29.5 -2.5q14 -8 19 -23.5t-2 -29.5l-129 -236q10 -42 10 -74v-696q0 -119 -83.5 -202t-201.5 -83q-71 0 -132.5 32.5t-100.5 88.5l-101 -182 q-7 -16 -23.5 -20t-29.5 4q-16 7 -20 23.5zM229 354l396 727q-23 56 -75 90.5t-114 34.5q-86 0 -146.5 -60.5t-60.5 -146.5v-645zM246 221q25 -57 75.5 -91t114.5 -34q85 0 146 60.5t61 146.5v645z" />
+<glyph unicode="&#xd9;" horiz-adv-x="868" d="M150 303v942q0 39 38 39q17 0 28 -11t11 -28v-942q0 -86 60.5 -146.5t146.5 -60.5q85 0 146 60.5t61 146.5v942q0 39 39 39q17 0 28 -11t11 -28v-942q0 -119 -83.5 -202t-201.5 -83t-201 83t-83 202zM293 1694q0 15 10 28t27 13q13 0 28 -11l205 -184q12 -9 12 -27 q0 -15 -10 -28q-9 -12 -26 -12q-16 0 -29 10l-205 184q-12 9 -12 27z" />
+<glyph unicode="&#xda;" horiz-adv-x="868" d="M150 303v942q0 39 38 39q39 0 39 -39v-942q0 -86 60.5 -146.5t146.5 -60.5t146.5 60.5t60.5 146.5v942q0 39 39 39t39 -39v-942q0 -119 -83.5 -202t-201.5 -83t-201 83t-83 202zM293 1513q0 18 12 27l205 184q15 11 29 11q16 0 26 -13t10 -28q0 -18 -12 -27l-205 -184 q-13 -10 -28 -10q-18 0 -27 12q-10 13 -10 28z" />
+<glyph unicode="&#xdb;" horiz-adv-x="868" d="M150 303v942q0 17 10.5 28t27.5 11t29 -11t12 -28v-942q0 -86 60 -146.5t145 -60.5q86 0 146.5 60.5t60.5 146.5v942q0 17 11 28t28 11t29 -11t12 -28v-942q0 -118 -84 -201.5t-203 -83.5q-117 0 -200.5 83.5t-83.5 201.5zM211 1516q2 15 16 26l185 144q22 19 47 0 l184 -144q14 -11 16 -26t-8 -29q-11 -12 -27 -14t-28 8q-42 31 -68 51q-17 12 -36 27t-36 29t-22 18q-6 -5 -39 -32t-53 -42q-26 -20 -68 -51q-12 -10 -28 -8t-27 14q-10 14 -8 29z" />
+<glyph unicode="&#xdc;" horiz-adv-x="868" d="M150 303v942q0 17 10.5 28t27.5 11t29 -11t12 -28v-942q0 -86 60 -146.5t145 -60.5q86 0 146.5 60.5t60.5 146.5v942q0 17 11 28t28 11t29 -11t12 -28v-942q0 -118 -84 -201.5t-203 -83.5q-117 0 -200.5 83.5t-83.5 201.5zM211 1593q0 25 17 42.5t42 17.5t43.5 -17.5 t18.5 -42.5q0 -24 -18.5 -41.5t-43.5 -17.5t-42 17t-17 42zM539 1593q0 25 17 42.5t42 17.5t43 -17.5t18 -42.5t-18 -42t-43 -17t-42 17t-17 42z" />
+<glyph unicode="&#xdd;" horiz-adv-x="823" d="M90 1227q-16 38 19 53q14 7 29.5 2.5t21.5 -18.5q66 -136 106 -211q33 -68 88 -177t58 -116q5 10 60 117.5t87 175.5q87 173 105 211q17 31 53 16q31 -17 16 -53l-282 -565v-605q0 -39 -39 -39q-17 0 -28 11t-11 28v605zM270 1513q0 17 13 27l204 184q15 11 29 11 q17 0 27 -13q12 -12 11 -28t-13 -27l-205 -184q-13 -10 -29 -10q-17 0 -26 12q-11 15 -11 28z" />
+<glyph unicode="&#xde;" horiz-adv-x="819" d="M164 57v1180q0 17 12 28t29 11t28 -11t11 -28v-219h207q117 0 200.5 -83.5t83.5 -201.5v-123q0 -118 -83.5 -201t-200.5 -83h-207v-269q0 -17 -11 -28t-28 -11t-29 11t-12 28zM244 403h207q85 0 144.5 60.5t59.5 146.5v123q0 86 -59.5 146.5t-144.5 60.5h-207v-537z" />
+<glyph unicode="&#xdf;" horiz-adv-x="876" d="M168 57v1024q0 68 33.5 124.5t93.5 88.5t126 28.5t122 -40.5q51 -35 79.5 -88t28.5 -113q0 -18 -2 -22q-9 -102 -90 -168q79 -33 126.5 -103t47.5 -157v-328q0 -117 -82.5 -200t-199.5 -83q-37 0 -37 37t37 37q86 0 147 61.5t61 147.5v328q0 86 -61 147.5t-147 61.5 q-31 0 -37 28q-3 32 22 43l37 15q44 19 72.5 56t32.5 83q5 48 -15 89t-61 67q-39 26 -84.5 28.5t-87.5 -20.5q-88 -50 -88 -148v-1024q0 -37 -37 -37t-37 37z" />
+<glyph unicode="&#xe0;" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 49 -28 90t-75 61q-47 19 -95 10t-83 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q51 51 121.5 65.5t136.5 -14.5q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5t-72 169.5zM160 270 q0 -70 48.5 -119t119.5 -49h168v336h-168q-71 0 -119.5 -49t-48.5 -119zM229 1285.5q-1 14.5 9 25.5q11 11 26 11.5t27 -9.5l205 -185q11 -11 11.5 -25.5t-9.5 -25.5q-11 -11 -26 -11.5t-26 9.5l-204 185q-12 11 -13 25.5z" />
+<glyph unicode="&#xe1;" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 49 -28 90t-75 61q-47 19 -95 10t-83 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q51 51 121.5 65.5t136.5 -14.5q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5t-72 169.5zM160 270 q0 -69 49.5 -118.5t118.5 -49.5h168v336h-168q-69 0 -118.5 -49.5t-49.5 -118.5zM229 1102.5q1 14.5 13 25.5l204 185q11 10 26 9.5t26 -11.5q10 -11 9.5 -25.5t-11.5 -25.5l-205 -185q-12 -10 -27 -9.5t-26 11.5q-10 11 -9 25.5z" />
+<glyph unicode="&#xe2;" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 49 -28 90t-75 61q-47 19 -95 10t-83 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q51 51 121.5 65.5t136.5 -14.5q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5t-72 169.5zM148 1105 q2 14 14 25l184 144q21 15 45 0l184 -144q29 -26 7 -51q-11 -12 -25.5 -14t-26.5 8q-4 4 -28.5 24t-38.5 29q-19 15 -52.5 40t-41.5 32l-38 -28.5t-57 -43.5q-14 -9 -38.5 -29t-28.5 -24q-12 -10 -26 -8t-25 14q-10 12 -8 26zM160 270q0 -69 49.5 -118.5t118.5 -49.5h168 v336h-168q-69 0 -118.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="&#xe3;" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 50 -27 90.5t-74 60.5q-47 19 -96 10t-84 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q51 51 121.5 65.5t136.5 -14.5q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5t-72 169.5zM154 1180 q40 57 106 69t123 -30l16 -13q26 -20 66 -14q38 6 57 37q7 12 23 15t28 -5q12 -11 15 -25t-4 -26q-40 -58 -107 -70q-65 -12 -123 31l-16 12q-27 21 -66 15q-38 -6 -57 -37q-11 -12 -25 -15t-26 4q-31 28 -10 52zM160 270q0 -69 49.5 -118.5t118.5 -49.5h168v336h-168 q-69 0 -118.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="&#xe4;" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 50 -27 90.5t-74 60.5q-47 19 -96 10t-84 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q51 51 121.5 65.5t136.5 -14.5q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5t-72 169.5zM147 1184 q0 24 16.5 40.5t41.5 16.5q23 0 40 -17t17 -40t-17 -40.5t-40 -17.5q-24 0 -41 17t-17 41zM160 270q0 -69 49.5 -118.5t118.5 -49.5h168v336h-168q-69 0 -118.5 -49.5t-49.5 -118.5zM475 1184q0 24 16.5 40.5t40.5 16.5q23 0 40.5 -17t17.5 -40t-17.5 -40.5t-40.5 -17.5 q-24 0 -40.5 17t-16.5 41z" />
+<glyph unicode="&#xe5;" horiz-adv-x="720" d="M86 270q0 98 72 170t170 72h168v164q0 50 -27 90.5t-74 60.5q-47 19 -96 10t-84 -44q-10 -13 -27 -13q-14 0 -24 13q-10 10 -10 24q0 17 10 27q51 51 121.5 65.5t136.5 -14.5q68 -29 107.5 -88t39.5 -131v-610q0 -37 -37 -37h-204q-98 0 -170 71.5t-72 169.5zM160 270 q0 -69 49.5 -118.5t118.5 -49.5h168v336h-168q-69 0 -118.5 -49.5t-49.5 -118.5zM213 1204q0 66 45 111t111 45t110.5 -45t44.5 -111t-44.5 -110.5t-110.5 -44.5t-111 44.5t-45 110.5zM279 1204q0 -37 26.5 -63.5t63.5 -26.5t63.5 26.5t26.5 63.5t-26.5 63.5t-63.5 26.5 t-63.5 -26.5t-26.5 -63.5z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1107" d="M90 215q-14 72 16 139q29 68 90 109t134 41h168v168q0 51 -28.5 93t-76.5 62q-47 19 -97 9t-85 -45q-11 -11 -27 -11q-15 0 -26 11t-11 24q0 16 11 27q54 54 125.5 68t138.5 -15q74 -31 113 -94q33 52 86 82t118 30q98 0 170 -71.5t72 -169.5v-205q0 -37 -37 -37h-373 v-168q0 -51 28 -93.5t75 -62.5t97 -10t87 47q11 11 25 11q15 0 26 -11t11 -26q0 -14 -11 -25q-54 -54 -124.5 -68t-137.5 15q-74 29 -112 94q-39 -63 -113 -94q-67 -29 -138.5 -15t-125.5 68q-54 51 -68 123zM164 229.5q10 -49.5 47 -86.5q35 -37 85 -47t97 10t76 62.5 t29 93.5v168h-168q-53 0 -94 -28t-62 -76q-20 -47 -10 -96.5zM571 504h336v168q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-168z" />
+<glyph unicode="&#xe7;" horiz-adv-x="677" d="M143 262v410q0 73 41 133.5t109 89.5q67 29 138.5 15t125.5 -68q10 -10 10 -27q0 -14 -10 -24q-11 -11 -27 -11q-15 0 -26 11q-35 35 -85 45t-97 -9q-48 -20 -76.5 -62t-28.5 -93v-410q0 -51 29 -93.5t76 -62.5q30 -12 63 -12h2q68 0 117 49q11 11 26 11q16 0 27 -11 q10 -10 10 -26q0 -15 -10 -25q-57 -57 -135 -69v-91q0 -54 -26.5 -101t-73.5 -73q-14 -7 -29 -3t-21 16q-8 13 -4 28.5t17 22.5q63 37 63 110v91q-6 2 -25.5 6.5t-29.5 9.5q-68 29 -109 89.5t-41 133.5z" />
+<glyph unicode="&#xe8;" horiz-adv-x="753" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-205q0 -37 -37 -37h-373v-168q0 -51 29 -93.5t76 -62.5t97 -10t85 47q11 11 26 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-54 -54 -125.5 -68t-138.5 15q-68 29 -109 89.5t-41 133.5zM217 504h336v168 q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-168zM246.5 1285.5q-0.5 14.5 9.5 25.5q11 11 25.5 11.5t25.5 -9.5l205 -185q12 -11 13 -25.5t-9 -25.5q-11 -11 -26 -11.5t-27 9.5l-205 185q-11 11 -11.5 25.5z" />
+<glyph unicode="&#xe9;" horiz-adv-x="753" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-205q0 -37 -37 -37h-373v-168q0 -51 29 -93.5t76 -62.5t97 -10t85 47q11 11 26 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-54 -54 -125.5 -68t-138.5 15q-68 29 -109 89.5t-41 133.5zM217 504h336v168 q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-168zM246.5 1102.5q0.5 14.5 11.5 25.5l205 185q12 10 27 9.5t26 -11.5q10 -11 9 -25.5t-13 -25.5l-205 -185q-11 -10 -25.5 -9.5t-25.5 11.5q-10 11 -9.5 25.5z" />
+<glyph unicode="&#xea;" horiz-adv-x="753" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-205q0 -37 -37 -37h-373v-168q0 -51 29 -93.5t76 -62.5t97 -10t85 47q11 11 26 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-54 -54 -125.5 -68t-138.5 15q-68 29 -109 89.5t-41 133.5zM164 1105q2 14 14 25 l184 144q25 15 46 0l184 -144q13 -11 15.5 -24.5t-7.5 -26.5q-11 -12 -26 -14t-27 8q-66 51 -68 53q-19 15 -52.5 40t-41.5 32q-3 -2 -37 -28t-57 -44q-2 -2 -68 -53q-11 -10 -25.5 -8t-25.5 14q-10 12 -8 26zM217 504h336v168q0 70 -48.5 119t-119.5 49q-69 0 -118.5 -49.5 t-49.5 -118.5v-168z" />
+<glyph unicode="&#xeb;" horiz-adv-x="753" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-205q0 -37 -37 -37h-373v-168q0 -51 29 -93.5t76 -62.5t97 -10t85 47q11 11 26 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-54 -54 -125.5 -68t-138.5 15q-68 29 -109 89.5t-41 133.5zM164 1184q0 23 17 40 t40 17q25 0 41.5 -16.5t16.5 -40.5t-17 -41t-41 -17q-23 0 -40 17.5t-17 40.5zM217 504h336v168q0 70 -48.5 119t-119.5 49q-69 0 -118.5 -49.5t-49.5 -118.5v-168zM492 1184q0 23 17 40t40 17q24 0 40.5 -16.5t16.5 -40.5t-16.5 -41t-40.5 -17q-23 0 -40 17.5t-17 40.5z " />
+<glyph unicode="&#xec;" horiz-adv-x="409" d="M25.5 1285.5q-0.5 14.5 9.5 25.5q11 11 26 11.5t27 -9.5l205 -185q11 -11 11.5 -25.5t-9.5 -25.5q-11 -11 -26 -11.5t-27 9.5l-205 185q-11 11 -11.5 25.5zM168 57v820q0 16 11 26t26 10q37 0 37 -36v-820q0 -37 -37 -37q-14 0 -25.5 10.5t-11.5 26.5z" />
+<glyph unicode="&#xed;" horiz-adv-x="409" d="M106 1102.5q1 14.5 13 25.5l205 185q11 10 25.5 9.5t25.5 -11.5q10 -11 9.5 -25.5t-11.5 -25.5l-205 -185q-12 -10 -27 -9.5t-26 11.5q-10 11 -9 25.5zM168 57v820q0 36 37 36t37 -36v-820q0 -37 -37 -37t-37 37z" />
+<glyph unicode="&#xee;" horiz-adv-x="409" d="M-15.5 1105.5q2.5 13.5 15.5 24.5l184 144q21 15 45 0l185 -144q12 -11 14 -25t-8 -26q-11 -12 -25.5 -14t-25.5 8q-65 51 -68 53q-19 15 -52.5 40t-41.5 32q-3 -2 -37 -28t-57 -44q-3 -2 -68 -53q-12 -10 -27 -8t-26 14q-10 13 -7.5 26.5zM170 57v820q0 36 37 36t37 -36 v-820q0 -37 -37 -37t-37 37z" />
+<glyph unicode="&#xef;" horiz-adv-x="409" d="M-16 1184q0 24 16.5 40.5t40.5 16.5q23 0 40 -17t17 -40t-17 -40.5t-40 -17.5q-24 0 -40.5 17t-16.5 41zM168 57v820q0 36 37 36t37 -36v-820q0 -37 -37 -37t-37 37zM311 1184q0 24 16.5 40.5t41.5 16.5q23 0 40 -17t17 -40t-17 -40.5t-40 -17.5q-24 0 -41 17t-17 41z " />
+<glyph unicode="&#xf0;" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5q79 0 139 -45q-43 146 -135 254l-106 -71q-24 -21 -52 10q-7 12 -4 26t15 25l96 64q-32 32 -98 79q-12 7 -15.5 23.5t4.5 28.5q23 29 52 10q71 -49 120 -98l86 57q12 8 28.5 5t23.5 -15q10 -12 6 -28t-17 -24l-77 -51q51 -60 84 -125 q92 -168 92 -366v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410z" />
+<glyph unicode="&#xf1;" horiz-adv-x="802" d="M168 57v811q0 37 37 37h205q98 0 169.5 -71.5t71.5 -169.5v-607q0 -37 -37 -37q-36 0 -36 37v607q0 69 -49.5 118t-118.5 49h-168v-774q0 -37 -37 -37t-37 37zM195 1180q40 57 106 69t123 -30l16 -13q26 -20 66 -14q35 6 57 37q7 12 22.5 15t28.5 -5q12 -11 15 -25 t-4 -26q-37 -58 -107 -70q-67 -12 -123 31l-16 12q-27 21 -66 15q-38 -6 -57 -37q-11 -12 -25 -15t-26 4q-31 28 -10 52z" />
+<glyph unicode="&#xf2;" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410zM245 1285.5 q-1 14.5 9 25.5q11 11 26.5 11.5t26.5 -9.5l205 -185q11 -11 11.5 -25.5t-9.5 -25.5q-11 -11 -25.5 -11.5t-25.5 9.5l-205 185q-12 11 -13 25.5z" />
+<glyph unicode="&#xf3;" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410zM246.5 1102.5 q0.5 14.5 11.5 25.5l205 185q11 10 25.5 9.5t25.5 -11.5q10 -11 9.5 -25.5t-11.5 -25.5l-205 -185q-11 -10 -25.5 -9.5t-25.5 11.5q-10 11 -9.5 25.5z" />
+<glyph unicode="&#xf4;" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM164 1105q2 14 14 25l184 144q23 20 46 0l184 -144q12 -11 14 -25t-8 -26q-11 -12 -25 -14t-26 8q-66 51 -68 53q-19 15 -52.5 40t-41.5 32q-3 -2 -37 -28 t-57 -44q-2 -2 -68 -53q-12 -10 -26 -8t-25 14q-10 12 -8 26zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410z" />
+<glyph unicode="&#xf5;" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM170 1180q40 57 106 69q72 9 123 -30l17 -13q26 -20 65 -14t58 37q23 29 51 10q31 -27 10 -51q-40 -58 -106 -70q-65 -12 -123 31l-17 12q-27 21 -65 15 q-37 -6 -58 -37q-7 -12 -23 -15.5t-28 4.5q-29 23 -10 52zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410z" />
+<glyph unicode="&#xf6;" horiz-adv-x="770" d="M143 262v410q0 98 72 169.5t170 71.5t170 -71.5t72 -169.5v-410q0 -98 -72 -170t-170 -72t-170 72t-72 170zM164 1184q0 23 17 40t40 17t40.5 -17t17.5 -40t-17.5 -40.5t-40.5 -17.5t-40 17.5t-17 40.5zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5 v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410zM492 1184q0 23 17 40t40 17t40 -17t17 -40t-17 -40.5t-40 -17.5t-40 17.5t-17 40.5z" />
+<glyph unicode="&#xf7;" d="M139 590q0 17 12 28t29 11h492q17 0 28 -11t11 -28t-11 -28t-28 -11h-492q-17 0 -29 11t-12 28zM365 324q0 25 17.5 42t43.5 17q25 0 42 -17t17 -42t-17 -42.5t-42 -17.5q-26 0 -43.5 17.5t-17.5 42.5zM365 856q0 25 17.5 42t43.5 17q25 0 42 -17t17 -42t-17 -42t-42 -17 q-26 0 -43.5 17t-17.5 42z" />
+<glyph unicode="&#xf8;" horiz-adv-x="790" d="M72.5 -34.5q-4.5 15.5 3.5 28.5l98 168q-20 50 -20 100v410q0 98 71.5 169.5t169.5 71.5q107 0 174 -73l80 137q8 14 22.5 18t28.5 -6q14 -8 18.5 -22.5t-5.5 -26.5l-99 -168q23 -54 23 -100v-410q0 -98 -72 -170t-170 -72q-106 0 -174 74l-82 -137q-7 -13 -22 -17.5 t-27 3.5q-13 7 -17.5 22.5zM227 252l303 520q-51 68 -135 68q-69 0 -118.5 -49.5t-49.5 -118.5v-410v-10zM260 162q51 -68 135 -68q69 0 118.5 49.5t49.5 118.5v410v10z" />
+<glyph unicode="&#xf9;" horiz-adv-x="802" d="M152 270v607q0 36 36 36q37 0 37 -36v-607q0 -69 49.5 -118.5t118.5 -49.5h168v775q0 36 37 36t37 -36v-811q0 -37 -37 -37h-205q-98 0 -169.5 71.5t-71.5 169.5zM254.5 1285.5q-0.5 14.5 9.5 25.5q11 11 25.5 11.5t25.5 -9.5l205 -185q12 -11 13 -25.5t-9 -25.5 q-11 -11 -26 -11.5t-27 9.5l-205 185q-11 11 -11.5 25.5z" />
+<glyph unicode="&#xfa;" horiz-adv-x="802" d="M152 270v607q0 36 36 36q37 0 37 -36v-607q0 -69 49.5 -118.5t118.5 -49.5h168v775q0 36 37 36t37 -36v-811q0 -37 -37 -37h-205q-98 0 -169.5 71.5t-71.5 169.5zM254.5 1102.5q0.5 14.5 11.5 25.5l205 185q12 10 27 9.5t26 -11.5q10 -11 9 -25.5t-13 -25.5l-205 -185 q-11 -10 -25.5 -9.5t-25.5 11.5q-10 11 -9.5 25.5z" />
+<glyph unicode="&#xfb;" horiz-adv-x="802" d="M152 270v607q0 36 36 36q37 0 37 -36v-607q0 -69 49.5 -118.5t118.5 -49.5h168v775q0 36 37 36t37 -36v-811q0 -37 -37 -37h-205q-98 0 -169.5 71.5t-71.5 169.5zM180 1079q-21 24 6 51l185 144q24 15 45 0l184 -144q13 -11 15.5 -24.5t-7.5 -26.5q-11 -12 -26 -14t-27 8 q-66 51 -68 53q-19 15 -52.5 40t-41.5 32q-3 -2 -37 -28t-57 -44q-3 -2 -68 -53q-11 -10 -25.5 -8t-25.5 14z" />
+<glyph unicode="&#xfc;" horiz-adv-x="802" d="M152 270v607q0 16 11 26t25 10q37 0 37 -36v-607q0 -69 49.5 -118.5t118.5 -49.5h168v775q0 16 11 26t26 10q37 0 37 -36v-811q0 -37 -37 -37h-205q-98 0 -169.5 71.5t-71.5 169.5zM172 1184q0 23 17 40t40 17q25 0 41.5 -16.5t16.5 -40.5t-17 -41t-41 -17 q-23 0 -40 17.5t-17 40.5zM500 1184q0 23 17 40t40 17q24 0 40.5 -16.5t16.5 -40.5t-16.5 -41t-40.5 -17q-23 0 -40 17.5t-17 40.5z" />
+<glyph unicode="&#xfd;" horiz-adv-x="755" d="M66 -352q0 37 36 37q58 0 101 32q46 37 61 86l76 254l-244 809q-4 13 3.5 26.5t21.5 18.5q38 10 47 -24l211 -701l209 701q9 34 47 24q14 -5 21 -18.5t3 -26.5l-245 -819l-80 -264q-20 -72 -86 -125q-65 -47 -146 -47q-36 0 -36 37zM239 1102.5q1 14.5 13 25.5l205 185 q11 10 25.5 9.5t25.5 -11.5q10 -11 9.5 -25.5t-11.5 -25.5l-205 -185q-12 -10 -27 -9.5t-26 11.5q-10 11 -9 25.5z" />
+<glyph unicode="&#xfe;" horiz-adv-x="794" d="M168 -352v1638q0 37 37 37t37 -37v-373h168q98 0 169.5 -71.5t71.5 -169.5v-410q0 -98 -71.5 -170t-169.5 -72h-168v-372q0 -37 -37 -37t-37 37zM242 94h168q69 0 118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5h-168v-746z" />
+<glyph unicode="&#xff;" horiz-adv-x="755" d="M66 -352q0 37 36 37q58 0 101 32q46 37 61 86l76 254l-244 809q-4 13 3.5 26.5t21.5 18.5q38 10 47 -24l211 -701l209 701q9 34 47 24q14 -5 21 -18.5t3 -26.5l-245 -819l-80 -264q-20 -72 -86 -125q-65 -47 -146 -47q-36 0 -36 37zM158 1184q0 24 16.5 40.5t40.5 16.5 q23 0 40 -17t17 -40t-17 -40.5t-40 -17.5q-24 0 -40.5 17t-16.5 41zM485 1184q0 24 16.5 40.5t41.5 16.5q23 0 40 -17t17 -40t-17 -40.5t-40 -17.5q-24 0 -41 17t-17 41z" />
+<glyph unicode="&#x152;" horiz-adv-x="1220" d="M141 303v696q0 119 83.5 202t201.5 83q119 0 207 -88v41q0 39 39 39h409q17 0 28 -11t11 -28t-11 -28t-28 -11h-370v-240v-247h329q17 0 28 -11t11 -28t-11 -28t-28 -11h-329v-289v-240h370q17 0 28 -10.5t11 -27.5t-11 -28t-28 -11h-409q-39 0 -39 39v40 q-88 -88 -207 -88q-118 0 -201.5 83t-83.5 202zM219 303q0 -86 60.5 -146.5t146.5 -60.5q85 0 146 60.5t61 146.5v41v614v41q0 86 -61 146.5t-146 60.5q-86 0 -146.5 -60.5t-60.5 -146.5v-696z" />
+<glyph unicode="&#x153;" horiz-adv-x="1163" d="M143 262v410q0 98 72 169.5t170 71.5q65 0 118.5 -30t86.5 -82q33 52 86.5 82t118.5 30q98 0 169.5 -71.5t71.5 -169.5v-205q0 -37 -37 -37h-372v-168q0 -51 27.5 -93.5t74.5 -62.5t97 -10t87 47q11 11 25 11q16 0 27 -11q10 -10 10 -26q0 -15 -10 -25q-54 -54 -125 -68 t-138 15q-74 29 -112 94q-33 -52 -86.5 -82.5t-118.5 -30.5q-98 0 -170 72t-72 170zM217 262q0 -69 49.5 -118.5t118.5 -49.5t118.5 49.5t49.5 118.5v410q0 69 -49.5 118.5t-118.5 49.5t-118.5 -49.5t-49.5 -118.5v-410zM627 504h336v168q0 69 -49.5 118.5t-118.5 49.5 t-118.5 -49.5t-49.5 -118.5v-168z" />
+<glyph unicode="&#x178;" horiz-adv-x="823" d="M88 1227q-17 38 18 53q14 7 30 2.5t24 -18.5q56 -114 104 -211q33 -68 88 -177t58 -116q5 10 60 117.5t87 175.5q87 173 105 211q17 31 53 16q31 -17 18 -53l-282 -565v-605q0 -17 -12 -28t-29 -11t-28 11t-11 28v605zM186 1593q0 25 17.5 42.5t42.5 17.5t43 -17.5 t18 -42.5t-18 -42t-43 -17t-42.5 17t-17.5 42zM514 1593q0 25 17 42.5t42 17.5t43.5 -17.5t18.5 -42.5q0 -24 -18.5 -41.5t-43.5 -17.5t-42 17t-17 42z" />
+<glyph unicode="&#x2c6;" horiz-adv-x="1024" d="M291 1105q2 14 14 25l184 144q25 15 46 0l184 -144q12 -11 14 -25t-8 -26q-11 -12 -25 -14t-26 8q-65 51 -68 53q-19 15 -52.5 40t-41.5 32q-3 -2 -37 -28t-57 -44q-3 -2 -68 -53q-12 -10 -26 -8t-25 14q-10 12 -8 26z" />
+<glyph unicode="&#x2dc;" horiz-adv-x="1024" d="M297 1180q40 57 106 69q68 13 123 -30l17 -13q26 -20 65 -14t58 37q23 29 51 10q31 -27 10 -51q-40 -58 -106 -70q-65 -12 -123 31l-17 12q-27 21 -65 15q-37 -6 -58 -37q-7 -12 -23 -15.5t-28 4.5q-29 23 -10 52z" />
+<glyph unicode="&#x2000;" horiz-adv-x="886" />
+<glyph unicode="&#x2001;" horiz-adv-x="1772" />
+<glyph unicode="&#x2002;" horiz-adv-x="886" />
+<glyph unicode="&#x2003;" horiz-adv-x="1772" />
+<glyph unicode="&#x2004;" horiz-adv-x="590" />
+<glyph unicode="&#x2005;" horiz-adv-x="443" />
+<glyph unicode="&#x2006;" horiz-adv-x="295" />
+<glyph unicode="&#x2007;" horiz-adv-x="295" />
+<glyph unicode="&#x2008;" horiz-adv-x="221" />
+<glyph unicode="&#x2009;" horiz-adv-x="354" />
+<glyph unicode="&#x200a;" horiz-adv-x="98" />
+<glyph unicode="&#x2010;" d="M139 590q0 17 12 28t29 11h492q17 0 28 -11t11 -28t-11 -28t-28 -11h-492q-17 0 -29 11t-12 28z" />
+<glyph unicode="&#x2011;" d="M139 590q0 17 12 28t29 11h492q17 0 28 -11t11 -28t-11 -28t-28 -11h-492q-17 0 -29 11t-12 28z" />
+<glyph unicode="&#x2012;" d="M139 590q0 17 12 28t29 11h492q17 0 28 -11t11 -28t-11 -28t-28 -11h-492q-17 0 -29 11t-12 28z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1024" d="M-39 590q0 17 11 28t28 11h1024q39 0 39 -39t-39 -39h-1024q-17 0 -28 11t-11 28z" />
+<glyph unicode="&#x2014;" horiz-adv-x="2048" d="M-39 590q0 17 11 28t28 11h2048q39 0 39 -39t-39 -39h-2048q-17 0 -28 11t-11 28z" />
+<glyph unicode="&#x2018;" horiz-adv-x="393" d="M125 1049v98q0 43 23.5 79.5t64.5 53.5q35 12 49 -20q6 -13 0.5 -28.5t-18.5 -21.5q-43 -15 -43 -63v-62h28q37 0 37 -36q0 -37 -37 -37h-65q-17 0 -28 10t-11 27z" />
+<glyph unicode="&#x2019;" horiz-adv-x="393" d="M125 1237q0 17 11 27t28 10h65q37 0 37 -37v-98q0 -43 -23.5 -79.5t-64.5 -53.5q-35 -12 -49 20q-6 13 -0.5 28t18.5 21q43 15 43 64v61h-26q-17 0 -28 10t-11 27z" />
+<glyph unicode="&#x201a;" horiz-adv-x="391" d="M125 98q0 17 11 27t28 10h65q37 0 37 -37v-98q0 -43 -23.5 -79.5t-64.5 -53.5q-35 -12 -49 20q-6 13 -0.5 28.5t18.5 21.5q43 15 43 63v61h-26q-17 0 -28 10t-11 27z" />
+<glyph unicode="&#x201c;" horiz-adv-x="759" d="M125 1049v98q0 43 23.5 79.5t64.5 53.5q13 6 28 0t21 -20q6 -13 0 -28.5t-20 -21.5q-43 -15 -43 -63v-62h28q37 0 37 -36q0 -37 -37 -37h-65q-37 0 -37 37zM494 1049v98q0 43 23.5 79.5t64.5 53.5q13 6 28 0t21 -20q6 -13 -0.5 -28.5t-20.5 -21.5q-43 -15 -43 -63v-62h29 q37 0 37 -36q0 -37 -37 -37h-66q-36 0 -36 37z" />
+<glyph unicode="&#x201d;" horiz-adv-x="761" d="M127 1237q0 37 37 37h65q37 0 37 -37v-98q0 -43 -23.5 -79.5t-64.5 -53.5q-13 -6 -28 0t-21 20q-6 13 0.5 28t20.5 21q43 15 43 64v61h-29q-37 0 -37 37zM496 1237q0 37 36 37h66q37 0 37 -37v-98q0 -43 -23.5 -79.5t-64.5 -53.5q-13 -6 -28 0t-21 20q-6 13 0 28t20 21 q43 15 43 64v61h-29q-36 0 -36 37z" />
+<glyph unicode="&#x201e;" horiz-adv-x="761" d="M127 98q0 37 37 37h65q37 0 37 -37v-98q0 -43 -23.5 -79.5t-64.5 -53.5q-13 -6 -28 0t-21 20q-6 13 0.5 28.5t20.5 21.5q43 15 43 63v61h-29q-37 0 -37 37zM496 98q0 37 36 37h66q37 0 37 -37v-98q0 -43 -23.5 -79.5t-64.5 -53.5q-13 -6 -28 0t-21 20q-6 13 0 28.5 t20 21.5q43 15 43 63v61h-29q-36 0 -36 37z" />
+<glyph unicode="&#x2022;" horiz-adv-x="524" d="M84 578q0 74 52 126t126 52t127 -52.5t53 -125.5t-53 -126t-127 -53q-73 0 -125.5 52.5t-52.5 126.5zM156 578q0 -45 31 -77t75 -32t76.5 32.5t32.5 76.5t-32.5 76t-76.5 32t-75 -32t-31 -76z" />
+<glyph unicode="&#x2026;" horiz-adv-x="1179" d="M121 78q0 25 17 42t42 17q26 0 44 -17t18 -42t-18 -42.5t-44 -17.5q-25 0 -42 17.5t-17 42.5zM530 78q0 25 17.5 42t42.5 17q26 0 43.5 -17t17.5 -42t-17.5 -42.5t-43.5 -17.5q-25 0 -42.5 17.5t-17.5 42.5zM940 78q0 25 17 42t42 17q26 0 44 -17t18 -42t-18 -42.5 t-44 -17.5q-25 0 -42 17.5t-17 42.5z" />
+<glyph unicode="&#x202f;" horiz-adv-x="354" />
+<glyph unicode="&#x2039;" horiz-adv-x="489" d="M131 578q-12 18 0 38l131 238q17 31 53 16q21 -15 21 -34q0 -10 -6 -19q-9 -18 -27.5 -50.5t-26.5 -47.5q-14 -23 -28.5 -49.5t-25.5 -47t-13 -24.5q14 -29 72 -131q27 -49 57 -105q6 -9 6 -18q0 -21 -20 -35q-39 -17 -54 17z" />
+<glyph unicode="&#x203a;" horiz-adv-x="489" d="M147 344q0 8 7 18q30 56 57 105q40 71 70 131q-26 46 -66 121q-8 15 -26 47.5t-27 50.5q-6 9 -6 19q0 20 20 34q14 7 29.5 2.5t23.5 -18.5l131 -238q9 -20 0 -38l-139 -252q-7 -16 -23.5 -20.5t-29.5 3.5q-21 15 -21 35z" />
+<glyph unicode="&#x205f;" horiz-adv-x="443" />
+<glyph unicode="&#x20ac;" d="M61 487q0 17 12 28t29 11h123v250h-123q-17 0 -29 11t-12 28t12 28t29 11h123v145q0 98 59.5 174t153.5 101q93 26 181.5 -10.5t138.5 -120.5l20 -37q8 -13 3.5 -29.5t-17.5 -23.5q-13 -8 -29.5 -3.5t-23.5 17.5l-21 35q-35 61 -99 88t-132 8q-67 -17 -110.5 -73.5 t-43.5 -125.5v-145h289q17 0 28 -11t11 -28t-11 -28t-28 -11h-289v-250h207q17 0 28 -11t11 -28t-11 -27.5t-28 -10.5h-207v-146q0 -72 43 -127t111 -72q67 -19 131.5 8t99.5 89l21 35q7 13 23.5 17.5t29.5 -3.5q13 -7 17.5 -23.5t-3.5 -29.5l-20 -37q-47 -84 -137 -121 t-183 -12q-94 26 -153.5 102t-59.5 174v146h-123q-18 0 -29.5 10.5t-11.5 27.5z" />
+<glyph unicode="&#x2122;" horiz-adv-x="1372" d="M80 1237q0 37 37 37h409q17 0 28 -10t11 -27t-11 -27t-28 -10h-166v-569q0 -17 -10.5 -27t-27.5 -10q-37 0 -37 37v569h-168q-37 0 -37 37zM735 631v614q0 29 27 35q26 10 41 -12q10 -13 34 -48.5t37 -52.5q99 -139 103 -145q27 36 104 145q12 17 35.5 51.5t34.5 49.5 q18 21 43 12q27 -9 27 -35v-614q0 -17 -11 -27t-28 -10q-37 0 -37 37v497q-7 -10 -41.5 -57t-50.5 -70q-7 -11 -22.5 -32t-22.5 -33q-8 -14 -31 -14q-17 0 -31 14q-7 12 -22.5 33t-22.5 32q-38 54 -90 127v-497q0 -17 -11 -27t-28 -10q-37 0 -37 37z" />
+<glyph unicode="&#xe000;" horiz-adv-x="911" d="M0 0v911h911v-911h-911z" />
+<hkern u1="A" u2="&#x201d;" k="143" />
+<hkern u1="A" u2="&#x2019;" k="143" />
+<hkern u1="A" u2="y" k="20" />
+<hkern u1="A" u2="w" k="20" />
+<hkern u1="A" u2="v" k="20" />
+<hkern u1="A" u2="Y" k="102" />
+<hkern u1="A" u2="W" k="82" />
+<hkern u1="A" u2="V" k="82" />
+<hkern u1="A" u2="T" k="102" />
+<hkern u1="B" u2="A" k="20" />
+<hkern u1="D" u2="Y" k="41" />
+<hkern u1="D" u2="W" k="20" />
+<hkern u1="D" u2="V" k="20" />
+<hkern u1="D" u2="A" k="20" />
+<hkern u1="F" u2="&#xf6;" k="20" />
+<hkern u1="F" u2="&#xe5;" k="20" />
+<hkern u1="F" u2="&#xe4;" k="20" />
+<hkern u1="F" u2="r" k="20" />
+<hkern u1="F" u2="o" k="41" />
+<hkern u1="F" u2="i" k="20" />
+<hkern u1="F" u2="e" k="41" />
+<hkern u1="F" u2="a" k="20" />
+<hkern u1="F" u2="A" k="82" />
+<hkern u1="F" u2="&#x2e;" k="225" />
+<hkern u1="F" u2="&#x2c;" k="225" />
+<hkern u1="J" u2="&#x2e;" k="41" />
+<hkern u1="J" u2="&#x2c;" k="41" />
+<hkern u1="K" u2="&#xf6;" k="41" />
+<hkern u1="K" u2="y" k="41" />
+<hkern u1="K" u2="u" k="20" />
+<hkern u1="K" u2="o" k="41" />
+<hkern u1="K" u2="e" k="41" />
+<hkern u1="K" u2="O" k="20" />
+<hkern u1="L" u2="&#x201d;" k="164" />
+<hkern u1="L" u2="&#x2019;" k="184" />
+<hkern u1="L" u2="y" k="82" />
+<hkern u1="L" u2="Y" k="123" />
+<hkern u1="L" u2="W" k="123" />
+<hkern u1="L" u2="V" k="123" />
+<hkern u1="L" u2="T" k="123" />
+<hkern u1="O" u2="Y" k="41" />
+<hkern u1="O" u2="W" k="41" />
+<hkern u1="O" u2="V" k="20" />
+<hkern u1="O" u2="T" k="41" />
+<hkern u1="O" u2="A" k="20" />
+<hkern u1="P" u2="&#xf6;" k="20" />
+<hkern u1="P" u2="&#xe5;" k="41" />
+<hkern u1="P" u2="&#xe4;" k="41" />
+<hkern u1="P" u2="o" k="20" />
+<hkern u1="P" u2="e" k="20" />
+<hkern u1="P" u2="a" k="41" />
+<hkern u1="P" u2="A" k="82" />
+<hkern u1="P" u2="&#x2e;" k="225" />
+<hkern u1="P" u2="&#x2c;" k="225" />
+<hkern u1="R" u2="Y" k="41" />
+<hkern u1="R" u2="T" k="20" />
+<hkern u1="T" u2="&#xf6;" k="61" />
+<hkern u1="T" u2="&#xe5;" k="102" />
+<hkern u1="T" u2="&#xe4;" k="61" />
+<hkern u1="T" u2="y" k="143" />
+<hkern u1="T" u2="w" k="143" />
+<hkern u1="T" u2="u" k="143" />
+<hkern u1="T" u2="r" k="143" />
+<hkern u1="T" u2="o" k="143" />
+<hkern u1="T" u2="i" k="41" />
+<hkern u1="T" u2="e" k="143" />
+<hkern u1="T" u2="a" k="164" />
+<hkern u1="T" u2="O" k="20" />
+<hkern u1="T" u2="A" k="102" />
+<hkern u1="T" u2="&#x3b;" k="123" />
+<hkern u1="T" u2="&#x3a;" k="123" />
+<hkern u1="T" u2="&#x2e;" k="164" />
+<hkern u1="T" u2="&#x2d;" k="164" />
+<hkern u1="T" u2="&#x2c;" k="164" />
+<hkern u1="U" u2="A" k="20" />
+<hkern u1="V" u2="&#xf6;" k="61" />
+<hkern u1="V" u2="&#xe5;" k="82" />
+<hkern u1="V" u2="&#xe4;" k="61" />
+<hkern u1="V" u2="u" k="82" />
+<hkern u1="V" u2="o" k="102" />
+<hkern u1="V" u2="e" k="102" />
+<hkern u1="V" u2="a" k="102" />
+<hkern u1="V" u2="O" k="20" />
+<hkern u1="V" u2="A" k="82" />
+<hkern u1="V" u2="&#x3b;" k="61" />
+<hkern u1="V" u2="&#x3a;" k="61" />
+<hkern u1="V" u2="&#x2e;" k="205" />
+<hkern u1="V" u2="&#x2d;" k="102" />
+<hkern u1="V" u2="&#x2c;" k="205" />
+<hkern u1="W" u2="y" k="61" />
+<hkern u1="W" u2="u" k="82" />
+<hkern u1="W" u2="o" k="82" />
+<hkern u1="W" u2="e" k="82" />
+<hkern u1="W" u2="a" k="82" />
+<hkern u1="W" u2="O" k="41" />
+<hkern u1="W" u2="A" k="82" />
+<hkern u1="W" u2="&#x3b;" k="82" />
+<hkern u1="W" u2="&#x3a;" k="82" />
+<hkern u1="W" u2="&#x2e;" k="184" />
+<hkern u1="W" u2="&#x2d;" k="82" />
+<hkern u1="W" u2="&#x2c;" k="184" />
+<hkern u1="Y" u2="x" k="102" />
+<hkern u1="Y" u2="v" k="102" />
+<hkern u1="Y" u2="u" k="123" />
+<hkern u1="Y" u2="s" k="143" />
+<hkern u1="Y" u2="p" k="123" />
+<hkern u1="Y" u2="o" k="143" />
+<hkern u1="Y" u2="n" k="123" />
+<hkern u1="Y" u2="m" k="123" />
+<hkern u1="Y" u2="i" k="41" />
+<hkern u1="Y" u2="g" k="143" />
+<hkern u1="Y" u2="e" k="143" />
+<hkern u1="Y" u2="a" k="143" />
+<hkern u1="Y" u2="O" k="41" />
+<hkern u1="Y" u2="A" k="102" />
+<hkern u1="Y" u2="&#x3b;" k="102" />
+<hkern u1="Y" u2="&#x3a;" k="102" />
+<hkern u1="Y" u2="&#x2e;" k="184" />
+<hkern u1="Y" u2="&#x2d;" k="184" />
+<hkern u1="Y" u2="&#x2c;" k="184" />
+<hkern u1="a" u2="y" k="20" />
+<hkern u1="a" u2="w" k="20" />
+<hkern u1="a" u2="v" k="20" />
+<hkern u1="b" u2="y" k="20" />
+<hkern u1="b" u2="v" k="20" />
+<hkern u1="c" u2="y" k="20" />
+<hkern u1="e" u2="y" k="20" />
+<hkern u1="e" u2="w" k="20" />
+<hkern u1="e" u2="v" k="20" />
+<hkern u1="f" u2="&#x201d;" k="-41" />
+<hkern u1="f" u2="&#x2019;" k="-41" />
+<hkern u1="k" u2="&#xf6;" k="20" />
+<hkern u1="k" u2="o" k="20" />
+<hkern u1="k" u2="e" k="20" />
+<hkern u1="o" u2="y" k="20" />
+<hkern u1="o" u2="w" k="20" />
+<hkern u1="o" u2="v" k="20" />
+<hkern u1="p" u2="y" k="20" />
+<hkern u1="r" u2="&#xf6;" k="20" />
+<hkern u1="r" u2="&#xe5;" k="20" />
+<hkern u1="r" u2="&#xe4;" k="20" />
+<hkern u1="r" u2="y" k="-41" />
+<hkern u1="r" u2="v" k="-41" />
+<hkern u1="r" u2="t" k="-41" />
+<hkern u1="r" u2="o" k="20" />
+<hkern u1="r" u2="c" k="20" />
+<hkern u1="r" u2="a" k="20" />
+<hkern u1="r" u2="&#x2e;" k="164" />
+<hkern u1="r" u2="&#x2d;" k="61" />
+<hkern u1="r" u2="&#x2c;" k="164" />
+<hkern u1="s" u2="w" k="20" />
+<hkern u1="v" u2="&#xf6;" k="20" />
+<hkern u1="v" u2="&#xe5;" k="20" />
+<hkern u1="v" u2="&#xe4;" k="20" />
+<hkern u1="v" u2="o" k="20" />
+<hkern u1="v" u2="e" k="20" />
+<hkern u1="v" u2="a" k="20" />
+<hkern u1="v" u2="&#x2e;" k="143" />
+<hkern u1="v" u2="&#x2c;" k="143" />
+<hkern u1="w" u2="s" k="20" />
+<hkern u1="w" u2="o" k="20" />
+<hkern u1="w" u2="e" k="20" />
+<hkern u1="w" u2="a" k="20" />
+<hkern u1="w" u2="&#x2e;" k="123" />
+<hkern u1="w" u2="&#x2c;" k="123" />
+<hkern u1="y" u2="o" k="20" />
+<hkern u1="y" u2="e" k="20" />
+<hkern u1="y" u2="a" k="20" />
+<hkern u1="y" u2="&#x2e;" k="143" />
+<hkern u1="y" u2="&#x2c;" k="143" />
+<hkern u1="z" u2="o" k="20" />
+<hkern u1="z" u2="e" k="20" />
+<hkern u1="&#xe4;" u2="v" k="20" />
+<hkern u1="&#xe5;" u2="v" k="20" />
+<hkern u1="&#xf6;" u2="v" k="20" />
+<hkern u1="&#x2018;" u2="A" k="143" />
+<hkern u1="&#x2019;" u2="s" k="123" />
+<hkern u1="&#x2019;" u2="d" k="164" />
+<hkern u1="&#x201c;" u2="A" k="143" />
+</font>
+</defs></svg> \ No newline at end of file
diff --git a/tipboard/static/fonts/miso-light-webfont.ttf b/tipboard/static/fonts/miso-light-webfont.ttf
new file mode 100644
index 0000000..a29c141
--- /dev/null
+++ b/tipboard/static/fonts/miso-light-webfont.ttf
Binary files differ
diff --git a/tipboard/static/fonts/miso-light-webfont.woff b/tipboard/static/fonts/miso-light-webfont.woff
new file mode 100644
index 0000000..53c6f81
--- /dev/null
+++ b/tipboard/static/fonts/miso-light-webfont.woff
Binary files differ
diff --git a/tipboard/static/fonts/miso-webfont.eot b/tipboard/static/fonts/miso-webfont.eot
new file mode 100644
index 0000000..956e766
--- /dev/null
+++ b/tipboard/static/fonts/miso-webfont.eot
Binary files differ
diff --git a/tipboard/static/fonts/miso-webfont.svg b/tipboard/static/fonts/miso-webfont.svg
new file mode 100644
index 0000000..e33256c
--- /dev/null
+++ b/tipboard/static/fonts/miso-webfont.svg
@@ -0,0 +1,420 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="misoregular" horiz-adv-x="851" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="393" />
+<glyph horiz-adv-x="2048" />
+<glyph horiz-adv-x="2048" />
+<glyph unicode="&#xd;" horiz-adv-x="2048" />
+<glyph unicode=" " horiz-adv-x="393" />
+<glyph unicode="&#x09;" horiz-adv-x="393" />
+<glyph unicode="&#xa0;" horiz-adv-x="393" />
+<glyph unicode="!" horiz-adv-x="409" d="M119 78q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5zM139 385v819q0 29 19.5 47.5t46.5 18.5q29 0 47 -18.5t18 -47.5v-819q0 -29 -18 -47.5t-47 -18.5q-27 0 -46.5 18.5t-19.5 47.5z" />
+<glyph unicode="&#x22;" horiz-adv-x="720" d="M131 1040v205q0 29 18.5 47.5t47.5 18.5t47 -18.5t18 -47.5v-205q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47zM459 1040v205q0 29 18 47.5t47 18.5t47.5 -18.5t18.5 -47.5v-205q0 -29 -18.5 -47t-47.5 -18t-47 18t-18 47z" />
+<glyph unicode="#" horiz-adv-x="937" d="M39 508q0 26 17.5 43.5t43.5 17.5h144v203h-144q-26 0 -43.5 18.5t-17.5 45.5q0 26 17.5 43.5t43.5 17.5h144v348q0 26 17.5 44t43.5 18t45 -18t19 -44v-348h202v348q0 26 18 44t44 18t44.5 -18t18.5 -44v-348h142q26 0 44.5 -17.5t18.5 -43.5t-18.5 -45t-44.5 -19h-142 v-203h142q26 0 44.5 -17.5t18.5 -43.5t-18.5 -45t-44.5 -19h-142v-387q0 -26 -18.5 -44.5t-44.5 -18.5t-44 18.5t-18 44.5v387h-202v-387q0 -26 -19 -44.5t-45 -18.5t-43.5 18.5t-17.5 44.5v387h-144q-26 0 -43.5 18.5t-17.5 45.5zM369 569h202v203h-202v-203z" />
+<glyph unicode="$" d="M146 272.5q7 26.5 30 40.5q23 12 50 5.5t40 -29.5q40 -70 117 -82q4 -2 12 -2h15h2q61 0 108 41q60 51 60 129q-3 79 -64 127l-256 211q-106 86 -106 221q-3 140 102 225q53 47 123 62v106q0 29 18 47.5t47 18.5q27 0 46.5 -18.5t19.5 -47.5v-106q123 -28 186 -138 q12 -23 5.5 -50t-29.5 -40t-49.5 -5.5t-40.5 30.5q-35 66 -111 78q-4 2 -14 2h-13h-2q-57 0 -102 -39q-58 -49 -55 -123q0 -76 57 -121l258 -211q53 -40 81 -100t30 -129q0 -66 -28 -126.5t-79 -103.5q-50 -42 -127 -65v-105q0 -29 -19 -47t-46 -18q-29 0 -47.5 18t-18.5 47 v107q-126 25 -194 141q-13 23 -6 49.5z" />
+<glyph unicode="%" horiz-adv-x="1214" d="M78 1061q0 100 71.5 172t172.5 72q100 0 171.5 -72t71.5 -172q0 -101 -71.5 -172.5t-171.5 -71.5q-101 0 -172.5 71.5t-71.5 172.5zM197 1061q0 -54 35.5 -89.5t89.5 -35.5t89 35.5t35 89.5t-35 89.5t-89 35.5t-89.5 -35.5t-35.5 -89.5zM259 35.5q-9 25.5 3 50.5 l574 1188q12 24 37.5 33t50.5 -2q24 -12 32.5 -37.5t-2.5 -50.5l-573 -1188q-12 -25 -37.5 -34t-50.5 3t-34 37.5zM651 242q0 100 71.5 171.5t172.5 71.5q100 0 172 -71.5t72 -171.5q0 -101 -72 -172.5t-172 -71.5q-101 0 -172.5 71.5t-71.5 172.5zM770 242 q0 -54 35.5 -89.5t89.5 -35.5t89.5 35.5t35.5 89.5t-35.5 89.5t-89.5 35.5t-89.5 -35.5t-35.5 -89.5z" />
+<glyph unicode="&#x26;" horiz-adv-x="1009" d="M102 254q-21 131 62 240l164 213l-140 180l-2 2v2h-2v2q-41 63 -44 137.5t34 140.5q37 68 103 106t143 34q78 -5 140.5 -46t94.5 -110q33 -69 25 -144.5t-55 -136.5l-131 -167l155 -203l51 -68l64 105q13 24 40 30t50 -8t29.5 -39.5t-6.5 -50.5l-91 -147l162 -207 q17 -23 14 -49t-26 -43q-23 -18 -49 -15.5t-43 25.5l-129 168l-37 -62q-67 -108 -201 -145q-127 -27 -241 47q-113 78 -134 209zM233 274q8 -69 76 -120q60 -43 140 -27q71 15 116 84l64 104l-84 109l-133 174l-144 -184q-47 -65 -35 -140zM289 1108q-39 -74 4 -143 l119 -152l108 141q25 32 29.5 71.5t-12.5 74.5t-50 56.5t-71 23.5q-84 3 -127 -72z" />
+<glyph unicode="'" horiz-adv-x="393" d="M131 1040v205q0 29 18.5 47.5t47.5 18.5q27 0 46 -18.5t19 -47.5v-205q0 -29 -19 -47t-46 -18q-29 0 -47.5 18t-18.5 47z" />
+<glyph unicode="(" horiz-adv-x="548" d="M115 651q0 223 69 418q69 204 205 381q17 23 43 25.5t49 -15.5q23 -17 26.5 -43t-13.5 -49q-121 -154 -187 -342q-61 -180 -61 -375t61 -375q66 -188 187 -342q17 -23 13.5 -49t-26.5 -43q-23 -18 -49 -15t-43 26q-138 175 -205 380q-69 195 -69 418z" />
+<glyph unicode=")" horiz-adv-x="550" d="M41.5 -94q-3.5 25 13.5 49q121 154 187 342q61 180 61 375t-61 375q-66 188 -187 342q-17 24 -13.5 49t24.5 43q23 18 49 15t43 -26q136 -176 207 -380q69 -195 69 -418t-69 -418q-69 -205 -207 -381q-17 -23 -43 -25.5t-49 15.5q-21 18 -24.5 43z" />
+<glyph unicode="*" d="M99 801.5q7 24.5 30 36.5l172 100l-172 100q-23 12 -30 36.5t7 47.5q12 23 36.5 30t47.5 -7l175 -98v198q0 26 17.5 44t43.5 18t44.5 -18t18.5 -44v-198l173 98q23 13 47 6.5t36 -29.5q14 -23 7.5 -47.5t-29.5 -36.5l-172 -100l172 -100q23 -12 29.5 -36.5t-7.5 -47.5 q-12 -23 -36 -29.5t-47 6.5l-173 98v-198q0 -26 -18.5 -45t-44.5 -19t-43.5 18.5t-17.5 45.5v198l-175 -98q-23 -14 -47.5 -7t-36.5 30q-14 23 -7 47.5z" />
+<glyph unicode="+" d="M115 590q0 29 18 47t47 18h180v181q0 29 18.5 47t47.5 18q27 0 46.5 -18.5t19.5 -46.5v-181h180q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-180v-180q0 -28 -19.5 -46.5t-46.5 -18.5q-29 0 -47.5 18t-18.5 47v180h-180q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="," horiz-adv-x="393" d="M102 98q0 26 18 44t44 18h65q26 0 45 -18t19 -44v-98q0 -51 -29 -93.5t-76 -62.5q-25 -10 -48 -0.5t-34 33.5q-10 25 -0.5 48t33.5 34q29 10 29 41v35h-4q-26 0 -44 18.5t-18 44.5z" />
+<glyph unicode="-" d="M115 590q0 29 18 47t47 18h492q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-492q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="." horiz-adv-x="393" d="M111 78q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="/" d="M119 -2l491 1352q11 25 35.5 37t48.5 2q27 -11 38 -34.5t1 -49.5l-491 -1352q-11 -27 -34.5 -38t-49.5 -1q-26 11 -37.5 34.5t-1.5 49.5z" />
+<glyph unicode="0" d="M115 303v696q0 129 91 220.5t220 91.5q128 0 219.5 -91.5t91.5 -220.5v-696q0 -129 -91.5 -220t-219.5 -91q-129 0 -220 91t-91 220zM246 303q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v696q0 74 -53.5 127.5t-126.5 53.5q-75 0 -127.5 -53.5t-52.5 -127.5 v-696z" />
+<glyph unicode="1" d="M164 999q0 27 18 48l246 245q29 29 72 15q41 -15 41 -62v-1188q0 -28 -19.5 -46.5t-46.5 -18.5q-29 0 -47 18t-18 47v1030l-136 -135q-18 -18 -45 -18q-26 0 -47 18q-18 21 -18 47z" />
+<glyph unicode="2" d="M125.5 1105.5q-7.5 26.5 5.5 49.5q64 113 186 143q122 35 234 -26t147 -182q38 -120 -22 -236q-15 -29 -73 -139t-87 -166q-36 -68 -117 -222.5t-102 -195.5h350q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-459q-40 0 -55 31q-21 34 -2 65l430 819 q34 63 12 136q-21 71 -86 106t-135 17q-71 -21 -106 -84q-14 -23 -40.5 -30.5t-49.5 5.5q-23 14 -30.5 40.5z" />
+<glyph unicode="3" d="M96.5 197q7.5 27 30.5 41q23 12 50 5t40 -30q31 -54 87 -77.5t116 -6.5t96.5 65t36.5 109v123q0 74 -53.5 127t-126.5 53q-29 0 -47.5 18.5t-18.5 47.5t18.5 47t47.5 18q74 0 127 53t53 128v81q0 62 -36.5 110t-96.5 65t-116 -6.5t-87 -77.5q-14 -23 -40.5 -30.5 t-49.5 5.5q-23 14 -30.5 40.5t5.5 49.5q55 92 152 133t199 12q104 -29 167.5 -112t63.5 -189v-81q0 -74 -32.5 -138.5t-88.5 -107.5q56 -43 88.5 -107.5t32.5 -138.5v-123q0 -106 -63.5 -189t-167.5 -112q-102 -28 -200.5 12.5t-150.5 132.5q-13 23 -5.5 50z" />
+<glyph unicode="4" d="M88 365l287 901q11 26 35 38.5t49 2.5q45 -15 45 -62q0 -12 -4 -20q-26 -87 -113 -357q-142 -449 -145 -458h254v221q0 29 18 47t47 18t47.5 -18t18.5 -47v-221h98q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47t-47.5 -18h-98v-222q0 -29 -18.5 -47t-47.5 -18t-47 18t-18 47 v222h-344q-34 0 -54 26q-19 25 -10 60z" />
+<glyph unicode="5" d="M145 651v586q0 29 18.5 47.5t47.5 18.5h426q29 0 47 -18.5t18 -47.5t-18 -47.5t-47 -18.5h-361v-368q105 57 228 26q104 -29 167.5 -112t63.5 -189v-225q0 -106 -63.5 -189t-167.5 -112q-102 -28 -200 12.5t-150 132.5q-12 23 -5.5 50.5t29.5 40.5q23 12 50 5t40 -30 q31 -54 87 -77.5t116 -6.5t96.5 65t36.5 109v225q0 61 -36.5 109t-96.5 65t-116 -6.5t-87 -77.5q-22 -43 -73 -30q-50 13 -50 63z" />
+<glyph unicode="6" d="M133 303v696q0 105 64.5 188.5t167.5 112.5q102 29 198.5 -12t151.5 -133q13 -23 5.5 -49.5t-30.5 -40.5q-23 -13 -49.5 -5.5t-40.5 30.5q-29 54 -86 77.5t-117 6.5t-96.5 -65t-36.5 -110v-237q77 57 180 57q129 0 220.5 -91t91.5 -220v-205q0 -129 -91.5 -220 t-220.5 -91t-220 91t-91 220zM264 303q0 -75 53 -127.5t127 -52.5q75 0 128 52.5t53 127.5v205q0 74 -53 127t-128 53q-74 0 -127 -53t-53 -127v-205z" />
+<glyph unicode="7" d="M109 1073v164q0 29 19 47.5t46 18.5h492q29 0 51 -25q22 -28 12 -57l-287 -1180q-6 -26 -28.5 -39.5t-48.5 -7.5q-23 4 -37.5 22t-14.5 41q0 3 1 8.5t1 8.5q34 141 121 495q103 400 148 602h-344v-98q0 -29 -18.5 -47t-47.5 -18q-27 0 -46 18t-19 47z" />
+<glyph unicode="8" d="M115 303v123q0 74 32.5 138.5t88.5 107.5q-56 43 -88.5 107.5t-32.5 138.5v81q0 129 91.5 220.5t219.5 91.5q129 0 220 -91.5t91 -220.5v-81q0 -74 -32 -138t-87 -108q55 -44 87 -108t32 -138v-123q0 -129 -91 -220t-220 -91q-128 0 -219.5 91t-91.5 220zM246 303 q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5t52.5 127.5v123q0 74 -52.5 127t-127.5 53q-74 0 -127 -53t-53 -127v-123zM246 918q0 -75 53 -128t127 -53q75 0 127.5 53t52.5 128v81q0 74 -52.5 127.5t-127.5 53.5q-73 0 -126.5 -53.5t-53.5 -127.5v-81z" />
+<glyph unicode="9" d="M96 795v204q0 129 92 220.5t220 91.5q129 0 220 -91.5t91 -220.5v-696q0 -106 -63.5 -189t-166.5 -112t-199.5 11.5t-150.5 133.5q-14 23 -7 50.5t30 40.5q25 12 51 5t39 -30q31 -54 87 -77.5t116 -6.5t96.5 65t36.5 109v238q-76 -58 -180 -58q-128 0 -220 91.5 t-92 220.5zM227 795q0 -75 53.5 -128t127.5 -53q75 0 127.5 53t52.5 128v204q0 74 -52.5 127.5t-127.5 53.5q-74 0 -127.5 -54t-53.5 -127v-204z" />
+<glyph unicode=":" horiz-adv-x="393" d="M111 78q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5zM111 856q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5z" />
+<glyph unicode=";" horiz-adv-x="393" d="M102 98q0 26 18 44t44 18h65q26 0 45 -18t19 -44v-98q0 -51 -29 -93.5t-76 -62.5q-25 -10 -48 -0.5t-34 33.5q-10 25 -0.5 48t33.5 34q29 10 29 41v35h-4q-26 0 -44 18.5t-18 44.5zM111 856q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5 t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#x3c;" d="M98 590q0 28 23 51l491 410q23 18 49.5 15t43.5 -26q16 -22 16 -41q0 -29 -25 -51q-29 -24 -90 -74t-90 -75q-230 -192 -250 -209q20 -17 250 -209q30 -26 92 -77t88 -73q25 -22 25 -51q0 -21 -16 -43q-17 -20 -43.5 -22.5t-49.5 14.5l-491 410q-23 20 -23 51z" />
+<glyph unicode="=" d="M115 426q0 29 19 47.5t46 18.5h492q29 0 47 -18.5t18 -47.5t-18 -47.5t-47 -18.5h-492q-27 0 -46 18.5t-19 47.5zM115 754q0 29 19 47t46 18h492q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-492q-27 0 -46 18.5t-19 47.5z" />
+<glyph unicode="&#x3e;" d="M131 180q0 28 23 51q107 86 182 150q245 205 250 209q-4 4 -250 209q-55 47 -182 149q-23 23 -23 51q0 27 14 41q18 23 45.5 26t47.5 -15l491 -410q25 -22 25 -51q0 -32 -25 -51l-491 -410q-20 -17 -47 -14.5t-46 22.5q-14 14 -14 43z" />
+<glyph unicode="?" horiz-adv-x="731" d="M60.5 1105q-6.5 27 7.5 50q72 123 206 150q137 28 250 -58q54 -41 86.5 -100t36.5 -125q9 -142 -90 -242l-102 -104q-54 -54 -54 -127v-164q0 -29 -18 -47.5t-47 -18.5q-27 0 -46.5 18.5t-19.5 47.5v164q0 126 90 219l105 104q57 57 51 140q-10 86 -70 131 q-63 48 -145 33q-80 -15 -121 -86q-14 -23 -39.5 -30.5t-50.5 5.5q-23 13 -29.5 40zM250 78q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="@" horiz-adv-x="1564" d="M127 283v368q0 130 49.5 249.5t142.5 213.5q94 93 213.5 143t249.5 50q269 0 465 -193q92 -94 141.5 -213.5t49.5 -249.5v-381q0 -110 -78.5 -188t-188.5 -78h-409q-109 0 -187.5 78t-78.5 188q0 109 78.5 188t187.5 79h143v139q0 42 -23.5 76t-64.5 51q-42 17 -83 8.5 t-72 -39.5q-19 -16 -46 -16q-25 0 -43 18q-16 19 -16 43q0 28 18 43q57 57 135.5 72.5t151.5 -14.5q75 -29 120.5 -95.5t45.5 -146.5v-549h143q60 0 102 41.5t42 101.5v381q0 106 -40.5 203t-115.5 172q-152 158 -377 158q-222 0 -374 -158q-158 -152 -158 -375v-368 q0 -225 158 -377q75 -75 171.5 -115.5t202.5 -40.5h226q26 0 43.5 -18t17.5 -43q0 -26 -17.5 -44t-43.5 -18h-226q-130 0 -249.5 49.5t-213.5 141.5q-192 195 -192 465zM618 270q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102z" />
+<glyph unicode="A" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q3 -10 3 -20q-1 -16 -9 -30q-14 -23 -42 -30q-9 -2 -18 -2q-16 0 -30 8q-22 13 -31 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30 -39q-14 -8 -32 -9q-10 0 -20 3 q-26 8 -39 31q-8 15 -8 31q0 9 2 18zM281 410h323q-25 93 -83 302t-79 287q-19 -66 -161 -589z" />
+<glyph unicode="B" horiz-adv-x="876" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h246q129 0 220 -91.5t91 -220.5v-73q0 -74 -32.5 -138.5t-88.5 -107.5q56 -43 88.5 -107.5t32.5 -138.5v-115q0 -129 -91 -220t-220 -91h-246q-29 0 -47.5 18.5t-18.5 47.5zM270 131h181q74 0 127 52.5t53 127.5v115 q0 74 -53.5 127t-126.5 53h-181v-475zM270 737h181q74 0 127 53t53 128v73q0 73 -53.5 126.5t-126.5 53.5h-181v-434z" />
+<glyph unicode="C" horiz-adv-x="788" d="M115 303v696q0 106 63.5 189t167.5 112q42 12 84 12q58 0 114 -24q96 -41 152 -133q8 -15 8 -32q0 -9 -2 -18q-6 -27 -29.5 -40t-50 -5.5t-40.5 30.5q-31 54 -87 78q-32 14 -66 13q-24 0 -50 -7q-60 -17 -96.5 -65t-36.5 -110v-696q0 -61 36.5 -109t96.5 -65q25 -7 50 -7 q34 0 66 14q56 24 87 77q13 23 40 30t50 -5q24 -12 30 -40q2 -10 2 -19q0 -17 -8 -32q-52 -93 -150 -133q-58 -24 -117 -23q-41 0 -83 11q-104 29 -167.5 112t-63.5 189z" />
+<glyph unicode="D" horiz-adv-x="876" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h246q129 0 220 -91.5t91 -220.5v-680q0 -129 -91 -220t-220 -91h-246q-29 0 -47.5 18.5t-18.5 47.5zM270 131h181q75 0 127.5 52.5t52.5 127.5v680q0 74 -52.5 127t-127.5 53h-181v-1040z" />
+<glyph unicode="E" horiz-adv-x="753" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h409q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47.5t-47.5 -18.5h-344v-434h303q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-303v-475h344q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-409q-29 0 -47.5 18.5t-18.5 47.5z " />
+<glyph unicode="F" horiz-adv-x="737" d="M139 66v1171q0 29 19.5 47.5t46.5 18.5h409q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47.5t-47.5 -18.5h-344v-434h303q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-303v-540q0 -29 -18 -47.5t-47 -18.5q-27 0 -46.5 18.5t-19.5 47.5z" />
+<glyph unicode="G" horiz-adv-x="843" d="M115 303v696q0 105 64 188.5t167 112.5q42 12 84 12q58 0 114 -24q97 -41 152 -133q14 -23 7 -50t-31 -40q-23 -13 -49.5 -5.5t-40.5 30.5q-29 54 -85 78q-32 13 -66 13q-25 0 -50 -7q-60 -17 -97.5 -65.5t-37.5 -109.5v-696q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5 t52.5 127.5v262h-139q-27 0 -46.5 18.5t-19.5 47.5q0 28 19.5 46.5t46.5 18.5h205q29 0 47 -18t18 -47v-328q0 -129 -91 -220t-220 -91t-220 91t-91 220z" />
+<glyph unicode="H" horiz-adv-x="901" d="M139 57v1188q0 29 19.5 47.5t46.5 18.5q29 0 47 -18.5t18 -47.5v-508h361v508q0 29 19 47.5t46 18.5q29 0 47.5 -18.5t18.5 -47.5v-1188q0 -29 -18.5 -47t-47.5 -18q-27 0 -46 18t-19 47v549h-361v-549q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5t-19.5 46.5z" />
+<glyph unicode="I" horiz-adv-x="409" d="M139 57v1188q0 29 19.5 47.5t46.5 18.5q29 0 47 -18.5t18 -47.5v-1188q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5t-19.5 46.5z" />
+<glyph unicode="J" horiz-adv-x="681" d="M52 73q-2 9 -2 18q0 17 9 32q14 23 40 30t51 -6q40 -24 88 -24t92 24q41 23 65.5 66t24.5 90v942q0 29 18 47.5t47 18.5q27 0 46.5 -18.5t19.5 -47.5v-942q0 -84 -42 -156.5t-114 -113.5q-71 -41 -155 -41q-85 0 -156 41q-24 13 -32 40z" />
+<glyph unicode="K" horiz-adv-x="862" d="M139 57v1188q0 29 19.5 47.5t46.5 18.5q29 0 47 -18.5t18 -47.5v-543l414 582q17 23 43 27q5 1 10 1q21 0 39 -14q24 -17 28 -43q1 -5 0 -11q0 -20 -13 -38l-318 -446l324 -674q7 -14 7 -29q0 -11 -4 -21q-9 -26 -34 -38q-14 -7 -29 -7q-11 0 -21 4q-26 9 -36 34 l-295 608l-115 -160v-420q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5t-19.5 46.5z" />
+<glyph unicode="L" horiz-adv-x="712" d="M139 66v1179q0 29 18.5 47.5t47.5 18.5q27 0 46 -18.5t19 -47.5v-1114h344q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-409q-29 0 -47.5 18.5t-18.5 47.5z" />
+<glyph unicode="M" horiz-adv-x="1064" d="M139 57v1188q0 24 14 42t37 22q7 1 13 1q16 0 30 -6q20 -10 31 -30q44 -95 105 -221q156 -337 163 -351q142 307 164 351q61 126 105 221q11 20 31 30q14 7 29 6q7 0 13 -1q23 -4 37.5 -22.5t14.5 -41.5v-1188q0 -28 -19.5 -46.5t-46.5 -18.5q-29 0 -47 18t-18 47v895 q-17 -37 -46.5 -99t-52 -110t-39.5 -84q-35 -72 -65 -139q-21 -37 -60 -37q-44 0 -59 37q-30 67 -65 139q-112 236 -138 293v-895q0 -29 -19 -47t-46 -18q-29 0 -47.5 18t-18.5 47z" />
+<glyph unicode="N" horiz-adv-x="983" d="M139 57v1188q0 24 14 42t37 22t43 -5.5t31 -29.5q68 -145 166 -346q21 -44 130 -268t153 -316v901q0 29 18 47.5t47 18.5t47.5 -18.5t18.5 -47.5v-1188q0 -23 -14 -41t-37 -22q-9 -2 -17 -2q-14 0 -26 6q-20 10 -31 31q-30 62 -86 178t-82 168q-86 178 -281 583v-901 q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47z" />
+<glyph unicode="O" d="M115 303v696q0 129 91 220.5t220 91.5t220 -91.5t91 -220.5v-696q0 -129 -91 -220t-220 -91t-220 91t-91 220zM246 303q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v696q0 74 -53.5 127.5t-126.5 53.5q-75 0 -127.5 -53.5t-52.5 -127.5v-696z" />
+<glyph unicode="P" horiz-adv-x="819" d="M139 57v1180q0 29 18.5 47.5t47.5 18.5h246q129 0 220 -91.5t91 -220.5v-114q0 -129 -91 -220.5t-220 -91.5h-181v-508q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47zM270 696h181q74 0 127 53t53 128v114q0 73 -53.5 126.5t-126.5 53.5h-181v-475z" />
+<glyph unicode="Q" d="M115 303v696q0 129 91 220.5t220 91.5t220 -91.5t91 -220.5v-696q0 -139 -102 -231l94 -162q8 -15 8 -31q0 -9 -2 -19q-7 -26 -30 -40q-15 -8 -32 -8q-9 0 -19 2q-27 7 -40 30l-92 162q-54 -14 -96 -14q-129 0 -220 91t-91 220zM246 303q0 -75 53 -127.5t127 -52.5 q75 0 127.5 52.5t52.5 127.5v696q0 74 -52.5 127.5t-127.5 53.5q-73 0 -126.5 -53.5t-53.5 -127.5v-696z" />
+<glyph unicode="R" d="M139 57v1180q0 29 19.5 47.5t46.5 18.5h246q129 0 220 -91.5t91 -220.5v-114q0 -97 -53 -175t-140 -114l189 -508q5 -12 5 -23q-1 -13 -7 -26q-12 -24 -37 -35q-12 -5 -23 -5q-13 1 -26 7q-24 12 -35 37l-197 530h-168v-508q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5 t-19.5 46.5zM270 696h181q75 0 127.5 53t52.5 128v114q0 74 -52.5 127t-127.5 53h-181v-475z" />
+<glyph unicode="S" horiz-adv-x="782" d="M93 203q7 26 30 39q23 14 50 6.5t40 -31.5q43 -75 127 -90q21 -4 40 -4q62 0 112 39q69 51 75 137q1 11 1 21q0 72 -54 126l-328 336q-48 46 -70 110q-18 50 -17 102q0 14 1 28q9 138 123 225q85 65 184 65q32 0 66 -7q135 -28 207 -150q14 -23 6.5 -50t-31.5 -40 q-23 -13 -49.5 -5.5t-38.5 30.5q-41 71 -121 86q-20 4 -38 4q-59 0 -107 -37q-64 -46 -70 -129q-1 -8 0 -15q0 -71 50 -125l327 -337q91 -95 91 -224q0 -12 -1 -24q-13 -147 -127 -232q-89 -66 -191 -66q-33 0 -67 7q-67 13 -123.5 53.5t-91.5 100.5q-8 16 -8 32q0 9 3 19z " />
+<glyph unicode="T" horiz-adv-x="778" d="M57 1237q0 29 19.5 47.5t46.5 18.5h532q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47.5t-47.5 -18.5h-200v-1114q0 -29 -18.5 -47t-47.5 -18q-27 0 -46 18t-19 47v1114h-201q-27 0 -46.5 18.5t-19.5 47.5z" />
+<glyph unicode="U" horiz-adv-x="868" d="M123 303v942q0 29 18 47.5t47 18.5q27 0 46.5 -18.5t19.5 -47.5v-942q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v942q0 29 18.5 47.5t47.5 18.5q27 0 46 -18.5t19 -47.5v-942q0 -129 -91.5 -220t-219.5 -91q-129 0 -220 91t-91 220z" />
+<glyph unicode="V" horiz-adv-x="917" d="M70 1227q-3 10 -3 19q1 16 9 32q14 24 39 31q8 2 16 2q19 1 35 -9q24 -14 29 -40q24 -84 102 -367q127 -468 164 -592q17 67 44 164.5t63.5 228.5t54.5 199q78 283 102 367q5 26 29 40q16 9 35 9q8 0 16 -2q25 -7 39 -31q8 -15 9 -32q0 -9 -3 -19l-328 -1188 q-5 -21 -21.5 -34t-39.5 -13t-40.5 13t-23.5 34z" />
+<glyph unicode="W" horiz-adv-x="1429" d="M78 1229q-2 9 -2 17q0 18 10 34q14 22 39 29q27 6 50.5 -7.5t29.5 -41.5q16 -69 46.5 -195.5t35.5 -146.5q29 -130 141 -582q13 53 62 264t77 318q21 86 84 342q4 23 22 37t42 14q48 0 63 -51q16 -69 46.5 -195.5t35.5 -146.5q29 -130 141 -582q12 48 62 261t78 321 q21 86 84 342q6 28 28.5 41.5t49.5 7.5q27 -7 41 -29q10 -16 10 -34q0 -8 -2 -17l-287 -1188q-13 -49 -64 -49q-24 0 -41.5 13t-21.5 36q-44 173 -84 342q-28 107 -76.5 316.5t-62.5 267.5q-118 -479 -142 -584q-41 -169 -81 -342q-13 -49 -64 -49q-24 0 -41.5 13t-21.5 36z " />
+<glyph unicode="X" horiz-adv-x="864" d="M62 36q-4 11 -3 21q0 15 7 29l292 586l-272 545q-12 23 -3 49.5t34 38.5q13 6 26 6q11 0 23 -4q26 -9 39 -33l227 -455l227 455q12 24 38 33q12 4 23 4q14 0 28 -6q24 -12 32.5 -38.5t-2.5 -49.5l-272 -545l293 -586q6 -14 6 -28q0 -10 -4 -22q-9 -26 -33 -38 q-14 -7 -29 -7q-11 0 -21 4q-26 9 -38 34l-248 495l-248 -495q-12 -25 -38 -34q-11 -4 -23 -4q-14 0 -27 7q-26 12 -34 38z" />
+<glyph unicode="Y" horiz-adv-x="823" d="M62 1266q9 27 34 39q13 6 26 6q12 0 24 -4q26 -9 38 -33l228 -455l227 455q12 24 37 33q12 4 23 4q14 0 28 -6q25 -12 34 -39q4 -11 4 -22q0 -14 -7 -27l-281 -562v-598q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47v598l-280 562q-7 13 -7 28q0 11 3 21z" />
+<glyph unicode="Z" horiz-adv-x="786" d="M82 66q0 12 6 24q39 94 106.5 255t94.5 226q109 261 252 600h-394q-29 0 -47 18.5t-18 47.5t18 47.5t47 18.5h492q36 0 55 -29q11 -26 11 -37l-5 -25q-65 -152 -202 -483q-64 -147 -154 -363t-98 -235h393q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-492 q-36 0 -55 29q-10 23 -10 37z" />
+<glyph unicode="[" horiz-adv-x="524" d="M139 -98v1499q0 29 18.5 47t47.5 18h205q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-140v-1368h140q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-205q-29 0 -47.5 18.5t-18.5 47.5z" />
+<glyph unicode="\" d="M119 1305q-10 26 1.5 49.5t37.5 34.5q26 10 49.5 -1.5t34.5 -37.5l491 -1352q10 -26 -1 -49.5t-38 -34.5q-25 -10 -49 1.5t-35 37.5z" />
+<glyph unicode="]" horiz-adv-x="524" d="M49 -98q0 29 18.5 47t47.5 18h139v1368h-139q-29 0 -47.5 18.5t-18.5 47.5t18.5 47t47.5 18h204q27 0 46.5 -18.5t19.5 -46.5v-1499q0 -29 -19.5 -47.5t-46.5 -18.5h-204q-29 0 -47.5 18.5t-18.5 47.5z" />
+<glyph unicode="^" horiz-adv-x="1024" d="M123.5 873q-1.5 27 15.5 47l324 368q20 23 49 23t49 -23l324 -368q17 -20 15.5 -47t-21.5 -46q-24 -18 -50 -16t-45 23q-76 89 -108 124q-28 33 -87 99.5t-77 87.5q-20 -21 -164 -187q-32 -35 -108 -124q-19 -21 -45 -23t-50 16q-20 19 -21.5 46z" />
+<glyph unicode="_" horiz-adv-x="1024" d="M-66 -229q0 29 18.5 47t47.5 18h1024q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-1024q-29 0 -47.5 18.5t-18.5 47.5z" />
+<glyph unicode="`" horiz-adv-x="1024" d="M345 1282.5q-2 26.5 15 46.5q19 20 47 22.5t48 -16.5l204 -184q20 -19 22 -45.5t-17 -46.5q-18 -23 -45.5 -24t-47.5 18l-204 184q-20 19 -22 45.5z" />
+<glyph unicode="a" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 42 -23.5 76.5t-62.5 52.5q-28 11 -55 11q-14 0 -27 -2q-40 -8 -70 -40q-18 -18 -45 -18q-25 0 -43 18t-18 43q0 28 18 43q57 57 135 73q27 5 53 5q51 0 99 -20q74 -31 118 -97t44 -145v-610q0 -26 -18.5 -44t-43.5 -18 h-204q-110 0 -188.5 78t-78.5 188zM184 270q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102z" />
+<glyph unicode="b" horiz-adv-x="794" d="M143 66v1220q0 26 18 44t44 18q25 0 43 -18t18 -44v-356h144q109 0 187.5 -78t78.5 -188v-394q0 -110 -78.5 -188t-187.5 -78h-205q-26 0 -44 18t-18 44zM266 127h144q60 0 101.5 41.5t41.5 101.5v394q0 60 -41.5 101.5t-101.5 41.5h-144v-680z" />
+<glyph unicode="c" d="M119 262v410q0 82 44.5 148.5t119.5 97.5q49 20 100 20q27 0 53 -5q78 -16 137 -73q17 -20 17 -45q0 -23 -17 -43q-19 -16 -45 -16q-24 0 -43 16q-31 32 -73 41q-14 3 -28 3q-27 0 -54 -11q-41 -17 -64.5 -53.5t-23.5 -79.5v-410q0 -43 23.5 -79.5t64.5 -53.5 q28 -11 56 -11q14 0 27 2q41 8 72 40q18 18 43 18q27 0 45 -18q17 -17 17 -43q0 -23 -17 -43q-59 -57 -137 -73q-26 -5 -53 -5q-51 0 -100 20q-75 31 -119.5 97.5t-44.5 148.5z" />
+<glyph unicode="d" horiz-adv-x="794" d="M119 270v394q0 110 78 188t188 78h143v356q0 26 18 44t44 18q25 0 43 -18t18 -44v-1220q0 -26 -18 -44t-43 -18h-205q-110 0 -188 78t-78 188zM242 270q0 -60 41.5 -101.5t101.5 -41.5h143v680h-143q-60 0 -101.5 -41.5t-41.5 -101.5v-394z" />
+<glyph unicode="e" horiz-adv-x="753" d="M119 262v410q0 110 78 188t188 78t188 -78t78 -188v-205q0 -26 -17.5 -43.5t-43.5 -17.5h-348v-144q0 -43 23.5 -79.5t64.5 -53.5q28 -11 56 -11q14 0 27 2q41 8 72 40q18 18 43 18q27 0 45 -18q19 -16 19 -43q0 -24 -19 -43q-57 -57 -136 -73q-27 -5 -53 -5 q-51 0 -101 20q-75 31 -119.5 97.5t-44.5 148.5zM242 528h286v144q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-144z" />
+<glyph unicode="f" horiz-adv-x="606" d="M53 868q0 26 18 44t44 18h102v151q0 110 78 188.5t188 78.5q26 0 44 -18t18 -44t-18 -43.5t-44 -17.5q-60 0 -101.5 -42t-41.5 -102v-151h143q26 0 44 -18t18 -44t-18 -43.5t-44 -17.5h-143v-750q0 -26 -17.5 -43.5t-43.5 -17.5t-44 17.5t-18 43.5v750h-102 q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="g" horiz-adv-x="786" d="M119 270v394q0 110 78 188t188 78h205q26 0 43.5 -18t17.5 -44v-1015q0 -82 -44.5 -148.5t-119.5 -97.5q-50 -21 -101 -21q-26 0 -52 6q-77 15 -137 72q-19 19 -19 45q0 25 19 41q19 19 43 19q26 0 45 -19q31 -31 72 -40q14 -2 27 -2q28 0 56 11q41 17 64.5 54t23.5 80 v151h-143q-110 0 -188 78t-78 188zM242 270q0 -60 41.5 -101.5t101.5 -41.5h143v680h-143q-60 0 -101.5 -41.5t-41.5 -101.5v-394z" />
+<glyph unicode="h" horiz-adv-x="802" d="M143 57v1229q0 26 18.5 44t43.5 18q26 0 43.5 -18t17.5 -44v-356h144q110 0 188 -78t78 -188v-607q0 -26 -18 -43.5t-44 -17.5q-25 0 -43 17.5t-18 43.5v607q0 60 -41.5 101.5t-101.5 41.5h-144v-750q0 -26 -17.5 -43.5t-43.5 -17.5q-25 0 -43.5 17.5t-18.5 43.5z" />
+<glyph unicode="i" horiz-adv-x="409" d="M123 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM143 57v820q0 26 18.5 43.5t43.5 17.5q26 0 43.5 -17.5t17.5 -43.5v-820q0 -26 -17.5 -43.5t-43.5 -17.5q-25 0 -43.5 17.5t-18.5 43.5z" />
+<glyph unicode="j" horiz-adv-x="425" d="M-45 -352q0 26 18 43.5t43 17.5q60 0 102 42t42 102v1024q0 26 18 43.5t43 17.5q26 0 44 -17.5t18 -43.5v-1024q0 -110 -78.5 -188.5t-188.5 -78.5q-25 0 -43 18t-18 44zM139 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5 t-23.5 58.5z" />
+<glyph unicode="k" horiz-adv-x="778" d="M143 57v1229q0 26 18 44t44 18q25 0 43 -18t18 -44v-751l342 383q17 19 42.5 20t45.5 -16q20 -18 20 -45q0 -23 -16 -41l-249 -279l258 -471q8 -15 8 -29q0 -9 -3 -17q-7 -24 -30 -36q-15 -8 -31 -8q-9 0 -17 2q-24 6 -36 29l-235 432l-99 -109v-293q0 -26 -18 -43.5 t-43 -17.5q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="l" horiz-adv-x="409" d="M137 199v1087q0 26 18.5 44t43.5 18q26 0 43.5 -18t17.5 -44v-1087q0 -58 51 -88q23 -13 30 -37q2 -8 2 -16q0 -16 -9 -31q-12 -23 -36 -29q-8 -2 -17 -2q-16 0 -31 8q-52 31 -82.5 83.5t-30.5 111.5z" />
+<glyph unicode="m" horiz-adv-x="1212" d="M143 57v811q0 26 18 44t44 18h614q109 0 187.5 -78t78.5 -188v-607q0 -26 -18 -43.5t-43 -17.5q-26 0 -43.5 17.5t-17.5 43.5v607q0 60 -42 101.5t-102 41.5h-143v-750q0 -26 -18.5 -43.5t-43.5 -17.5q-26 0 -43.5 17.5t-17.5 43.5v750h-287v-750q0 -26 -18 -43.5 t-43 -17.5q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="n" horiz-adv-x="802" d="M143 57v811q0 26 18 44t44 18h205q109 0 187.5 -78t78.5 -188v-607q0 -26 -18.5 -43.5t-43.5 -17.5q-26 0 -43.5 17.5t-17.5 43.5v607q0 60 -41.5 101.5t-101.5 41.5h-144v-750q0 -26 -18 -43.5t-43 -17.5q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="o" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78q109 0 187.5 -78t78.5 -188v-410q0 -110 -78.5 -188t-187.5 -78q-110 0 -188 78t-78 188zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410z" />
+<glyph unicode="p" horiz-adv-x="794" d="M143 -352v1220q0 26 18 44t44 18h205q109 0 187.5 -78t78.5 -188v-394q0 -110 -78.5 -188t-187.5 -78h-144v-356q0 -26 -18 -44t-43 -18q-26 0 -44 18t-18 44zM266 127h144q60 0 101.5 41.5t41.5 101.5v394q0 60 -41.5 101.5t-101.5 41.5h-144v-680z" />
+<glyph unicode="q" horiz-adv-x="794" d="M119 270v394q0 110 78 188t188 78h205q25 0 43 -18t18 -44v-1220q0 -26 -18 -44t-43 -18q-26 0 -44 18t-18 44v356h-143q-110 0 -188 78t-78 188zM242 270q0 -60 41.5 -101.5t101.5 -41.5h143v680h-143q-60 0 -101.5 -41.5t-41.5 -101.5v-394z" />
+<glyph unicode="r" horiz-adv-x="612" d="M143 57v811q0 26 18 44t44 18h205q68 0 133 -35q23 -13 29 -37q2 -8 1 -15q0 -16 -8 -32q-12 -23 -36.5 -30t-47.5 7q-36 19 -71 19h-144v-750q0 -26 -17.5 -43.5t-43.5 -17.5t-44 17.5t-18 43.5z" />
+<glyph unicode="s" horiz-adv-x="673" d="M98 170q6 24 29 36.5t47.5 6.5t36.5 -29q36 -54 94 -63q11 -2 21 -2q48 0 88 35q42 39 42 91q0 7 -1 13q-3 60 -56 98l-190 136q-95 65 -105 184q-1 11 -1 21q0 103 81 175q72 67 165 67q19 0 38 -3q118 -22 176 -121q13 -23 7 -47.5t-29 -36.5t-47.5 -6t-36.5 29 q-27 50 -88 59q-11 2 -21 2q-46 0 -82 -33q-39 -36 -39 -90v-8q3 -56 54 -92l190 -133q98 -73 107 -193q1 -11 1 -21q0 -107 -83 -182q-70 -68 -169 -68q-20 0 -40 3q-124 22 -183 125q-8 15 -8 31q0 8 2 16z" />
+<glyph unicode="t" horiz-adv-x="606" d="M53 868q0 26 18 44t44 18h102v356q0 26 18 44t44 18t43.5 -18t17.5 -44v-356h143q26 0 44 -18t18 -44t-18 -43.5t-44 -17.5h-143v-545q0 -60 41.5 -101.5t101.5 -41.5q26 0 44 -18t18 -44t-18 -43.5t-44 -17.5q-110 0 -188 78t-78 188v545h-102q-26 0 -44 17.5t-18 43.5z " />
+<glyph unicode="u" horiz-adv-x="802" d="M127 270v607q0 26 17.5 43.5t43.5 17.5t44 -17.5t18 -43.5v-607q0 -60 41.5 -101.5t101.5 -41.5h144v750q0 26 17.5 43.5t43.5 17.5t43.5 -17.5t17.5 -43.5v-811q0 -26 -17.5 -44t-43.5 -18h-205q-110 0 -188 78t-78 188z" />
+<glyph unicode="v" horiz-adv-x="757" d="M74 858q-2 8 -2 16q0 16 8 32q12 23 37 30q8 2 15 2l31 -8q23 -12 30 -35l67 -227q18 -61 59.5 -199.5t59.5 -198.5q25 86 119 398l67 227q7 23 30 35q15 8 32 8q8 0 16 -2q25 -7 37 -30q7 -14 7 -28q0 -10 -3 -20l-246 -819q-6 -19 -22.5 -31t-36.5 -12t-37 12t-23 31z " />
+<glyph unicode="w" horiz-adv-x="1101" d="M80 862q-2 8 -2 15q0 15 9 31q13 22 38 28t46.5 -7t27.5 -38q33 -126 51 -201q17 -64 49.5 -197.5t44.5 -181.5q9 34 43.5 173t52.5 206q8 34 24.5 100.5t24.5 100.5q15 47 60 47q50 0 59 -47q33 -126 51 -201q18 -65 51 -200t44 -179q9 34 43.5 173t52.5 206 q8 34 24.5 100.5t24.5 100.5q6 25 28 38t48 7q25 -6 37 -28q8 -14 8 -30q0 -8 -2 -16l-205 -819q-9 -47 -59 -47q-45 0 -60 47q-8 34 -24.5 100.5t-24.5 100.5q-7 27 -96 379q-9 -37 -42.5 -175t-51.5 -204q-10 -41 -52 -201q-9 -47 -59 -47q-44 0 -59 47z" />
+<glyph unicode="x" horiz-adv-x="737" d="M81 40q-3 9 -3 17q0 15 8 29l213 389l-205 371q-12 23 -5 47.5t30 36.5q15 8 30 8q9 0 17 -3q24 -7 37 -30l166 -303l168 303q13 23 37 30q9 3 17 3q15 0 30 -8q23 -12 29.5 -36.5t-5.5 -47.5l-205 -371l213 -389q8 -15 7 -30q0 -8 -2 -16q-6 -24 -29 -36q-15 -8 -31 -8 q-8 0 -16 2q-24 6 -37 29l-176 319l-174 -319q-13 -23 -37 -29q-8 -2 -16 -2q-16 0 -31 8q-23 12 -30 36z" />
+<glyph unicode="y" horiz-adv-x="755" d="M41 -352q0 26 17.5 43.5t43.5 17.5q48 0 86.5 28t51.5 75l73 245l-239 801q-8 25 4 48t37 30q8 2 16 2q16 0 30 -8q21 -12 29 -35l187 -625l188 625q6 23 29 35q14 8 31 8q8 0 16 -2q25 -7 37 -30q7 -14 7 -28q0 -10 -3 -20l-246 -819l-78 -262q-26 -86 -96 -137 q-73 -54 -160 -54q-26 0 -43.5 18t-17.5 44z" />
+<glyph unicode="z" horiz-adv-x="704" d="M86 66q0 13 6 28q24 48 76.5 150t77.5 153q41 78 116.5 226.5t94.5 183.5h-310q-26 0 -43.5 17.5t-17.5 43.5t17.5 44t43.5 18h410q35 0 51 -29q10 -17 10 -33q0 -15 -8 -28q-12 -23 -68.5 -135.5t-84.5 -167.5q-196 -383 -209 -410h309q25 0 43 -17.5t18 -43.5t-18 -44 t-43 -18h-410q-37 0 -53 29q-8 13 -8 33z" />
+<glyph unicode="{" horiz-adv-x="524" d="M33 672q0 29 18 47t47 18q41 0 70 29t29 70v409q0 94 67.5 162t161.5 68q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47.5t-47.5 -18.5q-41 0 -69.5 -28.5t-28.5 -69.5v-409q0 -97 -70 -164q70 -67 70 -164v-451q0 -41 28.5 -69.5t69.5 -28.5q29 0 47.5 -18t18.5 -47 t-18.5 -47.5t-47.5 -18.5q-94 0 -161.5 67.5t-67.5 161.5v451q0 41 -29 69.5t-70 28.5q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="|" horiz-adv-x="425" d="M158 -352v2048q0 29 18 47t47 18t47.5 -18t18.5 -47v-2048q0 -29 -18.5 -47.5t-47.5 -18.5t-47 18.5t-18 47.5z" />
+<glyph unicode="}" horiz-adv-x="524" d="M33 -106q0 29 18 47t47 18q41 0 70 28.5t29 69.5v451q0 100 67 164q-67 64 -67 164v409q0 41 -29 69.5t-70 28.5q-29 0 -47 18.5t-18 47.5t18 47.5t47 18.5q94 0 162 -68t68 -162v-409q0 -41 28.5 -70t69.5 -29q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5 t-46.5 -18.5q-41 0 -69.5 -28.5t-28.5 -69.5v-451q0 -94 -68 -161.5t-162 -67.5q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="~" d="M89 482q-5 25 9 48q61 93 164 107q102 22 191 -45l24 -19q37 -32 94 -22q53 9 80 53q13 23 38 27t48 -10q23 -13 28 -38t-9 -48q-63 -92 -164 -107q-107 -18 -191 45l-24 19q-37 32 -94 22q-53 -9 -80 -53q-14 -24 -38.5 -29t-47.5 12q-23 13 -28 38z" />
+<glyph unicode="&#xa1;" horiz-adv-x="409" d="M119 856q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5zM139 -270v819q0 28 19.5 46.5t46.5 18.5q29 0 47 -18t18 -47v-819q0 -29 -18 -47.5t-47 -18.5q-27 0 -46.5 18.5t-19.5 47.5z" />
+<glyph unicode="&#xa2;" d="M184 262v410q0 84 45 150.5t121 99.5q7 3 19.5 6t19.5 6v106q0 29 18.5 47.5t47.5 18.5q27 0 46 -18.5t19 -47.5v-106q82 -25 125 -72q19 -19 19 -45q0 -25 -19 -47q-21 -18 -47 -18q-27 0 -45 18q-41 41 -96 41h-2h-2q-22 0 -52 -10q-41 -17 -63.5 -51.5t-22.5 -77.5 v-410q0 -43 22.5 -77.5t63.5 -51.5q30 -10 52 -10h2h2q55 0 96 41q18 18 45 18q26 0 47 -18q19 -22 19 -47q0 -26 -19 -45q-43 -47 -125 -72v-106q0 -29 -19 -47.5t-46 -18.5q-29 0 -47.5 18.5t-18.5 47.5v106q-21 3 -39 12q-76 33 -121 99.5t-45 150.5z" />
+<glyph unicode="&#xa3;" d="M74 672q0 29 19 47t46 18h99v303q0 92 56 164.5t144 95.5q90 25 173.5 -9.5t131.5 -114.5q12 -23 5.5 -50.5t-29.5 -40.5t-49.5 -5.5t-40.5 30.5q-23 41 -67.5 59.5t-88.5 6.5q-47 -12 -75.5 -50t-28.5 -86v-303h180q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-180v-309 q0 -89 -47 -166h391q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-574q-23 0 -41 15t-22 40q-5 23 7.5 44t35.5 28q54 20 86.5 66.5t32.5 103.5v309h-99q-27 0 -46 18.5t-19 47.5z" />
+<glyph unicode="&#xa4;" d="M70 381q0 30 20 47l74 76q-49 74 -49 168t49 168l-74 75q-20 17 -20 46q0 30 20 47q18 18 45 18q29 0 47 -18l76 -74q74 49 168 49t168 -49l76 74q18 18 45 18q29 0 47 -18q20 -17 20 -47q0 -29 -20 -46l-74 -75q49 -74 49 -168t-49 -168l74 -76q20 -17 20 -47 q0 -28 -20 -45q-18 -21 -47 -21q-27 0 -45 21l-76 74q-76 -50 -168 -50t-168 50l-76 -74q-18 -21 -47 -21q-27 0 -45 21q-20 17 -20 45zM246 672q0 -75 52.5 -127.5t127.5 -52.5t127.5 52.5t52.5 127.5q0 74 -52.5 127t-127.5 53t-127.5 -53t-52.5 -127z" />
+<glyph unicode="&#xa5;" d="M77 1266.5q9 26.5 34 38.5q23 11 49.5 2t38.5 -33l227 -455l227 455q12 24 37.5 33t50.5 -2q24 -12 33 -38.5t-2 -49.5l-240 -480h148q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-188v-196h188q29 0 47 -18.5t18 -47.5t-18 -47t-47 -18h-188v-222q0 -29 -18.5 -47 t-47.5 -18t-47.5 18t-18.5 47v222h-172q-29 0 -47 18t-18 47t18 47.5t47 18.5h172v196h-172q-29 0 -47 18.5t-18 47.5t18 47t47 18h131l-239 480q-12 23 -3 49.5z" />
+<glyph unicode="&#xa6;" horiz-adv-x="425" d="M147 -352v819q0 29 18.5 47t47.5 18t47.5 -18t18.5 -47v-819q0 -29 -18.5 -47.5t-47.5 -18.5t-47.5 18.5t-18.5 47.5zM147 877v819q0 29 18.5 47t47.5 18t47.5 -18t18.5 -47v-819q0 -29 -18.5 -47.5t-47.5 -18.5t-47.5 18.5t-18.5 47.5z" />
+<glyph unicode="&#xa7;" d="M170 758q15 84 78 141q-57 66 -60 158q0 119 88 192q42 37 96 52.5t109 5.5q115 -22 174 -121q14 -23 7 -50t-31 -40q-23 -13 -48.5 -6t-39.5 30q-28 49 -84 58q-55 9 -97 -27q-43 -34 -43 -92q3 -53 46 -90l229 -185q55 -43 79.5 -107t10.5 -132q-14 -78 -78 -142 q58 -67 58 -157q3 -119 -86 -193q-87 -76 -205 -57q-115 22 -174 121q-14 23 -7 50t31 40q23 12 49 5t39 -30q28 -48 84 -57q55 -9 97 27q43 37 40 92q0 56 -43 90l-229 182h-2q-54 44 -77.5 109.5t-10.5 132.5zM297 735q-12 -71 43 -114l164 -132q41 27 51 78q6 32 -5 63.5 t-36 51.5l-164 131q-44 -30 -53 -78z" />
+<glyph unicode="&#xa8;" horiz-adv-x="1024" d="M262 1184q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5zM590 1184q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1470" d="M80 651q0 273 192 465q92 92 212 141.5t251 49.5q133 0 253 -49.5t212 -141.5q193 -193 193 -465t-193 -465q-93 -93 -212.5 -142.5t-252.5 -49.5q-130 0 -250.5 50t-212.5 142q-192 192 -192 465zM205 651q0 -106 40 -204t115 -173t172 -115t203 -40q222 0 377 155 q75 75 115.5 173t40.5 204t-40.5 203t-115.5 172q-156 156 -377 156q-106 0 -203 -40.5t-172 -115.5t-115 -172t-40 -203zM510 508v287q0 77 47 138t121 81q73 20 144.5 -10.5t109.5 -96.5q14 -23 7 -47.5t-30 -36.5q-23 -13 -47.5 -6.5t-38.5 29.5q-18 30 -49 43.5t-63 3.5 q-35 -11 -55.5 -37t-20.5 -61v-287q0 -35 20.5 -61t55.5 -37q32 -10 63 3.5t49 43.5q13 23 38 29.5t48 -7.5q23 -12 30 -36.5t-7 -47.5q-38 -67 -109 -96.5t-145 -9.5t-121 81t-47 138z" />
+<glyph unicode="&#xaa;" horiz-adv-x="624" d="M111 815q0 83 58.5 143t143.5 60h84v84q0 30 -16 52.5t-45 29.5q-58 15 -96 -39q-12 -23 -34.5 -29t-45.5 6t-29.5 36t6.5 44q35 60 99 86.5t131 7.5q66 -17 107.5 -71t41.5 -123v-430q0 -25 -17 -42.5t-42 -17.5h-144q-86 0 -144 58.5t-58 144.5zM229 815 q0 -35 24.5 -59.5t59.5 -24.5h84v168h-84q-35 0 -59.5 -24.5t-24.5 -59.5z" />
+<glyph unicode="&#xab;" horiz-adv-x="858" d="M106 565q-15 30 0 64l132 237q14 25 39.5 32.5t50.5 -5.5q32 -19 32 -57q0 -23 -8 -31q-8 -16 -24 -44.5t-23 -41.5q-60 -113 -65 -121q6 -11 30 -57.5t39 -73.5q8 -17 27 -50t24 -42q9 -15 9 -31q0 -37 -33 -57q-25 -14 -51 -6.5t-39 32.5zM475 565q-15 30 0 64l131 237 q14 25 39.5 32.5t50.5 -5.5q33 -20 33 -57q0 -23 -8 -31q-8 -16 -24 -44.5t-23 -41.5q-53 -100 -66 -121q6 -11 30.5 -58t39.5 -73q8 -17 27 -50t24 -42q8 -13 8 -31q0 -38 -32 -57q-25 -14 -51.5 -6.5t-39.5 32.5z" />
+<glyph unicode="&#xac;" d="M115 590q0 29 19 47t46 18h492q29 0 47 -18t18 -47v-246q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5t-19.5 46.5v180h-426q-27 0 -46 18.5t-19 47.5z" />
+<glyph unicode="&#xad;" d="M115 590q0 29 18 47t47 18h492q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-492q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="&#xae;" horiz-adv-x="1470" d="M80 651q0 273 192 465q92 92 212 141.5t251 49.5q133 0 253 -49.5t212 -141.5q193 -193 193 -465t-193 -465q-93 -93 -212.5 -142.5t-252.5 -49.5q-130 0 -250.5 50t-212.5 142q-192 192 -192 465zM205 651q0 -106 40 -204t115 -173t172 -115t203 -40q222 0 377 155 q75 75 115.5 173t40.5 204t-40.5 203t-115.5 172q-156 156 -377 156q-106 0 -203 -40.5t-172 -115.5t-115 -172t-40 -203zM530 344v606q0 26 18.5 44t43.5 18h164q93 0 160 -66.5t67 -159.5q0 -66 -34.5 -119t-92.5 -83l119 -209q12 -23 6 -48t-29 -38q-23 -12 -48.5 -5.5 t-37.5 29.5l-141 246h-70v-215q0 -26 -18.5 -44.5t-44.5 -18.5q-25 0 -43.5 18.5t-18.5 44.5zM655 684h101q42 0 72 30t30 72t-30 71.5t-72 29.5h-101v-203z" />
+<glyph unicode="&#xaf;" horiz-adv-x="1024" d="M-66 1532q0 29 18.5 47t47.5 18h1024q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-1024q-29 0 -47.5 18.5t-18.5 47.5z" />
+<glyph unicode="&#xb0;" horiz-adv-x="641" d="M78 1061q0 100 71.5 172t172.5 72q100 0 171.5 -72t71.5 -172q0 -101 -71.5 -172.5t-171.5 -71.5q-101 0 -172.5 71.5t-71.5 172.5zM197 1061q0 -54 35.5 -89.5t89.5 -35.5t89 35.5t35 89.5t-35 89.5t-89 35.5t-89.5 -35.5t-35.5 -89.5z" />
+<glyph unicode="&#xb1;" d="M115 66q0 29 19 47t46 18h492q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-492q-27 0 -46 18.5t-19 47.5zM115 590q0 29 19 47t46 18h180v181q0 28 19.5 46.5t46.5 18.5q29 0 47.5 -18t18.5 -47v-181h180q29 0 47 -18t18 -47t-18 -47.5t-47 -18.5h-180v-180 q0 -29 -18.5 -47t-47.5 -18q-27 0 -46.5 18.5t-19.5 46.5v180h-180q-27 0 -46 18.5t-19 47.5z" />
+<glyph unicode="&#xb2;" horiz-adv-x="608" d="M111 641q0 16 10 29l246 387q20 25 10 63q-6 34 -39 54q-26 20 -64 10q-31 -8 -53 -39q-12 -23 -34.5 -29t-45.5 6t-29 36t7 44q45 76 125 96q86 19 155 -24q75 -41 93 -127q19 -85 -27 -154q-52 -84 -68 -108q-66 -107 -118 -187h159q25 0 42.5 -17t17.5 -42t-17.5 -42 t-42.5 -17h-268q-36 0 -51 30q-8 13 -8 31z" />
+<glyph unicode="&#xb3;" horiz-adv-x="606" d="M110 718q6 24 29 36t45.5 6t34.5 -29q38 -54 96 -39q29 7 45.5 29.5t16.5 52.5v41q0 35 -24.5 59.5t-59.5 24.5q-25 0 -42.5 17t-17.5 42t17.5 42.5t42.5 17.5q47 0 74 41q21 43 0 86q-27 41 -74 41t-74 -41q-12 -23 -34.5 -29t-45.5 6t-29 36t7 44q30 48 76.5 75.5 t99.5 27.5q54 0 101 -28t73 -75q29 -50 29 -100q0 -51 -29 -101q-12 -24 -31 -43q60 -60 60 -143v-41q0 -69 -42 -123t-108 -71q-67 -19 -130.5 7.5t-98.5 86.5q-13 20 -7 44z" />
+<glyph unicode="&#xb4;" horiz-adv-x="1024" d="M345 1105.5q2 26.5 22 45.5l204 184q20 18 47 16.5t46 -22.5q19 -20 17 -46.5t-22 -45.5l-204 -184q-20 -19 -48 -18t-45 24q-19 20 -17 46.5z" />
+<glyph unicode="&#xb5;" horiz-adv-x="819" d="M143 -352v1229q0 26 18 43.5t44 17.5t43.5 -17.5t17.5 -43.5v-615q0 -60 42 -101.5t102 -41.5t101.5 41.5t41.5 101.5v615q0 26 17.5 43.5t43.5 17.5t44 -17.5t18 -43.5v-820q0 -26 -18 -43.5t-44 -17.5q-45 0 -59 43q-64 -43 -145 -43q-79 0 -144 41v-389 q0 -26 -17.5 -44t-43.5 -18t-44 18t-18 44z" />
+<glyph unicode="&#xb6;" horiz-adv-x="983" d="M70 991q0 127 90 217t217 90h512q26 0 44.5 -17.5t18.5 -43.5t-18.5 -44.5t-44.5 -18.5h-80v-1280q0 -26 -19 -45t-45 -19t-43.5 18.5t-17.5 45.5v1280h-162v-1280q0 -26 -18.5 -45t-44.5 -19t-44 19t-18 45v788h-20q-127 0 -217 90.5t-90 218.5zM195 991 q0 -75 53.5 -129.5t128.5 -54.5h20v367h-20q-75 0 -128.5 -54t-53.5 -129z" />
+<glyph unicode="&#xb7;" horiz-adv-x="393" d="M111 795q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xb8;" horiz-adv-x="1024" d="M371.5 -193q6.5 26 29.5 39q50 27 50 86v125q0 29 18 47.5t47 18.5t47.5 -18.5t18.5 -47.5v-125q0 -62 -30.5 -114.5t-84.5 -83.5q-23 -14 -50 -7.5t-40 29.5q-12 25 -5.5 51z" />
+<glyph unicode="&#xb9;" horiz-adv-x="479" d="M55 1063q0 23 17 43l182 180q25 28 63 14q37 -15 37 -55v-614q0 -25 -17 -42.5t-42 -17.5t-42 17.5t-17 42.5v473q-13 -13 -31.5 -32t-24.5 -25q-14 -17 -24 -25q-19 -16 -43 -16q-25 0 -41 16q-17 17 -17 41z" />
+<glyph unicode="&#xba;" horiz-adv-x="624" d="M111 815v287q0 83 58.5 143t143.5 60q82 0 142.5 -60.5t60.5 -142.5v-287q0 -85 -60 -144t-143 -59q-86 0 -144 58.5t-58 144.5zM229 815q0 -35 24.5 -59.5t59.5 -24.5t59.5 24.5t24.5 59.5v287q0 35 -24.5 59.5t-59.5 24.5t-59.5 -24.5t-24.5 -59.5v-287z" />
+<glyph unicode="&#xbb;" horiz-adv-x="858" d="M121 344q0 12 8 31q5 9 24 42t27 50q10 19 37 68t35 63q-29 47 -68 121q-7 13 -23 41.5t-24 44.5q-8 13 -8 31q0 39 35 57q23 14 49.5 6t38.5 -33l131 -237q18 -34 0 -64l-139 -252q-12 -25 -38.5 -32.5t-49.5 6.5q-35 18 -35 57zM489 344q0 10 9 31q5 9 24 42t27 50 q10 19 37 68t35 63q-29 47 -68 121q-7 13 -23 41.5t-24 44.5q-8 13 -8 31q0 39 34 57q23 14 50 6t39 -33l131 -237q18 -34 0 -64l-140 -252q-12 -25 -38.5 -32.5t-49.5 6.5q-35 18 -35 57z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1224" d="M55 1063q0 23 17 43l182 180q25 28 63 14q37 -15 37 -55v-614q0 -25 -17 -42.5t-42 -17.5t-42 17.5t-17 42.5v473q-13 -13 -31.5 -32t-24.5 -25q-14 -17 -24 -25q-19 -16 -43 -16q-25 0 -41 16q-17 17 -17 41zM232.5 35.5q-8.5 25.5 3.5 50.5l573 1188q12 24 37.5 33 t50.5 -2q24 -12 33 -37.5t-2 -50.5l-574 -1188q-12 -25 -37.5 -34t-50.5 3t-33.5 37.5zM672 244l182 450q11 24 33 33.5t45 -0.5q37 -15 37 -55q0 -7 -4 -23q-11 -27 -31.5 -75t-28.5 -68q-15 -37 -47.5 -116.5t-44.5 -108.5h119v63q0 25 17 42t42 17t42.5 -17t17.5 -42v-63 h47q25 0 42 -17.5t17 -42.5t-17 -42t-42 -17h-47v-105q0 -25 -17.5 -42t-42.5 -17t-42 17t-17 42v105h-205q-35 0 -49 26q-19 28 -6 56z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1220" d="M55 1063q0 23 17 43l182 180q25 28 63 14q37 -15 37 -55v-614q0 -25 -17 -42.5t-42 -17.5t-42 17.5t-17 42.5v473q-13 -13 -31.5 -32t-24.5 -25q-14 -17 -24 -25q-19 -16 -43 -16q-25 0 -41 16q-17 17 -17 41zM232.5 35.5q-8.5 25.5 3.5 50.5l573 1188q12 24 37.5 33 t50.5 -2q24 -12 33 -37.5t-2 -50.5l-574 -1188q-12 -25 -37.5 -34t-50.5 3t-33.5 37.5zM766 68q0 15 10 28l246 387q20 26 10 64q-6 33 -39 53q-25 20 -63 10q-31 -8 -53 -39q-12 -23 -34.5 -28.5t-45.5 6.5t-29.5 36t6.5 44q45 76 125 96q85 19 156 -25q74 -41 92 -127 q18 -84 -27 -153q-7 -11 -30.5 -50t-36.5 -59q-30 -48 -119 -186h160q25 0 42 -17t17 -42t-17 -42.5t-42 -17.5h-269q-35 0 -51 31q-8 13 -8 31z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1286" d="M71 718q6 24 29 36t45.5 6t34.5 -29q38 -54 96 -39q29 7 45.5 29.5t16.5 52.5v41q0 35 -24.5 59.5t-59.5 24.5q-25 0 -42 17t-17 42t17 42.5t42 17.5q47 0 74 41q21 43 0 86q-27 41 -74 41t-74 -41q-12 -23 -34.5 -29t-45.5 6t-29 36t7 44q30 48 76.5 75.5t99.5 27.5 q54 0 101 -28t73 -75q29 -50 29 -100q0 -51 -29 -101q-12 -24 -31 -43q60 -60 60 -143v-41q0 -69 -42 -123t-108 -71q-67 -19 -130.5 7.5t-98.5 86.5q-13 20 -7 44zM294 35.5q-9 25.5 3 50.5l573 1188q12 24 37.5 33t50.5 -2q24 -12 33 -37.5t-2 -50.5l-573 -1188 q-12 -25 -37.5 -34t-50.5 3t-34 37.5zM733 244l182 450q11 24 33 33.5t45 -0.5q37 -15 37 -55q0 -7 -4 -23q-11 -28 -31 -76t-28 -67q-79 -192 -93 -225h119v63q0 25 17.5 42t42.5 17t42 -17t17 -42v-63h47q25 0 42.5 -17.5t17.5 -42.5t-17.5 -42t-42.5 -17h-47v-105 q0 -25 -17 -42t-42 -17t-42.5 17t-17.5 42v105h-205q-35 0 -49 26q-19 28 -6 56z" />
+<glyph unicode="&#xbf;" horiz-adv-x="731" d="M84 -88q-5 68 18 131.5t70 110.5l104 104q52 55 52 127v164q0 29 19 47t46 18q29 0 47.5 -18t18.5 -47v-164q0 -129 -90 -219l-103 -105q-57 -57 -51 -139q3 -79 70 -131q63 -48 145 -33q81 17 121 86q12 23 38.5 30t49.5 -5q24 -13 31.5 -40t-6.5 -50 q-74 -125 -209 -150q-137 -28 -250 58q-111 85 -121 225zM307 856q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xc0;" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q8 -27 -5.5 -50t-42.5 -30q-26 -7 -48.5 6t-30.5 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30.5 -39.5t-51.5 -5.5q-26 8 -39 31t-6 49zM273 1692q-1 27 18 47 q19 21 45.5 22.5t46.5 -16.5l205 -184q21 -19 22.5 -46t-16.5 -47q-18 -23 -45 -24t-47 18l-205 185q-23 18 -24 45zM281 410h323q-25 93 -83 302t-79 287q-19 -66 -161 -589z" />
+<glyph unicode="&#xc1;" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q8 -27 -5.5 -50t-42.5 -30q-26 -7 -48.5 6t-30.5 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30.5 -39.5t-51.5 -5.5q-26 8 -39 31t-6 49zM273 1515.5q1 27.5 24 45.5 l205 184q20 18 46.5 16.5t45.5 -22.5q19 -20 18 -47t-24 -45l-205 -185q-20 -19 -47 -18t-45 24q-19 20 -18 47.5zM281 410h323q-25 93 -83 302t-79 287q-19 -66 -161 -589z" />
+<glyph unicode="&#xc2;" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q8 -27 -5.5 -50t-42.5 -30q-26 -7 -48.5 6t-30.5 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30.5 -39.5t-51.5 -5.5q-26 8 -39 31t-6 49zM195 1520q3 23 24 41 l184 143q14 14 39 14t39 -14l185 -143q21 -18 24 -41t-14 -47q-18 -21 -41 -24.5t-47 13.5l-56 43q-15 11 -46 36t-44 34q-13 -9 -44 -34t-46 -36l-55 -43q-21 -17 -45.5 -13.5t-42.5 24.5q-17 24 -14 47zM281 410h323q-25 93 -83 302t-79 287q-19 -66 -161 -589z" />
+<glyph unicode="&#xc3;" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q8 -27 -5.5 -50t-42.5 -30q-26 -7 -48.5 6t-30.5 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30.5 -39.5t-51.5 -5.5q-26 8 -39 31t-6 49zM196.5 1556 q-3.5 25 10.5 48q46 67 123 79q81 13 143 -34l16 -13q22 -16 46 -10q25 3 40 27q14 23 39 28t48 -12q23 -13 27.5 -38t-9.5 -48q-47 -68 -123 -80q-78 -12 -143 35l-17 12q-24 14 -47 11q-22 -3 -39 -27q-18 -24 -41 -28.5t-47 12.5q-23 13 -26.5 38zM281 410h323 q-25 93 -83 302t-79 287q-19 -66 -161 -589z" />
+<glyph unicode="&#xc4;" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q8 -27 -5.5 -50t-42.5 -30q-26 -7 -48.5 6t-30.5 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30.5 -39.5t-51.5 -5.5q-26 8 -39 31t-6 49zM193 1593q0 35 25.5 60.5 t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5zM281 410h323q-25 93 -83 302t-79 287q-19 -66 -161 -589zM520 1593q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xc5;" horiz-adv-x="884" d="M51 74l328 1188q13 49 63 49q51 0 64 -49l328 -1188q8 -27 -5.5 -50t-42.5 -30q-26 -7 -48.5 6t-30.5 39q-15 49 -19 67q-16 50 -47 173h-397q-13 -39 -27 -96.5t-20 -76.5q-4 -18 -19 -67q-8 -27 -30.5 -39.5t-51.5 -5.5q-26 8 -39 31t-6 49zM268 1614q0 72 51 123 t123 51q73 0 124.5 -51t51.5 -123t-51.5 -124t-124.5 -52t-123.5 51.5t-50.5 124.5zM281 410h323q-25 93 -83 302t-79 287q-19 -66 -161 -589zM373 1614q0 -30 20 -51t49 -21t50.5 21.5t21.5 50.5t-21 49t-51 20q-29 0 -49 -20t-20 -49z" />
+<glyph unicode="&#xc6;" horiz-adv-x="1263" d="M37.5 33.5q-9.5 25.5 1.5 50.5l528 1180q14 39 62 39h493q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47.5t-47.5 -18.5h-344v-434h303q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-303v-475h344q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-409q-27 0 -46.5 18.5 t-19.5 47.5v213h-377q-12 -27 -30 -66.5t-30 -66.5t-20 -46q-22 -45 -30 -69q-12 -25 -37.5 -34.5t-50.5 1.5q-25 10 -34.5 35.5zM330 410h317v712q-25 -60 -141 -317.5t-176 -394.5z" />
+<glyph unicode="&#xc7;" horiz-adv-x="806" d="M115 303v696q0 106 63 189t166 112q102 29 198.5 -12t151.5 -133l21 -35q14 -25 7 -51t-32 -39q-23 -14 -49 -7t-39 32l-20 35q-31 54 -87 77.5t-116 6.5t-96.5 -65t-36.5 -110v-696q0 -61 36.5 -109t96.5 -65t116 6.5t87 77.5l20 35q13 24 39 31t49 -7q25 -13 32 -39 t-7 -51l-21 -35q-71 -120 -202 -149v-70q0 -61 -31.5 -114t-85.5 -84q-23 -14 -49 -7.5t-39 29.5q-14 25 -7 51t31 39q49 28 49 86v70q-2 2 -8 2t-8 2q-103 29 -166 112t-63 189z" />
+<glyph unicode="&#xc8;" horiz-adv-x="753" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h409q27 0 46.5 -18.5t19.5 -47.5t-19.5 -47.5t-46.5 -18.5h-344v-434h303q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-303v-475h344q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-409 q-29 0 -47.5 18.5t-18.5 47.5zM240.5 1692.5q-1.5 26.5 17.5 46.5q19 21 45.5 22.5t46.5 -16.5l205 -184q20 -19 21.5 -46t-17.5 -47q-17 -23 -44.5 -24t-47.5 18l-205 185q-20 19 -21.5 45.5z" />
+<glyph unicode="&#xc9;" horiz-adv-x="753" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h409q27 0 46.5 -18.5t19.5 -47.5t-19.5 -47.5t-46.5 -18.5h-344v-434h303q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-303v-475h344q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-409 q-29 0 -47.5 18.5t-18.5 47.5zM240.5 1515q1.5 27 21.5 46l205 184q20 19 47.5 16.5t44.5 -22.5q19 -20 17.5 -46.5t-21.5 -45.5l-205 -185q-20 -19 -47 -18t-45 24q-19 20 -17.5 47z" />
+<glyph unicode="&#xca;" horiz-adv-x="753" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h409q27 0 46.5 -18.5t19.5 -47.5t-19.5 -47.5t-46.5 -18.5h-344v-434h303q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-303v-475h344q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-409 q-29 0 -47.5 18.5t-18.5 47.5zM162 1520q3 23 24 41l185 143q14 14 37 14q24 0 38 -14l185 -143q23 -17 26 -41t-14 -47q-18 -21 -42.5 -24.5t-45.5 13.5l-55 43q-11 8 -90 70q-14 -10 -47 -35.5t-46 -34.5l-55 -43q-21 -17 -44.5 -13.5t-41.5 24.5q-17 24 -14 47z" />
+<glyph unicode="&#xcb;" horiz-adv-x="753" d="M139 66v1171q0 29 18.5 47.5t47.5 18.5h409q29 0 47.5 -18.5t18.5 -47.5t-18.5 -47.5t-47.5 -18.5h-344v-434h303q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-303v-475h344q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-409q-29 0 -47.5 18.5t-18.5 47.5z M160 1593q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5zM487 1593q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xcc;" horiz-adv-x="409" d="M35 1692q-1 27 18 47q19 21 45.5 22.5t46.5 -16.5l205 -184q21 -19 22.5 -46t-16.5 -47q-18 -23 -45 -24t-47 18l-205 185q-23 18 -24 45zM139 57v1188q0 29 18.5 47.5t47.5 18.5t47 -18.5t18 -47.5v-1188q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47z" />
+<glyph unicode="&#xcd;" horiz-adv-x="409" d="M35 1515.5q1 27.5 24 45.5l205 184q20 18 46.5 16.5t45.5 -22.5q18 -20 16.5 -46.5t-22.5 -45.5l-205 -185q-20 -19 -47 -18t-45 24q-19 20 -18 47.5zM139 57v1188q0 29 18.5 47.5t47.5 18.5t47 -18.5t18 -47.5v-1188q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47z" />
+<glyph unicode="&#xce;" horiz-adv-x="409" d="M-42.5 1520q3.5 23 24.5 41l184 143q14 14 39 14t39 -14l184 -143q21 -18 24 -41t-14 -47q-18 -21 -41 -24.5t-47 13.5l-55 43q-11 8 -90 70l-45.5 -35t-46.5 -35q-9 -7 -27 -21.5t-27 -21.5q-24 -17 -47 -13.5t-41 24.5q-17 24 -13.5 47zM139 57v1188q0 29 18.5 47.5 t47.5 18.5t47 -18.5t18 -47.5v-1188q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47z" />
+<glyph unicode="&#xcf;" horiz-adv-x="409" d="M-45 1593q0 35 25.5 60.5t60.5 25.5t60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5zM139 57v1188q0 29 18.5 47.5t47.5 18.5t47 -18.5t18 -47.5v-1188q0 -29 -18 -47t-47 -18t-47.5 18t-18.5 47zM283 1593q0 35 25.5 60.5t60.5 25.5t60.5 -25.5 t25.5 -60.5t-25.5 -60.5t-60.5 -25.5t-60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xd0;" horiz-adv-x="903" d="M6 672q0 26 18.5 43.5t43.5 17.5h98v504q0 29 19 47.5t46 18.5h246q129 0 220 -91.5t91 -220.5v-680q0 -129 -91 -220t-220 -91h-246q-27 0 -46 18.5t-19 47.5v542h-98q-25 0 -43.5 19t-18.5 45zM297 131h180q75 0 127.5 52.5t52.5 127.5v680q0 74 -52.5 127t-127.5 53 h-180v-438h139q27 0 45.5 -17.5t18.5 -43.5t-19 -45t-45 -19h-139v-477z" />
+<glyph unicode="&#xd1;" horiz-adv-x="983" d="M139 57v1188q0 24 14 42t37 22t43 -5.5t31 -29.5q30 -62 86 -178t82 -168q13 -28 281 -584v901q0 29 19 47.5t46 18.5q29 0 47.5 -18.5t18.5 -47.5v-1188q0 -23 -14 -41t-37 -22q-23 -6 -43 4t-31 31q-68 145 -166 346q-35 72 -137.5 283t-145.5 300v-901q0 -29 -18 -47 t-47 -18q-27 0 -46.5 18.5t-19.5 46.5zM244 1556.5q-5 24.5 12 47.5q40 67 123 79q81 13 141 -34l17 -13q23 -16 47 -10q26 3 41 27q14 23 38.5 28t47.5 -12q23 -13 27.5 -38t-9.5 -48q-47 -68 -123 -80q-78 -12 -143 35l-17 12q-24 14 -47 11q-22 -3 -39 -27 q-18 -24 -41 -28.5t-47 12.5q-23 14 -28 38.5z" />
+<glyph unicode="&#xd2;" d="M115 303v696q0 129 91 220.5t220 91.5t220 -91.5t91 -220.5v-696q0 -129 -91 -220t-220 -91t-220 91t-91 220zM246 303q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5t52.5 127.5v696q0 74 -52.5 127.5t-127.5 53.5q-73 0 -126.5 -53.5t-53.5 -127.5v-696zM259 1692.5 q-2 26.5 15 46.5q19 20 47 22.5t48 -16.5l204 -184q20 -19 22 -46t-17 -47q-18 -23 -45.5 -24t-47.5 18l-204 185q-20 19 -22 45.5z" />
+<glyph unicode="&#xd3;" d="M115 303v696q0 129 91.5 220.5t219.5 91.5q129 0 220 -91.5t91 -220.5v-696q0 -129 -91 -220t-220 -91q-128 0 -219.5 91t-91.5 220zM246 303q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5t52.5 127.5v696q0 74 -52.5 127.5t-127.5 53.5q-73 0 -126.5 -53.5t-53.5 -127.5 v-696zM259 1515q2 27 22 46l204 184q20 18 47 16.5t46 -22.5q19 -20 17 -46.5t-22 -45.5l-204 -185q-20 -19 -48 -18t-45 24q-19 20 -17 47z" />
+<glyph unicode="&#xd4;" d="M115 303v696q0 129 91.5 220.5t219.5 91.5q129 0 220 -91.5t91 -220.5v-696q0 -129 -91 -220t-220 -91q-128 0 -219.5 91t-91.5 220zM179 1520q3 24 26 41l184 143q14 14 37 14q25 0 39 -14l184 -143q23 -17 26.5 -41t-13.5 -47q-18 -21 -43 -24.5t-46 13.5l-55 43 q-14 10 -46.5 35l-45.5 35q-13 -9 -44 -34t-46 -36l-55 -43q-21 -17 -45.5 -13.5t-42.5 24.5q-17 23 -14 47zM246 303q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5t52.5 127.5v696q0 74 -52.5 127.5t-127.5 53.5q-73 0 -126.5 -53.5t-53.5 -127.5v-696z" />
+<glyph unicode="&#xd5;" d="M115 303v696q0 129 91 220.5t220 91.5q128 0 219.5 -91.5t91.5 -220.5v-696q0 -129 -91.5 -220t-219.5 -91q-129 0 -220 91t-91 220zM179 1556q-5 25 9 48q46 67 123 79q80 12 144 -34l16 -13q23 -16 47 -10q22 3 39 27q18 24 41 28.5t47 -12.5q23 -13 27 -38t-10 -48 q-47 -68 -123 -80q-80 -13 -144 35l-16 12q-20 14 -45 11q-26 -3 -41 -27q-14 -23 -38.5 -28t-47.5 12q-23 13 -28 38zM246 303q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v696q0 74 -53.5 127.5t-126.5 53.5q-75 0 -127.5 -53.5t-52.5 -127.5v-696z" />
+<glyph unicode="&#xd6;" d="M115 303v696q0 129 91 220.5t220 91.5q128 0 219.5 -91.5t91.5 -220.5v-696q0 -129 -91.5 -220t-219.5 -91q-129 0 -220 91t-91 220zM176 1593q0 36 25 61t61 25q35 0 60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5q-36 0 -61 25t-25 61zM246 303q0 -75 52.5 -127.5 t127.5 -52.5q74 0 127 52.5t53 127.5v696q0 74 -53.5 127.5t-126.5 53.5q-75 0 -127.5 -53.5t-52.5 -127.5v-696zM504 1593q0 36 25 61t61 25q35 0 60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5q-36 0 -61 25t-25 61z" />
+<glyph unicode="&#xd7;" d="M156 385q0 26 18 47l160 158l-160 158q-18 21 -18 47t18 47q21 18 47 18t47 -18l158 -160l158 160q21 18 47 18t47 -18q18 -21 18 -47t-18 -47l-160 -158l160 -158q18 -21 18 -47t-18 -47q-22 -19 -47 -19t-47 19l-158 160l-158 -160q-22 -19 -47 -19t-47 19 q-18 21 -18 47z" />
+<glyph unicode="&#xd8;" horiz-adv-x="872" d="M7 -42.5q-7 25.5 5 48.5l121 221q-8 31 -8 76v696q0 129 91 220.5t220 91.5q137 0 232 -105l82 152q12 23 36.5 29.5t47.5 -5.5t30.5 -37.5t-4.5 -48.5l-123 -221q11 -49 11 -76v-696q0 -129 -91.5 -220t-220.5 -91q-68 0 -128.5 28t-102.5 76l-82 -151q-13 -23 -38 -30 t-48 5t-30 37.5zM256 451l342 628q-24 46 -66.5 73.5t-95.5 27.5q-75 0 -127.5 -53.5t-52.5 -127.5v-548zM274 223q24 -46 66.5 -73t95.5 -27q74 0 127 52.5t53 127.5v549z" />
+<glyph unicode="&#xd9;" horiz-adv-x="868" d="M123 303v942q0 29 18 47.5t47 18.5t47.5 -18.5t18.5 -47.5v-942q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v942q0 29 18.5 47.5t47.5 18.5t47 -18.5t18 -47.5v-942q0 -129 -91 -220t-220 -91t-220 91t-91 220zM265.5 1692.5q-1.5 26.5 17.5 46.5 q19 21 45.5 22.5t46.5 -16.5l205 -184q20 -19 21.5 -46t-15.5 -47q-18 -23 -44.5 -24t-49.5 18l-205 185q-20 19 -21.5 45.5z" />
+<glyph unicode="&#xda;" horiz-adv-x="868" d="M123 303v942q0 29 18 47.5t47 18.5t47.5 -18.5t18.5 -47.5v-942q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v942q0 29 18.5 47.5t47.5 18.5t47 -18.5t18 -47.5v-942q0 -129 -91 -220t-220 -91t-220 91t-91 220zM265 1515.5q1 27.5 24 45.5l205 184 q20 18 46.5 16.5t45.5 -22.5q18 -20 16.5 -46.5t-22.5 -45.5l-205 -185q-20 -19 -47 -18t-45 24q-19 20 -18 47.5z" />
+<glyph unicode="&#xdb;" horiz-adv-x="868" d="M123 303v942q0 29 19 47.5t46 18.5q29 0 47.5 -18.5t18.5 -47.5v-942q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5t52.5 127.5v942q0 29 19.5 47.5t46.5 18.5q29 0 47 -18.5t18 -47.5v-942q0 -129 -91 -220t-220 -91q-128 0 -219.5 91t-91.5 220zM187 1520q3 24 26 41 l184 143q14 14 37 14q25 0 39 -14l184 -143q21 -18 24.5 -41t-13.5 -47q-18 -21 -41.5 -24.5t-44.5 13.5l-56 43q-14 10 -46.5 35l-45.5 35q-13 -9 -44 -34t-46 -36l-55 -43q-21 -17 -45.5 -13.5t-42.5 24.5q-17 23 -14 47z" />
+<glyph unicode="&#xdc;" horiz-adv-x="868" d="M123 303v942q0 29 19 47.5t46 18.5q29 0 47.5 -18.5t18.5 -47.5v-942q0 -75 53 -127.5t127 -52.5q75 0 127.5 52.5t52.5 127.5v942q0 29 19.5 47.5t46.5 18.5q29 0 47 -18.5t18 -47.5v-942q0 -129 -91 -220t-220 -91q-128 0 -219.5 91t-91.5 220zM184 1593 q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5zM512 1593q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#xdd;" horiz-adv-x="823" d="M64 1266.5q8 26.5 32 38.5q25 11 50.5 2t37.5 -33l228 -455q21 40 70 140t71 143q34 69 88 172q12 24 37.5 33t50.5 -2q23 -12 32 -38.5t-3 -49.5l-281 -562v-598q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5t-19.5 46.5v598l-278 562q-12 23 -4 49.5zM244.5 1515 q1.5 27 21.5 46l205 184q20 18 46.5 16.5t45.5 -22.5q19 -20 17.5 -46.5t-21.5 -45.5l-205 -185q-23 -19 -49.5 -18t-44.5 24q-17 20 -15.5 47z" />
+<glyph unicode="&#xde;" horiz-adv-x="819" d="M139 57v1180q0 29 18.5 47.5t47.5 18.5q27 0 46 -18.5t19 -47.5v-193h181q128 0 219.5 -91t91.5 -220v-123q0 -129 -91.5 -220t-219.5 -91h-181v-242q0 -29 -19 -47t-46 -18q-29 0 -47.5 18t-18.5 47zM270 430h181q74 0 127 52.5t53 127.5v123q0 73 -53.5 126.5 t-126.5 53.5h-181v-483z" />
+<glyph unicode="&#xdf;" horiz-adv-x="876" d="M143 57v1024q0 74 38 137.5t104 98.5t138 31t134 -45q55 -37 87 -96.5t32 -125.5q0 -20 -2 -26q-9 -92 -72 -158q71 -43 113.5 -112.5t42.5 -153.5v-328q0 -127 -90 -217t-217 -90q-26 0 -44 17.5t-18 43.5t18 44t44 18q78 0 131 53t53 131v328q0 78 -53 131t-131 53 q-24 0 -41 13t-21 36q-9 46 37 70l39 14q79 37 88 119q9 81 -64 133q-72 48 -147 8q-76 -44 -76 -127v-1024q0 -26 -18 -43.5t-43 -17.5q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="&#xe0;" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 42 -23.5 76.5t-62.5 52.5q-42 17 -82 8.5t-70 -39.5q-18 -18 -45 -18q-25 0 -43 18t-18 43q0 28 18 43q57 57 135 73t152 -15t118 -97t44 -145v-610q0 -26 -18.5 -44t-43.5 -18h-204q-110 0 -188.5 78t-78.5 188zM184 270 q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102zM203.5 1283q-1.5 25 15.5 44t42.5 20t45.5 -16l205 -184q19 -17 20 -42t-16 -44t-42 -20t-44 16l-205 184q-20 17 -21.5 42z" />
+<glyph unicode="&#xe1;" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 42 -23.5 76.5t-62.5 52.5q-42 17 -82 8.5t-70 -39.5q-18 -18 -45 -18q-25 0 -43 18q-16 19 -16 43q0 27 16 43q57 57 135 73t152 -15t118 -97t44 -145v-610q0 -26 -18.5 -44t-43.5 -18h-204q-110 0 -188.5 78t-78.5 188z M184 270q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102zM203.5 1105q1.5 25 21.5 42l205 184q19 17 44 16t42 -20t16 -44t-20 -42l-205 -184q-20 -17 -44.5 -16t-43.5 20q-17 19 -15.5 44z" />
+<glyph unicode="&#xe2;" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 42 -23.5 76.5t-62.5 52.5q-42 17 -82 8.5t-70 -39.5q-18 -18 -45 -18q-25 0 -43 18q-16 19 -16 43q0 27 16 43q57 57 135 73t152 -15t118 -97t44 -145v-610q0 -26 -18.5 -44t-43.5 -18h-204q-110 0 -188.5 78t-78.5 188z M126 1109.5q3 24.5 21 37.5l185 143q37 27 71 0l185 -143q21 -14 24 -38t-14 -44q-13 -18 -37.5 -21t-44.5 11q-9 7 -27.5 22.5t-27.5 22.5q-18 15 -52 40.5t-40 30.5q-46 -33 -93 -71q-4 -3 -24.5 -20t-32.5 -25q-19 -14 -42 -11.5t-40 21.5q-14 20 -11 44.5zM184 270 q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102z" />
+<glyph unicode="&#xe3;" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 42 -23.5 76.5t-62.5 52.5q-42 17 -82 8.5t-70 -39.5q-18 -18 -45 -18q-25 0 -43 18q-16 19 -16 43q0 27 16 43q57 57 135 73t152 -15t118 -97t44 -145v-610q0 -26 -18 -44t-44 -18h-204q-110 0 -188.5 78t-78.5 188z M127.5 1147.5q-4.5 24.5 7.5 44.5q48 67 121 78q79 12 139 -33l17 -12q22 -19 49 -13q29 3 43 29q13 20 37.5 24t44.5 -8q20 -13 24 -37t-8 -44q-48 -67 -121 -78q-80 -13 -139 32l-16 13q-21 15 -50 12t-43 -29q-13 -20 -37 -24t-44 8q-20 13 -24.5 37.5zM184 270 q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102z" />
+<glyph unicode="&#xe4;" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 42 -23.5 76.5t-62.5 52.5q-42 17 -82 8.5t-70 -39.5q-18 -18 -45 -18q-25 0 -43 18q-16 19 -16 43q0 27 16 43q57 57 135 73t152 -15t118 -97t44 -145v-610q0 -26 -18.5 -44t-43.5 -18h-204q-110 0 -188.5 78t-78.5 188z M123 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM184 270q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102zM451 1184q0 35 23 58.5t58 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5 t-58 23.5t-23 58.5z" />
+<glyph unicode="&#xe5;" horiz-adv-x="720" d="M61 270q0 110 78.5 188.5t188.5 78.5h143v139q0 43 -22.5 77.5t-63.5 51.5q-38 17 -80.5 8.5t-71.5 -39.5q-18 -18 -45 -18q-26 0 -41 18q-18 18 -18 43q0 28 18 43q56 57 133.5 73t151.5 -15t118 -97t44 -145v-610q0 -26 -18 -44t-44 -18h-204q-110 0 -188.5 78 t-78.5 188zM184 270q0 -60 42 -101.5t102 -41.5h143v287h-143q-60 0 -102 -42t-42 -102zM197 1204q0 72 50 122t122 50t122 -50t50 -122t-50 -122t-122 -50t-122 50t-50 122zM295 1204q0 -31 21.5 -52.5t52.5 -21.5t52 21.5t21 52.5t-21 52.5t-52 21.5t-52.5 -21.5 t-21.5 -52.5z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1107" d="M66.5 210.5q-15.5 80.5 17.5 154.5q31 74 98 118.5t148 44.5h143v144q0 43 -23.5 79.5t-64.5 53.5t-84 8t-74 -41q-16 -16 -43 -16q-24 0 -43 16q-18 18 -18 43q0 27 18 45q57 57 136.5 73t152.5 -15q65 -24 105 -76q80 96 204 96q109 0 188 -78t79 -188v-205 q0 -26 -18.5 -43.5t-43.5 -17.5h-348v-144q0 -43 23.5 -79.5t64.5 -53.5t83 -8.5t73 39.5q18 18 43 18q27 0 45 -18q16 -16 16 -43q0 -24 -16 -43q-60 -57 -138 -73t-153 15q-62 24 -102 76q-40 -52 -105 -76q-73 -31 -152.5 -15t-136.5 73q-59 56 -74.5 136.5zM197 317 q-36 -87 30 -157q31 -31 74 -39.5t84 8.5t64.5 53.5t23.5 79.5v144h-143q-44 0 -80 -24t-53 -65zM596 528h287v144q0 60 -42 101.5t-102 41.5t-101.5 -41.5t-41.5 -101.5v-144z" />
+<glyph unicode="&#xe7;" d="M119 262v410q0 81 45 148t119 98q75 31 154 15t136 -73q19 -19 19 -45q0 -24 -19 -43q-19 -16 -45 -16q-25 0 -41 16q-31 32 -73.5 41t-83.5 -8t-64.5 -53.5t-23.5 -79.5v-410q0 -43 23.5 -79.5t64.5 -53.5q33 -10 53 -10h2h2q53 0 100 41q15 18 41 18q27 0 45 -18 q19 -16 19 -43q0 -24 -19 -43q-51 -51 -127 -72v-70q0 -61 -29.5 -113.5t-82.5 -82.5q-23 -13 -47.5 -7t-36.5 29q-13 23 -7 47.5t29 36.5q52 28 52 90v70q-30 9 -41 14q-74 31 -119 98t-45 148z" />
+<glyph unicode="&#xe8;" horiz-adv-x="753" d="M119 262v410q0 110 78.5 188t187.5 78q110 0 188 -78t78 -188v-205q0 -26 -17.5 -43.5t-43.5 -17.5h-348v-144q0 -43 23.5 -79.5t64.5 -53.5t83.5 -8.5t73.5 39.5q15 18 41 18q27 0 45 -18q19 -16 19 -43q0 -24 -19 -43q-57 -57 -136 -73t-154 15q-74 31 -119 98t-45 148 zM222 1283q-1 25 16 44t42 20t44 -16l204 -184q20 -17 21.5 -42t-17.5 -44q-17 -19 -42 -20t-44 16l-204 184q-19 17 -20 42zM242 528h286v144q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-144z" />
+<glyph unicode="&#xe9;" horiz-adv-x="753" d="M119 262v410q0 110 78.5 188t187.5 78q110 0 188 -78t78 -188v-205q0 -26 -17.5 -43.5t-43.5 -17.5h-348v-144q0 -43 23.5 -79.5t64.5 -53.5t83.5 -8.5t73.5 39.5q15 18 41 18q27 0 45 -18q19 -16 19 -43q0 -24 -19 -43q-57 -57 -135.5 -73t-152.5 15q-75 31 -120.5 98 t-45.5 148zM222 1105q1 25 20 42l204 184q20 17 45 16t44 -20q17 -19 15 -44t-22 -42l-204 -184q-19 -17 -44 -16t-42 20t-16 44zM242 528h286v144q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-144z" />
+<glyph unicode="&#xea;" horiz-adv-x="753" d="M119 262v410q0 110 78.5 188t187.5 78q110 0 188 -78t78 -188v-205q0 -26 -17.5 -43.5t-43.5 -17.5h-348v-144q0 -43 24.5 -79.5t65.5 -53.5q42 -17 83 -8.5t72 39.5q18 18 43 18q28 0 43 -18q19 -16 19 -43q0 -24 -19 -43q-57 -57 -135.5 -73t-152.5 15 q-75 31 -120.5 98t-45.5 148zM142 1109q3 24 24 38l184 143q16 13 35 13q21 0 37 -13l184 -143q18 -13 21 -37.5t-11 -44.5q-17 -19 -39.5 -21.5t-41.5 11.5q-1 1 -58 45q-18 15 -52 40.5t-40 30.5q-66 -52 -90 -71q-4 -3 -24.5 -20t-32.5 -25q-20 -14 -44.5 -11t-37.5 21 q-17 20 -14 44zM242 528h286v144q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-144z" />
+<glyph unicode="&#xeb;" horiz-adv-x="753" d="M119 262v410q0 110 78.5 188t187.5 78q110 0 188 -78t78 -188v-205q0 -26 -17.5 -43.5t-43.5 -17.5h-348v-144q0 -43 24.5 -79.5t65.5 -53.5q42 -17 83 -8.5t72 39.5q18 18 43 18q28 0 43 -18q19 -16 19 -43q0 -24 -19 -43q-57 -57 -135.5 -73t-152.5 15 q-75 31 -120.5 98t-45.5 148zM139 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM242 528h286v144q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-144zM467 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5 t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5z" />
+<glyph unicode="&#xec;" horiz-adv-x="409" d="M0 1283q-1 25 16 44t42.5 20t45.5 -16l205 -184q19 -17 20 -42t-16 -44t-42.5 -20t-45.5 16l-205 184q-19 17 -20 42zM143 57v820q0 26 18.5 43.5t43.5 17.5q26 0 43.5 -17.5t17.5 -43.5v-820q0 -26 -17.5 -43.5t-43.5 -17.5q-25 0 -43.5 17.5t-18.5 43.5z" />
+<glyph unicode="&#xed;" horiz-adv-x="409" d="M80.5 1105q1.5 25 21.5 42l205 184q19 17 44 16t42 -20t16 -44t-20 -42l-205 -184q-20 -17 -45.5 -16t-42.5 20t-15.5 44zM143 57v820q0 26 18 43.5t44 17.5q25 0 43 -17.5t18 -43.5v-820q0 -26 -18 -43.5t-43 -17.5q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="&#xee;" horiz-adv-x="409" d="M-37.5 1109.5q3.5 23.5 23.5 37.5l184 143q16 13 35 13q21 0 37 -13l184 -143q21 -14 24 -38t-14 -44q-37 -40 -82 -10q-12 8 -32.5 25t-24.5 20q-24 19 -90 71q-37 -27 -92 -71q-57 -44 -58 -45q-19 -14 -42 -11.5t-40 21.5q-16 21 -12.5 44.5zM145 57v820q0 26 18 43.5 t44 17.5q25 0 43 -17.5t18 -43.5v-820q0 -26 -18 -43.5t-43 -17.5q-26 0 -44 17.5t-18 43.5z" />
+<glyph unicode="&#xef;" horiz-adv-x="409" d="M-41 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM143 57v820q0 26 18 43.5t44 17.5q25 0 43 -17.5t18 -43.5v-820q0 -26 -18 -43.5t-43 -17.5q-26 0 -44 17.5t-18 43.5zM287 1184q0 35 23.5 58.5t58.5 23.5 t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5z" />
+<glyph unicode="&#xf0;" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78q53 0 96 -18q-39 96 -98 172l-88 -60q-20 -12 -44.5 -7.5t-37.5 24.5q-12 20 -8 44t24 37l72 48q-19 19 -76 57q-20 14 -24.5 38.5t8.5 47.5q14 20 38.5 24.5t47.5 -8.5q55 -37 111 -90l69 47q20 12 44.5 8t37.5 -24q14 -20 9 -44.5 t-25 -37.5l-56 -37q30 -39 72 -110q94 -175 94 -377v-410q0 -110 -78 -188t-188 -78t-188 78t-78 188zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410z" />
+<glyph unicode="&#xf1;" horiz-adv-x="802" d="M143 57v811q0 26 18 44t44 18h205q109 0 187.5 -78t78.5 -188v-607q0 -26 -18.5 -43.5t-43.5 -17.5q-26 0 -43.5 17.5t-17.5 43.5v607q0 60 -41.5 101.5t-101.5 41.5h-144v-750q0 -26 -18 -43.5t-43 -17.5q-26 0 -44 17.5t-18 43.5zM168 1148q-5 24 8 44q45 66 121 78 t139 -33l17 -12q22 -19 49 -13q29 3 43 29q13 20 36 24t46 -8q20 -13 24 -37t-8 -44q-48 -67 -121 -78q-80 -13 -139 32l-16 13q-22 15 -52 12q-28 -3 -41 -29q-13 -20 -37 -24t-44 8q-20 14 -25 38z" />
+<glyph unicode="&#xf2;" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78t188 -78t78 -188v-410q0 -110 -78 -188t-188 -78t-188 78t-78 188zM220.5 1283q-1.5 25 17.5 44q17 19 42 20t44 -16l204 -184q19 -17 20 -42t-16 -44t-42 -20t-44 16l-204 184q-20 17 -21.5 42zM242 262q0 -60 41.5 -101.5t101.5 -41.5 t101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410z" />
+<glyph unicode="&#xf3;" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78t188 -78t78 -188v-410q0 -110 -78 -188t-188 -78t-188 78t-78 188zM222 1105q1 25 20 42l204 184q19 17 44 16t42 -20t16 -44t-20 -42l-204 -184q-19 -17 -44 -16t-42 20t-16 44zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5 t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410z" />
+<glyph unicode="&#xf4;" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78t188 -78t78 -188v-410q0 -110 -78 -188t-188 -78t-188 78t-78 188zM143 1109.5q3 24.5 21 37.5l184 143q38 27 72 0l184 -143q21 -14 24 -38t-14 -44q-13 -18 -37.5 -21t-44.5 11q-9 7 -27.5 22.5t-27.5 22.5q-18 15 -52 40.5t-40 30.5 q-37 -27 -92 -71q-4 -3 -24.5 -20t-32.5 -25q-19 -14 -42 -11.5t-40 21.5q-14 20 -11 44.5zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410z" />
+<glyph unicode="&#xf5;" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78t188 -78t78 -188v-410q0 -110 -78 -188t-188 -78t-188 78t-78 188zM144 1147.5q-4 24.5 8 44.5q48 67 120 78q80 13 140 -33l16 -12q22 -19 49 -13q30 3 43 29q13 20 37.5 24t44.5 -8q20 -13 25 -37t-9 -44q-48 -67 -120 -78 q-81 -13 -140 32l-16 13q-21 15 -49 12q-29 -3 -43 -29q-13 -20 -37.5 -24t-44.5 8q-20 13 -24 37.5zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410z" />
+<glyph unicode="&#xf6;" horiz-adv-x="770" d="M119 262v410q0 110 78 188t188 78t188 -78t78 -188v-410q0 -110 -78 -188t-188 -78t-188 78t-78 188zM139 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5 t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410zM467 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5z" />
+<glyph unicode="&#xf7;" d="M115 590q0 29 18 47t47 18h492q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-492q-29 0 -47 18.5t-18 47.5zM340 324q0 36 25 61t61 25q35 0 60.5 -25.5t25.5 -60.5t-25.5 -60.5t-60.5 -25.5q-36 0 -61 25t-25 61zM340 856q0 36 25 61t61 25q35 0 60.5 -25.5t25.5 -60.5 t-25.5 -60.5t-60.5 -25.5q-36 0 -61 25t-25 61z" />
+<glyph unicode="&#xf8;" horiz-adv-x="790" d="M50.5 -40q-6.5 24 6.5 44l93 156q-21 46 -21 102v410q0 110 78 188t188 78q100 0 170 -61l66 110q12 22 36 28t44 -5q22 -11 28 -35.5t-6 -44.5l-92 -158q21 -53 21 -100v-410q0 -110 -78.5 -188t-188.5 -78q-100 0 -170 61l-65 -110q-12 -23 -36 -29.5t-44 6.5 q-23 12 -29.5 36zM252 338l250 428q-43 49 -107 49q-60 0 -101.5 -41.5t-41.5 -101.5v-334zM289 168q40 -49 106 -49q60 0 102 41.5t42 101.5v334z" />
+<glyph unicode="&#xf9;" horiz-adv-x="802" d="M127 270v607q0 26 18 43.5t43 17.5q26 0 44 -17.5t18 -43.5v-607q0 -60 41.5 -101.5t101.5 -41.5h144v750q0 26 18 43.5t43 17.5q26 0 43.5 -17.5t17.5 -43.5v-811q0 -26 -17.5 -44t-43.5 -18h-205q-109 0 -187.5 78t-78.5 188zM230 1283q-1 25 16 44t42 20t44 -16 l205 -184q20 -17 21.5 -42t-15.5 -44q-19 -19 -43.5 -20t-44.5 16l-205 184q-19 17 -20 42z" />
+<glyph unicode="&#xfa;" horiz-adv-x="802" d="M127 270v607q0 26 18 43.5t43 17.5q26 0 44 -17.5t18 -43.5v-607q0 -60 41.5 -101.5t101.5 -41.5h144v750q0 26 18 43.5t43 17.5q26 0 43.5 -17.5t17.5 -43.5v-811q0 -26 -17.5 -44t-43.5 -18h-205q-109 0 -187.5 78t-78.5 188zM230 1105q1 25 20 42l205 184 q20 17 45.5 16t42.5 -20t15.5 -44t-21.5 -42l-205 -184q-19 -17 -44 -16t-42 20t-16 44z" />
+<glyph unicode="&#xfb;" horiz-adv-x="802" d="M127 270v607q0 26 18 43.5t43 17.5q26 0 44 -17.5t18 -43.5v-607q0 -60 41.5 -101.5t101.5 -41.5h144v750q0 26 18 43.5t43 17.5q26 0 43.5 -17.5t17.5 -43.5v-811q0 -26 -17.5 -44t-43.5 -18h-205q-109 0 -187.5 78t-78.5 188zM151 1109.5q4 23.5 23 37.5l184 143 q16 13 35 13q21 0 37 -13l184 -143q20 -14 23.5 -37.5t-12.5 -44.5q-17 -19 -40 -21.5t-42 11.5q-1 1 -58 45q-18 15 -52 40.5t-40 30.5q-66 -52 -90 -71q-4 -3 -24.5 -20t-32.5 -25q-45 -30 -82 10q-17 21 -13 44.5z" />
+<glyph unicode="&#xfc;" horiz-adv-x="802" d="M127 270v607q0 26 18 43.5t43 17.5q26 0 44 -17.5t18 -43.5v-607q0 -60 41.5 -101.5t101.5 -41.5h144v750q0 26 18 43.5t43 17.5q26 0 43.5 -17.5t17.5 -43.5v-811q0 -26 -17.5 -44t-43.5 -18h-205q-109 0 -187.5 78t-78.5 188zM147 1184q0 35 23.5 58.5t58.5 23.5 t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM475 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5z" />
+<glyph unicode="&#xfd;" horiz-adv-x="755" d="M41 -352q0 26 18 43.5t43 17.5q48 0 86.5 28t51.5 75l73 245l-239 801q-8 25 4 48t37 30q25 6 47 -6t28 -35l189 -625l186 625q8 23 29.5 35t46.5 6q25 -7 37 -30t4 -48l-246 -819l-78 -262q-23 -83 -96 -137t-160 -54q-25 0 -43 18t-18 44zM213 1105q1 25 20 42l205 184 q20 17 45.5 16t42.5 -20t16 -44t-20 -42l-205 -184q-20 -17 -45.5 -16t-42.5 20t-16 44z" />
+<glyph unicode="&#xfe;" horiz-adv-x="794" d="M143 -352v1638q0 26 18 44t44 18t43.5 -18t17.5 -44v-348h144q110 0 188 -78t78 -188v-410q0 -110 -78 -188t-188 -78h-144v-348q0 -26 -17.5 -44t-43.5 -18t-44 18t-18 44zM266 119h144q60 0 101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5h-144v-696z" />
+<glyph unicode="&#xff;" horiz-adv-x="755" d="M41 -352q0 26 18 43.5t43 17.5q48 0 86.5 28t51.5 75l73 245l-239 801q-8 25 4 48t37 30q25 6 47 -6t28 -35l189 -625l186 625q8 23 29.5 35t46.5 6q25 -7 37 -30t4 -48l-246 -819l-78 -262q-23 -83 -96 -137t-160 -54q-25 0 -43 18t-18 44zM133 1184q0 35 23.5 58.5 t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5zM461 1184q0 35 23.5 58.5t58.5 23.5t58.5 -23.5t23.5 -58.5t-23.5 -58.5t-58.5 -23.5t-58.5 23.5t-23.5 58.5z" />
+<glyph unicode="&#x152;" horiz-adv-x="1220" d="M115 303v696q0 129 91 220.5t220 91.5q104 0 182 -60q4 23 22.5 37.5t41.5 14.5h409q27 0 46.5 -18.5t19.5 -47.5t-19.5 -47.5t-46.5 -18.5h-344v-213v-221h303q27 0 46.5 -18.5t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-303v-262v-213h344q27 0 46.5 -18.5 t19.5 -46.5q0 -29 -19.5 -47.5t-46.5 -18.5h-409q-24 0 -42 14t-22 37q-77 -59 -182 -59q-129 0 -220 91t-91 220zM246 303q0 -75 52.5 -127.5t127.5 -52.5q74 0 127 52.5t53 127.5v41v614v41q0 74 -53.5 127.5t-126.5 53.5q-75 0 -127.5 -53.5t-52.5 -127.5v-696z" />
+<glyph unicode="&#x153;" horiz-adv-x="1163" d="M119 262v410q0 110 78 188t188 78q125 0 205 -96q80 96 205 96q109 0 187.5 -78t78.5 -188v-205q0 -26 -18.5 -43.5t-43.5 -17.5h-348v-144q0 -43 23.5 -79.5t64.5 -53.5t83 -8.5t73 39.5q18 18 43 18q27 0 45 -18q16 -16 16 -43q0 -24 -16 -43q-60 -57 -138 -73t-153 15 q-62 24 -102 76q-80 -96 -205 -96q-110 0 -188 78t-78 188zM242 262q0 -60 41.5 -101.5t101.5 -41.5t101.5 41.5t41.5 101.5v410q0 60 -41.5 101.5t-101.5 41.5t-101.5 -41.5t-41.5 -101.5v-410zM651 528h287v144q0 60 -41.5 101.5t-101.5 41.5t-102 -41.5t-42 -101.5v-144z " />
+<glyph unicode="&#x178;" horiz-adv-x="823" d="M62 1266.5q8 26.5 32 38.5q25 11 50.5 2t37.5 -33l228 -455q63 119 143 283l86 172q12 24 37.5 33t50.5 -2q23 -12 32 -38.5t-3 -49.5l-281 -562v-598q0 -29 -18 -47t-47 -18q-27 0 -46.5 18.5t-19.5 46.5v598l-278 562q-12 23 -4 49.5zM160 1593q0 35 25.5 60.5 t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5zM487 1593q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#x2c6;" horiz-adv-x="1024" d="M262.5 1110q3.5 26 26.5 43l184 143q15 15 39 15q23 0 41 -15l184 -143q23 -17 26.5 -43t-13.5 -49t-43.5 -25.5t-49.5 15.5q-9 7 -27.5 20.5t-27.5 20.5q-15 11 -46 35.5t-44 33.5q-13 -9 -44 -33.5t-46 -35.5q-9 -7 -26.5 -20.5t-26.5 -20.5q-23 -18 -49.5 -15.5 t-43.5 25.5t-13.5 49z" />
+<glyph unicode="&#x2dc;" horiz-adv-x="1024" d="M263 1147q-5 26 9 49q48 66 125 82q81 12 146 -35l16 -12q21 -15 45 -12q25 6 37 24q18 24 43 30t49 -11q24 -14 30 -40.5t-11 -50.5q-45 -66 -125 -81q-82 -12 -146 34l-16 13q-18 15 -45 12q-28 -9 -37 -27q-17 -23 -43 -27.5t-49 9.5q-23 17 -28 43z" />
+<glyph unicode="&#x2000;" horiz-adv-x="894" />
+<glyph unicode="&#x2001;" horiz-adv-x="1788" />
+<glyph unicode="&#x2002;" horiz-adv-x="894" />
+<glyph unicode="&#x2003;" horiz-adv-x="1788" />
+<glyph unicode="&#x2004;" horiz-adv-x="596" />
+<glyph unicode="&#x2005;" horiz-adv-x="447" />
+<glyph unicode="&#x2006;" horiz-adv-x="298" />
+<glyph unicode="&#x2007;" horiz-adv-x="298" />
+<glyph unicode="&#x2008;" horiz-adv-x="223" />
+<glyph unicode="&#x2009;" horiz-adv-x="357" />
+<glyph unicode="&#x200a;" horiz-adv-x="99" />
+<glyph unicode="&#x2010;" d="M115 590q0 29 18 47t47 18h492q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-492q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="&#x2011;" d="M115 590q0 29 18 47t47 18h492q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-492q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="&#x2012;" d="M115 590q0 29 18 47t47 18h492q27 0 46 -18t19 -47t-19 -47.5t-46 -18.5h-492q-29 0 -47 18.5t-18 47.5z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1024" d="M-66 590q0 28 19.5 46.5t46.5 18.5h1024q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-1024q-27 0 -46.5 18.5t-19.5 47.5z" />
+<glyph unicode="&#x2014;" horiz-adv-x="2048" d="M-66 590q0 28 19.5 46.5t46.5 18.5h2048q29 0 47.5 -18t18.5 -47t-18.5 -47.5t-47.5 -18.5h-2048q-27 0 -46.5 18.5t-19.5 47.5z" />
+<glyph unicode="&#x2018;" horiz-adv-x="393" d="M100 1049v98q0 51 29 93.5t76 62.5q23 10 47 0t35 -33q10 -25 0 -48t-35 -34q-27 -15 -27 -41v-37h4q25 0 43.5 -17.5t18.5 -43.5t-18.5 -45t-43.5 -19h-65q-26 0 -45 19t-19 45z" />
+<glyph unicode="&#x2019;" horiz-adv-x="393" d="M100 1237q0 26 18.5 43.5t45.5 17.5h65q25 0 43.5 -17.5t18.5 -43.5v-98q0 -51 -29 -93.5t-76 -62.5q-23 -10 -47 0t-35 33q-10 25 0 48t35 34q27 9 27 41v35h-2q-26 0 -45 18.5t-19 44.5z" />
+<glyph unicode="&#x201a;" horiz-adv-x="391" d="M100 98q0 26 19 44t45 18h65q25 0 43.5 -18t18.5 -44v-98q0 -51 -29 -93.5t-76 -62.5q-23 -10 -47 0t-35 33q-10 25 0 48t35 34q27 9 27 41v35h-2q-26 0 -45 18.5t-19 44.5z" />
+<glyph unicode="&#x201c;" horiz-adv-x="759" d="M100 1049v98q0 51 28 93.5t75 62.5q25 10 48 0.5t34 -33.5q10 -25 0.5 -48t-33.5 -34q-27 -15 -27 -41v-37h2q27 0 45.5 -17.5t18.5 -43.5t-19 -45t-45 -19h-65q-26 0 -44 19t-18 45zM469 1049v98q0 51 27.5 93.5t74.5 62.5q25 10 48 0.5t34 -33.5q10 -25 1 -48t-33 -34 q-27 -15 -27 -41v-37h2q26 0 44.5 -17.5t18.5 -43.5t-18.5 -45t-44.5 -19h-66q-26 0 -43.5 18.5t-17.5 45.5z" />
+<glyph unicode="&#x201d;" horiz-adv-x="761" d="M102 1237q0 26 18 43.5t44 17.5h65q27 0 45.5 -17.5t18.5 -43.5v-98q0 -51 -29 -93.5t-76 -62.5q-25 -10 -48 -0.5t-34 33.5q-10 25 -0.5 48t33.5 34q29 10 29 41v35h-4q-26 0 -44 18.5t-18 44.5zM471 1237q0 26 17.5 43.5t43.5 17.5h66q27 0 45.5 -17.5t18.5 -43.5v-98 q0 -51 -29 -93.5t-76 -62.5q-25 -10 -48 -0.5t-34 33.5q-10 25 -0.5 48t33.5 34q29 10 29 41v35h-5q-26 0 -43.5 18.5t-17.5 44.5z" />
+<glyph unicode="&#x201e;" horiz-adv-x="761" d="M100 98q0 26 19 44t45 18h65q26 0 44 -18t18 -44v-98q0 -51 -28 -93.5t-75 -62.5q-25 -10 -48 -0.5t-34 33.5q-10 25 -0.5 48t33.5 34q27 9 27 41v35h-2q-26 0 -45 18.5t-19 44.5zM469 98q0 26 18.5 44t44.5 18h66q26 0 43.5 -18t17.5 -44v-98q0 -51 -27.5 -93.5 t-74.5 -62.5q-25 -10 -48 -0.5t-34 33.5q-10 25 -0.5 48t33.5 34q27 9 27 41v35h-3q-26 0 -44.5 18.5t-18.5 44.5z" />
+<glyph unicode="&#x2022;" horiz-adv-x="524" d="M61 578q0 82 59 142t142 60q85 0 144 -59.5t59 -142.5q0 -86 -58.5 -144.5t-144.5 -58.5q-85 0 -143 58.5t-58 144.5zM178 578q0 -35 24.5 -59.5t59.5 -24.5t60.5 24.5t25.5 59.5t-25.5 59.5t-60.5 24.5t-59.5 -24.5t-24.5 -59.5z" />
+<glyph unicode="&#x2026;" horiz-adv-x="1179" d="M94 78q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5zM504 78q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61t-61 -25q-35 0 -60.5 25.5t-25.5 60.5zM913 78q0 35 25.5 60.5t60.5 25.5q36 0 61 -25t25 -61t-25 -61 t-61 -25q-35 0 -60.5 25.5t-25.5 60.5z" />
+<glyph unicode="&#x202f;" horiz-adv-x="357" />
+<glyph unicode="&#x2039;" horiz-adv-x="489" d="M106 565q-15 30 0 64l132 237q14 25 39.5 32.5t50.5 -5.5q32 -19 32 -57q0 -23 -8 -31q-8 -16 -24 -44.5t-23 -41.5q-60 -113 -65 -121q6 -11 30 -57.5t39 -73.5q8 -17 27 -50t24 -42q9 -15 9 -31q0 -37 -33 -57q-25 -14 -51 -6.5t-39 32.5z" />
+<glyph unicode="&#x203a;" horiz-adv-x="489" d="M121 344q0 12 8 31q5 9 24 42t27 50q10 19 37 68t35 63q-29 47 -68 121q-7 13 -23 41.5t-24 44.5q-8 13 -8 31q0 39 35 57q23 14 49.5 6t38.5 -33l131 -237q18 -34 0 -64l-139 -252q-12 -25 -38.5 -32.5t-49.5 6.5q-35 18 -35 57z" />
+<glyph unicode="&#x205f;" horiz-adv-x="447" />
+<glyph unicode="&#x20ac;" d="M37 487q0 29 18 47.5t47 18.5h99v197h-99q-29 0 -47 18t-18 47t18 47.5t47 18.5h99v118q0 106 63 189t166 112t199.5 -11.5t150.5 -133.5l21 -35q13 -25 6.5 -50.5t-29.5 -39.5q-25 -14 -51 -7t-39 32l-20 35q-31 54 -87 77.5t-116 6.5t-96.5 -65t-36.5 -110v-118h262 q27 0 46 -18.5t19 -47.5t-19 -47t-46 -18h-262v-197h180q27 0 46.5 -18.5t19.5 -47.5q0 -28 -19.5 -46.5t-46.5 -18.5h-180v-119q0 -61 36.5 -109t96.5 -65t116 6.5t87 77.5l20 35q13 24 39 31t51 -7q23 -14 29.5 -39.5t-6.5 -50.5l-21 -35q-54 -93 -150.5 -133.5 t-199.5 -11.5t-166 112t-63 189v119h-99q-29 0 -47 18t-18 47z" />
+<glyph unicode="&#x2122;" horiz-adv-x="1372" d="M55 1237q0 26 18 43.5t44 17.5h409q27 0 45.5 -17.5t18.5 -43.5t-19 -44.5t-45 -18.5h-141v-543q0 -26 -18.5 -45t-44.5 -19t-44 19t-18 45v543h-143q-26 0 -44 18.5t-18 44.5zM711 631v614q0 21 11.5 37.5t31.5 22.5q41 12 69 -23q2 -3 56 -78q49 -74 98 -139 q49 65 98 139q41 56 55 78q28 35 72 23q19 -6 31 -23t12 -37v-614q0 -26 -18.5 -45t-44.5 -19t-44 19t-18 45v420q-9 -13 -31.5 -43.5t-31.5 -44.5q-25 -35 -29 -41q-21 -27 -51 -27t-51 27q-6 7 -16.5 23t-12.5 18q-4 6 -61 88v-420q0 -26 -19 -45t-45 -19t-43.5 18.5 t-17.5 45.5z" />
+<glyph unicode="&#xe000;" horiz-adv-x="942" d="M0 0v942h942v-942h-942z" />
+<hkern u1="A" u2="&#x201d;" k="143" />
+<hkern u1="A" u2="&#x2019;" k="143" />
+<hkern u1="A" u2="y" k="20" />
+<hkern u1="A" u2="w" k="20" />
+<hkern u1="A" u2="v" k="20" />
+<hkern u1="A" u2="Y" k="102" />
+<hkern u1="A" u2="W" k="82" />
+<hkern u1="A" u2="V" k="82" />
+<hkern u1="A" u2="T" k="102" />
+<hkern u1="B" u2="A" k="20" />
+<hkern u1="D" u2="Y" k="41" />
+<hkern u1="D" u2="W" k="20" />
+<hkern u1="D" u2="V" k="20" />
+<hkern u1="D" u2="A" k="20" />
+<hkern u1="F" u2="&#xf6;" k="20" />
+<hkern u1="F" u2="&#xe5;" k="20" />
+<hkern u1="F" u2="&#xe4;" k="20" />
+<hkern u1="F" u2="r" k="20" />
+<hkern u1="F" u2="o" k="41" />
+<hkern u1="F" u2="i" k="20" />
+<hkern u1="F" u2="e" k="41" />
+<hkern u1="F" u2="a" k="20" />
+<hkern u1="F" u2="A" k="82" />
+<hkern u1="F" u2="&#x2e;" k="225" />
+<hkern u1="F" u2="&#x2c;" k="225" />
+<hkern u1="J" u2="&#x2e;" k="41" />
+<hkern u1="J" u2="&#x2c;" k="41" />
+<hkern u1="K" u2="&#xf6;" k="41" />
+<hkern u1="K" u2="y" k="41" />
+<hkern u1="K" u2="u" k="20" />
+<hkern u1="K" u2="o" k="41" />
+<hkern u1="K" u2="e" k="41" />
+<hkern u1="K" u2="O" k="20" />
+<hkern u1="L" u2="&#x201d;" k="164" />
+<hkern u1="L" u2="&#x2019;" k="184" />
+<hkern u1="L" u2="y" k="82" />
+<hkern u1="L" u2="Y" k="123" />
+<hkern u1="L" u2="W" k="123" />
+<hkern u1="L" u2="V" k="123" />
+<hkern u1="L" u2="T" k="123" />
+<hkern u1="O" u2="Y" k="41" />
+<hkern u1="O" u2="W" k="41" />
+<hkern u1="O" u2="V" k="20" />
+<hkern u1="O" u2="T" k="41" />
+<hkern u1="O" u2="A" k="20" />
+<hkern u1="P" u2="&#xf6;" k="20" />
+<hkern u1="P" u2="&#xe5;" k="41" />
+<hkern u1="P" u2="&#xe4;" k="41" />
+<hkern u1="P" u2="o" k="20" />
+<hkern u1="P" u2="e" k="20" />
+<hkern u1="P" u2="a" k="41" />
+<hkern u1="P" u2="A" k="82" />
+<hkern u1="P" u2="&#x2e;" k="225" />
+<hkern u1="P" u2="&#x2c;" k="225" />
+<hkern u1="R" u2="Y" k="41" />
+<hkern u1="R" u2="T" k="20" />
+<hkern u1="T" u2="&#xf6;" k="61" />
+<hkern u1="T" u2="&#xe5;" k="102" />
+<hkern u1="T" u2="&#xe4;" k="61" />
+<hkern u1="T" u2="y" k="143" />
+<hkern u1="T" u2="w" k="143" />
+<hkern u1="T" u2="u" k="143" />
+<hkern u1="T" u2="r" k="143" />
+<hkern u1="T" u2="o" k="143" />
+<hkern u1="T" u2="i" k="41" />
+<hkern u1="T" u2="e" k="143" />
+<hkern u1="T" u2="a" k="164" />
+<hkern u1="T" u2="O" k="20" />
+<hkern u1="T" u2="A" k="102" />
+<hkern u1="T" u2="&#x3b;" k="123" />
+<hkern u1="T" u2="&#x3a;" k="123" />
+<hkern u1="T" u2="&#x2e;" k="164" />
+<hkern u1="T" u2="&#x2d;" k="164" />
+<hkern u1="T" u2="&#x2c;" k="164" />
+<hkern u1="U" u2="A" k="20" />
+<hkern u1="V" u2="&#xf6;" k="61" />
+<hkern u1="V" u2="&#xe5;" k="82" />
+<hkern u1="V" u2="&#xe4;" k="61" />
+<hkern u1="V" u2="u" k="82" />
+<hkern u1="V" u2="o" k="102" />
+<hkern u1="V" u2="e" k="102" />
+<hkern u1="V" u2="a" k="102" />
+<hkern u1="V" u2="O" k="20" />
+<hkern u1="V" u2="A" k="82" />
+<hkern u1="V" u2="&#x3b;" k="61" />
+<hkern u1="V" u2="&#x3a;" k="61" />
+<hkern u1="V" u2="&#x2e;" k="205" />
+<hkern u1="V" u2="&#x2d;" k="102" />
+<hkern u1="V" u2="&#x2c;" k="205" />
+<hkern u1="W" u2="y" k="61" />
+<hkern u1="W" u2="u" k="82" />
+<hkern u1="W" u2="o" k="82" />
+<hkern u1="W" u2="e" k="82" />
+<hkern u1="W" u2="a" k="82" />
+<hkern u1="W" u2="O" k="41" />
+<hkern u1="W" u2="A" k="82" />
+<hkern u1="W" u2="&#x3b;" k="82" />
+<hkern u1="W" u2="&#x3a;" k="82" />
+<hkern u1="W" u2="&#x2e;" k="184" />
+<hkern u1="W" u2="&#x2d;" k="82" />
+<hkern u1="W" u2="&#x2c;" k="184" />
+<hkern u1="Y" u2="x" k="102" />
+<hkern u1="Y" u2="v" k="102" />
+<hkern u1="Y" u2="u" k="123" />
+<hkern u1="Y" u2="s" k="143" />
+<hkern u1="Y" u2="p" k="123" />
+<hkern u1="Y" u2="o" k="143" />
+<hkern u1="Y" u2="n" k="123" />
+<hkern u1="Y" u2="m" k="123" />
+<hkern u1="Y" u2="i" k="41" />
+<hkern u1="Y" u2="g" k="143" />
+<hkern u1="Y" u2="e" k="143" />
+<hkern u1="Y" u2="a" k="143" />
+<hkern u1="Y" u2="O" k="41" />
+<hkern u1="Y" u2="A" k="102" />
+<hkern u1="Y" u2="&#x3b;" k="102" />
+<hkern u1="Y" u2="&#x3a;" k="102" />
+<hkern u1="Y" u2="&#x2e;" k="184" />
+<hkern u1="Y" u2="&#x2d;" k="184" />
+<hkern u1="Y" u2="&#x2c;" k="184" />
+<hkern u1="a" u2="y" k="20" />
+<hkern u1="a" u2="w" k="20" />
+<hkern u1="a" u2="v" k="20" />
+<hkern u1="b" u2="y" k="20" />
+<hkern u1="b" u2="v" k="20" />
+<hkern u1="c" u2="y" k="20" />
+<hkern u1="e" u2="y" k="20" />
+<hkern u1="e" u2="w" k="20" />
+<hkern u1="e" u2="v" k="20" />
+<hkern u1="f" u2="&#x201d;" k="-41" />
+<hkern u1="f" u2="&#x2019;" k="-41" />
+<hkern u1="k" u2="&#xf6;" k="20" />
+<hkern u1="k" u2="o" k="20" />
+<hkern u1="k" u2="e" k="20" />
+<hkern u1="o" u2="y" k="20" />
+<hkern u1="o" u2="w" k="20" />
+<hkern u1="o" u2="v" k="20" />
+<hkern u1="p" u2="y" k="20" />
+<hkern u1="r" u2="&#xf6;" k="20" />
+<hkern u1="r" u2="&#xe5;" k="20" />
+<hkern u1="r" u2="&#xe4;" k="20" />
+<hkern u1="r" u2="y" k="-41" />
+<hkern u1="r" u2="v" k="-41" />
+<hkern u1="r" u2="t" k="-41" />
+<hkern u1="r" u2="o" k="20" />
+<hkern u1="r" u2="c" k="20" />
+<hkern u1="r" u2="a" k="20" />
+<hkern u1="r" u2="&#x2e;" k="164" />
+<hkern u1="r" u2="&#x2d;" k="61" />
+<hkern u1="r" u2="&#x2c;" k="164" />
+<hkern u1="s" u2="w" k="20" />
+<hkern u1="v" u2="&#xf6;" k="20" />
+<hkern u1="v" u2="&#xe5;" k="20" />
+<hkern u1="v" u2="&#xe4;" k="20" />
+<hkern u1="v" u2="o" k="20" />
+<hkern u1="v" u2="e" k="20" />
+<hkern u1="v" u2="a" k="20" />
+<hkern u1="v" u2="&#x2e;" k="143" />
+<hkern u1="v" u2="&#x2c;" k="143" />
+<hkern u1="w" u2="s" k="20" />
+<hkern u1="w" u2="o" k="20" />
+<hkern u1="w" u2="e" k="20" />
+<hkern u1="w" u2="a" k="20" />
+<hkern u1="w" u2="&#x2e;" k="123" />
+<hkern u1="w" u2="&#x2c;" k="123" />
+<hkern u1="y" u2="o" k="20" />
+<hkern u1="y" u2="e" k="20" />
+<hkern u1="y" u2="a" k="20" />
+<hkern u1="y" u2="&#x2e;" k="143" />
+<hkern u1="y" u2="&#x2c;" k="143" />
+<hkern u1="z" u2="o" k="20" />
+<hkern u1="z" u2="e" k="20" />
+<hkern u1="&#xe4;" u2="v" k="20" />
+<hkern u1="&#xe5;" u2="v" k="20" />
+<hkern u1="&#xf6;" u2="v" k="20" />
+<hkern u1="&#x2018;" u2="A" k="143" />
+<hkern u1="&#x2019;" u2="s" k="123" />
+<hkern u1="&#x2019;" u2="d" k="164" />
+<hkern u1="&#x201c;" u2="A" k="143" />
+</font>
+</defs></svg> \ No newline at end of file
diff --git a/tipboard/static/fonts/miso-webfont.ttf b/tipboard/static/fonts/miso-webfont.ttf
new file mode 100644
index 0000000..fbfb784
--- /dev/null
+++ b/tipboard/static/fonts/miso-webfont.ttf
Binary files differ
diff --git a/tipboard/static/fonts/miso-webfont.woff b/tipboard/static/fonts/miso-webfont.woff
new file mode 100644
index 0000000..cea4929
--- /dev/null
+++ b/tipboard/static/fonts/miso-webfont.woff
Binary files differ
diff --git a/tipboard/static/js/flipboard.js b/tipboard/static/js/flipboard.js
new file mode 100644
index 0000000..c63841e
--- /dev/null
+++ b/tipboard/static/js/flipboard.js
@@ -0,0 +1,73 @@
+(function ($) {
+ 'use strict';
+
+ var showError = function (msg) {
+ $('.error-message').html(msg);
+ $('iframe').hide();
+ $('.error-wrapper').show();
+ };
+
+ var Flipboard = {
+ currentPathIdx: -1,
+ dashboardsPaths: [],
+
+ init: function (paths) {
+ this.dashboardsPaths = paths;
+ },
+
+ getNextDashboardPath: function () {
+ this.currentPathIdx += 1;
+ var lastIdx = this.dashboardsPaths.length - 1;
+ if (this.currentPathIdx > lastIdx) {
+ this.currentPathIdx = 0;
+ }
+ var path = this.dashboardsPaths[this.currentPathIdx];
+ return path;
+ },
+
+ showNextDashboard: function () {
+ var nextDashboardPath = this.getNextDashboardPath();
+ $('.error-wrapper').hide();
+ var activeIframe = $($('iframe')[0]);
+ if (nextDashboardPath === $(activeIframe).attr('src')) {
+ console.log('same dashboard - SKIPPING');
+ return false;
+ }
+ var clonedIframe = $(activeIframe.clone());
+ clonedIframe.attr('src', nextDashboardPath);
+ $('body').append(clonedIframe);
+ $(clonedIframe).on('load', function() {
+ $(activeIframe).remove();
+ $(clonedIframe).addClass('fadeIn');
+ });
+ },
+
+ };
+
+ window.Flipboard = Flipboard;
+
+ $(document).ready(function () {
+ $.ajax({
+ method: 'post',
+ url: "/flipboard/getDashboardsPaths",
+ success: function(data) {
+ Flipboard.init(data.paths);
+ Flipboard.showNextDashboard();
+ var flipInterval = $('iframe').attr('data-fliptime-interval');
+ if (parseInt(flipInterval, 10) > 0) {
+ setInterval(function () {
+ Flipboard.showNextDashboard();
+ }, flipInterval * 1000);
+ }
+ },
+ error: function(request, textStatus, error) {
+ console.log(request, textStatus, error);
+ var msg = [
+ 'Error occured.',
+ 'For more details check javascript logs.'
+ ].join('<br>');
+ showError(msg);
+ }
+ });
+ });
+}($));
diff --git a/tipboard/static/js/lib/html5shiv.js b/tipboard/static/js/lib/html5shiv.js
new file mode 100644
index 0000000..dcf351c
--- /dev/null
+++ b/tipboard/static/js/lib/html5shiv.js
@@ -0,0 +1,8 @@
+/*
+ HTML5 Shiv v3.6.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
+*/
+(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
+a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}</style>";
+c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
+"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup main mark meter nav output progress section summary time video",version:"3.6.2",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);if(g)return a.createDocumentFragment();
+for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);
diff --git a/tipboard/static/js/lib/jqplot/excanvas.min.js b/tipboard/static/js/lib/jqplot/excanvas.min.js
new file mode 100644
index 0000000..83373a5
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/excanvas.min.js
@@ -0,0 +1,3 @@
+/* jqPlot @VERSION | (c) 2009-2013 Chris Leonello | jplot.com
+ jsDate | (c) 2010-2013 Chris Leonello
+ */if(!document.createElement("canvas").getContext){(function(){var ab=Math;var n=ab.round;var l=ab.sin;var A=ab.cos;var H=ab.abs;var N=ab.sqrt;var d=10;var f=d/2;var z=+navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];function y(){return this.context_||(this.context_=new D(this))}var t=Array.prototype.slice;function g(j,m,p){var i=t.call(arguments,2);return function(){return j.apply(m,i.concat(t.call(arguments)))}}function af(i){return String(i).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}function Y(m,j,i){if(!m.namespaces[j]){m.namespaces.add(j,i,"#default#VML")}}function R(j){Y(j,"g_vml_","urn:schemas-microsoft-com:vml");Y(j,"g_o_","urn:schemas-microsoft-com:office:office");if(!j.styleSheets.ex_canvas_){var i=j.createStyleSheet();i.owningElement.id="ex_canvas_";i.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}R(document);var e={init:function(i){var j=i||document;j.createElement("canvas");j.attachEvent("onreadystatechange",g(this.init_,this,j))},init_:function(p){var m=p.getElementsByTagName("canvas");for(var j=0;j<m.length;j++){this.initElement(m[j])}},initElement:function(j){if(!j.getContext){j.getContext=y;R(j.ownerDocument);j.innerHTML="";j.attachEvent("onpropertychange",x);j.attachEvent("onresize",W);var i=j.attributes;if(i.width&&i.width.specified){j.style.width=i.width.nodeValue+"px"}else{j.width=j.clientWidth}if(i.height&&i.height.specified){j.style.height=i.height.nodeValue+"px"}else{j.height=j.clientHeight}}return j},uninitElement:function(j){if(j.getContext){var i=j.getContext();delete i.element_;delete i.canvas;j.innerHTML="";j.context_=null;j.getContext=null;j.detachEvent("onpropertychange",x);j.detachEvent("onresize",W)}}};function x(j){var i=j.srcElement;switch(j.propertyName){case"width":i.getContext().clearRect();i.style.width=i.attributes.width.nodeValue+"px";i.firstChild.style.width=i.clientWidth+"px";break;case"height":i.getContext().clearRect();i.style.height=i.attributes.height.nodeValue+"px";i.firstChild.style.height=i.clientHeight+"px";break}}function W(j){var i=j.srcElement;if(i.firstChild){i.firstChild.style.width=i.clientWidth+"px";i.firstChild.style.height=i.clientHeight+"px"}}e.init();var k=[];for(var ae=0;ae<16;ae++){for(var ad=0;ad<16;ad++){k[ae*16+ad]=ae.toString(16)+ad.toString(16)}}function B(){return[[1,0,0],[0,1,0],[0,0,1]]}function J(p,m){var j=B();for(var i=0;i<3;i++){for(var ah=0;ah<3;ah++){var Z=0;for(var ag=0;ag<3;ag++){Z+=p[i][ag]*m[ag][ah]}j[i][ah]=Z}}return j}function v(j,i){i.fillStyle=j.fillStyle;i.lineCap=j.lineCap;i.lineJoin=j.lineJoin;i.lineWidth=j.lineWidth;i.miterLimit=j.miterLimit;i.shadowBlur=j.shadowBlur;i.shadowColor=j.shadowColor;i.shadowOffsetX=j.shadowOffsetX;i.shadowOffsetY=j.shadowOffsetY;i.strokeStyle=j.strokeStyle;i.globalAlpha=j.globalAlpha;i.font=j.font;i.textAlign=j.textAlign;i.textBaseline=j.textBaseline;i.arcScaleX_=j.arcScaleX_;i.arcScaleY_=j.arcScaleY_;i.lineScale_=j.lineScale_}var b={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"};function M(j){var p=j.indexOf("(",3);var i=j.indexOf(")",p+1);var m=j.substring(p+1,i).split(",");if(m.length!=4||j.charAt(3)!="a"){m[3]=1}return m}function c(i){return parseFloat(i)/100}function r(j,m,i){return Math.min(i,Math.max(m,j))}function I(ag){var i,ai,aj,ah,ak,Z;ah=parseFloat(ag[0])/360%360;if(ah<0){ah++}ak=r(c(ag[1]),0,1);Z=r(c(ag[2]),0,1);if(ak==0){i=ai=aj=Z}else{var j=Z<0.5?Z*(1+ak):Z+ak-Z*ak;var m=2*Z-j;i=a(m,j,ah+1/3);ai=a(m,j,ah);aj=a(m,j,ah-1/3)}return"#"+k[Math.floor(i*255)]+k[Math.floor(ai*255)]+k[Math.floor(aj*255)]}function a(j,i,m){if(m<0){m++}if(m>1){m--}if(6*m<1){return j+(i-j)*6*m}else{if(2*m<1){return i}else{if(3*m<2){return j+(i-j)*(2/3-m)*6}else{return j}}}}var C={};function F(j){if(j in C){return C[j]}var ag,Z=1;j=String(j);if(j.charAt(0)=="#"){ag=j}else{if(/^rgb/.test(j)){var p=M(j);var ag="#",ah;for(var m=0;m<3;m++){if(p[m].indexOf("%")!=-1){ah=Math.floor(c(p[m])*255)}else{ah=+p[m]}ag+=k[r(ah,0,255)]}Z=+p[3]}else{if(/^hsl/.test(j)){var p=M(j);ag=I(p);Z=p[3]}else{ag=b[j]||j}}}return C[j]={color:ag,alpha:Z}}var o={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var L={};function E(i){if(L[i]){return L[i]}var p=document.createElement("div");var m=p.style;try{m.font=i}catch(j){}return L[i]={style:m.fontStyle||o.style,variant:m.fontVariant||o.variant,weight:m.fontWeight||o.weight,size:m.fontSize||o.size,family:m.fontFamily||o.family}}function u(m,j){var i={};for(var ah in m){i[ah]=m[ah]}var ag=parseFloat(j.currentStyle.fontSize),Z=parseFloat(m.size);if(typeof m.size=="number"){i.size=m.size}else{if(m.size.indexOf("px")!=-1){i.size=Z}else{if(m.size.indexOf("em")!=-1){i.size=ag*Z}else{if(m.size.indexOf("%")!=-1){i.size=(ag/100)*Z}else{if(m.size.indexOf("pt")!=-1){i.size=Z/0.75}else{i.size=ag}}}}}i.size*=0.981;i.family="'"+i.family.replace(/(\'|\")/g,"").replace(/\s*,\s*/g,"', '")+"'";return i}function ac(i){return i.style+" "+i.variant+" "+i.weight+" "+i.size+"px "+i.family}var s={butt:"flat",round:"round"};function S(i){return s[i]||"square"}function D(i){this.m_=B();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=d*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var m="width:"+i.clientWidth+"px;height:"+i.clientHeight+"px;overflow:hidden;position:absolute";var j=i.ownerDocument.createElement("div");j.style.cssText=m;i.appendChild(j);var p=j.cloneNode(false);p.style.backgroundColor="red";p.style.filter="alpha(opacity=0)";i.appendChild(p);this.element_=j;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var q=D.prototype;q.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};q.beginPath=function(){this.currentPath_=[]};q.moveTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"moveTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.lineTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"lineTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.bezierCurveTo=function(m,j,ak,aj,ai,ag){var i=V(this,ai,ag);var ah=V(this,m,j);var Z=V(this,ak,aj);K(this,ah,Z,i)};function K(i,Z,m,j){i.currentPath_.push({type:"bezierCurveTo",cp1x:Z.x,cp1y:Z.y,cp2x:m.x,cp2y:m.y,x:j.x,y:j.y});i.currentX_=j.x;i.currentY_=j.y}q.quadraticCurveTo=function(ai,m,j,i){var ah=V(this,ai,m);var ag=V(this,j,i);var aj={x:this.currentX_+2/3*(ah.x-this.currentX_),y:this.currentY_+2/3*(ah.y-this.currentY_)};var Z={x:aj.x+(ag.x-this.currentX_)/3,y:aj.y+(ag.y-this.currentY_)/3};K(this,aj,Z,ag)};q.arc=function(al,aj,ak,ag,j,m){ak*=d;var ap=m?"at":"wa";var am=al+A(ag)*ak-f;var ao=aj+l(ag)*ak-f;var i=al+A(j)*ak-f;var an=aj+l(j)*ak-f;if(am==i&&!m){am+=0.125}var Z=V(this,al,aj);var ai=V(this,am,ao);var ah=V(this,i,an);this.currentPath_.push({type:ap,x:Z.x,y:Z.y,radius:ak,xStart:ai.x,yStart:ai.y,xEnd:ah.x,yEnd:ah.y})};q.rect=function(m,j,i,p){this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath()};q.strokeRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.stroke();this.currentPath_=Z};q.fillRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.fill();this.currentPath_=Z};q.createLinearGradient=function(j,p,i,m){var Z=new U("gradient");Z.x0_=j;Z.y0_=p;Z.x1_=i;Z.y1_=m;return Z};q.createRadialGradient=function(p,ag,m,j,Z,i){var ah=new U("gradientradial");ah.x0_=p;ah.y0_=ag;ah.r0_=m;ah.x1_=j;ah.y1_=Z;ah.r1_=i;return ah};q.drawImage=function(aq,m){var aj,ah,al,ay,ao,am,at,aA;var ak=aq.runtimeStyle.width;var ap=aq.runtimeStyle.height;aq.runtimeStyle.width="auto";aq.runtimeStyle.height="auto";var ai=aq.width;var aw=aq.height;aq.runtimeStyle.width=ak;aq.runtimeStyle.height=ap;if(arguments.length==3){aj=arguments[1];ah=arguments[2];ao=am=0;at=al=ai;aA=ay=aw}else{if(arguments.length==5){aj=arguments[1];ah=arguments[2];al=arguments[3];ay=arguments[4];ao=am=0;at=ai;aA=aw}else{if(arguments.length==9){ao=arguments[1];am=arguments[2];at=arguments[3];aA=arguments[4];aj=arguments[5];ah=arguments[6];al=arguments[7];ay=arguments[8]}else{throw Error("Invalid number of arguments")}}}var az=V(this,aj,ah);var p=at/2;var j=aA/2;var ax=[];var i=10;var ag=10;ax.push(" <g_vml_:group",' coordsize="',d*i,",",d*ag,'"',' coordorigin="0,0"',' style="width:',i,"px;height:",ag,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]||this.m_[1][1]!=1||this.m_[1][0]){var Z=[];Z.push("M11=",this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",n(az.x/d),",","Dy=",n(az.y/d),"");var av=az;var au=V(this,aj+al,ah);var ar=V(this,aj,ah+ay);var an=V(this,aj+al,ah+ay);av.x=ab.max(av.x,au.x,ar.x,an.x);av.y=ab.max(av.y,au.y,ar.y,an.y);ax.push("padding:0 ",n(av.x/d),"px ",n(av.y/d),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",Z.join(""),", sizingmethod='clip');")}else{ax.push("top:",n(az.y/d),"px;left:",n(az.x/d),"px;")}ax.push(' ">','<g_vml_:image src="',aq.src,'"',' style="width:',d*al,"px;"," height:",d*ay,'px"',' cropleft="',ao/ai,'"',' croptop="',am/aw,'"',' cropright="',(ai-ao-at)/ai,'"',' cropbottom="',(aw-am-aA)/aw,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",ax.join(""))};q.stroke=function(al){var aj=[];var Z=false;var m=10;var am=10;aj.push("<g_vml_:shape",' filled="',!!al,'"',' style="position:absolute;width:',m,"px;height:",am,'px;"',' coordorigin="0,0"',' coordsize="',d*m,",",d*am,'"',' stroked="',!al,'"',' path="');var an=false;var ag={x:null,y:null};var ak={x:null,y:null};for(var ah=0;ah<this.currentPath_.length;ah++){var j=this.currentPath_[ah];var ai;switch(j.type){case"moveTo":ai=j;aj.push(" m ",n(j.x),",",n(j.y));break;case"lineTo":aj.push(" l ",n(j.x),",",n(j.y));break;case"close":aj.push(" x ");j=null;break;case"bezierCurveTo":aj.push(" c ",n(j.cp1x),",",n(j.cp1y),",",n(j.cp2x),",",n(j.cp2y),",",n(j.x),",",n(j.y));break;case"at":case"wa":aj.push(" ",j.type," ",n(j.x-this.arcScaleX_*j.radius),",",n(j.y-this.arcScaleY_*j.radius)," ",n(j.x+this.arcScaleX_*j.radius),",",n(j.y+this.arcScaleY_*j.radius)," ",n(j.xStart),",",n(j.yStart)," ",n(j.xEnd),",",n(j.yEnd));break}if(j){if(ag.x==null||j.x<ag.x){ag.x=j.x}if(ak.x==null||j.x>ak.x){ak.x=j.x}if(ag.y==null||j.y<ag.y){ag.y=j.y}if(ak.y==null||j.y>ak.y){ak.y=j.y}}}aj.push(' ">');if(!al){w(this,aj)}else{G(this,aj,ag,ak)}aj.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",aj.join(""))};function w(m,ag){var j=F(m.strokeStyle);var p=j.color;var Z=j.alpha*m.globalAlpha;var i=m.lineScale_*m.lineWidth;if(i<1){Z*=i}ag.push("<g_vml_:stroke",' opacity="',Z,'"',' joinstyle="',m.lineJoin,'"',' miterlimit="',m.miterLimit,'"',' endcap="',S(m.lineCap),'"',' weight="',i,'px"',' color="',p,'" />')}function G(aq,ai,aK,ar){var aj=aq.fillStyle;var aB=aq.arcScaleX_;var aA=aq.arcScaleY_;var j=ar.x-aK.x;var p=ar.y-aK.y;if(aj instanceof U){var an=0;var aF={x:0,y:0};var ax=0;var am=1;if(aj.type_=="gradient"){var al=aj.x0_/aB;var m=aj.y0_/aA;var ak=aj.x1_/aB;var aM=aj.y1_/aA;var aJ=V(aq,al,m);var aI=V(aq,ak,aM);var ag=aI.x-aJ.x;var Z=aI.y-aJ.y;an=Math.atan2(ag,Z)*180/Math.PI;if(an<0){an+=360}if(an<0.000001){an=0}}else{var aJ=V(aq,aj.x0_,aj.y0_);aF={x:(aJ.x-aK.x)/j,y:(aJ.y-aK.y)/p};j/=aB*d;p/=aA*d;var aD=ab.max(j,p);ax=2*aj.r0_/aD;am=2*aj.r1_/aD-ax}var av=aj.colors_;av.sort(function(aN,i){return aN.offset-i.offset});var ap=av.length;var au=av[0].color;var at=av[ap-1].color;var az=av[0].alpha*aq.globalAlpha;var ay=av[ap-1].alpha*aq.globalAlpha;var aE=[];for(var aH=0;aH<ap;aH++){var ao=av[aH];aE.push(ao.offset*am+ax+" "+ao.color)}ai.push('<g_vml_:fill type="',aj.type_,'"',' method="none" focus="100%"',' color="',au,'"',' color2="',at,'"',' colors="',aE.join(","),'"',' opacity="',ay,'"',' g_o_:opacity2="',az,'"',' angle="',an,'"',' focusposition="',aF.x,",",aF.y,'" />')}else{if(aj instanceof T){if(j&&p){var ah=-aK.x;var aC=-aK.y;ai.push("<g_vml_:fill",' position="',ah/j*aB*aB,",",aC/p*aA*aA,'"',' type="tile"',' src="',aj.src_,'" />')}}else{var aL=F(aq.fillStyle);var aw=aL.color;var aG=aL.alpha*aq.globalAlpha;ai.push('<g_vml_:fill color="',aw,'" opacity="',aG,'" />')}}}q.fill=function(){this.stroke(true)};q.closePath=function(){this.currentPath_.push({type:"close"})};function V(j,Z,p){var i=j.m_;return{x:d*(Z*i[0][0]+p*i[1][0]+i[2][0])-f,y:d*(Z*i[0][1]+p*i[1][1]+i[2][1])-f}}q.save=function(){var i={};v(this,i);this.aStack_.push(i);this.mStack_.push(this.m_);this.m_=J(B(),this.m_)};q.restore=function(){if(this.aStack_.length){v(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function h(i){return isFinite(i[0][0])&&isFinite(i[0][1])&&isFinite(i[1][0])&&isFinite(i[1][1])&&isFinite(i[2][0])&&isFinite(i[2][1])}function aa(j,i,p){if(!h(i)){return}j.m_=i;if(p){var Z=i[0][0]*i[1][1]-i[0][1]*i[1][0];j.lineScale_=N(H(Z))}}q.translate=function(m,j){var i=[[1,0,0],[0,1,0],[m,j,1]];aa(this,J(i,this.m_),false)};q.rotate=function(j){var p=A(j);var m=l(j);var i=[[p,m,0],[-m,p,0],[0,0,1]];aa(this,J(i,this.m_),false)};q.scale=function(m,j){this.arcScaleX_*=m;this.arcScaleY_*=j;var i=[[m,0,0],[0,j,0],[0,0,1]];aa(this,J(i,this.m_),true)};q.transform=function(Z,p,ah,ag,j,i){var m=[[Z,p,0],[ah,ag,0],[j,i,1]];aa(this,J(m,this.m_),true)};q.setTransform=function(ag,Z,ai,ah,p,j){var i=[[ag,Z,0],[ai,ah,0],[p,j,1]];aa(this,i,true)};q.drawText_=function(am,ak,aj,ap,ai){var ao=this.m_,at=1000,j=0,ar=at,ah={x:0,y:0},ag=[];var i=u(E(this.font),this.element_);var p=ac(i);var au=this.element_.currentStyle;var Z=this.textAlign.toLowerCase();switch(Z){case"left":case"center":case"right":break;case"end":Z=au.direction=="ltr"?"right":"left";break;case"start":Z=au.direction=="rtl"?"right":"left";break;default:Z="left"}switch(this.textBaseline){case"hanging":case"top":ah.y=i.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":ah.y=-i.size/2.25;break}switch(Z){case"right":j=at;ar=0.05;break;case"center":j=ar=at/2;break}var aq=V(this,ak+ah.x,aj+ah.y);ag.push('<g_vml_:line from="',-j,' 0" to="',ar,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!ai,'" stroked="',!!ai,'" style="position:absolute;width:1px;height:1px;">');if(ai){w(this,ag)}else{G(this,ag,{x:-j,y:0},{x:ar,y:i.size})}var an=ao[0][0].toFixed(3)+","+ao[1][0].toFixed(3)+","+ao[0][1].toFixed(3)+","+ao[1][1].toFixed(3)+",0,0";var al=n(aq.x/d+1-ao[0][0])+","+n(aq.y/d-2*ao[1][0]);ag.push('<g_vml_:skew on="t" matrix="',an,'" ',' offset="',al,'" origin="',j,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',af(am),'" style="v-text-align:',Z,";font:",af(p),'" /></g_vml_:line>');this.element_.insertAdjacentHTML("beforeEnd",ag.join(""))};q.fillText=function(m,i,p,j){this.drawText_(m,i,p,j,false)};q.strokeText=function(m,i,p,j){this.drawText_(m,i,p,j,true)};q.measureText=function(m){if(!this.textMeasureEl_){var i='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';this.element_.insertAdjacentHTML("beforeEnd",i);this.textMeasureEl_=this.element_.lastChild}var j=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(j.createTextNode(m));return{width:this.textMeasureEl_.offsetWidth}};q.clip=function(){};q.arcTo=function(){};q.createPattern=function(j,i){return new T(j,i)};function U(i){this.type_=i;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}U.prototype.addColorStop=function(j,i){i=F(i);this.colors_.push({offset:j,color:i.color,alpha:i.alpha})};function T(j,i){Q(j);switch(i){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=i;break;default:O("SYNTAX_ERR")}this.src_=j.src;this.width_=j.width;this.height_=j.height}function O(i){throw new P(i)}function Q(i){if(!i||i.nodeType!=1||i.tagName!="IMG"){O("TYPE_MISMATCH_ERR")}if(i.readyState!="complete"){O("INVALID_STATE_ERR")}}function P(i){this.code=this[i];this.message=i+": DOM Exception "+this.code}var X=P.prototype=new Error;X.INDEX_SIZE_ERR=1;X.DOMSTRING_SIZE_ERR=2;X.HIERARCHY_REQUEST_ERR=3;X.WRONG_DOCUMENT_ERR=4;X.INVALID_CHARACTER_ERR=5;X.NO_DATA_ALLOWED_ERR=6;X.NO_MODIFICATION_ALLOWED_ERR=7;X.NOT_FOUND_ERR=8;X.NOT_SUPPORTED_ERR=9;X.INUSE_ATTRIBUTE_ERR=10;X.INVALID_STATE_ERR=11;X.SYNTAX_ERR=12;X.INVALID_MODIFICATION_ERR=13;X.NAMESPACE_ERR=14;X.INVALID_ACCESS_ERR=15;X.VALIDATION_ERR=16;X.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=e;CanvasRenderingContext2D=D;CanvasGradient=U;CanvasPattern=T;DOMException=P;G_vmlCanvasManager._version=888})()}; \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/jquery.jqplot.js b/tipboard/static/js/lib/jqplot/jquery.jqplot.js
new file mode 100644
index 0000000..49812b7
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/jquery.jqplot.js
@@ -0,0 +1,11411 @@
+/**
+ * Title: jqPlot Charts
+ *
+ * Pure JavaScript plotting plugin for jQuery.
+ *
+ * About: Version
+ *
+ * version: 1.0.8
+ * revision: 1250
+ *
+ * About: Copyright & License
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT and GPL version 2.0 licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * See <GPL Version 2> and <MIT License> contained within this distribution for further information.
+ *
+ * The author would appreciate an email letting him know of any substantial
+ * use of jqPlot. You can reach the author at: chris at jqplot dot com
+ * or see http://www.jqplot.com/info.php. This is, of course, not required.
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php.
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ *
+ * About: Introduction
+ *
+ * jqPlot requires jQuery (1.4+ required for certain features). jQuery 1.4.2 is included in the distribution.
+ * To use jqPlot include jQuery, the jqPlot jQuery plugin, the jqPlot css file and optionally
+ * the excanvas script for IE support in your web page:
+ *
+ * > <!--[if lt IE 9]><script language="javascript" type="text/javascript" src="excanvas.js"></script><![endif]-->
+ * > <script language="javascript" type="text/javascript" src="jquery-1.4.4.min.js"></script>
+ * > <script language="javascript" type="text/javascript" src="jquery.jqplot.min.js"></script>
+ * > <link rel="stylesheet" type="text/css" href="jquery.jqplot.css" />
+ *
+ * jqPlot can be customized by overriding the defaults of any of the objects which make
+ * up the plot. The general usage of jqplot is:
+ *
+ * > chart = $.jqplot('targetElemId', [dataArray,...], {optionsObject});
+ *
+ * The options available to jqplot are detailed in <jqPlot Options> in the jqPlotOptions.txt file.
+ *
+ * An actual call to $.jqplot() may look like the
+ * examples below:
+ *
+ * > chart = $.jqplot('chartdiv', [[[1, 2],[3,5.12],[5,13.1],[7,33.6],[9,85.9],[11,219.9]]]);
+ *
+ * or
+ *
+ * > dataArray = [34,12,43,55,77];
+ * > chart = $.jqplot('targetElemId', [dataArray, ...], {title:'My Plot', axes:{yaxis:{min:20, max:100}}});
+ *
+ * For more inforrmation, see <jqPlot Usage>.
+ *
+ * About: Usage
+ *
+ * See <jqPlot Usage>
+ *
+ * About: Available Options
+ *
+ * See <jqPlot Options> for a list of options available thorugh the options object (not complete yet!)
+ *
+ * About: Options Usage
+ *
+ * See <Options Tutorial>
+ *
+ * About: Changes
+ *
+ * See <Change Log>
+ *
+ */
+
+(function($) {
+ // make sure undefined is undefined
+ var undefined;
+
+ $.fn.emptyForce = function() {
+ for ( var i = 0, elem; (elem = $(this)[i]) != null; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( elem.nodeType === 1 ) {
+ $.cleanData( elem.getElementsByTagName("*") );
+ }
+
+ // Remove any remaining nodes
+ if ($.jqplot.use_excanvas) {
+ elem.outerHTML = "";
+ }
+ else {
+ while ( elem.firstChild ) {
+ elem.removeChild( elem.firstChild );
+ }
+ }
+
+ elem = null;
+ }
+
+ return $(this);
+ };
+
+ $.fn.removeChildForce = function(parent) {
+ while ( parent.firstChild ) {
+ this.removeChildForce( parent.firstChild );
+ parent.removeChild( parent.firstChild );
+ }
+ };
+
+ $.fn.jqplot = function() {
+ var datas = [];
+ var options = [];
+ // see how many data arrays we have
+ for (var i=0, l=arguments.length; i<l; i++) {
+ if ($.isArray(arguments[i])) {
+ datas.push(arguments[i]);
+ }
+ else if ($.isPlainObject(arguments[i])) {
+ options.push(arguments[i]);
+ }
+ }
+
+ return this.each(function(index) {
+ var tid,
+ plot,
+ $this = $(this),
+ dl = datas.length,
+ ol = options.length,
+ data,
+ opts;
+
+ if (index < dl) {
+ data = datas[index];
+ }
+ else {
+ data = dl ? datas[dl-1] : null;
+ }
+
+ if (index < ol) {
+ opts = options[index];
+ }
+ else {
+ opts = ol ? options[ol-1] : null;
+ }
+
+ // does el have an id?
+ // if not assign it one.
+ tid = $this.attr('id');
+ if (tid === undefined) {
+ tid = 'jqplot_target_' + $.jqplot.targetCounter++;
+ $this.attr('id', tid);
+ }
+
+ plot = $.jqplot(tid, data, opts);
+
+ $this.data('jqplot', plot);
+ });
+ };
+
+
+ /**
+ * Namespace: $.jqplot
+ * jQuery function called by the user to create a plot.
+ *
+ * Parameters:
+ * target - ID of target element to render the plot into.
+ * data - an array of data series.
+ * options - user defined options object. See the individual classes for available options.
+ *
+ * Properties:
+ * config - object to hold configuration information for jqPlot plot object.
+ *
+ * attributes:
+ * enablePlugins - False to disable plugins by default. Plugins must then be explicitly
+ * enabled in the individual plot options. Default: false.
+ * This property sets the "show" property of certain plugins to true or false.
+ * Only plugins that can be immediately active upon loading are affected. This includes
+ * non-renderer plugins like cursor, dragable, highlighter, and trendline.
+ * defaultHeight - Default height for plots where no css height specification exists. This
+ * is a jqplot wide default.
+ * defaultWidth - Default height for plots where no css height specification exists. This
+ * is a jqplot wide default.
+ */
+
+ $.jqplot = function(target, data, options) {
+ var _data = null, _options = null;
+
+ if (arguments.length === 3) {
+ _data = data;
+ _options = options;
+ }
+
+ else if (arguments.length === 2) {
+ if ($.isArray(data)) {
+ _data = data;
+ }
+
+ else if ($.isPlainObject(data)) {
+ _options = data;
+ }
+ }
+
+ if (_data === null && _options !== null && _options.data) {
+ _data = _options.data;
+ }
+
+ var plot = new jqPlot();
+ // remove any error class that may be stuck on target.
+ $('#'+target).removeClass('jqplot-error');
+
+ if ($.jqplot.config.catchErrors) {
+ try {
+ plot.init(target, _data, _options);
+ plot.draw();
+ plot.themeEngine.init.call(plot);
+ return plot;
+ }
+ catch(e) {
+ var msg = $.jqplot.config.errorMessage || e.message;
+ $('#'+target).append('<div class="jqplot-error-message">'+msg+'</div>');
+ $('#'+target).addClass('jqplot-error');
+ document.getElementById(target).style.background = $.jqplot.config.errorBackground;
+ document.getElementById(target).style.border = $.jqplot.config.errorBorder;
+ document.getElementById(target).style.fontFamily = $.jqplot.config.errorFontFamily;
+ document.getElementById(target).style.fontSize = $.jqplot.config.errorFontSize;
+ document.getElementById(target).style.fontStyle = $.jqplot.config.errorFontStyle;
+ document.getElementById(target).style.fontWeight = $.jqplot.config.errorFontWeight;
+ }
+ }
+ else {
+ plot.init(target, _data, _options);
+ plot.draw();
+ plot.themeEngine.init.call(plot);
+ return plot;
+ }
+ };
+
+ $.jqplot.version = "1.0.8";
+ $.jqplot.revision = "1250";
+
+ $.jqplot.targetCounter = 1;
+
+ // canvas manager to reuse canvases on the plot.
+ // Should help solve problem of canvases not being freed and
+ // problem of waiting forever for firefox to decide to free memory.
+ $.jqplot.CanvasManager = function() {
+ // canvases are managed globally so that they can be reused
+ // across plots after they have been freed
+ if (typeof $.jqplot.CanvasManager.canvases == 'undefined') {
+ $.jqplot.CanvasManager.canvases = [];
+ $.jqplot.CanvasManager.free = [];
+ }
+
+ var myCanvases = [];
+
+ this.getCanvas = function() {
+ var canvas;
+ var makeNew = true;
+
+ if (!$.jqplot.use_excanvas) {
+ for (var i = 0, l = $.jqplot.CanvasManager.canvases.length; i < l; i++) {
+ if ($.jqplot.CanvasManager.free[i] === true) {
+ makeNew = false;
+ canvas = $.jqplot.CanvasManager.canvases[i];
+ // $(canvas).removeClass('jqplot-canvasManager-free').addClass('jqplot-canvasManager-inuse');
+ $.jqplot.CanvasManager.free[i] = false;
+ myCanvases.push(i);
+ break;
+ }
+ }
+ }
+
+ if (makeNew) {
+ canvas = document.createElement('canvas');
+ myCanvases.push($.jqplot.CanvasManager.canvases.length);
+ $.jqplot.CanvasManager.canvases.push(canvas);
+ $.jqplot.CanvasManager.free.push(false);
+ }
+
+ return canvas;
+ };
+
+ // this method has to be used after settings the dimesions
+ // on the element returned by getCanvas()
+ this.initCanvas = function(canvas) {
+ if ($.jqplot.use_excanvas) {
+ return window.G_vmlCanvasManager.initElement(canvas);
+ }
+ return canvas;
+ };
+
+ this.freeAllCanvases = function() {
+ for (var i = 0, l=myCanvases.length; i < l; i++) {
+ this.freeCanvas(myCanvases[i]);
+ }
+ myCanvases = [];
+ };
+
+ this.freeCanvas = function(idx) {
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ // excanvas can't be reused, but properly unset
+ window.G_vmlCanvasManager.uninitElement($.jqplot.CanvasManager.canvases[idx]);
+ $.jqplot.CanvasManager.canvases[idx] = null;
+ }
+ else {
+ var canvas = $.jqplot.CanvasManager.canvases[idx];
+ canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
+ $(canvas).unbind().removeAttr('class').removeAttr('style');
+ // Style attributes seemed to be still hanging around. wierd. Some ticks
+ // still retained a left: 0px attribute after reusing a canvas.
+ $(canvas).css({left: '', top: '', position: ''});
+ // setting size to 0 may save memory of unused canvases?
+ canvas.width = 0;
+ canvas.height = 0;
+ $.jqplot.CanvasManager.free[idx] = true;
+ }
+ };
+
+ };
+
+
+ // Convienence function that won't hang IE or FF without FireBug.
+ $.jqplot.log = function() {
+ if (window.console) {
+ window.console.log.apply(window.console, arguments);
+ }
+ };
+
+ $.jqplot.config = {
+ addDomReference: false,
+ enablePlugins:false,
+ defaultHeight:300,
+ defaultWidth:400,
+ UTCAdjust:false,
+ timezoneOffset: new Date(new Date().getTimezoneOffset() * 60000),
+ errorMessage: '',
+ errorBackground: '',
+ errorBorder: '',
+ errorFontFamily: '',
+ errorFontSize: '',
+ errorFontStyle: '',
+ errorFontWeight: '',
+ catchErrors: false,
+ defaultTickFormatString: "%.1f",
+ defaultColors: [ "#4bb2c5", "#EAA228", "#c5b47f", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc", "#c747a3", "#cddf54", "#FBD178", "#26B4E3", "#bd70c7"],
+ defaultNegativeColors: [ "#498991", "#C08840", "#9F9274", "#546D61", "#646C4A", "#6F6621", "#6E3F5F", "#4F64B0", "#A89050", "#C45923", "#187399", "#945381", "#959E5C", "#C7AF7B", "#478396", "#907294"],
+ dashLength: 4,
+ gapLength: 4,
+ dotGapLength: 2.5,
+ srcLocation: 'jqplot/src/',
+ pluginLocation: 'jqplot/src/plugins/'
+ };
+
+
+ $.jqplot.arrayMax = function( array ){
+ return Math.max.apply( Math, array );
+ };
+
+ $.jqplot.arrayMin = function( array ){
+ return Math.min.apply( Math, array );
+ };
+
+ $.jqplot.enablePlugins = $.jqplot.config.enablePlugins;
+
+ // canvas related tests taken from modernizer:
+ // Copyright (c) 2009 - 2010 Faruk Ates.
+ // http://www.modernizr.com
+
+ $.jqplot.support_canvas = function() {
+ if (typeof $.jqplot.support_canvas.result == 'undefined') {
+ $.jqplot.support_canvas.result = !!document.createElement('canvas').getContext;
+ }
+ return $.jqplot.support_canvas.result;
+ };
+
+ $.jqplot.support_canvas_text = function() {
+ if (typeof $.jqplot.support_canvas_text.result == 'undefined') {
+ if (window.G_vmlCanvasManager !== undefined && window.G_vmlCanvasManager._version > 887) {
+ $.jqplot.support_canvas_text.result = true;
+ }
+ else {
+ $.jqplot.support_canvas_text.result = !!(document.createElement('canvas').getContext && typeof document.createElement('canvas').getContext('2d').fillText == 'function');
+ }
+
+ }
+ return $.jqplot.support_canvas_text.result;
+ };
+
+ $.jqplot.use_excanvas = ((!$.support.boxModel || !$.support.objectAll || !$support.leadingWhitespace) && !$.jqplot.support_canvas()) ? true : false;
+
+ /**
+ *
+ * Hooks: jqPlot Pugin Hooks
+ *
+ * $.jqplot.preInitHooks - called before initialization.
+ * $.jqplot.postInitHooks - called after initialization.
+ * $.jqplot.preParseOptionsHooks - called before user options are parsed.
+ * $.jqplot.postParseOptionsHooks - called after user options are parsed.
+ * $.jqplot.preDrawHooks - called before plot draw.
+ * $.jqplot.postDrawHooks - called after plot draw.
+ * $.jqplot.preDrawSeriesHooks - called before each series is drawn.
+ * $.jqplot.postDrawSeriesHooks - called after each series is drawn.
+ * $.jqplot.preDrawLegendHooks - called before the legend is drawn.
+ * $.jqplot.addLegendRowHooks - called at the end of legend draw, so plugins
+ * can add rows to the legend table.
+ * $.jqplot.preSeriesInitHooks - called before series is initialized.
+ * $.jqplot.postSeriesInitHooks - called after series is initialized.
+ * $.jqplot.preParseSeriesOptionsHooks - called before series related options
+ * are parsed.
+ * $.jqplot.postParseSeriesOptionsHooks - called after series related options
+ * are parsed.
+ * $.jqplot.eventListenerHooks - called at the end of plot drawing, binds
+ * listeners to the event canvas which lays on top of the grid area.
+ * $.jqplot.preDrawSeriesShadowHooks - called before series shadows are drawn.
+ * $.jqplot.postDrawSeriesShadowHooks - called after series shadows are drawn.
+ *
+ */
+
+ $.jqplot.preInitHooks = [];
+ $.jqplot.postInitHooks = [];
+ $.jqplot.preParseOptionsHooks = [];
+ $.jqplot.postParseOptionsHooks = [];
+ $.jqplot.preDrawHooks = [];
+ $.jqplot.postDrawHooks = [];
+ $.jqplot.preDrawSeriesHooks = [];
+ $.jqplot.postDrawSeriesHooks = [];
+ $.jqplot.preDrawLegendHooks = [];
+ $.jqplot.addLegendRowHooks = [];
+ $.jqplot.preSeriesInitHooks = [];
+ $.jqplot.postSeriesInitHooks = [];
+ $.jqplot.preParseSeriesOptionsHooks = [];
+ $.jqplot.postParseSeriesOptionsHooks = [];
+ $.jqplot.eventListenerHooks = [];
+ $.jqplot.preDrawSeriesShadowHooks = [];
+ $.jqplot.postDrawSeriesShadowHooks = [];
+
+ // A superclass holding some common properties and methods.
+ $.jqplot.ElemContainer = function() {
+ this._elem;
+ this._plotWidth;
+ this._plotHeight;
+ this._plotDimensions = {height:null, width:null};
+ };
+
+ $.jqplot.ElemContainer.prototype.createElement = function(el, offsets, clss, cssopts, attrib) {
+ this._offsets = offsets;
+ var klass = clss || 'jqplot';
+ var elem = document.createElement(el);
+ this._elem = $(elem);
+ this._elem.addClass(klass);
+ this._elem.css(cssopts);
+ this._elem.attr(attrib);
+ // avoid memory leak;
+ elem = null;
+ return this._elem;
+ };
+
+ $.jqplot.ElemContainer.prototype.getWidth = function() {
+ if (this._elem) {
+ return this._elem.outerWidth(true);
+ }
+ else {
+ return null;
+ }
+ };
+
+ $.jqplot.ElemContainer.prototype.getHeight = function() {
+ if (this._elem) {
+ return this._elem.outerHeight(true);
+ }
+ else {
+ return null;
+ }
+ };
+
+ $.jqplot.ElemContainer.prototype.getPosition = function() {
+ if (this._elem) {
+ return this._elem.position();
+ }
+ else {
+ return {top:null, left:null, bottom:null, right:null};
+ }
+ };
+
+ $.jqplot.ElemContainer.prototype.getTop = function() {
+ return this.getPosition().top;
+ };
+
+ $.jqplot.ElemContainer.prototype.getLeft = function() {
+ return this.getPosition().left;
+ };
+
+ $.jqplot.ElemContainer.prototype.getBottom = function() {
+ return this._elem.css('bottom');
+ };
+
+ $.jqplot.ElemContainer.prototype.getRight = function() {
+ return this._elem.css('right');
+ };
+
+
+ /**
+ * Class: Axis
+ * An individual axis object. Cannot be instantiated directly, but created
+ * by the Plot object. Axis properties can be set or overridden by the
+ * options passed in from the user.
+ *
+ */
+ function Axis(name) {
+ $.jqplot.ElemContainer.call(this);
+ // Group: Properties
+ //
+ // Axes options are specified within an axes object at the top level of the
+ // plot options like so:
+ // > {
+ // > axes: {
+ // > xaxis: {min: 5},
+ // > yaxis: {min: 2, max: 8, numberTicks:4},
+ // > x2axis: {pad: 1.5},
+ // > y2axis: {ticks:[22, 44, 66, 88]}
+ // > }
+ // > }
+ // There are 2 x axes, 'xaxis' and 'x2axis', and
+ // 9 yaxes, 'yaxis', 'y2axis'. 'y3axis', ... Any or all of which may be specified.
+ this.name = name;
+ this._series = [];
+ // prop: show
+ // Wether to display the axis on the graph.
+ this.show = false;
+ // prop: tickRenderer
+ // A class of a rendering engine for creating the ticks labels displayed on the plot,
+ // See <$.jqplot.AxisTickRenderer>.
+ this.tickRenderer = $.jqplot.AxisTickRenderer;
+ // prop: tickOptions
+ // Options that will be passed to the tickRenderer, see <$.jqplot.AxisTickRenderer> options.
+ this.tickOptions = {};
+ // prop: labelRenderer
+ // A class of a rendering engine for creating an axis label.
+ this.labelRenderer = $.jqplot.AxisLabelRenderer;
+ // prop: labelOptions
+ // Options passed to the label renderer.
+ this.labelOptions = {};
+ // prop: label
+ // Label for the axis
+ this.label = null;
+ // prop: showLabel
+ // true to show the axis label.
+ this.showLabel = true;
+ // prop: min
+ // minimum value of the axis (in data units, not pixels).
+ this.min = null;
+ // prop: max
+ // maximum value of the axis (in data units, not pixels).
+ this.max = null;
+ // prop: autoscale
+ // DEPRECATED
+ // the default scaling algorithm produces superior results.
+ this.autoscale = false;
+ // prop: pad
+ // Padding to extend the range above and below the data bounds.
+ // The data range is multiplied by this factor to determine minimum and maximum axis bounds.
+ // A value of 0 will be interpreted to mean no padding, and pad will be set to 1.0.
+ this.pad = 1.2;
+ // prop: padMax
+ // Padding to extend the range above data bounds.
+ // The top of the data range is multiplied by this factor to determine maximum axis bounds.
+ // A value of 0 will be interpreted to mean no padding, and padMax will be set to 1.0.
+ this.padMax = null;
+ // prop: padMin
+ // Padding to extend the range below data bounds.
+ // The bottom of the data range is multiplied by this factor to determine minimum axis bounds.
+ // A value of 0 will be interpreted to mean no padding, and padMin will be set to 1.0.
+ this.padMin = null;
+ // prop: ticks
+ // 1D [val, val, ...] or 2D [[val, label], [val, label], ...] array of ticks for the axis.
+ // If no label is specified, the value is formatted into an appropriate label.
+ this.ticks = [];
+ // prop: numberTicks
+ // Desired number of ticks. Default is to compute automatically.
+ this.numberTicks;
+ // prop: tickInterval
+ // number of units between ticks. Mutually exclusive with numberTicks.
+ this.tickInterval;
+ // prop: renderer
+ // A class of a rendering engine that handles tick generation,
+ // scaling input data to pixel grid units and drawing the axis element.
+ this.renderer = $.jqplot.LinearAxisRenderer;
+ // prop: rendererOptions
+ // renderer specific options. See <$.jqplot.LinearAxisRenderer> for options.
+ this.rendererOptions = {};
+ // prop: showTicks
+ // Wether to show the ticks (both marks and labels) or not.
+ // Will not override showMark and showLabel options if specified on the ticks themselves.
+ this.showTicks = true;
+ // prop: showTickMarks
+ // Wether to show the tick marks (line crossing grid) or not.
+ // Overridden by showTicks and showMark option of tick itself.
+ this.showTickMarks = true;
+ // prop: showMinorTicks
+ // Wether or not to show minor ticks. This is renderer dependent.
+ this.showMinorTicks = true;
+ // prop: drawMajorGridlines
+ // True to draw gridlines for major axis ticks.
+ this.drawMajorGridlines = true;
+ // prop: drawMinorGridlines
+ // True to draw gridlines for minor ticks.
+ this.drawMinorGridlines = false;
+ // prop: drawMajorTickMarks
+ // True to draw tick marks for major axis ticks.
+ this.drawMajorTickMarks = true;
+ // prop: drawMinorTickMarks
+ // True to draw tick marks for minor ticks. This is renderer dependent.
+ this.drawMinorTickMarks = true;
+ // prop: useSeriesColor
+ // Use the color of the first series associated with this axis for the
+ // tick marks and line bordering this axis.
+ this.useSeriesColor = false;
+ // prop: borderWidth
+ // width of line stroked at the border of the axis. Defaults
+ // to the width of the grid boarder.
+ this.borderWidth = null;
+ // prop: borderColor
+ // color of the border adjacent to the axis. Defaults to grid border color.
+ this.borderColor = null;
+ // prop: scaleToHiddenSeries
+ // True to include hidden series when computing axes bounds and scaling.
+ this.scaleToHiddenSeries = false;
+ // minimum and maximum values on the axis.
+ this._dataBounds = {min:null, max:null};
+ // statistics (min, max, mean) as well as actual data intervals for each series attached to axis.
+ // holds collection of {intervals:[], min:, max:, mean: } objects for each series on axis.
+ this._intervalStats = [];
+ // pixel position from the top left of the min value and max value on the axis.
+ this._offsets = {min:null, max:null};
+ this._ticks=[];
+ this._label = null;
+ // prop: syncTicks
+ // true to try and synchronize tick spacing across multiple axes so that ticks and
+ // grid lines line up. This has an impact on autoscaling algorithm, however.
+ // In general, autoscaling an individual axis will work better if it does not
+ // have to sync ticks.
+ this.syncTicks = null;
+ // prop: tickSpacing
+ // Approximate pixel spacing between ticks on graph. Used during autoscaling.
+ // This number will be an upper bound, actual spacing will be less.
+ this.tickSpacing = 75;
+ // Properties to hold the original values for min, max, ticks, tickInterval and numberTicks
+ // so they can be restored if altered by plugins.
+ this._min = null;
+ this._max = null;
+ this._tickInterval = null;
+ this._numberTicks = null;
+ this.__ticks = null;
+ // hold original user options.
+ this._options = {};
+ }
+
+ Axis.prototype = new $.jqplot.ElemContainer();
+ Axis.prototype.constructor = Axis;
+
+ Axis.prototype.init = function() {
+ if ($.isFunction(this.renderer)) {
+ this.renderer = new this.renderer();
+ }
+ // set the axis name
+ this.tickOptions.axis = this.name;
+ // if showMark or showLabel tick options not specified, use value of axis option.
+ // showTicks overrides showTickMarks.
+ if (this.tickOptions.showMark == null) {
+ this.tickOptions.showMark = this.showTicks;
+ }
+ if (this.tickOptions.showMark == null) {
+ this.tickOptions.showMark = this.showTickMarks;
+ }
+ if (this.tickOptions.showLabel == null) {
+ this.tickOptions.showLabel = this.showTicks;
+ }
+
+ if (this.label == null || this.label == '') {
+ this.showLabel = false;
+ }
+ else {
+ this.labelOptions.label = this.label;
+ }
+ if (this.showLabel == false) {
+ this.labelOptions.show = false;
+ }
+ // set the default padMax, padMin if not specified
+ // special check, if no padding desired, padding
+ // should be set to 1.0
+ if (this.pad == 0) {
+ this.pad = 1.0;
+ }
+ if (this.padMax == 0) {
+ this.padMax = 1.0;
+ }
+ if (this.padMin == 0) {
+ this.padMin = 1.0;
+ }
+ if (this.padMax == null) {
+ this.padMax = (this.pad-1)/2 + 1;
+ }
+ if (this.padMin == null) {
+ this.padMin = (this.pad-1)/2 + 1;
+ }
+ // now that padMin and padMax are correctly set, reset pad in case user has supplied
+ // padMin and/or padMax
+ this.pad = this.padMax + this.padMin - 1;
+ if (this.min != null || this.max != null) {
+ this.autoscale = false;
+ }
+ // if not set, sync ticks for y axes but not x by default.
+ if (this.syncTicks == null && this.name.indexOf('y') > -1) {
+ this.syncTicks = true;
+ }
+ else if (this.syncTicks == null){
+ this.syncTicks = false;
+ }
+ this.renderer.init.call(this, this.rendererOptions);
+
+ };
+
+ Axis.prototype.draw = function(ctx, plot) {
+ // Memory Leaks patch
+ if (this.__ticks) {
+ this.__ticks = null;
+ }
+
+ return this.renderer.draw.call(this, ctx, plot);
+
+ };
+
+ Axis.prototype.set = function() {
+ this.renderer.set.call(this);
+ };
+
+ Axis.prototype.pack = function(pos, offsets) {
+ if (this.show) {
+ this.renderer.pack.call(this, pos, offsets);
+ }
+ // these properties should all be available now.
+ if (this._min == null) {
+ this._min = this.min;
+ this._max = this.max;
+ this._tickInterval = this.tickInterval;
+ this._numberTicks = this.numberTicks;
+ this.__ticks = this._ticks;
+ }
+ };
+
+ // reset the axis back to original values if it has been scaled, zoomed, etc.
+ Axis.prototype.reset = function() {
+ this.renderer.reset.call(this);
+ };
+
+ Axis.prototype.resetScale = function(opts) {
+ $.extend(true, this, {min: null, max: null, numberTicks: null, tickInterval: null, _ticks: [], ticks: []}, opts);
+ this.resetDataBounds();
+ };
+
+ Axis.prototype.resetDataBounds = function() {
+ // Go through all the series attached to this axis and find
+ // the min/max bounds for this axis.
+ var db = this._dataBounds;
+ db.min = null;
+ db.max = null;
+ var l, s, d;
+ // check for when to force min 0 on bar series plots.
+ var doforce = (this.show) ? true : false;
+ for (var i=0; i<this._series.length; i++) {
+ s = this._series[i];
+ if (s.show || this.scaleToHiddenSeries) {
+ d = s._plotData;
+ if (s._type === 'line' && s.renderer.bands.show && this.name.charAt(0) !== 'x') {
+ d = [[0, s.renderer.bands._min], [1, s.renderer.bands._max]];
+ }
+
+ var minyidx = 1, maxyidx = 1;
+
+ if (s._type != null && s._type == 'ohlc') {
+ minyidx = 3;
+ maxyidx = 2;
+ }
+
+ for (var j=0, l=d.length; j<l; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ if ((d[j][0] != null && d[j][0] < db.min) || db.min == null) {
+ db.min = d[j][0];
+ }
+ if ((d[j][0] != null && d[j][0] > db.max) || db.max == null) {
+ db.max = d[j][0];
+ }
+ }
+ else {
+ if ((d[j][minyidx] != null && d[j][minyidx] < db.min) || db.min == null) {
+ db.min = d[j][minyidx];
+ }
+ if ((d[j][maxyidx] != null && d[j][maxyidx] > db.max) || db.max == null) {
+ db.max = d[j][maxyidx];
+ }
+ }
+ }
+
+ // Hack to not pad out bottom of bar plots unless user has specified a padding.
+ // every series will have a chance to set doforce to false. once it is set to
+ // false, it cannot be reset to true.
+ // If any series attached to axis is not a bar, wont force 0.
+ if (doforce && s.renderer.constructor !== $.jqplot.BarRenderer) {
+ doforce = false;
+ }
+
+ else if (doforce && this._options.hasOwnProperty('forceTickAt0') && this._options.forceTickAt0 == false) {
+ doforce = false;
+ }
+
+ else if (doforce && s.renderer.constructor === $.jqplot.BarRenderer) {
+ if (s.barDirection == 'vertical' && this.name != 'xaxis' && this.name != 'x2axis') {
+ if (this._options.pad != null || this._options.padMin != null) {
+ doforce = false;
+ }
+ }
+
+ else if (s.barDirection == 'horizontal' && (this.name == 'xaxis' || this.name == 'x2axis')) {
+ if (this._options.pad != null || this._options.padMin != null) {
+ doforce = false;
+ }
+ }
+
+ }
+ }
+ }
+
+ if (doforce && this.renderer.constructor === $.jqplot.LinearAxisRenderer && db.min >= 0) {
+ this.padMin = 1.0;
+ this.forceTickAt0 = true;
+ }
+ };
+
+ /**
+ * Class: Legend
+ * Legend object. Cannot be instantiated directly, but created
+ * by the Plot object. Legend properties can be set or overridden by the
+ * options passed in from the user.
+ */
+ function Legend(options) {
+ $.jqplot.ElemContainer.call(this);
+ // Group: Properties
+
+ // prop: show
+ // Wether to display the legend on the graph.
+ this.show = false;
+ // prop: location
+ // Placement of the legend. one of the compass directions: nw, n, ne, e, se, s, sw, w
+ this.location = 'ne';
+ // prop: labels
+ // Array of labels to use. By default the renderer will look for labels on the series.
+ // Labels specified in this array will override labels specified on the series.
+ this.labels = [];
+ // prop: showLabels
+ // true to show the label text on the legend.
+ this.showLabels = true;
+ // prop: showSwatch
+ // true to show the color swatches on the legend.
+ this.showSwatches = true;
+ // prop: placement
+ // "insideGrid" places legend inside the grid area of the plot.
+ // "outsideGrid" places the legend outside the grid but inside the plot container,
+ // shrinking the grid to accomodate the legend.
+ // "inside" synonym for "insideGrid",
+ // "outside" places the legend ouside the grid area, but does not shrink the grid which
+ // can cause the legend to overflow the plot container.
+ this.placement = "insideGrid";
+ // prop: xoffset
+ // DEPRECATED. Set the margins on the legend using the marginTop, marginLeft, etc.
+ // properties or via CSS margin styling of the .jqplot-table-legend class.
+ this.xoffset = 0;
+ // prop: yoffset
+ // DEPRECATED. Set the margins on the legend using the marginTop, marginLeft, etc.
+ // properties or via CSS margin styling of the .jqplot-table-legend class.
+ this.yoffset = 0;
+ // prop: border
+ // css spec for the border around the legend box.
+ this.border;
+ // prop: background
+ // css spec for the background of the legend box.
+ this.background;
+ // prop: textColor
+ // css color spec for the legend text.
+ this.textColor;
+ // prop: fontFamily
+ // css font-family spec for the legend text.
+ this.fontFamily;
+ // prop: fontSize
+ // css font-size spec for the legend text.
+ this.fontSize ;
+ // prop: rowSpacing
+ // css padding-top spec for the rows in the legend.
+ this.rowSpacing = '0.5em';
+ // renderer
+ // A class that will create a DOM object for the legend,
+ // see <$.jqplot.TableLegendRenderer>.
+ this.renderer = $.jqplot.TableLegendRenderer;
+ // prop: rendererOptions
+ // renderer specific options passed to the renderer.
+ this.rendererOptions = {};
+ // prop: predraw
+ // Wether to draw the legend before the series or not.
+ // Used with series specific legend renderers for pie, donut, mekko charts, etc.
+ this.preDraw = false;
+ // prop: marginTop
+ // CSS margin for the legend DOM element. This will set an element
+ // CSS style for the margin which will override any style sheet setting.
+ // The default will be taken from the stylesheet.
+ this.marginTop = null;
+ // prop: marginRight
+ // CSS margin for the legend DOM element. This will set an element
+ // CSS style for the margin which will override any style sheet setting.
+ // The default will be taken from the stylesheet.
+ this.marginRight = null;
+ // prop: marginBottom
+ // CSS margin for the legend DOM element. This will set an element
+ // CSS style for the margin which will override any style sheet setting.
+ // The default will be taken from the stylesheet.
+ this.marginBottom = null;
+ // prop: marginLeft
+ // CSS margin for the legend DOM element. This will set an element
+ // CSS style for the margin which will override any style sheet setting.
+ // The default will be taken from the stylesheet.
+ this.marginLeft = null;
+ // prop: escapeHtml
+ // True to escape special characters with their html entity equivalents
+ // in legend text. "<" becomes &lt; and so on, so html tags are not rendered.
+ this.escapeHtml = false;
+ this._series = [];
+
+ $.extend(true, this, options);
+ }
+
+ Legend.prototype = new $.jqplot.ElemContainer();
+ Legend.prototype.constructor = Legend;
+
+ Legend.prototype.setOptions = function(options) {
+ $.extend(true, this, options);
+
+ // Try to emulate deprecated behaviour
+ // if user has specified xoffset or yoffset, copy these to
+ // the margin properties.
+
+ if (this.placement == 'inside') {
+ this.placement = 'insideGrid';
+ }
+
+ if (this.xoffset >0) {
+ if (this.placement == 'insideGrid') {
+ switch (this.location) {
+ case 'nw':
+ case 'w':
+ case 'sw':
+ if (this.marginLeft == null) {
+ this.marginLeft = this.xoffset + 'px';
+ }
+ this.marginRight = '0px';
+ break;
+ case 'ne':
+ case 'e':
+ case 'se':
+ default:
+ if (this.marginRight == null) {
+ this.marginRight = this.xoffset + 'px';
+ }
+ this.marginLeft = '0px';
+ break;
+ }
+ }
+ else if (this.placement == 'outside') {
+ switch (this.location) {
+ case 'nw':
+ case 'w':
+ case 'sw':
+ if (this.marginRight == null) {
+ this.marginRight = this.xoffset + 'px';
+ }
+ this.marginLeft = '0px';
+ break;
+ case 'ne':
+ case 'e':
+ case 'se':
+ default:
+ if (this.marginLeft == null) {
+ this.marginLeft = this.xoffset + 'px';
+ }
+ this.marginRight = '0px';
+ break;
+ }
+ }
+ this.xoffset = 0;
+ }
+
+ if (this.yoffset >0) {
+ if (this.placement == 'outside') {
+ switch (this.location) {
+ case 'sw':
+ case 's':
+ case 'se':
+ if (this.marginTop == null) {
+ this.marginTop = this.yoffset + 'px';
+ }
+ this.marginBottom = '0px';
+ break;
+ case 'ne':
+ case 'n':
+ case 'nw':
+ default:
+ if (this.marginBottom == null) {
+ this.marginBottom = this.yoffset + 'px';
+ }
+ this.marginTop = '0px';
+ break;
+ }
+ }
+ else if (this.placement == 'insideGrid') {
+ switch (this.location) {
+ case 'sw':
+ case 's':
+ case 'se':
+ if (this.marginBottom == null) {
+ this.marginBottom = this.yoffset + 'px';
+ }
+ this.marginTop = '0px';
+ break;
+ case 'ne':
+ case 'n':
+ case 'nw':
+ default:
+ if (this.marginTop == null) {
+ this.marginTop = this.yoffset + 'px';
+ }
+ this.marginBottom = '0px';
+ break;
+ }
+ }
+ this.yoffset = 0;
+ }
+
+ // TO-DO:
+ // Handle case where offsets are < 0.
+ //
+ };
+
+ Legend.prototype.init = function() {
+ if ($.isFunction(this.renderer)) {
+ this.renderer = new this.renderer();
+ }
+ this.renderer.init.call(this, this.rendererOptions);
+ };
+
+ Legend.prototype.draw = function(offsets, plot) {
+ for (var i=0; i<$.jqplot.preDrawLegendHooks.length; i++){
+ $.jqplot.preDrawLegendHooks[i].call(this, offsets);
+ }
+ return this.renderer.draw.call(this, offsets, plot);
+ };
+
+ Legend.prototype.pack = function(offsets) {
+ this.renderer.pack.call(this, offsets);
+ };
+
+ /**
+ * Class: Title
+ * Plot Title object. Cannot be instantiated directly, but created
+ * by the Plot object. Title properties can be set or overridden by the
+ * options passed in from the user.
+ *
+ * Parameters:
+ * text - text of the title.
+ */
+ function Title(text) {
+ $.jqplot.ElemContainer.call(this);
+ // Group: Properties
+
+ // prop: text
+ // text of the title;
+ this.text = text;
+ // prop: show
+ // whether or not to show the title
+ this.show = true;
+ // prop: fontFamily
+ // css font-family spec for the text.
+ this.fontFamily;
+ // prop: fontSize
+ // css font-size spec for the text.
+ this.fontSize ;
+ // prop: textAlign
+ // css text-align spec for the text.
+ this.textAlign;
+ // prop: textColor
+ // css color spec for the text.
+ this.textColor;
+ // prop: renderer
+ // A class for creating a DOM element for the title,
+ // see <$.jqplot.DivTitleRenderer>.
+ this.renderer = $.jqplot.DivTitleRenderer;
+ // prop: rendererOptions
+ // renderer specific options passed to the renderer.
+ this.rendererOptions = {};
+ // prop: escapeHtml
+ // True to escape special characters with their html entity equivalents
+ // in title text. "<" becomes &lt; and so on, so html tags are not rendered.
+ this.escapeHtml = false;
+ }
+
+ Title.prototype = new $.jqplot.ElemContainer();
+ Title.prototype.constructor = Title;
+
+ Title.prototype.init = function() {
+ if ($.isFunction(this.renderer)) {
+ this.renderer = new this.renderer();
+ }
+ this.renderer.init.call(this, this.rendererOptions);
+ };
+
+ Title.prototype.draw = function(width) {
+ return this.renderer.draw.call(this, width);
+ };
+
+ Title.prototype.pack = function() {
+ this.renderer.pack.call(this);
+ };
+
+
+ /**
+ * Class: Series
+ * An individual data series object. Cannot be instantiated directly, but created
+ * by the Plot object. Series properties can be set or overridden by the
+ * options passed in from the user.
+ */
+ function Series(options) {
+ options = options || {};
+ $.jqplot.ElemContainer.call(this);
+ // Group: Properties
+ // Properties will be assigned from a series array at the top level of the
+ // options. If you had two series and wanted to change the color and line
+ // width of the first and set the second to use the secondary y axis with
+ // no shadow and supply custom labels for each:
+ // > {
+ // > series:[
+ // > {color: '#ff4466', lineWidth: 5, label:'good line'},
+ // > {yaxis: 'y2axis', shadow: false, label:'bad line'}
+ // > ]
+ // > }
+
+ // prop: show
+ // whether or not to draw the series.
+ this.show = true;
+ // prop: xaxis
+ // which x axis to use with this series, either 'xaxis' or 'x2axis'.
+ this.xaxis = 'xaxis';
+ this._xaxis;
+ // prop: yaxis
+ // which y axis to use with this series, either 'yaxis' or 'y2axis'.
+ this.yaxis = 'yaxis';
+ this._yaxis;
+ this.gridBorderWidth = 2.0;
+ // prop: renderer
+ // A class of a renderer which will draw the series,
+ // see <$.jqplot.LineRenderer>.
+ this.renderer = $.jqplot.LineRenderer;
+ // prop: rendererOptions
+ // Options to pass on to the renderer.
+ this.rendererOptions = {};
+ this.data = [];
+ this.gridData = [];
+ // prop: label
+ // Line label to use in the legend.
+ this.label = '';
+ // prop: showLabel
+ // true to show label for this series in the legend.
+ this.showLabel = true;
+ // prop: color
+ // css color spec for the series
+ this.color;
+ // prop: negativeColor
+ // css color spec used for filled (area) plots that are filled to zero and
+ // the "useNegativeColors" option is true.
+ this.negativeColor;
+ // prop: lineWidth
+ // width of the line in pixels. May have different meanings depending on renderer.
+ this.lineWidth = 2.5;
+ // prop: lineJoin
+ // Canvas lineJoin style between segments of series.
+ this.lineJoin = 'round';
+ // prop: lineCap
+ // Canvas lineCap style at ends of line.
+ this.lineCap = 'round';
+ // prop: linePattern
+ // line pattern 'dashed', 'dotted', 'solid', some combination
+ // of '-' and '.' characters such as '.-.' or a numerical array like
+ // [draw, skip, draw, skip, ...] such as [1, 10] to draw a dotted line,
+ // [1, 10, 20, 10] to draw a dot-dash line, and so on.
+ this.linePattern = 'solid';
+ this.shadow = true;
+ // prop: shadowAngle
+ // Shadow angle in degrees
+ this.shadowAngle = 45;
+ // prop: shadowOffset
+ // Shadow offset from line in pixels
+ this.shadowOffset = 1.25;
+ // prop: shadowDepth
+ // Number of times shadow is stroked, each stroke offset shadowOffset from the last.
+ this.shadowDepth = 3;
+ // prop: shadowAlpha
+ // Alpha channel transparency of shadow. 0 = transparent.
+ this.shadowAlpha = '0.1';
+ // prop: breakOnNull
+ // Wether line segments should be be broken at null value.
+ // False will join point on either side of line.
+ this.breakOnNull = false;
+ // prop: markerRenderer
+ // A class of a renderer which will draw marker (e.g. circle, square, ...) at the data points,
+ // see <$.jqplot.MarkerRenderer>.
+ this.markerRenderer = $.jqplot.MarkerRenderer;
+ // prop: markerOptions
+ // renderer specific options to pass to the markerRenderer,
+ // see <$.jqplot.MarkerRenderer>.
+ this.markerOptions = {};
+ // prop: showLine
+ // whether to actually draw the line or not. Series will still be renderered, even if no line is drawn.
+ this.showLine = true;
+ // prop: showMarker
+ // whether or not to show the markers at the data points.
+ this.showMarker = true;
+ // prop: index
+ // 0 based index of this series in the plot series array.
+ this.index;
+ // prop: fill
+ // true or false, whether to fill under lines or in bars.
+ // May not be implemented in all renderers.
+ this.fill = false;
+ // prop: fillColor
+ // CSS color spec to use for fill under line. Defaults to line color.
+ this.fillColor;
+ // prop: fillAlpha
+ // Alpha transparency to apply to the fill under the line.
+ // Use this to adjust alpha separate from fill color.
+ this.fillAlpha;
+ // prop: fillAndStroke
+ // If true will stroke the line (with color this.color) as well as fill under it.
+ // Applies only when fill is true.
+ this.fillAndStroke = false;
+ // prop: disableStack
+ // true to not stack this series with other series in the plot.
+ // To render properly, non-stacked series must come after any stacked series
+ // in the plot's data series array. So, the plot's data series array would look like:
+ // > [stackedSeries1, stackedSeries2, ..., nonStackedSeries1, nonStackedSeries2, ...]
+ // disableStack will put a gap in the stacking order of series, and subsequent
+ // stacked series will not fill down through the non-stacked series and will
+ // most likely not stack properly on top of the non-stacked series.
+ this.disableStack = false;
+ // _stack is set by the Plot if the plot is a stacked chart.
+ // will stack lines or bars on top of one another to build a "mountain" style chart.
+ // May not be implemented in all renderers.
+ this._stack = false;
+ // prop: neighborThreshold
+ // how close or far (in pixels) the cursor must be from a point marker to detect the point.
+ this.neighborThreshold = 4;
+ // prop: fillToZero
+ // true will force bar and filled series to fill toward zero on the fill Axis.
+ this.fillToZero = false;
+ // prop: fillToValue
+ // fill a filled series to this value on the fill axis.
+ // Works in conjunction with fillToZero, so that must be true.
+ this.fillToValue = 0;
+ // prop: fillAxis
+ // Either 'x' or 'y'. Which axis to fill the line toward if fillToZero is true.
+ // 'y' means fill up/down to 0 on the y axis for this series.
+ this.fillAxis = 'y';
+ // prop: useNegativeColors
+ // true to color negative values differently in filled and bar charts.
+ this.useNegativeColors = true;
+ this._stackData = [];
+ // _plotData accounts for stacking. If plots not stacked, _plotData and data are same. If
+ // stacked, _plotData is accumulation of stacking data.
+ this._plotData = [];
+ // _plotValues hold the individual x and y values that will be plotted for this series.
+ this._plotValues = {x:[], y:[]};
+ // statistics about the intervals between data points. Used for auto scaling.
+ this._intervals = {x:{}, y:{}};
+ // data from the previous series, for stacked charts.
+ this._prevPlotData = [];
+ this._prevGridData = [];
+ this._stackAxis = 'y';
+ this._primaryAxis = '_xaxis';
+ // give each series a canvas to draw on. This should allow for redrawing speedups.
+ this.canvas = new $.jqplot.GenericCanvas();
+ this.shadowCanvas = new $.jqplot.GenericCanvas();
+ this.plugins = {};
+ // sum of y values in this series.
+ this._sumy = 0;
+ this._sumx = 0;
+ this._type = '';
+ }
+
+ Series.prototype = new $.jqplot.ElemContainer();
+ Series.prototype.constructor = Series;
+
+ Series.prototype.init = function(index, gridbw, plot) {
+ // weed out any null values in the data.
+ this.index = index;
+ this.gridBorderWidth = gridbw;
+ var d = this.data;
+ var temp = [], i, l;
+ for (i=0, l=d.length; i<l; i++) {
+ if (! this.breakOnNull) {
+ if (d[i] == null || d[i][0] == null || d[i][1] == null) {
+ continue;
+ }
+ else {
+ temp.push(d[i]);
+ }
+ }
+ else {
+ // TODO: figure out what to do with null values
+ // probably involve keeping nulls in data array
+ // and then updating renderers to break line
+ // when it hits null value.
+ // For now, just keep value.
+ temp.push(d[i]);
+ }
+ }
+ this.data = temp;
+
+ // parse the renderer options and apply default colors if not provided
+ // Set color even if not shown, so series don't change colors when other
+ // series on plot shown/hidden.
+ if (!this.color) {
+ this.color = plot.colorGenerator.get(this.index);
+ }
+ if (!this.negativeColor) {
+ this.negativeColor = plot.negativeColorGenerator.get(this.index);
+ }
+
+
+ if (!this.fillColor) {
+ this.fillColor = this.color;
+ }
+ if (this.fillAlpha) {
+ var comp = $.jqplot.normalize2rgb(this.fillColor);
+ var comp = $.jqplot.getColorComponents(comp);
+ this.fillColor = 'rgba('+comp[0]+','+comp[1]+','+comp[2]+','+this.fillAlpha+')';
+ }
+ if ($.isFunction(this.renderer)) {
+ this.renderer = new this.renderer();
+ }
+ this.renderer.init.call(this, this.rendererOptions, plot);
+ this.markerRenderer = new this.markerRenderer();
+ if (!this.markerOptions.color) {
+ this.markerOptions.color = this.color;
+ }
+ if (this.markerOptions.show == null) {
+ this.markerOptions.show = this.showMarker;
+ }
+ this.showMarker = this.markerOptions.show;
+ // the markerRenderer is called within its own scope, don't want to overwrite series options!!
+ this.markerRenderer.init(this.markerOptions);
+ };
+
+ // data - optional data point array to draw using this series renderer
+ // gridData - optional grid data point array to draw using this series renderer
+ // stackData - array of cumulative data for stacked plots.
+ Series.prototype.draw = function(sctx, opts, plot) {
+ var options = (opts == undefined) ? {} : opts;
+ sctx = (sctx == undefined) ? this.canvas._ctx : sctx;
+
+ var j, data, gridData;
+
+ // hooks get called even if series not shown
+ // we don't clear canvas here, it would wipe out all other series as well.
+ for (j=0; j<$.jqplot.preDrawSeriesHooks.length; j++) {
+ $.jqplot.preDrawSeriesHooks[j].call(this, sctx, options);
+ }
+ if (this.show) {
+ this.renderer.setGridData.call(this, plot);
+ if (!options.preventJqPlotSeriesDrawTrigger) {
+ $(sctx.canvas).trigger('jqplotSeriesDraw', [this.data, this.gridData]);
+ }
+ data = [];
+ if (options.data) {
+ data = options.data;
+ }
+ else if (!this._stack) {
+ data = this.data;
+ }
+ else {
+ data = this._plotData;
+ }
+ gridData = options.gridData || this.renderer.makeGridData.call(this, data, plot);
+
+ if (this._type === 'line' && this.renderer.smooth && this.renderer._smoothedData.length) {
+ gridData = this.renderer._smoothedData;
+ }
+
+ this.renderer.draw.call(this, sctx, gridData, options, plot);
+ }
+
+ for (j=0; j<$.jqplot.postDrawSeriesHooks.length; j++) {
+ $.jqplot.postDrawSeriesHooks[j].call(this, sctx, options, plot);
+ }
+
+ sctx = opts = plot = j = data = gridData = null;
+ };
+
+ Series.prototype.drawShadow = function(sctx, opts, plot) {
+ var options = (opts == undefined) ? {} : opts;
+ sctx = (sctx == undefined) ? this.shadowCanvas._ctx : sctx;
+
+ var j, data, gridData;
+
+ // hooks get called even if series not shown
+ // we don't clear canvas here, it would wipe out all other series as well.
+ for (j=0; j<$.jqplot.preDrawSeriesShadowHooks.length; j++) {
+ $.jqplot.preDrawSeriesShadowHooks[j].call(this, sctx, options);
+ }
+ if (this.shadow) {
+ this.renderer.setGridData.call(this, plot);
+
+ data = [];
+ if (options.data) {
+ data = options.data;
+ }
+ else if (!this._stack) {
+ data = this.data;
+ }
+ else {
+ data = this._plotData;
+ }
+ gridData = options.gridData || this.renderer.makeGridData.call(this, data, plot);
+
+ this.renderer.drawShadow.call(this, sctx, gridData, options, plot);
+ }
+
+ for (j=0; j<$.jqplot.postDrawSeriesShadowHooks.length; j++) {
+ $.jqplot.postDrawSeriesShadowHooks[j].call(this, sctx, options);
+ }
+
+ sctx = opts = plot = j = data = gridData = null;
+
+ };
+
+ // toggles series display on plot, e.g. show/hide series
+ Series.prototype.toggleDisplay = function(ev, callback) {
+ var s, speed;
+ if (ev.data.series) {
+ s = ev.data.series;
+ }
+ else {
+ s = this;
+ }
+
+ if (ev.data.speed) {
+ speed = ev.data.speed;
+ }
+ if (speed) {
+ // this can be tricky because series may not have a canvas element if replotting.
+ if (s.canvas._elem.is(':hidden') || !s.show) {
+ s.show = true;
+
+ s.canvas._elem.removeClass('jqplot-series-hidden');
+ if (s.shadowCanvas._elem) {
+ s.shadowCanvas._elem.fadeIn(speed);
+ }
+ s.canvas._elem.fadeIn(speed, callback);
+ s.canvas._elem.nextAll('.jqplot-point-label.jqplot-series-'+s.index).fadeIn(speed);
+ }
+ else {
+ s.show = false;
+
+ s.canvas._elem.addClass('jqplot-series-hidden');
+ if (s.shadowCanvas._elem) {
+ s.shadowCanvas._elem.fadeOut(speed);
+ }
+ s.canvas._elem.fadeOut(speed, callback);
+ s.canvas._elem.nextAll('.jqplot-point-label.jqplot-series-'+s.index).fadeOut(speed);
+ }
+ }
+ else {
+ // this can be tricky because series may not have a canvas element if replotting.
+ if (s.canvas._elem.is(':hidden') || !s.show) {
+ s.show = true;
+
+ s.canvas._elem.removeClass('jqplot-series-hidden');
+ if (s.shadowCanvas._elem) {
+ s.shadowCanvas._elem.show();
+ }
+ s.canvas._elem.show(0, callback);
+ s.canvas._elem.nextAll('.jqplot-point-label.jqplot-series-'+s.index).show();
+ }
+ else {
+ s.show = false;
+
+ s.canvas._elem.addClass('jqplot-series-hidden');
+ if (s.shadowCanvas._elem) {
+ s.shadowCanvas._elem.hide();
+ }
+ s.canvas._elem.hide(0, callback);
+ s.canvas._elem.nextAll('.jqplot-point-label.jqplot-series-'+s.index).hide();
+ }
+ }
+ };
+
+
+
+ /**
+ * Class: Grid
+ *
+ * Object representing the grid on which the plot is drawn. The grid in this
+ * context is the area bounded by the axes, the area which will contain the series.
+ * Note, the series are drawn on their own canvas.
+ * The Grid object cannot be instantiated directly, but is created by the Plot object.
+ * Grid properties can be set or overridden by the options passed in from the user.
+ */
+ function Grid() {
+ $.jqplot.ElemContainer.call(this);
+ // Group: Properties
+
+ // prop: drawGridlines
+ // whether to draw the gridlines on the plot.
+ this.drawGridlines = true;
+ // prop: gridLineColor
+ // color of the grid lines.
+ this.gridLineColor = '#cccccc';
+ // prop: gridLineWidth
+ // width of the grid lines.
+ this.gridLineWidth = 1.0;
+ // prop: background
+ // css spec for the background color.
+ this.background = '#fffdf6';
+ // prop: borderColor
+ // css spec for the color of the grid border.
+ this.borderColor = '#999999';
+ // prop: borderWidth
+ // width of the border in pixels.
+ this.borderWidth = 2.0;
+ // prop: drawBorder
+ // True to draw border around grid.
+ this.drawBorder = true;
+ // prop: shadow
+ // whether to show a shadow behind the grid.
+ this.shadow = true;
+ // prop: shadowAngle
+ // shadow angle in degrees
+ this.shadowAngle = 45;
+ // prop: shadowOffset
+ // Offset of each shadow stroke from the border in pixels
+ this.shadowOffset = 1.5;
+ // prop: shadowWidth
+ // width of the stoke for the shadow
+ this.shadowWidth = 3;
+ // prop: shadowDepth
+ // Number of times shadow is stroked, each stroke offset shadowOffset from the last.
+ this.shadowDepth = 3;
+ // prop: shadowColor
+ // an optional css color spec for the shadow in 'rgba(n, n, n, n)' form
+ this.shadowColor = null;
+ // prop: shadowAlpha
+ // Alpha channel transparency of shadow. 0 = transparent.
+ this.shadowAlpha = '0.07';
+ this._left;
+ this._top;
+ this._right;
+ this._bottom;
+ this._width;
+ this._height;
+ this._axes = [];
+ // prop: renderer
+ // Instance of a renderer which will actually render the grid,
+ // see <$.jqplot.CanvasGridRenderer>.
+ this.renderer = $.jqplot.CanvasGridRenderer;
+ // prop: rendererOptions
+ // Options to pass on to the renderer,
+ // see <$.jqplot.CanvasGridRenderer>.
+ this.rendererOptions = {};
+ this._offsets = {top:null, bottom:null, left:null, right:null};
+ }
+
+ Grid.prototype = new $.jqplot.ElemContainer();
+ Grid.prototype.constructor = Grid;
+
+ Grid.prototype.init = function() {
+ if ($.isFunction(this.renderer)) {
+ this.renderer = new this.renderer();
+ }
+ this.renderer.init.call(this, this.rendererOptions);
+ };
+
+ Grid.prototype.createElement = function(offsets,plot) {
+ this._offsets = offsets;
+ return this.renderer.createElement.call(this, plot);
+ };
+
+ Grid.prototype.draw = function() {
+ this.renderer.draw.call(this);
+ };
+
+ $.jqplot.GenericCanvas = function() {
+ $.jqplot.ElemContainer.call(this);
+ this._ctx;
+ };
+
+ $.jqplot.GenericCanvas.prototype = new $.jqplot.ElemContainer();
+ $.jqplot.GenericCanvas.prototype.constructor = $.jqplot.GenericCanvas;
+
+ $.jqplot.GenericCanvas.prototype.createElement = function(offsets, clss, plotDimensions, plot) {
+ this._offsets = offsets;
+ var klass = 'jqplot';
+ if (clss != undefined) {
+ klass = clss;
+ }
+ var elem;
+
+ elem = plot.canvasManager.getCanvas();
+
+ // if new plotDimensions supplied, use them.
+ if (plotDimensions != null) {
+ this._plotDimensions = plotDimensions;
+ }
+
+ elem.width = this._plotDimensions.width - this._offsets.left - this._offsets.right;
+ elem.height = this._plotDimensions.height - this._offsets.top - this._offsets.bottom;
+ this._elem = $(elem);
+ this._elem.css({ position: 'absolute', left: this._offsets.left, top: this._offsets.top });
+
+ this._elem.addClass(klass);
+
+ elem = plot.canvasManager.initCanvas(elem);
+
+ elem = null;
+ return this._elem;
+ };
+
+ $.jqplot.GenericCanvas.prototype.setContext = function() {
+ this._ctx = this._elem.get(0).getContext("2d");
+ return this._ctx;
+ };
+
+ // Memory Leaks patch
+ $.jqplot.GenericCanvas.prototype.resetCanvas = function() {
+ if (this._elem) {
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ window.G_vmlCanvasManager.uninitElement(this._elem.get(0));
+ }
+
+ //this._elem.remove();
+ this._elem.emptyForce();
+ }
+
+ this._ctx = null;
+ };
+
+ $.jqplot.HooksManager = function () {
+ this.hooks =[];
+ this.args = [];
+ };
+
+ $.jqplot.HooksManager.prototype.addOnce = function(fn, args) {
+ args = args || [];
+ var havehook = false;
+ for (var i=0, l=this.hooks.length; i<l; i++) {
+ if (this.hooks[i] == fn) {
+ havehook = true;
+ }
+ }
+ if (!havehook) {
+ this.hooks.push(fn);
+ this.args.push(args);
+ }
+ };
+
+ $.jqplot.HooksManager.prototype.add = function(fn, args) {
+ args = args || [];
+ this.hooks.push(fn);
+ this.args.push(args);
+ };
+
+ $.jqplot.EventListenerManager = function () {
+ this.hooks =[];
+ };
+
+ $.jqplot.EventListenerManager.prototype.addOnce = function(ev, fn) {
+ var havehook = false, h, i;
+ for (var i=0, l=this.hooks.length; i<l; i++) {
+ h = this.hooks[i];
+ if (h[0] == ev && h[1] == fn) {
+ havehook = true;
+ }
+ }
+ if (!havehook) {
+ this.hooks.push([ev, fn]);
+ }
+ };
+
+ $.jqplot.EventListenerManager.prototype.add = function(ev, fn) {
+ this.hooks.push([ev, fn]);
+ };
+
+
+ var _axisNames = ['yMidAxis', 'xaxis', 'yaxis', 'x2axis', 'y2axis', 'y3axis', 'y4axis', 'y5axis', 'y6axis', 'y7axis', 'y8axis', 'y9axis'];
+
+ /**
+ * Class: jqPlot
+ * Plot object returned by call to $.jqplot. Handles parsing user options,
+ * creating sub objects (Axes, legend, title, series) and rendering the plot.
+ */
+ function jqPlot() {
+ // Group: Properties
+ // These properties are specified at the top of the options object
+ // like so:
+ // > {
+ // > axesDefaults:{min:0},
+ // > series:[{color:'#6633dd'}],
+ // > title: 'A Plot'
+ // > }
+ //
+
+ // prop: animate
+ // True to animate the series on initial plot draw (renderer dependent).
+ // Actual animation functionality must be supported in the renderer.
+ this.animate = false;
+ // prop: animateReplot
+ // True to animate series after a call to the replot() method.
+ // Use with caution! Replots can happen very frequently under
+ // certain circumstances (e.g. resizing, dragging points) and
+ // animation in these situations can cause problems.
+ this.animateReplot = false;
+ // prop: axes
+ // up to 4 axes are supported, each with its own options,
+ // See <Axis> for axis specific options.
+ this.axes = {xaxis: new Axis('xaxis'), yaxis: new Axis('yaxis'), x2axis: new Axis('x2axis'), y2axis: new Axis('y2axis'), y3axis: new Axis('y3axis'), y4axis: new Axis('y4axis'), y5axis: new Axis('y5axis'), y6axis: new Axis('y6axis'), y7axis: new Axis('y7axis'), y8axis: new Axis('y8axis'), y9axis: new Axis('y9axis'), yMidAxis: new Axis('yMidAxis')};
+ this.baseCanvas = new $.jqplot.GenericCanvas();
+ // true to intercept right click events and fire a 'jqplotRightClick' event.
+ // this will also block the context menu.
+ this.captureRightClick = false;
+ // prop: data
+ // user's data. Data should *NOT* be specified in the options object,
+ // but be passed in as the second argument to the $.jqplot() function.
+ // The data property is described here soley for reference.
+ // The data should be in the form of an array of 2D or 1D arrays like
+ // > [ [[x1, y1], [x2, y2],...], [y1, y2, ...] ].
+ this.data = [];
+ // prop: dataRenderer
+ // A callable which can be used to preprocess data passed into the plot.
+ // Will be called with 3 arguments: the plot data, a reference to the plot,
+ // and the value of dataRendererOptions.
+ this.dataRenderer;
+ // prop: dataRendererOptions
+ // Options that will be passed to the dataRenderer.
+ // Can be of any type.
+ this.dataRendererOptions;
+ this.defaults = {
+ // prop: axesDefaults
+ // default options that will be applied to all axes.
+ // see <Axis> for axes options.
+ axesDefaults: {},
+ axes: {xaxis:{}, yaxis:{}, x2axis:{}, y2axis:{}, y3axis:{}, y4axis:{}, y5axis:{}, y6axis:{}, y7axis:{}, y8axis:{}, y9axis:{}, yMidAxis:{}},
+ // prop: seriesDefaults
+ // default options that will be applied to all series.
+ // see <Series> for series options.
+ seriesDefaults: {},
+ series:[]
+ };
+ // prop: defaultAxisStart
+ // 1-D data series are internally converted into 2-D [x,y] data point arrays
+ // by jqPlot. This is the default starting value for the missing x or y value.
+ // The added data will be a monotonically increasing series (e.g. [1, 2, 3, ...])
+ // starting at this value.
+ this.defaultAxisStart = 1;
+ // this.doCustomEventBinding = true;
+ // prop: drawIfHidden
+ // True to execute the draw method even if the plot target is hidden.
+ // Generally, this should be false. Most plot elements will not be sized/
+ // positioned correclty if renderered into a hidden container. To render into
+ // a hidden container, call the replot method when the container is shown.
+ this.drawIfHidden = false;
+ this.eventCanvas = new $.jqplot.GenericCanvas();
+ // prop: fillBetween
+ // Fill between 2 line series in a plot.
+ // Options object:
+ // {
+ // series1: first index (0 based) of series in fill
+ // series2: second index (0 based) of series in fill
+ // color: color of fill [default fillColor of series1]
+ // baseSeries: fill will be drawn below this series (0 based index)
+ // fill: false to turn off fill [default true].
+ // }
+ this.fillBetween = {
+ series1: null,
+ series2: null,
+ color: null,
+ baseSeries: 0,
+ fill: true
+ };
+ // prop; fontFamily
+ // css spec for the font-family attribute. Default for the entire plot.
+ this.fontFamily;
+ // prop: fontSize
+ // css spec for the font-size attribute. Default for the entire plot.
+ this.fontSize;
+ // prop: grid
+ // See <Grid> for grid specific options.
+ this.grid = new Grid();
+ // prop: legend
+ // see <$.jqplot.TableLegendRenderer>
+ this.legend = new Legend();
+ // prop: noDataIndicator
+ // Options to set up a mock plot with a data loading indicator if no data is specified.
+ this.noDataIndicator = {
+ show: false,
+ indicator: 'Loading Data...',
+ axes: {
+ xaxis: {
+ min: 0,
+ max: 10,
+ tickInterval: 2,
+ show: true
+ },
+ yaxis: {
+ min: 0,
+ max: 12,
+ tickInterval: 3,
+ show: true
+ }
+ }
+ };
+ // prop: negativeSeriesColors
+ // colors to use for portions of the line below zero.
+ this.negativeSeriesColors = $.jqplot.config.defaultNegativeColors;
+ // container to hold all of the merged options. Convienence for plugins.
+ this.options = {};
+ this.previousSeriesStack = [];
+ // Namespace to hold plugins. Generally non-renderer plugins add themselves to here.
+ this.plugins = {};
+ // prop: series
+ // Array of series object options.
+ // see <Series> for series specific options.
+ this.series = [];
+ // array of series indices. Keep track of order
+ // which series canvases are displayed, lowest
+ // to highest, back to front.
+ this.seriesStack = [];
+ // prop: seriesColors
+ // Ann array of CSS color specifications that will be applied, in order,
+ // to the series in the plot. Colors will wrap around so, if their
+ // are more series than colors, colors will be reused starting at the
+ // beginning. For pie charts, this specifies the colors of the slices.
+ this.seriesColors = $.jqplot.config.defaultColors;
+ // prop: sortData
+ // false to not sort the data passed in by the user.
+ // Many bar, stacked and other graphs as well as many plugins depend on
+ // having sorted data.
+ this.sortData = true;
+ // prop: stackSeries
+ // true or false, creates a stack or "mountain" plot.
+ // Not all series renderers may implement this option.
+ this.stackSeries = false;
+ // a shortcut for axis syncTicks options. Not implemented yet.
+ this.syncXTicks = true;
+ // a shortcut for axis syncTicks options. Not implemented yet.
+ this.syncYTicks = true;
+ // the jquery object for the dom target.
+ this.target = null;
+ // The id of the dom element to render the plot into
+ this.targetId = null;
+ // prop textColor
+ // css spec for the css color attribute. Default for the entire plot.
+ this.textColor;
+ // prop: title
+ // Title object. See <Title> for specific options. As a shortcut, you
+ // can specify the title option as just a string like: title: 'My Plot'
+ // and this will create a new title object with the specified text.
+ this.title = new Title();
+ // Count how many times the draw method has been called while the plot is visible.
+ // Mostly used to test if plot has never been dran (=0), has been successfully drawn
+ // into a visible container once (=1) or draw more than once into a visible container.
+ // Can use this in tests to see if plot has been visibly drawn at least one time.
+ // After plot has been visibly drawn once, it generally doesn't need redrawing if its
+ // container is hidden and shown.
+ this._drawCount = 0;
+ // sum of y values for all series in plot.
+ // used in mekko chart.
+ this._sumy = 0;
+ this._sumx = 0;
+ // array to hold the cumulative stacked series data.
+ // used to ajust the individual series data, which won't have access to other
+ // series data.
+ this._stackData = [];
+ // array that holds the data to be plotted. This will be the series data
+ // merged with the the appropriate data from _stackData according to the stackAxis.
+ this._plotData = [];
+ this._width = null;
+ this._height = null;
+ this._plotDimensions = {height:null, width:null};
+ this._gridPadding = {top:null, right:null, bottom:null, left:null};
+ this._defaultGridPadding = {top:10, right:10, bottom:23, left:10};
+
+ this._addDomReference = $.jqplot.config.addDomReference;
+
+ this.preInitHooks = new $.jqplot.HooksManager();
+ this.postInitHooks = new $.jqplot.HooksManager();
+ this.preParseOptionsHooks = new $.jqplot.HooksManager();
+ this.postParseOptionsHooks = new $.jqplot.HooksManager();
+ this.preDrawHooks = new $.jqplot.HooksManager();
+ this.postDrawHooks = new $.jqplot.HooksManager();
+ this.preDrawSeriesHooks = new $.jqplot.HooksManager();
+ this.postDrawSeriesHooks = new $.jqplot.HooksManager();
+ this.preDrawLegendHooks = new $.jqplot.HooksManager();
+ this.addLegendRowHooks = new $.jqplot.HooksManager();
+ this.preSeriesInitHooks = new $.jqplot.HooksManager();
+ this.postSeriesInitHooks = new $.jqplot.HooksManager();
+ this.preParseSeriesOptionsHooks = new $.jqplot.HooksManager();
+ this.postParseSeriesOptionsHooks = new $.jqplot.HooksManager();
+ this.eventListenerHooks = new $.jqplot.EventListenerManager();
+ this.preDrawSeriesShadowHooks = new $.jqplot.HooksManager();
+ this.postDrawSeriesShadowHooks = new $.jqplot.HooksManager();
+
+ this.colorGenerator = new $.jqplot.ColorGenerator();
+ this.negativeColorGenerator = new $.jqplot.ColorGenerator();
+
+ this.canvasManager = new $.jqplot.CanvasManager();
+
+ this.themeEngine = new $.jqplot.ThemeEngine();
+
+ var seriesColorsIndex = 0;
+
+ // Group: methods
+ //
+ // method: init
+ // sets the plot target, checks data and applies user
+ // options to plot.
+ this.init = function(target, data, options) {
+ options = options || {};
+ for (var i=0; i<$.jqplot.preInitHooks.length; i++) {
+ $.jqplot.preInitHooks[i].call(this, target, data, options);
+ }
+
+ for (var i=0; i<this.preInitHooks.hooks.length; i++) {
+ this.preInitHooks.hooks[i].call(this, target, data, options);
+ }
+
+ this.targetId = '#'+target;
+ this.target = $('#'+target);
+
+ //////
+ // Add a reference to plot
+ //////
+ if (this._addDomReference) {
+ this.target.data('jqplot', this);
+ }
+ // remove any error class that may be stuck on target.
+ this.target.removeClass('jqplot-error');
+ if (!this.target.get(0)) {
+ throw new Error("No plot target specified");
+ }
+
+ // make sure the target is positioned by some means and set css
+ if (this.target.css('position') == 'static') {
+ this.target.css('position', 'relative');
+ }
+ if (!this.target.hasClass('jqplot-target')) {
+ this.target.addClass('jqplot-target');
+ }
+
+ // if no height or width specified, use a default.
+ if (!this.target.height()) {
+ var h;
+ if (options && options.height) {
+ h = parseInt(options.height, 10);
+ }
+ else if (this.target.attr('data-height')) {
+ h = parseInt(this.target.attr('data-height'), 10);
+ }
+ else {
+ h = parseInt($.jqplot.config.defaultHeight, 10);
+ }
+ this._height = h;
+ this.target.css('height', h+'px');
+ }
+ else {
+ this._height = h = this.target.height();
+ }
+ if (!this.target.width()) {
+ var w;
+ if (options && options.width) {
+ w = parseInt(options.width, 10);
+ }
+ else if (this.target.attr('data-width')) {
+ w = parseInt(this.target.attr('data-width'), 10);
+ }
+ else {
+ w = parseInt($.jqplot.config.defaultWidth, 10);
+ }
+ this._width = w;
+ this.target.css('width', w+'px');
+ }
+ else {
+ this._width = w = this.target.width();
+ }
+
+ for (var i=0, l=_axisNames.length; i<l; i++) {
+ this.axes[_axisNames[i]] = new Axis(_axisNames[i]);
+ }
+
+ this._plotDimensions.height = this._height;
+ this._plotDimensions.width = this._width;
+ this.grid._plotDimensions = this._plotDimensions;
+ this.title._plotDimensions = this._plotDimensions;
+ this.baseCanvas._plotDimensions = this._plotDimensions;
+ this.eventCanvas._plotDimensions = this._plotDimensions;
+ this.legend._plotDimensions = this._plotDimensions;
+ if (this._height <=0 || this._width <=0 || !this._height || !this._width) {
+ throw new Error("Canvas dimension not set");
+ }
+
+ if (options.dataRenderer && $.isFunction(options.dataRenderer)) {
+ if (options.dataRendererOptions) {
+ this.dataRendererOptions = options.dataRendererOptions;
+ }
+ this.dataRenderer = options.dataRenderer;
+ data = this.dataRenderer(data, this, this.dataRendererOptions);
+ }
+
+ if (options.noDataIndicator && $.isPlainObject(options.noDataIndicator)) {
+ $.extend(true, this.noDataIndicator, options.noDataIndicator);
+ }
+
+ if (data == null || $.isArray(data) == false || data.length == 0 || $.isArray(data[0]) == false || data[0].length == 0) {
+
+ if (this.noDataIndicator.show == false) {
+ throw new Error("No data specified");
+ }
+
+ else {
+ // have to be descructive here in order for plot to not try and render series.
+ // This means that $.jqplot() will have to be called again when there is data.
+ //delete options.series;
+
+ for (var ax in this.noDataIndicator.axes) {
+ for (var prop in this.noDataIndicator.axes[ax]) {
+ this.axes[ax][prop] = this.noDataIndicator.axes[ax][prop];
+ }
+ }
+
+ this.postDrawHooks.add(function() {
+ var eh = this.eventCanvas.getHeight();
+ var ew = this.eventCanvas.getWidth();
+ var temp = $('<div class="jqplot-noData-container" style="position:absolute;"></div>');
+ this.target.append(temp);
+ temp.height(eh);
+ temp.width(ew);
+ temp.css('top', this.eventCanvas._offsets.top);
+ temp.css('left', this.eventCanvas._offsets.left);
+
+ var temp2 = $('<div class="jqplot-noData-contents" style="text-align:center; position:relative; margin-left:auto; margin-right:auto;"></div>');
+ temp.append(temp2);
+ temp2.html(this.noDataIndicator.indicator);
+ var th = temp2.height();
+ var tw = temp2.width();
+ temp2.height(th);
+ temp2.width(tw);
+ temp2.css('top', (eh - th)/2 + 'px');
+ });
+
+ }
+ }
+
+ // make a copy of the data
+ this.data = $.extend(true, [], data);
+
+ this.parseOptions(options);
+
+ if (this.textColor) {
+ this.target.css('color', this.textColor);
+ }
+ if (this.fontFamily) {
+ this.target.css('font-family', this.fontFamily);
+ }
+ if (this.fontSize) {
+ this.target.css('font-size', this.fontSize);
+ }
+
+ this.title.init();
+ this.legend.init();
+ this._sumy = 0;
+ this._sumx = 0;
+ this.computePlotData();
+ for (var i=0; i<this.series.length; i++) {
+ // set default stacking order for series canvases
+ this.seriesStack.push(i);
+ this.previousSeriesStack.push(i);
+ this.series[i].shadowCanvas._plotDimensions = this._plotDimensions;
+ this.series[i].canvas._plotDimensions = this._plotDimensions;
+ for (var j=0; j<$.jqplot.preSeriesInitHooks.length; j++) {
+ $.jqplot.preSeriesInitHooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ for (var j=0; j<this.preSeriesInitHooks.hooks.length; j++) {
+ this.preSeriesInitHooks.hooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ // this.populatePlotData(this.series[i], i);
+ this.series[i]._plotDimensions = this._plotDimensions;
+ this.series[i].init(i, this.grid.borderWidth, this);
+ for (var j=0; j<$.jqplot.postSeriesInitHooks.length; j++) {
+ $.jqplot.postSeriesInitHooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ for (var j=0; j<this.postSeriesInitHooks.hooks.length; j++) {
+ this.postSeriesInitHooks.hooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ this._sumy += this.series[i]._sumy;
+ this._sumx += this.series[i]._sumx;
+ }
+
+ var name,
+ axis;
+ for (var i=0, l=_axisNames.length; i<l; i++) {
+ name = _axisNames[i];
+ axis = this.axes[name];
+ axis._plotDimensions = this._plotDimensions;
+ axis.init();
+ if (this.axes[name].borderColor == null) {
+ if (name.charAt(0) !== 'x' && axis.useSeriesColor === true && axis.show) {
+ axis.borderColor = axis._series[0].color;
+ }
+ else {
+ axis.borderColor = this.grid.borderColor;
+ }
+ }
+ }
+
+ if (this.sortData) {
+ sortData(this.series);
+ }
+ this.grid.init();
+ this.grid._axes = this.axes;
+
+ this.legend._series = this.series;
+
+ for (var i=0; i<$.jqplot.postInitHooks.length; i++) {
+ $.jqplot.postInitHooks[i].call(this, target, this.data, options);
+ }
+
+ for (var i=0; i<this.postInitHooks.hooks.length; i++) {
+ this.postInitHooks.hooks[i].call(this, target, this.data, options);
+ }
+ };
+
+ // method: resetAxesScale
+ // Reset the specified axes min, max, numberTicks and tickInterval properties to null
+ // or reset these properties on all axes if no list of axes is provided.
+ //
+ // Parameters:
+ // axes - Boolean to reset or not reset all axes or an array or object of axis names to reset.
+ this.resetAxesScale = function(axes, options) {
+ var opts = options || {};
+ var ax = axes || this.axes;
+ if (ax === true) {
+ ax = this.axes;
+ }
+ if ($.isArray(ax)) {
+ for (var i = 0; i < ax.length; i++) {
+ this.axes[ax[i]].resetScale(opts[ax[i]]);
+ }
+ }
+ else if (typeof(ax) === 'object') {
+ for (var name in ax) {
+ this.axes[name].resetScale(opts[name]);
+ }
+ }
+ };
+ // method: reInitialize
+ // reinitialize plot for replotting.
+ // not called directly.
+ this.reInitialize = function (data, opts) {
+ // Plot should be visible and have a height and width.
+ // If plot doesn't have height and width for some
+ // reason, set it by other means. Plot must not have
+ // a display:none attribute, however.
+
+ var options = $.extend(true, {}, this.options, opts);
+
+ var target = this.targetId.substr(1);
+ var tdata = (data == null) ? this.data : data;
+
+ for (var i=0; i<$.jqplot.preInitHooks.length; i++) {
+ $.jqplot.preInitHooks[i].call(this, target, tdata, options);
+ }
+
+ for (var i=0; i<this.preInitHooks.hooks.length; i++) {
+ this.preInitHooks.hooks[i].call(this, target, tdata, options);
+ }
+
+ this._height = this.target.height();
+ this._width = this.target.width();
+
+ if (this._height <=0 || this._width <=0 || !this._height || !this._width) {
+ throw new Error("Target dimension not set");
+ }
+
+ this._plotDimensions.height = this._height;
+ this._plotDimensions.width = this._width;
+ this.grid._plotDimensions = this._plotDimensions;
+ this.title._plotDimensions = this._plotDimensions;
+ this.baseCanvas._plotDimensions = this._plotDimensions;
+ this.eventCanvas._plotDimensions = this._plotDimensions;
+ this.legend._plotDimensions = this._plotDimensions;
+
+ var name,
+ t,
+ j,
+ axis;
+
+ for (var i=0, l=_axisNames.length; i<l; i++) {
+ name = _axisNames[i];
+ axis = this.axes[name];
+
+ // Memory Leaks patch : clear ticks elements
+ t = axis._ticks;
+ for (var j = 0, tlen = t.length; j < tlen; j++) {
+ var el = t[j]._elem;
+ if (el) {
+ // if canvas renderer
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ window.G_vmlCanvasManager.uninitElement(el.get(0));
+ }
+ el.emptyForce();
+ el = null;
+ t._elem = null;
+ }
+ }
+ t = null;
+
+ delete axis.ticks;
+ delete axis._ticks;
+ this.axes[name] = new Axis(name);
+ this.axes[name]._plotWidth = this._width;
+ this.axes[name]._plotHeight = this._height;
+ }
+
+ if (data) {
+ if (options.dataRenderer && $.isFunction(options.dataRenderer)) {
+ if (options.dataRendererOptions) {
+ this.dataRendererOptions = options.dataRendererOptions;
+ }
+ this.dataRenderer = options.dataRenderer;
+ data = this.dataRenderer(data, this, this.dataRendererOptions);
+ }
+
+ // make a copy of the data
+ this.data = $.extend(true, [], data);
+ }
+
+ if (opts) {
+ this.parseOptions(options);
+ }
+
+ this.title._plotWidth = this._width;
+
+ if (this.textColor) {
+ this.target.css('color', this.textColor);
+ }
+ if (this.fontFamily) {
+ this.target.css('font-family', this.fontFamily);
+ }
+ if (this.fontSize) {
+ this.target.css('font-size', this.fontSize);
+ }
+
+ this.title.init();
+ this.legend.init();
+ this._sumy = 0;
+ this._sumx = 0;
+
+ this.seriesStack = [];
+ this.previousSeriesStack = [];
+
+ this.computePlotData();
+ for (var i=0, l=this.series.length; i<l; i++) {
+ // set default stacking order for series canvases
+ this.seriesStack.push(i);
+ this.previousSeriesStack.push(i);
+ this.series[i].shadowCanvas._plotDimensions = this._plotDimensions;
+ this.series[i].canvas._plotDimensions = this._plotDimensions;
+ for (var j=0; j<$.jqplot.preSeriesInitHooks.length; j++) {
+ $.jqplot.preSeriesInitHooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ for (var j=0; j<this.preSeriesInitHooks.hooks.length; j++) {
+ this.preSeriesInitHooks.hooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ // this.populatePlotData(this.series[i], i);
+ this.series[i]._plotDimensions = this._plotDimensions;
+ this.series[i].init(i, this.grid.borderWidth, this);
+ for (var j=0; j<$.jqplot.postSeriesInitHooks.length; j++) {
+ $.jqplot.postSeriesInitHooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ for (var j=0; j<this.postSeriesInitHooks.hooks.length; j++) {
+ this.postSeriesInitHooks.hooks[j].call(this.series[i], target, this.data, this.options.seriesDefaults, this.options.series[i], this);
+ }
+ this._sumy += this.series[i]._sumy;
+ this._sumx += this.series[i]._sumx;
+ }
+
+ for (var i=0, l=_axisNames.length; i<l; i++) {
+ name = _axisNames[i];
+ axis = this.axes[name];
+
+ axis._plotDimensions = this._plotDimensions;
+ axis.init();
+ if (axis.borderColor == null) {
+ if (name.charAt(0) !== 'x' && axis.useSeriesColor === true && axis.show) {
+ axis.borderColor = axis._series[0].color;
+ }
+ else {
+ axis.borderColor = this.grid.borderColor;
+ }
+ }
+ }
+
+ if (this.sortData) {
+ sortData(this.series);
+ }
+ this.grid.init();
+ this.grid._axes = this.axes;
+
+ this.legend._series = this.series;
+
+ for (var i=0, l=$.jqplot.postInitHooks.length; i<l; i++) {
+ $.jqplot.postInitHooks[i].call(this, target, this.data, options);
+ }
+
+ for (var i=0, l=this.postInitHooks.hooks.length; i<l; i++) {
+ this.postInitHooks.hooks[i].call(this, target, this.data, options);
+ }
+ };
+
+
+
+ // method: quickInit
+ //
+ // Quick reinitialization plot for replotting.
+ // Does not parse options ore recreate axes and series.
+ // not called directly.
+ this.quickInit = function () {
+ // Plot should be visible and have a height and width.
+ // If plot doesn't have height and width for some
+ // reason, set it by other means. Plot must not have
+ // a display:none attribute, however.
+
+ this._height = this.target.height();
+ this._width = this.target.width();
+
+ if (this._height <=0 || this._width <=0 || !this._height || !this._width) {
+ throw new Error("Target dimension not set");
+ }
+
+ this._plotDimensions.height = this._height;
+ this._plotDimensions.width = this._width;
+ this.grid._plotDimensions = this._plotDimensions;
+ this.title._plotDimensions = this._plotDimensions;
+ this.baseCanvas._plotDimensions = this._plotDimensions;
+ this.eventCanvas._plotDimensions = this._plotDimensions;
+ this.legend._plotDimensions = this._plotDimensions;
+
+ for (var n in this.axes) {
+ this.axes[n]._plotWidth = this._width;
+ this.axes[n]._plotHeight = this._height;
+ }
+
+ this.title._plotWidth = this._width;
+
+ if (this.textColor) {
+ this.target.css('color', this.textColor);
+ }
+ if (this.fontFamily) {
+ this.target.css('font-family', this.fontFamily);
+ }
+ if (this.fontSize) {
+ this.target.css('font-size', this.fontSize);
+ }
+
+ this._sumy = 0;
+ this._sumx = 0;
+ this.computePlotData();
+ for (var i=0; i<this.series.length; i++) {
+ // this.populatePlotData(this.series[i], i);
+ if (this.series[i]._type === 'line' && this.series[i].renderer.bands.show) {
+ this.series[i].renderer.initBands.call(this.series[i], this.series[i].renderer.options, this);
+ }
+ this.series[i]._plotDimensions = this._plotDimensions;
+ this.series[i].canvas._plotDimensions = this._plotDimensions;
+ //this.series[i].init(i, this.grid.borderWidth);
+ this._sumy += this.series[i]._sumy;
+ this._sumx += this.series[i]._sumx;
+ }
+
+ var name;
+
+ for (var j=0; j<12; j++) {
+ name = _axisNames[j];
+ // Memory Leaks patch : clear ticks elements
+ var t = this.axes[name]._ticks;
+ for (var i = 0; i < t.length; i++) {
+ var el = t[i]._elem;
+ if (el) {
+ // if canvas renderer
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ window.G_vmlCanvasManager.uninitElement(el.get(0));
+ }
+ el.emptyForce();
+ el = null;
+ t._elem = null;
+ }
+ }
+ t = null;
+
+ this.axes[name]._plotDimensions = this._plotDimensions;
+ this.axes[name]._ticks = [];
+ // this.axes[name].renderer.init.call(this.axes[name], {});
+ }
+
+ if (this.sortData) {
+ sortData(this.series);
+ }
+
+ this.grid._axes = this.axes;
+
+ this.legend._series = this.series;
+ };
+
+ // sort the series data in increasing order.
+ function sortData(series) {
+ var d, sd, pd, ppd, ret;
+ for (var i=0; i<series.length; i++) {
+ var check;
+ var bat = [series[i].data, series[i]._stackData, series[i]._plotData, series[i]._prevPlotData];
+ for (var n=0; n<4; n++) {
+ check = true;
+ d = bat[n];
+ if (series[i]._stackAxis == 'x') {
+ for (var j = 0; j < d.length; j++) {
+ if (typeof(d[j][1]) != "number") {
+ check = false;
+ break;
+ }
+ }
+ if (check) {
+ d.sort(function(a,b) { return a[1] - b[1]; });
+ }
+ }
+ else {
+ for (var j = 0; j < d.length; j++) {
+ if (typeof(d[j][0]) != "number") {
+ check = false;
+ break;
+ }
+ }
+ if (check) {
+ d.sort(function(a,b) { return a[0] - b[0]; });
+ }
+ }
+ }
+
+ }
+ }
+
+ this.computePlotData = function() {
+ this._plotData = [];
+ this._stackData = [];
+ var series,
+ index,
+ l;
+
+
+ for (index=0, l=this.series.length; index<l; index++) {
+ series = this.series[index];
+ this._plotData.push([]);
+ this._stackData.push([]);
+ var cd = series.data;
+ this._plotData[index] = $.extend(true, [], cd);
+ this._stackData[index] = $.extend(true, [], cd);
+ series._plotData = this._plotData[index];
+ series._stackData = this._stackData[index];
+ var plotValues = {x:[], y:[]};
+
+ if (this.stackSeries && !series.disableStack) {
+ series._stack = true;
+ ///////////////////////////
+ // have to check for nulls
+ ///////////////////////////
+ var sidx = (series._stackAxis === 'x') ? 0 : 1;
+
+ for (var k=0, cdl=cd.length; k<cdl; k++) {
+ var temp = cd[k][sidx];
+ if (temp == null) {
+ temp = 0;
+ }
+ this._plotData[index][k][sidx] = temp;
+ this._stackData[index][k][sidx] = temp;
+
+ if (index > 0) {
+ for (var j=index; j--;) {
+ var prevval = this._plotData[j][k][sidx];
+ // only need to sum up the stack axis column of data
+ // and only sum if it is of same sign.
+ // if previous series isn't same sign, keep looking
+ // at earlier series untill we find one of same sign.
+ if (temp * prevval >= 0) {
+ this._plotData[index][k][sidx] += prevval;
+ this._stackData[index][k][sidx] += prevval;
+ break;
+ }
+ }
+ }
+ }
+
+ }
+ else {
+ for (var i=0; i<series.data.length; i++) {
+ plotValues.x.push(series.data[i][0]);
+ plotValues.y.push(series.data[i][1]);
+ }
+ this._stackData.push(series.data);
+ this.series[index]._stackData = series.data;
+ this._plotData.push(series.data);
+ series._plotData = series.data;
+ series._plotValues = plotValues;
+ }
+ if (index>0) {
+ series._prevPlotData = this.series[index-1]._plotData;
+ }
+ series._sumy = 0;
+ series._sumx = 0;
+ for (i=series.data.length-1; i>-1; i--) {
+ series._sumy += series.data[i][1];
+ series._sumx += series.data[i][0];
+ }
+ }
+
+ };
+
+ // populate the _stackData and _plotData arrays for the plot and the series.
+ this.populatePlotData = function(series, index) {
+ // if a stacked chart, compute the stacked data
+ this._plotData = [];
+ this._stackData = [];
+ series._stackData = [];
+ series._plotData = [];
+ var plotValues = {x:[], y:[]};
+ if (this.stackSeries && !series.disableStack) {
+ series._stack = true;
+ var sidx = (series._stackAxis === 'x') ? 0 : 1;
+ // var idx = sidx ? 0 : 1;
+ // push the current data into stackData
+ //this._stackData.push(this.series[i].data);
+ var temp = $.extend(true, [], series.data);
+ // create the data that will be plotted for this series
+ var plotdata = $.extend(true, [], series.data);
+ var tempx, tempy, dval, stackval, comparator;
+ // for first series, nothing to add to stackData.
+ for (var j=0; j<index; j++) {
+ var cd = this.series[j].data;
+ for (var k=0; k<cd.length; k++) {
+ dval = cd[k];
+ tempx = (dval[0] != null) ? dval[0] : 0;
+ tempy = (dval[1] != null) ? dval[1] : 0;
+ temp[k][0] += tempx;
+ temp[k][1] += tempy;
+ stackval = (sidx) ? tempy : tempx;
+ // only need to sum up the stack axis column of data
+ // and only sum if it is of same sign.
+ if (series.data[k][sidx] * stackval >= 0) {
+ plotdata[k][sidx] += stackval;
+ }
+ }
+ }
+ for (var i=0; i<plotdata.length; i++) {
+ plotValues.x.push(plotdata[i][0]);
+ plotValues.y.push(plotdata[i][1]);
+ }
+ this._plotData.push(plotdata);
+ this._stackData.push(temp);
+ series._stackData = temp;
+ series._plotData = plotdata;
+ series._plotValues = plotValues;
+ }
+ else {
+ for (var i=0; i<series.data.length; i++) {
+ plotValues.x.push(series.data[i][0]);
+ plotValues.y.push(series.data[i][1]);
+ }
+ this._stackData.push(series.data);
+ this.series[index]._stackData = series.data;
+ this._plotData.push(series.data);
+ series._plotData = series.data;
+ series._plotValues = plotValues;
+ }
+ if (index>0) {
+ series._prevPlotData = this.series[index-1]._plotData;
+ }
+ series._sumy = 0;
+ series._sumx = 0;
+ for (i=series.data.length-1; i>-1; i--) {
+ series._sumy += series.data[i][1];
+ series._sumx += series.data[i][0];
+ }
+ };
+
+ // function to safely return colors from the color array and wrap around at the end.
+ this.getNextSeriesColor = (function(t) {
+ var idx = 0;
+ var sc = t.seriesColors;
+
+ return function () {
+ if (idx < sc.length) {
+ return sc[idx++];
+ }
+ else {
+ idx = 0;
+ return sc[idx++];
+ }
+ };
+ })(this);
+
+ this.parseOptions = function(options){
+ for (var i=0; i<this.preParseOptionsHooks.hooks.length; i++) {
+ this.preParseOptionsHooks.hooks[i].call(this, options);
+ }
+ for (var i=0; i<$.jqplot.preParseOptionsHooks.length; i++) {
+ $.jqplot.preParseOptionsHooks[i].call(this, options);
+ }
+ this.options = $.extend(true, {}, this.defaults, options);
+ var opts = this.options;
+ this.animate = opts.animate;
+ this.animateReplot = opts.animateReplot;
+ this.stackSeries = opts.stackSeries;
+ if ($.isPlainObject(opts.fillBetween)) {
+
+ var temp = ['series1', 'series2', 'color', 'baseSeries', 'fill'],
+ tempi;
+
+ for (var i=0, l=temp.length; i<l; i++) {
+ tempi = temp[i];
+ if (opts.fillBetween[tempi] != null) {
+ this.fillBetween[tempi] = opts.fillBetween[tempi];
+ }
+ }
+ }
+
+ if (opts.seriesColors) {
+ this.seriesColors = opts.seriesColors;
+ }
+ if (opts.negativeSeriesColors) {
+ this.negativeSeriesColors = opts.negativeSeriesColors;
+ }
+ if (opts.captureRightClick) {
+ this.captureRightClick = opts.captureRightClick;
+ }
+ this.defaultAxisStart = (options && options.defaultAxisStart != null) ? options.defaultAxisStart : this.defaultAxisStart;
+ this.colorGenerator.setColors(this.seriesColors);
+ this.negativeColorGenerator.setColors(this.negativeSeriesColors);
+ // var cg = new this.colorGenerator(this.seriesColors);
+ // var ncg = new this.colorGenerator(this.negativeSeriesColors);
+ // this._gridPadding = this.options.gridPadding;
+ $.extend(true, this._gridPadding, opts.gridPadding);
+ this.sortData = (opts.sortData != null) ? opts.sortData : this.sortData;
+ for (var i=0; i<12; i++) {
+ var n = _axisNames[i];
+ var axis = this.axes[n];
+ axis._options = $.extend(true, {}, opts.axesDefaults, opts.axes[n]);
+ $.extend(true, axis, opts.axesDefaults, opts.axes[n]);
+ axis._plotWidth = this._width;
+ axis._plotHeight = this._height;
+ }
+ // if (this.data.length == 0) {
+ // this.data = [];
+ // for (var i=0; i<this.options.series.length; i++) {
+ // this.data.push(this.options.series.data);
+ // }
+ // }
+
+ var normalizeData = function(data, dir, start) {
+ // return data as an array of point arrays,
+ // in form [[x1,y1...], [x2,y2...], ...]
+ var temp = [];
+ var i, l;
+ dir = dir || 'vertical';
+ if (!$.isArray(data[0])) {
+ // we have a series of scalars. One line with just y values.
+ // turn the scalar list of data into a data array of form:
+ // [[1, data[0]], [2, data[1]], ...]
+ for (i=0, l=data.length; i<l; i++) {
+ if (dir == 'vertical') {
+ temp.push([start + i, data[i]]);
+ }
+ else {
+ temp.push([data[i], start+i]);
+ }
+ }
+ }
+ else {
+ // we have a properly formatted data series, copy it.
+ $.extend(true, temp, data);
+ }
+ return temp;
+ };
+
+ var colorIndex = 0;
+ this.series = [];
+ for (var i=0; i<this.data.length; i++) {
+ var sopts = $.extend(true, {index: i}, {seriesColors:this.seriesColors, negativeSeriesColors:this.negativeSeriesColors}, this.options.seriesDefaults, this.options.series[i], {rendererOptions:{animation:{show: this.animate}}});
+ // pass in options in case something needs set prior to initialization.
+ var temp = new Series(sopts);
+ for (var j=0; j<$.jqplot.preParseSeriesOptionsHooks.length; j++) {
+ $.jqplot.preParseSeriesOptionsHooks[j].call(temp, this.options.seriesDefaults, this.options.series[i]);
+ }
+ for (var j=0; j<this.preParseSeriesOptionsHooks.hooks.length; j++) {
+ this.preParseSeriesOptionsHooks.hooks[j].call(temp, this.options.seriesDefaults, this.options.series[i]);
+ }
+ // Now go back and apply the options to the series. Really should just do this during initializaiton, but don't want to
+ // mess up preParseSeriesOptionsHooks at this point.
+ $.extend(true, temp, sopts);
+ var dir = 'vertical';
+ if (temp.renderer === $.jqplot.BarRenderer && temp.rendererOptions && temp.rendererOptions.barDirection == 'horizontal') {
+ dir = 'horizontal';
+ temp._stackAxis = 'x';
+ temp._primaryAxis = '_yaxis';
+ }
+ temp.data = normalizeData(this.data[i], dir, this.defaultAxisStart);
+ switch (temp.xaxis) {
+ case 'xaxis':
+ temp._xaxis = this.axes.xaxis;
+ break;
+ case 'x2axis':
+ temp._xaxis = this.axes.x2axis;
+ break;
+ default:
+ break;
+ }
+ temp._yaxis = this.axes[temp.yaxis];
+ temp._xaxis._series.push(temp);
+ temp._yaxis._series.push(temp);
+ if (temp.show) {
+ temp._xaxis.show = true;
+ temp._yaxis.show = true;
+ }
+ else {
+ if (temp._xaxis.scaleToHiddenSeries) {
+ temp._xaxis.show = true;
+ }
+ if (temp._yaxis.scaleToHiddenSeries) {
+ temp._yaxis.show = true;
+ }
+ }
+
+ // // parse the renderer options and apply default colors if not provided
+ // if (!temp.color && temp.show != false) {
+ // temp.color = cg.next();
+ // colorIndex = cg.getIndex() - 1;;
+ // }
+ // if (!temp.negativeColor && temp.show != false) {
+ // temp.negativeColor = ncg.get(colorIndex);
+ // ncg.setIndex(colorIndex);
+ // }
+ if (!temp.label) {
+ temp.label = 'Series '+ (i+1).toString();
+ }
+ // temp.rendererOptions.show = temp.show;
+ // $.extend(true, temp.renderer, {color:this.seriesColors[i]}, this.rendererOptions);
+ this.series.push(temp);
+ for (var j=0; j<$.jqplot.postParseSeriesOptionsHooks.length; j++) {
+ $.jqplot.postParseSeriesOptionsHooks[j].call(this.series[i], this.options.seriesDefaults, this.options.series[i]);
+ }
+ for (var j=0; j<this.postParseSeriesOptionsHooks.hooks.length; j++) {
+ this.postParseSeriesOptionsHooks.hooks[j].call(this.series[i], this.options.seriesDefaults, this.options.series[i]);
+ }
+ }
+
+ // copy the grid and title options into this object.
+ $.extend(true, this.grid, this.options.grid);
+ // if axis border properties aren't set, set default.
+ for (var i=0, l=_axisNames.length; i<l; i++) {
+ var n = _axisNames[i];
+ var axis = this.axes[n];
+ if (axis.borderWidth == null) {
+ axis.borderWidth =this.grid.borderWidth;
+ }
+ }
+
+ if (typeof this.options.title == 'string') {
+ this.title.text = this.options.title;
+ }
+ else if (typeof this.options.title == 'object') {
+ $.extend(true, this.title, this.options.title);
+ }
+ this.title._plotWidth = this._width;
+ this.legend.setOptions(this.options.legend);
+
+ for (var i=0; i<$.jqplot.postParseOptionsHooks.length; i++) {
+ $.jqplot.postParseOptionsHooks[i].call(this, options);
+ }
+ for (var i=0; i<this.postParseOptionsHooks.hooks.length; i++) {
+ this.postParseOptionsHooks.hooks[i].call(this, options);
+ }
+ };
+
+ // method: destroy
+ // Releases all resources occupied by the plot
+ this.destroy = function() {
+ this.canvasManager.freeAllCanvases();
+ if (this.eventCanvas && this.eventCanvas._elem) {
+ this.eventCanvas._elem.unbind();
+ }
+ // Couple of posts on Stack Overflow indicate that empty() doesn't
+ // always cear up the dom and release memory. Sometimes setting
+ // innerHTML property to null is needed. Particularly on IE, may
+ // have to directly set it to null, bypassing $.
+ this.target.empty();
+
+ this.target[0].innerHTML = '';
+ };
+
+ // method: replot
+ // Does a reinitialization of the plot followed by
+ // a redraw. Method could be used to interactively
+ // change plot characteristics and then replot.
+ //
+ // Parameters:
+ // options - Options used for replotting.
+ //
+ // Properties:
+ // clear - false to not clear (empty) the plot container before replotting (default: true).
+ // resetAxes - true to reset all axes min, max, numberTicks and tickInterval setting so axes will rescale themselves.
+ // optionally pass in list of axes to reset (e.g. ['xaxis', 'y2axis']) (default: false).
+ this.replot = function(options) {
+ var opts = options || {};
+ var data = opts.data || null;
+ var clear = (opts.clear === false) ? false : true;
+ var resetAxes = opts.resetAxes || false;
+ delete opts.data;
+ delete opts.clear;
+ delete opts.resetAxes;
+
+ this.target.trigger('jqplotPreReplot');
+
+ if (clear) {
+ this.destroy();
+ }
+ // if have data or other options, full reinit.
+ // otherwise, quickinit.
+ if (data || !$.isEmptyObject(opts)) {
+ this.reInitialize(data, opts);
+ }
+ else {
+ this.quickInit();
+ }
+
+ if (resetAxes) {
+ this.resetAxesScale(resetAxes, opts.axes);
+ }
+ this.draw();
+ this.target.trigger('jqplotPostReplot');
+ };
+
+ // method: redraw
+ // Empties the plot target div and redraws the plot.
+ // This enables plot data and properties to be changed
+ // and then to comletely clear the plot and redraw.
+ // redraw *will not* reinitialize any plot elements.
+ // That is, axes will not be autoscaled and defaults
+ // will not be reapplied to any plot elements. redraw
+ // is used primarily with zooming.
+ //
+ // Parameters:
+ // clear - false to not clear (empty) the plot container before redrawing (default: true).
+ this.redraw = function(clear) {
+ clear = (clear != null) ? clear : true;
+ this.target.trigger('jqplotPreRedraw');
+ if (clear) {
+ this.canvasManager.freeAllCanvases();
+ this.eventCanvas._elem.unbind();
+ // Dont think I bind any events to the target, this shouldn't be necessary.
+ // It will remove user's events.
+ // this.target.unbind();
+ this.target.empty();
+ }
+ for (var ax in this.axes) {
+ this.axes[ax]._ticks = [];
+ }
+ this.computePlotData();
+ // for (var i=0; i<this.series.length; i++) {
+ // this.populatePlotData(this.series[i], i);
+ // }
+ this._sumy = 0;
+ this._sumx = 0;
+ for (var i=0, tsl = this.series.length; i<tsl; i++) {
+ this._sumy += this.series[i]._sumy;
+ this._sumx += this.series[i]._sumx;
+ }
+ this.draw();
+ this.target.trigger('jqplotPostRedraw');
+ };
+
+ // method: draw
+ // Draws all elements of the plot into the container.
+ // Does not clear the container before drawing.
+ this.draw = function(){
+ if (this.drawIfHidden || this.target.is(':visible')) {
+ this.target.trigger('jqplotPreDraw');
+ var i,
+ j,
+ l,
+ tempseries;
+ for (i=0, l=$.jqplot.preDrawHooks.length; i<l; i++) {
+ $.jqplot.preDrawHooks[i].call(this);
+ }
+ for (i=0, l=this.preDrawHooks.hooks.length; i<l; i++) {
+ this.preDrawHooks.hooks[i].apply(this, this.preDrawSeriesHooks.args[i]);
+ }
+ // create an underlying canvas to be used for special features.
+ this.target.append(this.baseCanvas.createElement({left:0, right:0, top:0, bottom:0}, 'jqplot-base-canvas', null, this));
+ this.baseCanvas.setContext();
+ this.target.append(this.title.draw());
+ this.title.pack({top:0, left:0});
+
+ // make room for the legend between the grid and the edge.
+ // pass a dummy offsets object and a reference to the plot.
+ var legendElem = this.legend.draw({}, this);
+
+ var gridPadding = {top:0, left:0, bottom:0, right:0};
+
+ if (this.legend.placement == "outsideGrid") {
+ // temporarily append the legend to get dimensions
+ this.target.append(legendElem);
+ switch (this.legend.location) {
+ case 'n':
+ gridPadding.top += this.legend.getHeight();
+ break;
+ case 's':
+ gridPadding.bottom += this.legend.getHeight();
+ break;
+ case 'ne':
+ case 'e':
+ case 'se':
+ gridPadding.right += this.legend.getWidth();
+ break;
+ case 'nw':
+ case 'w':
+ case 'sw':
+ gridPadding.left += this.legend.getWidth();
+ break;
+ default: // same as 'ne'
+ gridPadding.right += this.legend.getWidth();
+ break;
+ }
+ legendElem = legendElem.detach();
+ }
+
+ var ax = this.axes;
+ var name;
+ // draw the yMidAxis first, so xaxis of pyramid chart can adjust itself if needed.
+ for (i=0; i<12; i++) {
+ name = _axisNames[i];
+ this.target.append(ax[name].draw(this.baseCanvas._ctx, this));
+ ax[name].set();
+ }
+ if (ax.yaxis.show) {
+ gridPadding.left += ax.yaxis.getWidth();
+ }
+ var ra = ['y2axis', 'y3axis', 'y4axis', 'y5axis', 'y6axis', 'y7axis', 'y8axis', 'y9axis'];
+ var rapad = [0, 0, 0, 0, 0, 0, 0, 0];
+ var gpr = 0;
+ var n;
+ for (n=0; n<8; n++) {
+ if (ax[ra[n]].show) {
+ gpr += ax[ra[n]].getWidth();
+ rapad[n] = gpr;
+ }
+ }
+ gridPadding.right += gpr;
+ if (ax.x2axis.show) {
+ gridPadding.top += ax.x2axis.getHeight();
+ }
+ if (this.title.show) {
+ gridPadding.top += this.title.getHeight();
+ }
+ if (ax.xaxis.show) {
+ gridPadding.bottom += ax.xaxis.getHeight();
+ }
+
+ // end of gridPadding adjustments.
+
+ // if user passed in gridDimensions option, check against calculated gridPadding
+ if (this.options.gridDimensions && $.isPlainObject(this.options.gridDimensions)) {
+ var gdw = parseInt(this.options.gridDimensions.width, 10) || 0;
+ var gdh = parseInt(this.options.gridDimensions.height, 10) || 0;
+ var widthAdj = (this._width - gridPadding.left - gridPadding.right - gdw)/2;
+ var heightAdj = (this._height - gridPadding.top - gridPadding.bottom - gdh)/2;
+
+ if (heightAdj >= 0 && widthAdj >= 0) {
+ gridPadding.top += heightAdj;
+ gridPadding.bottom += heightAdj;
+ gridPadding.left += widthAdj;
+ gridPadding.right += widthAdj;
+ }
+ }
+ var arr = ['top', 'bottom', 'left', 'right'];
+ for (var n in arr) {
+ if (this._gridPadding[arr[n]] == null && gridPadding[arr[n]] > 0) {
+ this._gridPadding[arr[n]] = gridPadding[arr[n]];
+ }
+ else if (this._gridPadding[arr[n]] == null) {
+ this._gridPadding[arr[n]] = this._defaultGridPadding[arr[n]];
+ }
+ }
+
+ var legendPadding = this._gridPadding;
+
+ if (this.legend.placement === 'outsideGrid') {
+ legendPadding = {top:this.title.getHeight(), left: 0, right: 0, bottom: 0};
+ if (this.legend.location === 's') {
+ legendPadding.left = this._gridPadding.left;
+ legendPadding.right = this._gridPadding.right;
+ }
+ }
+
+ ax.xaxis.pack({position:'absolute', bottom:this._gridPadding.bottom - ax.xaxis.getHeight(), left:0, width:this._width}, {min:this._gridPadding.left, max:this._width - this._gridPadding.right});
+ ax.yaxis.pack({position:'absolute', top:0, left:this._gridPadding.left - ax.yaxis.getWidth(), height:this._height}, {min:this._height - this._gridPadding.bottom, max: this._gridPadding.top});
+ ax.x2axis.pack({position:'absolute', top:this._gridPadding.top - ax.x2axis.getHeight(), left:0, width:this._width}, {min:this._gridPadding.left, max:this._width - this._gridPadding.right});
+ for (i=8; i>0; i--) {
+ ax[ra[i-1]].pack({position:'absolute', top:0, right:this._gridPadding.right - rapad[i-1]}, {min:this._height - this._gridPadding.bottom, max: this._gridPadding.top});
+ }
+ var ltemp = (this._width - this._gridPadding.left - this._gridPadding.right)/2.0 + this._gridPadding.left - ax.yMidAxis.getWidth()/2.0;
+ ax.yMidAxis.pack({position:'absolute', top:0, left:ltemp, zIndex:9, textAlign: 'center'}, {min:this._height - this._gridPadding.bottom, max: this._gridPadding.top});
+
+ this.target.append(this.grid.createElement(this._gridPadding, this));
+ this.grid.draw();
+
+ var series = this.series;
+ var seriesLength = series.length;
+ // put the shadow canvases behind the series canvases so shadows don't overlap on stacked bars.
+ for (i=0, l=seriesLength; i<l; i++) {
+ // draw series in order of stacking. This affects only
+ // order in which canvases are added to dom.
+ j = this.seriesStack[i];
+ this.target.append(series[j].shadowCanvas.createElement(this._gridPadding, 'jqplot-series-shadowCanvas', null, this));
+ series[j].shadowCanvas.setContext();
+ series[j].shadowCanvas._elem.data('seriesIndex', j);
+ }
+
+ for (i=0, l=seriesLength; i<l; i++) {
+ // draw series in order of stacking. This affects only
+ // order in which canvases are added to dom.
+ j = this.seriesStack[i];
+ this.target.append(series[j].canvas.createElement(this._gridPadding, 'jqplot-series-canvas', null, this));
+ series[j].canvas.setContext();
+ series[j].canvas._elem.data('seriesIndex', j);
+ }
+ // Need to use filled canvas to capture events in IE.
+ // Also, canvas seems to block selection of other elements in document on FF.
+ this.target.append(this.eventCanvas.createElement(this._gridPadding, 'jqplot-event-canvas', null, this));
+ this.eventCanvas.setContext();
+ this.eventCanvas._ctx.fillStyle = 'rgba(0,0,0,0)';
+ this.eventCanvas._ctx.fillRect(0,0,this.eventCanvas._ctx.canvas.width, this.eventCanvas._ctx.canvas.height);
+
+ // bind custom event handlers to regular events.
+ this.bindCustomEvents();
+
+ // draw legend before series if the series needs to know the legend dimensions.
+ if (this.legend.preDraw) {
+ this.eventCanvas._elem.before(legendElem);
+ this.legend.pack(legendPadding);
+ if (this.legend._elem) {
+ this.drawSeries({legendInfo:{location:this.legend.location, placement:this.legend.placement, width:this.legend.getWidth(), height:this.legend.getHeight(), xoffset:this.legend.xoffset, yoffset:this.legend.yoffset}});
+ }
+ else {
+ this.drawSeries();
+ }
+ }
+ else { // draw series before legend
+ this.drawSeries();
+ if (seriesLength) {
+ $(series[seriesLength-1].canvas._elem).after(legendElem);
+ }
+ this.legend.pack(legendPadding);
+ }
+
+ // register event listeners on the overlay canvas
+ for (var i=0, l=$.jqplot.eventListenerHooks.length; i<l; i++) {
+ // in the handler, this will refer to the eventCanvas dom element.
+ // make sure there are references back into plot objects.
+ this.eventCanvas._elem.bind($.jqplot.eventListenerHooks[i][0], {plot:this}, $.jqplot.eventListenerHooks[i][1]);
+ }
+
+ // register event listeners on the overlay canvas
+ for (var i=0, l=this.eventListenerHooks.hooks.length; i<l; i++) {
+ // in the handler, this will refer to the eventCanvas dom element.
+ // make sure there are references back into plot objects.
+ this.eventCanvas._elem.bind(this.eventListenerHooks.hooks[i][0], {plot:this}, this.eventListenerHooks.hooks[i][1]);
+ }
+
+ var fb = this.fillBetween;
+ if (fb.fill && fb.series1 !== fb.series2 && fb.series1 < seriesLength && fb.series2 < seriesLength && series[fb.series1]._type === 'line' && series[fb.series2]._type === 'line') {
+ this.doFillBetweenLines();
+ }
+
+ for (var i=0, l=$.jqplot.postDrawHooks.length; i<l; i++) {
+ $.jqplot.postDrawHooks[i].call(this);
+ }
+
+ for (var i=0, l=this.postDrawHooks.hooks.length; i<l; i++) {
+ this.postDrawHooks.hooks[i].apply(this, this.postDrawHooks.args[i]);
+ }
+
+ if (this.target.is(':visible')) {
+ this._drawCount += 1;
+ }
+
+ var temps,
+ tempr,
+ sel,
+ _els;
+ // ughh. ideally would hide all series then show them.
+ for (i=0, l=seriesLength; i<l; i++) {
+ temps = series[i];
+ tempr = temps.renderer;
+ sel = '.jqplot-point-label.jqplot-series-'+i;
+ if (tempr.animation && tempr.animation._supported && tempr.animation.show && (this._drawCount < 2 || this.animateReplot)) {
+ _els = this.target.find(sel);
+ _els.stop(true, true).hide();
+ temps.canvas._elem.stop(true, true).hide();
+ temps.shadowCanvas._elem.stop(true, true).hide();
+ temps.canvas._elem.jqplotEffect('blind', {mode: 'show', direction: tempr.animation.direction}, tempr.animation.speed);
+ temps.shadowCanvas._elem.jqplotEffect('blind', {mode: 'show', direction: tempr.animation.direction}, tempr.animation.speed);
+ _els.fadeIn(tempr.animation.speed*0.8);
+ }
+ }
+ _els = null;
+
+ this.target.trigger('jqplotPostDraw', [this]);
+ }
+ };
+
+ jqPlot.prototype.doFillBetweenLines = function () {
+ var fb = this.fillBetween;
+ var sid1 = fb.series1;
+ var sid2 = fb.series2;
+ // first series should always be lowest index
+ var id1 = (sid1 < sid2) ? sid1 : sid2;
+ var id2 = (sid2 > sid1) ? sid2 : sid1;
+
+ var series1 = this.series[id1];
+ var series2 = this.series[id2];
+
+ if (series2.renderer.smooth) {
+ var tempgd = series2.renderer._smoothedData.slice(0).reverse();
+ }
+ else {
+ var tempgd = series2.gridData.slice(0).reverse();
+ }
+
+ if (series1.renderer.smooth) {
+ var gd = series1.renderer._smoothedData.concat(tempgd);
+ }
+ else {
+ var gd = series1.gridData.concat(tempgd);
+ }
+
+ var color = (fb.color !== null) ? fb.color : this.series[sid1].fillColor;
+ var baseSeries = (fb.baseSeries !== null) ? fb.baseSeries : id1;
+
+ // now apply a fill to the shape on the lower series shadow canvas,
+ // so it is behind both series.
+ var sr = this.series[baseSeries].renderer.shapeRenderer;
+ var opts = {fillStyle: color, fill: true, closePath: true};
+ sr.draw(series1.shadowCanvas._ctx, gd, opts);
+ };
+
+ this.bindCustomEvents = function() {
+ this.eventCanvas._elem.bind('click', {plot:this}, this.onClick);
+ this.eventCanvas._elem.bind('dblclick', {plot:this}, this.onDblClick);
+ this.eventCanvas._elem.bind('mousedown', {plot:this}, this.onMouseDown);
+ this.eventCanvas._elem.bind('mousemove', {plot:this}, this.onMouseMove);
+ this.eventCanvas._elem.bind('mouseenter', {plot:this}, this.onMouseEnter);
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, this.onMouseLeave);
+ if (this.captureRightClick) {
+ this.eventCanvas._elem.bind('mouseup', {plot:this}, this.onRightClick);
+ this.eventCanvas._elem.get(0).oncontextmenu = function() {
+ return false;
+ };
+ }
+ else {
+ this.eventCanvas._elem.bind('mouseup', {plot:this}, this.onMouseUp);
+ }
+ };
+
+ function getEventPosition(ev) {
+ var plot = ev.data.plot;
+ var go = plot.eventCanvas._elem.offset();
+ var gridPos = {x:ev.pageX - go.left, y:ev.pageY - go.top};
+ var dataPos = {xaxis:null, yaxis:null, x2axis:null, y2axis:null, y3axis:null, y4axis:null, y5axis:null, y6axis:null, y7axis:null, y8axis:null, y9axis:null, yMidAxis:null};
+ var an = ['xaxis', 'yaxis', 'x2axis', 'y2axis', 'y3axis', 'y4axis', 'y5axis', 'y6axis', 'y7axis', 'y8axis', 'y9axis', 'yMidAxis'];
+ var ax = plot.axes;
+ var n, axis;
+ for (n=11; n>0; n--) {
+ axis = an[n-1];
+ if (ax[axis].show) {
+ dataPos[axis] = ax[axis].series_p2u(gridPos[axis.charAt(0)]);
+ }
+ }
+
+ return {offsets:go, gridPos:gridPos, dataPos:dataPos};
+ }
+
+
+ // function to check if event location is over a area area
+ function checkIntersection(gridpos, plot) {
+ var series = plot.series;
+ var i, j, k, s, r, x, y, theta, sm, sa, minang, maxang;
+ var d0, d, p, pp, points, bw, hp;
+ var threshold, t;
+ for (k=plot.seriesStack.length-1; k>=0; k--) {
+ i = plot.seriesStack[k];
+ s = series[i];
+ hp = s._highlightThreshold;
+ switch (s.renderer.constructor) {
+ case $.jqplot.BarRenderer:
+ x = gridpos.x;
+ y = gridpos.y;
+ for (j=0; j<s._barPoints.length; j++) {
+ points = s._barPoints[j];
+ p = s.gridData[j];
+ if (x>points[0][0] && x<points[2][0] && y>points[2][1] && y<points[0][1]) {
+ return {seriesIndex:s.index, pointIndex:j, gridData:p, data:s.data[j], points:s._barPoints[j]};
+ }
+ }
+ break;
+ case $.jqplot.PyramidRenderer:
+ x = gridpos.x;
+ y = gridpos.y;
+ for (j=0; j<s._barPoints.length; j++) {
+ points = s._barPoints[j];
+ p = s.gridData[j];
+ if (x > points[0][0] + hp[0][0] && x < points[2][0] + hp[2][0] && y > points[2][1] && y < points[0][1]) {
+ return {seriesIndex:s.index, pointIndex:j, gridData:p, data:s.data[j], points:s._barPoints[j]};
+ }
+ }
+ break;
+
+ case $.jqplot.DonutRenderer:
+ sa = s.startAngle/180*Math.PI;
+ x = gridpos.x - s._center[0];
+ y = gridpos.y - s._center[1];
+ r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+ if (x > 0 && -y >= 0) {
+ theta = 2*Math.PI - Math.atan(-y/x);
+ }
+ else if (x > 0 && -y < 0) {
+ theta = -Math.atan(-y/x);
+ }
+ else if (x < 0) {
+ theta = Math.PI - Math.atan(-y/x);
+ }
+ else if (x == 0 && -y > 0) {
+ theta = 3*Math.PI/2;
+ }
+ else if (x == 0 && -y < 0) {
+ theta = Math.PI/2;
+ }
+ else if (x == 0 && y == 0) {
+ theta = 0;
+ }
+ if (sa) {
+ theta -= sa;
+ if (theta < 0) {
+ theta += 2*Math.PI;
+ }
+ else if (theta > 2*Math.PI) {
+ theta -= 2*Math.PI;
+ }
+ }
+
+ sm = s.sliceMargin/180*Math.PI;
+ if (r < s._radius && r > s._innerRadius) {
+ for (j=0; j<s.gridData.length; j++) {
+ minang = (j>0) ? s.gridData[j-1][1]+sm : sm;
+ maxang = s.gridData[j][1];
+ if (theta > minang && theta < maxang) {
+ return {seriesIndex:s.index, pointIndex:j, gridData:[gridpos.x,gridpos.y], data:s.data[j]};
+ }
+ }
+ }
+ break;
+
+ case $.jqplot.PieRenderer:
+ sa = s.startAngle/180*Math.PI;
+ x = gridpos.x - s._center[0];
+ y = gridpos.y - s._center[1];
+ r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+ if (x > 0 && -y >= 0) {
+ theta = 2*Math.PI - Math.atan(-y/x);
+ }
+ else if (x > 0 && -y < 0) {
+ theta = -Math.atan(-y/x);
+ }
+ else if (x < 0) {
+ theta = Math.PI - Math.atan(-y/x);
+ }
+ else if (x == 0 && -y > 0) {
+ theta = 3*Math.PI/2;
+ }
+ else if (x == 0 && -y < 0) {
+ theta = Math.PI/2;
+ }
+ else if (x == 0 && y == 0) {
+ theta = 0;
+ }
+ if (sa) {
+ theta -= sa;
+ if (theta < 0) {
+ theta += 2*Math.PI;
+ }
+ else if (theta > 2*Math.PI) {
+ theta -= 2*Math.PI;
+ }
+ }
+
+ sm = s.sliceMargin/180*Math.PI;
+ if (r < s._radius) {
+ for (j=0; j<s.gridData.length; j++) {
+ minang = (j>0) ? s.gridData[j-1][1]+sm : sm;
+ maxang = s.gridData[j][1];
+ if (theta > minang && theta < maxang) {
+ return {seriesIndex:s.index, pointIndex:j, gridData:[gridpos.x,gridpos.y], data:s.data[j]};
+ }
+ }
+ }
+ break;
+
+ case $.jqplot.BubbleRenderer:
+ x = gridpos.x;
+ y = gridpos.y;
+ var ret = null;
+
+ if (s.show) {
+ for (var j=0; j<s.gridData.length; j++) {
+ p = s.gridData[j];
+ d = Math.sqrt( (x-p[0]) * (x-p[0]) + (y-p[1]) * (y-p[1]) );
+ if (d <= p[2] && (d <= d0 || d0 == null)) {
+ d0 = d;
+ ret = {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ if (ret != null) {
+ return ret;
+ }
+ }
+ break;
+
+ case $.jqplot.FunnelRenderer:
+ x = gridpos.x;
+ y = gridpos.y;
+ var v = s._vertices,
+ vfirst = v[0],
+ vlast = v[v.length-1],
+ lex,
+ rex,
+ cv;
+
+ // equations of right and left sides, returns x, y values given height of section (y value and 2 points)
+
+ function findedge (l, p1 , p2) {
+ var m = (p1[1] - p2[1])/(p1[0] - p2[0]);
+ var b = p1[1] - m*p1[0];
+ var y = l + p1[1];
+
+ return [(y - b)/m, y];
+ }
+
+ // check each section
+ lex = findedge(y, vfirst[0], vlast[3]);
+ rex = findedge(y, vfirst[1], vlast[2]);
+ for (j=0; j<v.length; j++) {
+ cv = v[j];
+ if (y >= cv[0][1] && y <= cv[3][1] && x >= lex[0] && x <= rex[0]) {
+ return {seriesIndex:s.index, pointIndex:j, gridData:null, data:s.data[j]};
+ }
+ }
+ break;
+
+ case $.jqplot.LineRenderer:
+ x = gridpos.x;
+ y = gridpos.y;
+ r = s.renderer;
+ if (s.show) {
+ if ((s.fill || (s.renderer.bands.show && s.renderer.bands.fill)) && (!plot.plugins.highlighter || !plot.plugins.highlighter.show)) {
+ // first check if it is in bounding box
+ var inside = false;
+ if (x>s._boundingBox[0][0] && x<s._boundingBox[1][0] && y>s._boundingBox[1][1] && y<s._boundingBox[0][1]) {
+ // now check the crossing number
+
+ var numPoints = s._areaPoints.length;
+ var ii;
+ var j = numPoints-1;
+
+ for(var ii=0; ii < numPoints; ii++) {
+ var vertex1 = [s._areaPoints[ii][0], s._areaPoints[ii][1]];
+ var vertex2 = [s._areaPoints[j][0], s._areaPoints[j][1]];
+
+ if (vertex1[1] < y && vertex2[1] >= y || vertex2[1] < y && vertex1[1] >= y) {
+ if (vertex1[0] + (y - vertex1[1]) / (vertex2[1] - vertex1[1]) * (vertex2[0] - vertex1[0]) < x) {
+ inside = !inside;
+ }
+ }
+
+ j = ii;
+ }
+ }
+ if (inside) {
+ return {seriesIndex:i, pointIndex:null, gridData:s.gridData, data:s.data, points:s._areaPoints};
+ }
+ break;
+
+ }
+
+ else {
+ t = s.markerRenderer.size/2+s.neighborThreshold;
+ threshold = (t > 0) ? t : 0;
+ for (var j=0; j<s.gridData.length; j++) {
+ p = s.gridData[j];
+ // neighbor looks different to OHLC chart.
+ if (r.constructor == $.jqplot.OHLCRenderer) {
+ if (r.candleStick) {
+ var yp = s._yaxis.series_u2p;
+ if (x >= p[0]-r._bodyWidth/2 && x <= p[0]+r._bodyWidth/2 && y >= yp(s.data[j][2]) && y <= yp(s.data[j][3])) {
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ // if an open hi low close chart
+ else if (!r.hlc){
+ var yp = s._yaxis.series_u2p;
+ if (x >= p[0]-r._tickLength && x <= p[0]+r._tickLength && y >= yp(s.data[j][2]) && y <= yp(s.data[j][3])) {
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ // a hi low close chart
+ else {
+ var yp = s._yaxis.series_u2p;
+ if (x >= p[0]-r._tickLength && x <= p[0]+r._tickLength && y >= yp(s.data[j][1]) && y <= yp(s.data[j][2])) {
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+
+ }
+ else if (p[0] != null && p[1] != null){
+ d = Math.sqrt( (x-p[0]) * (x-p[0]) + (y-p[1]) * (y-p[1]) );
+ if (d <= threshold && (d <= d0 || d0 == null)) {
+ d0 = d;
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ }
+ }
+ }
+ break;
+
+ default:
+ x = gridpos.x;
+ y = gridpos.y;
+ r = s.renderer;
+ if (s.show) {
+ t = s.markerRenderer.size/2+s.neighborThreshold;
+ threshold = (t > 0) ? t : 0;
+ for (var j=0; j<s.gridData.length; j++) {
+ p = s.gridData[j];
+ // neighbor looks different to OHLC chart.
+ if (r.constructor == $.jqplot.OHLCRenderer) {
+ if (r.candleStick) {
+ var yp = s._yaxis.series_u2p;
+ if (x >= p[0]-r._bodyWidth/2 && x <= p[0]+r._bodyWidth/2 && y >= yp(s.data[j][2]) && y <= yp(s.data[j][3])) {
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ // if an open hi low close chart
+ else if (!r.hlc){
+ var yp = s._yaxis.series_u2p;
+ if (x >= p[0]-r._tickLength && x <= p[0]+r._tickLength && y >= yp(s.data[j][2]) && y <= yp(s.data[j][3])) {
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ // a hi low close chart
+ else {
+ var yp = s._yaxis.series_u2p;
+ if (x >= p[0]-r._tickLength && x <= p[0]+r._tickLength && y >= yp(s.data[j][1]) && y <= yp(s.data[j][2])) {
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+
+ }
+ else {
+ d = Math.sqrt( (x-p[0]) * (x-p[0]) + (y-p[1]) * (y-p[1]) );
+ if (d <= threshold && (d <= d0 || d0 == null)) {
+ d0 = d;
+ return {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]};
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ return null;
+ }
+
+
+
+ this.onClick = function(ev) {
+ // Event passed in is normalized and will have data attribute.
+ // Event passed out is unnormalized.
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var neighbor = checkIntersection(positions.gridPos, p);
+ var evt = $.Event('jqplotClick');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, neighbor, p]);
+ };
+
+ this.onDblClick = function(ev) {
+ // Event passed in is normalized and will have data attribute.
+ // Event passed out is unnormalized.
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var neighbor = checkIntersection(positions.gridPos, p);
+ var evt = $.Event('jqplotDblClick');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, neighbor, p]);
+ };
+
+ this.onMouseDown = function(ev) {
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var neighbor = checkIntersection(positions.gridPos, p);
+ var evt = $.Event('jqplotMouseDown');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, neighbor, p]);
+ };
+
+ this.onMouseUp = function(ev) {
+ var positions = getEventPosition(ev);
+ var evt = $.Event('jqplotMouseUp');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, null, ev.data.plot]);
+ };
+
+ this.onRightClick = function(ev) {
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var neighbor = checkIntersection(positions.gridPos, p);
+ if (p.captureRightClick) {
+ if (ev.which == 3) {
+ var evt = $.Event('jqplotRightClick');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, neighbor, p]);
+ }
+ else {
+ var evt = $.Event('jqplotMouseUp');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, neighbor, p]);
+ }
+ }
+ };
+
+ this.onMouseMove = function(ev) {
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var neighbor = checkIntersection(positions.gridPos, p);
+ var evt = $.Event('jqplotMouseMove');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, neighbor, p]);
+ };
+
+ this.onMouseEnter = function(ev) {
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var evt = $.Event('jqplotMouseEnter');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ evt.relatedTarget = ev.relatedTarget;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, null, p]);
+ };
+
+ this.onMouseLeave = function(ev) {
+ var positions = getEventPosition(ev);
+ var p = ev.data.plot;
+ var evt = $.Event('jqplotMouseLeave');
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ evt.relatedTarget = ev.relatedTarget;
+ $(this).trigger(evt, [positions.gridPos, positions.dataPos, null, p]);
+ };
+
+ // method: drawSeries
+ // Redraws all or just one series on the plot. No axis scaling
+ // is performed and no other elements on the plot are redrawn.
+ // options is an options object to pass on to the series renderers.
+ // It can be an empty object {}. idx is the series index
+ // to redraw if only one series is to be redrawn.
+ this.drawSeries = function(options, idx){
+ var i, series, ctx;
+ // if only one argument passed in and it is a number, use it ad idx.
+ idx = (typeof(options) === "number" && idx == null) ? options : idx;
+ options = (typeof(options) === "object") ? options : {};
+ // draw specified series
+ if (idx != undefined) {
+ series = this.series[idx];
+ ctx = series.shadowCanvas._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ series.drawShadow(ctx, options, this);
+ ctx = series.canvas._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ series.draw(ctx, options, this);
+ if (series.renderer.constructor == $.jqplot.BezierCurveRenderer) {
+ if (idx < this.series.length - 1) {
+ this.drawSeries(idx+1);
+ }
+ }
+ }
+
+ else {
+ // if call series drawShadow method first, in case all series shadows
+ // should be drawn before any series. This will ensure, like for
+ // stacked bar plots, that shadows don't overlap series.
+ for (i=0; i<this.series.length; i++) {
+ // first clear the canvas
+ series = this.series[i];
+ ctx = series.shadowCanvas._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ series.drawShadow(ctx, options, this);
+ ctx = series.canvas._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ series.draw(ctx, options, this);
+ }
+ }
+ options = idx = i = series = ctx = null;
+ };
+
+ // method: moveSeriesToFront
+ // This method requires jQuery 1.4+
+ // Moves the specified series canvas in front of all other series canvases.
+ // This effectively "draws" the specified series on top of all other series,
+ // although it is performed through DOM manipulation, no redrawing is performed.
+ //
+ // Parameters:
+ // idx - 0 based index of the series to move. This will be the index of the series
+ // as it was first passed into the jqplot function.
+ this.moveSeriesToFront = function (idx) {
+ idx = parseInt(idx, 10);
+ var stackIndex = $.inArray(idx, this.seriesStack);
+ // if already in front, return
+ if (stackIndex == -1) {
+ return;
+ }
+ if (stackIndex == this.seriesStack.length -1) {
+ this.previousSeriesStack = this.seriesStack.slice(0);
+ return;
+ }
+ var opidx = this.seriesStack[this.seriesStack.length -1];
+ var serelem = this.series[idx].canvas._elem.detach();
+ var shadelem = this.series[idx].shadowCanvas._elem.detach();
+ this.series[opidx].shadowCanvas._elem.after(shadelem);
+ this.series[opidx].canvas._elem.after(serelem);
+ this.previousSeriesStack = this.seriesStack.slice(0);
+ this.seriesStack.splice(stackIndex, 1);
+ this.seriesStack.push(idx);
+ };
+
+ // method: moveSeriesToBack
+ // This method requires jQuery 1.4+
+ // Moves the specified series canvas behind all other series canvases.
+ //
+ // Parameters:
+ // idx - 0 based index of the series to move. This will be the index of the series
+ // as it was first passed into the jqplot function.
+ this.moveSeriesToBack = function (idx) {
+ idx = parseInt(idx, 10);
+ var stackIndex = $.inArray(idx, this.seriesStack);
+ // if already in back, return
+ if (stackIndex == 0 || stackIndex == -1) {
+ return;
+ }
+ var opidx = this.seriesStack[0];
+ var serelem = this.series[idx].canvas._elem.detach();
+ var shadelem = this.series[idx].shadowCanvas._elem.detach();
+ this.series[opidx].shadowCanvas._elem.before(shadelem);
+ this.series[opidx].canvas._elem.before(serelem);
+ this.previousSeriesStack = this.seriesStack.slice(0);
+ this.seriesStack.splice(stackIndex, 1);
+ this.seriesStack.unshift(idx);
+ };
+
+ // method: restorePreviousSeriesOrder
+ // This method requires jQuery 1.4+
+ // Restore the series canvas order to its previous state.
+ // Useful to put a series back where it belongs after moving
+ // it to the front.
+ this.restorePreviousSeriesOrder = function () {
+ var i, j, serelem, shadelem, temp, move, keep;
+ // if no change, return.
+ if (this.seriesStack == this.previousSeriesStack) {
+ return;
+ }
+ for (i=1; i<this.previousSeriesStack.length; i++) {
+ move = this.previousSeriesStack[i];
+ keep = this.previousSeriesStack[i-1];
+ serelem = this.series[move].canvas._elem.detach();
+ shadelem = this.series[move].shadowCanvas._elem.detach();
+ this.series[keep].shadowCanvas._elem.after(shadelem);
+ this.series[keep].canvas._elem.after(serelem);
+ }
+ temp = this.seriesStack.slice(0);
+ this.seriesStack = this.previousSeriesStack.slice(0);
+ this.previousSeriesStack = temp;
+ };
+
+ // method: restoreOriginalSeriesOrder
+ // This method requires jQuery 1.4+
+ // Restore the series canvas order to its original order
+ // when the plot was created.
+ this.restoreOriginalSeriesOrder = function () {
+ var i, j, arr=[], serelem, shadelem;
+ for (i=0; i<this.series.length; i++) {
+ arr.push(i);
+ }
+ if (this.seriesStack == arr) {
+ return;
+ }
+ this.previousSeriesStack = this.seriesStack.slice(0);
+ this.seriesStack = arr;
+ for (i=1; i<this.seriesStack.length; i++) {
+ serelem = this.series[i].canvas._elem.detach();
+ shadelem = this.series[i].shadowCanvas._elem.detach();
+ this.series[i-1].shadowCanvas._elem.after(shadelem);
+ this.series[i-1].canvas._elem.after(serelem);
+ }
+ };
+
+ this.activateTheme = function (name) {
+ this.themeEngine.activate(this, name);
+ };
+ }
+
+
+ // conpute a highlight color or array of highlight colors from given colors.
+ $.jqplot.computeHighlightColors = function(colors) {
+ var ret;
+ if ($.isArray(colors)) {
+ ret = [];
+ for (var i=0; i<colors.length; i++){
+ var rgba = $.jqplot.getColorComponents(colors[i]);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ newrgb[j] = (sum > 660) ? newrgb[j] * 0.85 : 0.73 * newrgb[j] + 90;
+ newrgb[j] = parseInt(newrgb[j], 10);
+ (newrgb[j] > 255) ? 255 : newrgb[j];
+ }
+ // newrgb[3] = (rgba[3] > 0.4) ? rgba[3] * 0.4 : rgba[3] * 1.5;
+ // newrgb[3] = (rgba[3] > 0.5) ? 0.8 * rgba[3] - .1 : rgba[3] + 0.2;
+ newrgb[3] = 0.3 + 0.35 * rgba[3];
+ ret.push('rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+','+newrgb[3]+')');
+ }
+ }
+ else {
+ var rgba = $.jqplot.getColorComponents(colors);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ // newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
+ // newrgb[j] = parseInt(newrgb[j], 10);
+ newrgb[j] = (sum > 660) ? newrgb[j] * 0.85 : 0.73 * newrgb[j] + 90;
+ newrgb[j] = parseInt(newrgb[j], 10);
+ (newrgb[j] > 255) ? 255 : newrgb[j];
+ }
+ // newrgb[3] = (rgba[3] > 0.4) ? rgba[3] * 0.4 : rgba[3] * 1.5;
+ // newrgb[3] = (rgba[3] > 0.5) ? 0.8 * rgba[3] - .1 : rgba[3] + 0.2;
+ newrgb[3] = 0.3 + 0.35 * rgba[3];
+ ret = 'rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+','+newrgb[3]+')';
+ }
+ return ret;
+ };
+
+ $.jqplot.ColorGenerator = function(colors) {
+ colors = colors || $.jqplot.config.defaultColors;
+ var idx = 0;
+
+ this.next = function () {
+ if (idx < colors.length) {
+ return colors[idx++];
+ }
+ else {
+ idx = 0;
+ return colors[idx++];
+ }
+ };
+
+ this.previous = function () {
+ if (idx > 0) {
+ return colors[idx--];
+ }
+ else {
+ idx = colors.length-1;
+ return colors[idx];
+ }
+ };
+
+ // get a color by index without advancing pointer.
+ this.get = function(i) {
+ var idx = i - colors.length * Math.floor(i/colors.length);
+ return colors[idx];
+ };
+
+ this.setColors = function(c) {
+ colors = c;
+ };
+
+ this.reset = function() {
+ idx = 0;
+ };
+
+ this.getIndex = function() {
+ return idx;
+ };
+
+ this.setIndex = function(index) {
+ idx = index;
+ };
+ };
+
+ // convert a hex color string to rgb string.
+ // h - 3 or 6 character hex string, with or without leading #
+ // a - optional alpha
+ $.jqplot.hex2rgb = function(h, a) {
+ h = h.replace('#', '');
+ if (h.length == 3) {
+ h = h.charAt(0)+h.charAt(0)+h.charAt(1)+h.charAt(1)+h.charAt(2)+h.charAt(2);
+ }
+ var rgb;
+ rgb = 'rgba('+parseInt(h.slice(0,2), 16)+', '+parseInt(h.slice(2,4), 16)+', '+parseInt(h.slice(4,6), 16);
+ if (a) {
+ rgb += ', '+a;
+ }
+ rgb += ')';
+ return rgb;
+ };
+
+ // convert an rgb color spec to a hex spec. ignore any alpha specification.
+ $.jqplot.rgb2hex = function(s) {
+ var pat = /rgba?\( *([0-9]{1,3}\.?[0-9]*%?) *, *([0-9]{1,3}\.?[0-9]*%?) *, *([0-9]{1,3}\.?[0-9]*%?) *(?:, *[0-9.]*)?\)/;
+ var m = s.match(pat);
+ var h = '#';
+ for (var i=1; i<4; i++) {
+ var temp;
+ if (m[i].search(/%/) != -1) {
+ temp = parseInt(255*m[i]/100, 10).toString(16);
+ if (temp.length == 1) {
+ temp = '0'+temp;
+ }
+ }
+ else {
+ temp = parseInt(m[i], 10).toString(16);
+ if (temp.length == 1) {
+ temp = '0'+temp;
+ }
+ }
+ h += temp;
+ }
+ return h;
+ };
+
+ // given a css color spec, return an rgb css color spec
+ $.jqplot.normalize2rgb = function(s, a) {
+ if (s.search(/^ *rgba?\(/) != -1) {
+ return s;
+ }
+ else if (s.search(/^ *#?[0-9a-fA-F]?[0-9a-fA-F]/) != -1) {
+ return $.jqplot.hex2rgb(s, a);
+ }
+ else {
+ throw new Error('Invalid color spec');
+ }
+ };
+
+ // extract the r, g, b, a color components out of a css color spec.
+ $.jqplot.getColorComponents = function(s) {
+ // check to see if a color keyword.
+ s = $.jqplot.colorKeywordMap[s] || s;
+ var rgb = $.jqplot.normalize2rgb(s);
+ var pat = /rgba?\( *([0-9]{1,3}\.?[0-9]*%?) *, *([0-9]{1,3}\.?[0-9]*%?) *, *([0-9]{1,3}\.?[0-9]*%?) *,? *([0-9.]* *)?\)/;
+ var m = rgb.match(pat);
+ var ret = [];
+ for (var i=1; i<4; i++) {
+ if (m[i].search(/%/) != -1) {
+ ret[i-1] = parseInt(255*m[i]/100, 10);
+ }
+ else {
+ ret[i-1] = parseInt(m[i], 10);
+ }
+ }
+ ret[3] = parseFloat(m[4]) ? parseFloat(m[4]) : 1.0;
+ return ret;
+ };
+
+ $.jqplot.colorKeywordMap = {
+ aliceblue: 'rgb(240, 248, 255)',
+ antiquewhite: 'rgb(250, 235, 215)',
+ aqua: 'rgb( 0, 255, 255)',
+ aquamarine: 'rgb(127, 255, 212)',
+ azure: 'rgb(240, 255, 255)',
+ beige: 'rgb(245, 245, 220)',
+ bisque: 'rgb(255, 228, 196)',
+ black: 'rgb( 0, 0, 0)',
+ blanchedalmond: 'rgb(255, 235, 205)',
+ blue: 'rgb( 0, 0, 255)',
+ blueviolet: 'rgb(138, 43, 226)',
+ brown: 'rgb(165, 42, 42)',
+ burlywood: 'rgb(222, 184, 135)',
+ cadetblue: 'rgb( 95, 158, 160)',
+ chartreuse: 'rgb(127, 255, 0)',
+ chocolate: 'rgb(210, 105, 30)',
+ coral: 'rgb(255, 127, 80)',
+ cornflowerblue: 'rgb(100, 149, 237)',
+ cornsilk: 'rgb(255, 248, 220)',
+ crimson: 'rgb(220, 20, 60)',
+ cyan: 'rgb( 0, 255, 255)',
+ darkblue: 'rgb( 0, 0, 139)',
+ darkcyan: 'rgb( 0, 139, 139)',
+ darkgoldenrod: 'rgb(184, 134, 11)',
+ darkgray: 'rgb(169, 169, 169)',
+ darkgreen: 'rgb( 0, 100, 0)',
+ darkgrey: 'rgb(169, 169, 169)',
+ darkkhaki: 'rgb(189, 183, 107)',
+ darkmagenta: 'rgb(139, 0, 139)',
+ darkolivegreen: 'rgb( 85, 107, 47)',
+ darkorange: 'rgb(255, 140, 0)',
+ darkorchid: 'rgb(153, 50, 204)',
+ darkred: 'rgb(139, 0, 0)',
+ darksalmon: 'rgb(233, 150, 122)',
+ darkseagreen: 'rgb(143, 188, 143)',
+ darkslateblue: 'rgb( 72, 61, 139)',
+ darkslategray: 'rgb( 47, 79, 79)',
+ darkslategrey: 'rgb( 47, 79, 79)',
+ darkturquoise: 'rgb( 0, 206, 209)',
+ darkviolet: 'rgb(148, 0, 211)',
+ deeppink: 'rgb(255, 20, 147)',
+ deepskyblue: 'rgb( 0, 191, 255)',
+ dimgray: 'rgb(105, 105, 105)',
+ dimgrey: 'rgb(105, 105, 105)',
+ dodgerblue: 'rgb( 30, 144, 255)',
+ firebrick: 'rgb(178, 34, 34)',
+ floralwhite: 'rgb(255, 250, 240)',
+ forestgreen: 'rgb( 34, 139, 34)',
+ fuchsia: 'rgb(255, 0, 255)',
+ gainsboro: 'rgb(220, 220, 220)',
+ ghostwhite: 'rgb(248, 248, 255)',
+ gold: 'rgb(255, 215, 0)',
+ goldenrod: 'rgb(218, 165, 32)',
+ gray: 'rgb(128, 128, 128)',
+ grey: 'rgb(128, 128, 128)',
+ green: 'rgb( 0, 128, 0)',
+ greenyellow: 'rgb(173, 255, 47)',
+ honeydew: 'rgb(240, 255, 240)',
+ hotpink: 'rgb(255, 105, 180)',
+ indianred: 'rgb(205, 92, 92)',
+ indigo: 'rgb( 75, 0, 130)',
+ ivory: 'rgb(255, 255, 240)',
+ khaki: 'rgb(240, 230, 140)',
+ lavender: 'rgb(230, 230, 250)',
+ lavenderblush: 'rgb(255, 240, 245)',
+ lawngreen: 'rgb(124, 252, 0)',
+ lemonchiffon: 'rgb(255, 250, 205)',
+ lightblue: 'rgb(173, 216, 230)',
+ lightcoral: 'rgb(240, 128, 128)',
+ lightcyan: 'rgb(224, 255, 255)',
+ lightgoldenrodyellow: 'rgb(250, 250, 210)',
+ lightgray: 'rgb(211, 211, 211)',
+ lightgreen: 'rgb(144, 238, 144)',
+ lightgrey: 'rgb(211, 211, 211)',
+ lightpink: 'rgb(255, 182, 193)',
+ lightsalmon: 'rgb(255, 160, 122)',
+ lightseagreen: 'rgb( 32, 178, 170)',
+ lightskyblue: 'rgb(135, 206, 250)',
+ lightslategray: 'rgb(119, 136, 153)',
+ lightslategrey: 'rgb(119, 136, 153)',
+ lightsteelblue: 'rgb(176, 196, 222)',
+ lightyellow: 'rgb(255, 255, 224)',
+ lime: 'rgb( 0, 255, 0)',
+ limegreen: 'rgb( 50, 205, 50)',
+ linen: 'rgb(250, 240, 230)',
+ magenta: 'rgb(255, 0, 255)',
+ maroon: 'rgb(128, 0, 0)',
+ mediumaquamarine: 'rgb(102, 205, 170)',
+ mediumblue: 'rgb( 0, 0, 205)',
+ mediumorchid: 'rgb(186, 85, 211)',
+ mediumpurple: 'rgb(147, 112, 219)',
+ mediumseagreen: 'rgb( 60, 179, 113)',
+ mediumslateblue: 'rgb(123, 104, 238)',
+ mediumspringgreen: 'rgb( 0, 250, 154)',
+ mediumturquoise: 'rgb( 72, 209, 204)',
+ mediumvioletred: 'rgb(199, 21, 133)',
+ midnightblue: 'rgb( 25, 25, 112)',
+ mintcream: 'rgb(245, 255, 250)',
+ mistyrose: 'rgb(255, 228, 225)',
+ moccasin: 'rgb(255, 228, 181)',
+ navajowhite: 'rgb(255, 222, 173)',
+ navy: 'rgb( 0, 0, 128)',
+ oldlace: 'rgb(253, 245, 230)',
+ olive: 'rgb(128, 128, 0)',
+ olivedrab: 'rgb(107, 142, 35)',
+ orange: 'rgb(255, 165, 0)',
+ orangered: 'rgb(255, 69, 0)',
+ orchid: 'rgb(218, 112, 214)',
+ palegoldenrod: 'rgb(238, 232, 170)',
+ palegreen: 'rgb(152, 251, 152)',
+ paleturquoise: 'rgb(175, 238, 238)',
+ palevioletred: 'rgb(219, 112, 147)',
+ papayawhip: 'rgb(255, 239, 213)',
+ peachpuff: 'rgb(255, 218, 185)',
+ peru: 'rgb(205, 133, 63)',
+ pink: 'rgb(255, 192, 203)',
+ plum: 'rgb(221, 160, 221)',
+ powderblue: 'rgb(176, 224, 230)',
+ purple: 'rgb(128, 0, 128)',
+ red: 'rgb(255, 0, 0)',
+ rosybrown: 'rgb(188, 143, 143)',
+ royalblue: 'rgb( 65, 105, 225)',
+ saddlebrown: 'rgb(139, 69, 19)',
+ salmon: 'rgb(250, 128, 114)',
+ sandybrown: 'rgb(244, 164, 96)',
+ seagreen: 'rgb( 46, 139, 87)',
+ seashell: 'rgb(255, 245, 238)',
+ sienna: 'rgb(160, 82, 45)',
+ silver: 'rgb(192, 192, 192)',
+ skyblue: 'rgb(135, 206, 235)',
+ slateblue: 'rgb(106, 90, 205)',
+ slategray: 'rgb(112, 128, 144)',
+ slategrey: 'rgb(112, 128, 144)',
+ snow: 'rgb(255, 250, 250)',
+ springgreen: 'rgb( 0, 255, 127)',
+ steelblue: 'rgb( 70, 130, 180)',
+ tan: 'rgb(210, 180, 140)',
+ teal: 'rgb( 0, 128, 128)',
+ thistle: 'rgb(216, 191, 216)',
+ tomato: 'rgb(255, 99, 71)',
+ turquoise: 'rgb( 64, 224, 208)',
+ violet: 'rgb(238, 130, 238)',
+ wheat: 'rgb(245, 222, 179)',
+ white: 'rgb(255, 255, 255)',
+ whitesmoke: 'rgb(245, 245, 245)',
+ yellow: 'rgb(255, 255, 0)',
+ yellowgreen: 'rgb(154, 205, 50)'
+ };
+
+
+
+
+ // class: $.jqplot.AxisLabelRenderer
+ // Renderer to place labels on the axes.
+ $.jqplot.AxisLabelRenderer = function(options) {
+ // Group: Properties
+ $.jqplot.ElemContainer.call(this);
+ // name of the axis associated with this tick
+ this.axis;
+ // prop: show
+ // whether or not to show the tick (mark and label).
+ this.show = true;
+ // prop: label
+ // The text or html for the label.
+ this.label = '';
+ this.fontFamily = null;
+ this.fontSize = null;
+ this.textColor = null;
+ this._elem;
+ // prop: escapeHTML
+ // true to escape HTML entities in the label.
+ this.escapeHTML = false;
+
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.AxisLabelRenderer.prototype = new $.jqplot.ElemContainer();
+ $.jqplot.AxisLabelRenderer.prototype.constructor = $.jqplot.AxisLabelRenderer;
+
+ $.jqplot.AxisLabelRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.AxisLabelRenderer.prototype.draw = function(ctx, plot) {
+ // Memory Leaks patch
+ if (this._elem) {
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ this._elem = $('<div style="position:absolute;" class="jqplot-'+this.axis+'-label"></div>');
+
+ if (Number(this.label)) {
+ this._elem.css('white-space', 'nowrap');
+ }
+
+ if (!this.escapeHTML) {
+ this._elem.html(this.label);
+ }
+ else {
+ this._elem.text(this.label);
+ }
+ if (this.fontFamily) {
+ this._elem.css('font-family', this.fontFamily);
+ }
+ if (this.fontSize) {
+ this._elem.css('font-size', this.fontSize);
+ }
+ if (this.textColor) {
+ this._elem.css('color', this.textColor);
+ }
+
+ return this._elem;
+ };
+
+ $.jqplot.AxisLabelRenderer.prototype.pack = function() {
+ };
+
+ // class: $.jqplot.AxisTickRenderer
+ // A "tick" object showing the value of a tick/gridline on the plot.
+ $.jqplot.AxisTickRenderer = function(options) {
+ // Group: Properties
+ $.jqplot.ElemContainer.call(this);
+ // prop: mark
+ // tick mark on the axis. One of 'inside', 'outside', 'cross', '' or null.
+ this.mark = 'outside';
+ // name of the axis associated with this tick
+ this.axis;
+ // prop: showMark
+ // whether or not to show the mark on the axis.
+ this.showMark = true;
+ // prop: showGridline
+ // whether or not to draw the gridline on the grid at this tick.
+ this.showGridline = true;
+ // prop: isMinorTick
+ // if this is a minor tick.
+ this.isMinorTick = false;
+ // prop: size
+ // Length of the tick beyond the grid in pixels.
+ // DEPRECATED: This has been superceeded by markSize
+ this.size = 4;
+ // prop: markSize
+ // Length of the tick marks in pixels. For 'cross' style, length
+ // will be stoked above and below axis, so total length will be twice this.
+ this.markSize = 6;
+ // prop: show
+ // whether or not to show the tick (mark and label).
+ // Setting this to false requires more testing. It is recommended
+ // to set showLabel and showMark to false instead.
+ this.show = true;
+ // prop: showLabel
+ // whether or not to show the label.
+ this.showLabel = true;
+ this.label = null;
+ this.value = null;
+ this._styles = {};
+ // prop: formatter
+ // A class of a formatter for the tick text. sprintf by default.
+ this.formatter = $.jqplot.DefaultTickFormatter;
+ // prop: prefix
+ // String to prepend to the tick label.
+ // Prefix is prepended to the formatted tick label.
+ this.prefix = '';
+ // prop: suffix
+ // String to append to the tick label.
+ // Suffix is appended to the formatted tick label.
+ this.suffix = '';
+ // prop: formatString
+ // string passed to the formatter.
+ this.formatString = '';
+ // prop: fontFamily
+ // css spec for the font-family css attribute.
+ this.fontFamily;
+ // prop: fontSize
+ // css spec for the font-size css attribute.
+ this.fontSize;
+ // prop: textColor
+ // css spec for the color attribute.
+ this.textColor;
+ // prop: escapeHTML
+ // true to escape HTML entities in the label.
+ this.escapeHTML = false;
+ this._elem;
+ this._breakTick = false;
+
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.AxisTickRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.AxisTickRenderer.prototype = new $.jqplot.ElemContainer();
+ $.jqplot.AxisTickRenderer.prototype.constructor = $.jqplot.AxisTickRenderer;
+
+ $.jqplot.AxisTickRenderer.prototype.setTick = function(value, axisName, isMinor) {
+ this.value = value;
+ this.axis = axisName;
+ if (isMinor) {
+ this.isMinorTick = true;
+ }
+ return this;
+ };
+
+ $.jqplot.AxisTickRenderer.prototype.draw = function() {
+ if (this.label === null) {
+ this.label = this.prefix + this.formatter(this.formatString, this.value) + this.suffix;
+ }
+ var style = {position: 'absolute'};
+ if (Number(this.label)) {
+ style['whitSpace'] = 'nowrap';
+ }
+
+ // Memory Leaks patch
+ if (this._elem) {
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ this._elem = $(document.createElement('div'));
+ this._elem.addClass("jqplot-"+this.axis+"-tick");
+
+ if (!this.escapeHTML) {
+ this._elem.html(this.label);
+ }
+ else {
+ this._elem.text(this.label);
+ }
+
+ this._elem.css(style);
+
+ for (var s in this._styles) {
+ this._elem.css(s, this._styles[s]);
+ }
+ if (this.fontFamily) {
+ this._elem.css('font-family', this.fontFamily);
+ }
+ if (this.fontSize) {
+ this._elem.css('font-size', this.fontSize);
+ }
+ if (this.textColor) {
+ this._elem.css('color', this.textColor);
+ }
+ if (this._breakTick) {
+ this._elem.addClass('jqplot-breakTick');
+ }
+
+ return this._elem;
+ };
+
+ $.jqplot.DefaultTickFormatter = function (format, val) {
+ if (typeof val == 'number') {
+ if (!format) {
+ format = $.jqplot.config.defaultTickFormatString;
+ }
+ return $.jqplot.sprintf(format, val);
+ }
+ else {
+ return String(val);
+ }
+ };
+
+ $.jqplot.PercentTickFormatter = function (format, val) {
+ if (typeof val == 'number') {
+ val = 100 * val;
+ if (!format) {
+ format = $.jqplot.config.defaultTickFormatString;
+ }
+ return $.jqplot.sprintf(format, val);
+ }
+ else {
+ return String(val);
+ }
+ };
+
+ $.jqplot.AxisTickRenderer.prototype.pack = function() {
+ };
+
+ // Class: $.jqplot.CanvasGridRenderer
+ // The default jqPlot grid renderer, creating a grid on a canvas element.
+ // The renderer has no additional options beyond the <Grid> class.
+ $.jqplot.CanvasGridRenderer = function(){
+ this.shadowRenderer = new $.jqplot.ShadowRenderer();
+ };
+
+ // called with context of Grid object
+ $.jqplot.CanvasGridRenderer.prototype.init = function(options) {
+ this._ctx;
+ $.extend(true, this, options);
+ // set the shadow renderer options
+ var sopts = {lineJoin:'miter', lineCap:'round', fill:false, isarc:false, angle:this.shadowAngle, offset:this.shadowOffset, alpha:this.shadowAlpha, depth:this.shadowDepth, lineWidth:this.shadowWidth, closePath:false, strokeStyle:this.shadowColor};
+ this.renderer.shadowRenderer.init(sopts);
+ };
+
+ // called with context of Grid.
+ $.jqplot.CanvasGridRenderer.prototype.createElement = function(plot) {
+ var elem;
+ // Memory Leaks patch
+ if (this._elem) {
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ elem = this._elem.get(0);
+ window.G_vmlCanvasManager.uninitElement(elem);
+ elem = null;
+ }
+
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ elem = plot.canvasManager.getCanvas();
+
+ var w = this._plotDimensions.width;
+ var h = this._plotDimensions.height;
+ elem.width = w;
+ elem.height = h;
+ this._elem = $(elem);
+ this._elem.addClass('jqplot-grid-canvas');
+ this._elem.css({ position: 'absolute', left: 0, top: 0 });
+
+ elem = plot.canvasManager.initCanvas(elem);
+
+ this._top = this._offsets.top;
+ this._bottom = h - this._offsets.bottom;
+ this._left = this._offsets.left;
+ this._right = w - this._offsets.right;
+ this._width = this._right - this._left;
+ this._height = this._bottom - this._top;
+ // avoid memory leak
+ elem = null;
+ return this._elem;
+ };
+
+ $.jqplot.CanvasGridRenderer.prototype.draw = function() {
+ this._ctx = this._elem.get(0).getContext("2d");
+ var ctx = this._ctx;
+ var axes = this._axes;
+ // Add the grid onto the grid canvas. This is the bottom most layer.
+ ctx.save();
+ ctx.clearRect(0, 0, this._plotDimensions.width, this._plotDimensions.height);
+ ctx.fillStyle = this.backgroundColor || this.background;
+ ctx.fillRect(this._left, this._top, this._width, this._height);
+
+ ctx.save();
+ ctx.lineJoin = 'miter';
+ ctx.lineCap = 'butt';
+ ctx.lineWidth = this.gridLineWidth;
+ ctx.strokeStyle = this.gridLineColor;
+ var b, e, s, m;
+ var ax = ['xaxis', 'yaxis', 'x2axis', 'y2axis'];
+ for (var i=4; i>0; i--) {
+ var name = ax[i-1];
+ var axis = axes[name];
+ var ticks = axis._ticks;
+ var numticks = ticks.length;
+ if (axis.show) {
+ if (axis.drawBaseline) {
+ var bopts = {};
+ if (axis.baselineWidth !== null) {
+ bopts.lineWidth = axis.baselineWidth;
+ }
+ if (axis.baselineColor !== null) {
+ bopts.strokeStyle = axis.baselineColor;
+ }
+ switch (name) {
+ case 'xaxis':
+ drawLine (this._left, this._bottom, this._right, this._bottom, bopts);
+ break;
+ case 'yaxis':
+ drawLine (this._left, this._bottom, this._left, this._top, bopts);
+ break;
+ case 'x2axis':
+ drawLine (this._left, this._bottom, this._right, this._bottom, bopts);
+ break;
+ case 'y2axis':
+ drawLine (this._right, this._bottom, this._right, this._top, bopts);
+ break;
+ }
+ }
+ for (var j=numticks; j>0; j--) {
+ var t = ticks[j-1];
+ if (t.show) {
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (name) {
+ case 'xaxis':
+ // draw the grid line if we should
+ if (t.showGridline && this.drawGridlines && ((!t.isMinorTick && axis.drawMajorGridlines) || (t.isMinorTick && axis.drawMinorGridlines)) ) {
+ drawLine(pos, this._top, pos, this._bottom);
+ }
+ // draw the mark
+ if (t.showMark && t.mark && ((!t.isMinorTick && axis.drawMajorTickMarks) || (t.isMinorTick && axis.drawMinorTickMarks)) ) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._bottom;
+ e = this._bottom+s;
+ break;
+ case 'inside':
+ b = this._bottom-s;
+ e = this._bottom;
+ break;
+ case 'cross':
+ b = this._bottom-s;
+ e = this._bottom+s;
+ break;
+ default:
+ b = this._bottom;
+ e = this._bottom+s;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[pos,b],[pos,e]], {lineCap:'butt', lineWidth:this.gridLineWidth, offset:this.gridLineWidth*0.75, depth:2, fill:false, closePath:false});
+ }
+ // draw the line
+ drawLine(pos, b, pos, e);
+ }
+ break;
+ case 'yaxis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && ((!t.isMinorTick && axis.drawMajorGridlines) || (t.isMinorTick && axis.drawMinorGridlines)) ) {
+ drawLine(this._right, pos, this._left, pos);
+ }
+ // draw the mark
+ if (t.showMark && t.mark && ((!t.isMinorTick && axis.drawMajorTickMarks) || (t.isMinorTick && axis.drawMinorTickMarks)) ) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._left-s;
+ e = this._left;
+ break;
+ case 'inside':
+ b = this._left;
+ e = this._left+s;
+ break;
+ case 'cross':
+ b = this._left-s;
+ e = this._left+s;
+ break;
+ default:
+ b = this._left-s;
+ e = this._left;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+ }
+ break;
+ case 'x2axis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && ((!t.isMinorTick && axis.drawMajorGridlines) || (t.isMinorTick && axis.drawMinorGridlines)) ) {
+ drawLine(pos, this._bottom, pos, this._top);
+ }
+ // draw the mark
+ if (t.showMark && t.mark && ((!t.isMinorTick && axis.drawMajorTickMarks) || (t.isMinorTick && axis.drawMinorTickMarks)) ) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._top-s;
+ e = this._top;
+ break;
+ case 'inside':
+ b = this._top;
+ e = this._top+s;
+ break;
+ case 'cross':
+ b = this._top-s;
+ e = this._top+s;
+ break;
+ default:
+ b = this._top-s;
+ e = this._top;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[pos,b],[pos,e]], {lineCap:'butt', lineWidth:this.gridLineWidth, offset:this.gridLineWidth*0.75, depth:2, fill:false, closePath:false});
+ }
+ drawLine(pos, b, pos, e);
+ }
+ break;
+ case 'y2axis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && ((!t.isMinorTick && axis.drawMajorGridlines) || (t.isMinorTick && axis.drawMinorGridlines)) ) {
+ drawLine(this._left, pos, this._right, pos);
+ }
+ // draw the mark
+ if (t.showMark && t.mark && ((!t.isMinorTick && axis.drawMajorTickMarks) || (t.isMinorTick && axis.drawMinorTickMarks)) ) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._right;
+ e = this._right+s;
+ break;
+ case 'inside':
+ b = this._right-s;
+ e = this._right;
+ break;
+ case 'cross':
+ b = this._right-s;
+ e = this._right+s;
+ break;
+ default:
+ b = this._right;
+ e = this._right+s;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ t = null;
+ }
+ axis = null;
+ ticks = null;
+ }
+ // Now draw grid lines for additional y axes
+ //////
+ // TO DO: handle yMidAxis
+ //////
+ ax = ['y3axis', 'y4axis', 'y5axis', 'y6axis', 'y7axis', 'y8axis', 'y9axis', 'yMidAxis'];
+ for (var i=7; i>0; i--) {
+ var axis = axes[ax[i-1]];
+ var ticks = axis._ticks;
+ if (axis.show) {
+ var tn = ticks[axis.numberTicks-1];
+ var t0 = ticks[0];
+ var left = axis.getLeft();
+ var points = [[left, tn.getTop() + tn.getHeight()/2], [left, t0.getTop() + t0.getHeight()/2 + 1.0]];
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, points, {lineCap:'butt', fill:false, closePath:false});
+ }
+ // draw the line
+ drawLine(points[0][0], points[0][1], points[1][0], points[1][1], {lineCap:'butt', strokeStyle:axis.borderColor, lineWidth:axis.borderWidth});
+ // draw the tick marks
+ for (var j=ticks.length; j>0; j--) {
+ var t = ticks[j-1];
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ if (t.showMark && t.mark) {
+ switch (m) {
+ case 'outside':
+ b = left;
+ e = left+s;
+ break;
+ case 'inside':
+ b = left-s;
+ e = left;
+ break;
+ case 'cross':
+ b = left-s;
+ e = left+s;
+ break;
+ default:
+ b = left;
+ e = left+s;
+ break;
+ }
+ points = [[b,pos], [e,pos]];
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, points, {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ // draw the line
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+ }
+ t = null;
+ }
+ t0 = null;
+ }
+ axis = null;
+ ticks = null;
+ }
+
+ ctx.restore();
+
+ function drawLine(bx, by, ex, ey, opts) {
+ ctx.save();
+ opts = opts || {};
+ if (opts.lineWidth == null || opts.lineWidth != 0){
+ $.extend(true, ctx, opts);
+ ctx.beginPath();
+ ctx.moveTo(bx, by);
+ ctx.lineTo(ex, ey);
+ ctx.stroke();
+ ctx.restore();
+ }
+ }
+
+ if (this.shadow) {
+ var points = [[this._left, this._bottom], [this._right, this._bottom], [this._right, this._top]];
+ this.renderer.shadowRenderer.draw(ctx, points);
+ }
+ // Now draw border around grid. Use axis border definitions. start at
+ // upper left and go clockwise.
+ if (this.borderWidth != 0 && this.drawBorder) {
+ drawLine (this._left, this._top, this._right, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
+ drawLine (this._right, this._top, this._right, this._bottom, {lineCap:'round', strokeStyle:axes.y2axis.borderColor, lineWidth:axes.y2axis.borderWidth});
+ drawLine (this._right, this._bottom, this._left, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
+ drawLine (this._left, this._bottom, this._left, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
+ }
+ // ctx.lineWidth = this.borderWidth;
+ // ctx.strokeStyle = this.borderColor;
+ // ctx.strokeRect(this._left, this._top, this._width, this._height);
+
+ ctx.restore();
+ ctx = null;
+ axes = null;
+ };
+
+ // Class: $.jqplot.DivTitleRenderer
+ // The default title renderer for jqPlot. This class has no options beyond the <Title> class.
+ $.jqplot.DivTitleRenderer = function() {
+ };
+
+ $.jqplot.DivTitleRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.DivTitleRenderer.prototype.draw = function() {
+ // Memory Leaks patch
+ if (this._elem) {
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ var r = this.renderer;
+ var elem = document.createElement('div');
+ this._elem = $(elem);
+ this._elem.addClass('jqplot-title');
+
+ if (!this.text) {
+ this.show = false;
+ this._elem.height(0);
+ this._elem.width(0);
+ }
+ else if (this.text) {
+ var color;
+ if (this.color) {
+ color = this.color;
+ }
+ else if (this.textColor) {
+ color = this.textColor;
+ }
+
+ // don't trust that a stylesheet is present, set the position.
+ var styles = {position:'absolute', top:'0px', left:'0px'};
+
+ if (this._plotWidth) {
+ styles['width'] = this._plotWidth+'px';
+ }
+ if (this.fontSize) {
+ styles['fontSize'] = this.fontSize;
+ }
+ if (typeof this.textAlign === 'string') {
+ styles['textAlign'] = this.textAlign;
+ }
+ else {
+ styles['textAlign'] = 'center';
+ }
+ if (color) {
+ styles['color'] = color;
+ }
+ if (this.paddingBottom) {
+ styles['paddingBottom'] = this.paddingBottom;
+ }
+ if (this.fontFamily) {
+ styles['fontFamily'] = this.fontFamily;
+ }
+
+ this._elem.css(styles);
+ if (this.escapeHtml) {
+ this._elem.text(this.text);
+ }
+ else {
+ this._elem.html(this.text);
+ }
+
+
+ // styletext += (this._plotWidth) ? 'width:'+this._plotWidth+'px;' : '';
+ // styletext += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
+ // styletext += (this.textAlign) ? 'text-align:'+this.textAlign+';' : 'text-align:center;';
+ // styletext += (color) ? 'color:'+color+';' : '';
+ // styletext += (this.paddingBottom) ? 'padding-bottom:'+this.paddingBottom+';' : '';
+ // this._elem = $('<div class="jqplot-title" style="'+styletext+'">'+this.text+'</div>');
+ // if (this.fontFamily) {
+ // this._elem.css('font-family', this.fontFamily);
+ // }
+ }
+
+ elem = null;
+
+ return this._elem;
+ };
+
+ $.jqplot.DivTitleRenderer.prototype.pack = function() {
+ // nothing to do here
+ };
+
+
+ var dotlen = 0.1;
+
+ $.jqplot.LinePattern = function (ctx, pattern) {
+
+ var defaultLinePatterns = {
+ dotted: [ dotlen, $.jqplot.config.dotGapLength ],
+ dashed: [ $.jqplot.config.dashLength, $.jqplot.config.gapLength ],
+ solid: null
+ };
+
+ if (typeof pattern === 'string') {
+ if (pattern[0] === '.' || pattern[0] === '-') {
+ var s = pattern;
+ pattern = [];
+ for (var i=0, imax=s.length; i<imax; i++) {
+ if (s[i] === '.') {
+ pattern.push( dotlen );
+ }
+ else if (s[i] === '-') {
+ pattern.push( $.jqplot.config.dashLength );
+ }
+ else {
+ continue;
+ }
+ pattern.push( $.jqplot.config.gapLength );
+ }
+ }
+ else {
+ pattern = defaultLinePatterns[pattern];
+ }
+ }
+
+ if (!(pattern && pattern.length)) {
+ return ctx;
+ }
+
+ var patternIndex = 0;
+ var patternDistance = pattern[0];
+ var px = 0;
+ var py = 0;
+ var pathx0 = 0;
+ var pathy0 = 0;
+
+ var moveTo = function (x, y) {
+ ctx.moveTo( x, y );
+ px = x;
+ py = y;
+ pathx0 = x;
+ pathy0 = y;
+ };
+
+ var lineTo = function (x, y) {
+ var scale = ctx.lineWidth;
+ var dx = x - px;
+ var dy = y - py;
+ var dist = Math.sqrt(dx*dx+dy*dy);
+ if ((dist > 0) && (scale > 0)) {
+ dx /= dist;
+ dy /= dist;
+ while (true) {
+ var dp = scale * patternDistance;
+ if (dp < dist) {
+ px += dp * dx;
+ py += dp * dy;
+ if ((patternIndex & 1) == 0) {
+ ctx.lineTo( px, py );
+ }
+ else {
+ ctx.moveTo( px, py );
+ }
+ dist -= dp;
+ patternIndex++;
+ if (patternIndex >= pattern.length) {
+ patternIndex = 0;
+ }
+ patternDistance = pattern[patternIndex];
+ }
+ else {
+ px = x;
+ py = y;
+ if ((patternIndex & 1) == 0) {
+ ctx.lineTo( px, py );
+ }
+ else {
+ ctx.moveTo( px, py );
+ }
+ patternDistance -= dist / scale;
+ break;
+ }
+ }
+ }
+ };
+
+ var beginPath = function () {
+ ctx.beginPath();
+ };
+
+ var closePath = function () {
+ lineTo( pathx0, pathy0 );
+ };
+
+ return {
+ moveTo: moveTo,
+ lineTo: lineTo,
+ beginPath: beginPath,
+ closePath: closePath
+ };
+ };
+
+ // Class: $.jqplot.LineRenderer
+ // The default line renderer for jqPlot, this class has no options beyond the <Series> class.
+ // Draws series as a line.
+ $.jqplot.LineRenderer = function(){
+ this.shapeRenderer = new $.jqplot.ShapeRenderer();
+ this.shadowRenderer = new $.jqplot.ShadowRenderer();
+ };
+
+ // called with scope of series.
+ $.jqplot.LineRenderer.prototype.init = function(options, plot) {
+ // Group: Properties
+ //
+ options = options || {};
+ this._type='line';
+ this.renderer.animation = {
+ show: false,
+ direction: 'left',
+ speed: 2500,
+ _supported: true
+ };
+ // prop: smooth
+ // True to draw a smoothed (interpolated) line through the data points
+ // with automatically computed number of smoothing points.
+ // Set to an integer number > 2 to specify number of smoothing points
+ // to use between each data point.
+ this.renderer.smooth = false; // true or a number > 2 for smoothing.
+ this.renderer.tension = null; // null to auto compute or a number typically > 6. Fewer points requires higher tension.
+ // prop: constrainSmoothing
+ // True to use a more accurate smoothing algorithm that will
+ // not overshoot any data points. False to allow overshoot but
+ // produce a smoother looking line.
+ this.renderer.constrainSmoothing = true;
+ // this is smoothed data in grid coordinates, like gridData
+ this.renderer._smoothedData = [];
+ // this is smoothed data in plot units (plot coordinates), like plotData.
+ this.renderer._smoothedPlotData = [];
+ this.renderer._hiBandGridData = [];
+ this.renderer._lowBandGridData = [];
+ this.renderer._hiBandSmoothedData = [];
+ this.renderer._lowBandSmoothedData = [];
+
+ // prop: bandData
+ // Data used to draw error bands or confidence intervals above/below a line.
+ //
+ // bandData can be input in 3 forms. jqPlot will figure out which is the
+ // low band line and which is the high band line for all forms:
+ //
+ // A 2 dimensional array like [[yl1, yl2, ...], [yu1, yu2, ...]] where
+ // [yl1, yl2, ...] are y values of the lower line and
+ // [yu1, yu2, ...] are y values of the upper line.
+ // In this case there must be the same number of y data points as data points
+ // in the series and the bands will inherit the x values of the series.
+ //
+ // A 2 dimensional array like [[[xl1, yl1], [xl2, yl2], ...], [[xh1, yh1], [xh2, yh2], ...]]
+ // where [xl1, yl1] are x,y data points for the lower line and
+ // [xh1, yh1] are x,y data points for the high line.
+ // x values do not have to correspond to the x values of the series and can
+ // be of any arbitrary length.
+ //
+ // Can be of form [[yl1, yu1], [yl2, yu2], [yl3, yu3], ...] where
+ // there must be 3 or more arrays and there must be the same number of arrays
+ // as there are data points in the series. In this case,
+ // [yl1, yu1] specifies the lower and upper y values for the 1st
+ // data point and so on. The bands will inherit the x
+ // values from the series.
+ this.renderer.bandData = [];
+
+ // Group: bands
+ // Banding around line, e.g error bands or confidence intervals.
+ this.renderer.bands = {
+ // prop: show
+ // true to show the bands. If bandData or interval is
+ // supplied, show will be set to true by default.
+ show: false,
+ hiData: [],
+ lowData: [],
+ // prop: color
+ // color of lines at top and bottom of bands [default: series color].
+ color: this.color,
+ // prop: showLines
+ // True to show lines at top and bottom of bands [default: false].
+ showLines: false,
+ // prop: fill
+ // True to fill area between bands [default: true].
+ fill: true,
+ // prop: fillColor
+ // css color spec for filled area. [default: series color].
+ fillColor: null,
+ _min: null,
+ _max: null,
+ // prop: interval
+ // User specified interval above and below line for bands [default: '3%''].
+ // Can be a value like 3 or a string like '3%'
+ // or an upper/lower array like [1, -2] or ['2%', '-1.5%']
+ interval: '3%'
+ };
+
+
+ var lopts = {highlightMouseOver: options.highlightMouseOver, highlightMouseDown: options.highlightMouseDown, highlightColor: options.highlightColor};
+
+ delete (options.highlightMouseOver);
+ delete (options.highlightMouseDown);
+ delete (options.highlightColor);
+
+ $.extend(true, this.renderer, options);
+
+ this.renderer.options = options;
+
+ // if we are given some band data, and bands aren't explicity set to false in options, turn them on.
+ if (this.renderer.bandData.length > 1 && (!options.bands || options.bands.show == null)) {
+ this.renderer.bands.show = true;
+ }
+
+ // if we are given an interval, and bands aren't explicity set to false in options, turn them on.
+ else if (options.bands && options.bands.show == null && options.bands.interval != null) {
+ this.renderer.bands.show = true;
+ }
+
+ // if plot is filled, turn off bands.
+ if (this.fill) {
+ this.renderer.bands.show = false;
+ }
+
+ if (this.renderer.bands.show) {
+ this.renderer.initBands.call(this, this.renderer.options, plot);
+ }
+
+
+ // smoothing is not compatible with stacked lines, disable
+ if (this._stack) {
+ this.renderer.smooth = false;
+ }
+
+ // set the shape renderer options
+ var opts = {lineJoin:this.lineJoin, lineCap:this.lineCap, fill:this.fill, isarc:false, strokeStyle:this.color, fillStyle:this.fillColor, lineWidth:this.lineWidth, linePattern:this.linePattern, closePath:this.fill};
+ this.renderer.shapeRenderer.init(opts);
+
+ var shadow_offset = options.shadowOffset;
+ // set the shadow renderer options
+ if (shadow_offset == null) {
+ // scale the shadowOffset to the width of the line.
+ if (this.lineWidth > 2.5) {
+ shadow_offset = 1.25 * (1 + (Math.atan((this.lineWidth/2.5))/0.785398163 - 1)*0.6);
+ // var shadow_offset = this.shadowOffset;
+ }
+ // for skinny lines, don't make such a big shadow.
+ else {
+ shadow_offset = 1.25 * Math.atan((this.lineWidth/2.5))/0.785398163;
+ }
+ }
+
+ var sopts = {lineJoin:this.lineJoin, lineCap:this.lineCap, fill:this.fill, isarc:false, angle:this.shadowAngle, offset:shadow_offset, alpha:this.shadowAlpha, depth:this.shadowDepth, lineWidth:this.lineWidth, linePattern:this.linePattern, closePath:this.fill};
+ this.renderer.shadowRenderer.init(sopts);
+ this._areaPoints = [];
+ this._boundingBox = [[],[]];
+
+ if (!this.isTrendline && this.fill || this.renderer.bands.show) {
+ // Group: Properties
+ //
+ // prop: highlightMouseOver
+ // True to highlight area on a filled plot when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on an area on a filled plot.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over an area on a filled plot.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColor
+ // color to use when highlighting an area on a filled plot.
+ this.highlightColor = null;
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (lopts.highlightMouseDown && lopts.highlightMouseOver == null) {
+ lopts.highlightMouseOver = false;
+ }
+
+ $.extend(true, this, {highlightMouseOver: lopts.highlightMouseOver, highlightMouseDown: lopts.highlightMouseDown, highlightColor: lopts.highlightColor});
+
+ if (!this.highlightColor) {
+ var fc = (this.renderer.bands.show) ? this.renderer.bands.fillColor : this.fillColor;
+ this.highlightColor = $.jqplot.computeHighlightColors(fc);
+ }
+ // turn off (disable) the highlighter plugin
+ if (this.highlighter) {
+ this.highlighter.show = false;
+ }
+ }
+
+ if (!this.isTrendline && plot) {
+ plot.plugins.lineRenderer = {};
+ plot.postInitHooks.addOnce(postInit);
+ plot.postDrawHooks.addOnce(postPlotDraw);
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+ plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
+ plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
+ plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
+ plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
+ }
+
+ };
+
+ $.jqplot.LineRenderer.prototype.initBands = function(options, plot) {
+ // use bandData if no data specified in bands option
+ //var bd = this.renderer.bandData;
+ var bd = options.bandData || [];
+ var bands = this.renderer.bands;
+ bands.hiData = [];
+ bands.lowData = [];
+ var data = this.data;
+ bands._max = null;
+ bands._min = null;
+ // If 2 arrays, and each array greater than 2 elements, assume it is hi and low data bands of y values.
+ if (bd.length == 2) {
+ // Do we have an array of x,y values?
+ // like [[[1,1], [2,4], [3,3]], [[1,3], [2,6], [3,5]]]
+ if ($.isArray(bd[0][0])) {
+ // since an arbitrary array of points, spin through all of them to determine max and min lines.
+
+ var p;
+ var bdminidx = 0, bdmaxidx = 0;
+ for (var i = 0, l = bd[0].length; i<l; i++) {
+ p = bd[0][i];
+ if ((p[1] != null && p[1] > bands._max) || bands._max == null) {
+ bands._max = p[1];
+ }
+ if ((p[1] != null && p[1] < bands._min) || bands._min == null) {
+ bands._min = p[1];
+ }
+ }
+ for (var i = 0, l = bd[1].length; i<l; i++) {
+ p = bd[1][i];
+ if ((p[1] != null && p[1] > bands._max) || bands._max == null) {
+ bands._max = p[1];
+ bdmaxidx = 1;
+ }
+ if ((p[1] != null && p[1] < bands._min) || bands._min == null) {
+ bands._min = p[1];
+ bdminidx = 1;
+ }
+ }
+
+ if (bdmaxidx === bdminidx) {
+ bands.show = false;
+ }
+
+ bands.hiData = bd[bdmaxidx];
+ bands.lowData = bd[bdminidx];
+ }
+ // else data is arrays of y values
+ // like [[1,4,3], [3,6,5]]
+ // must have same number of band data points as points in series
+ else if (bd[0].length === data.length && bd[1].length === data.length) {
+ var hi = (bd[0][0] > bd[1][0]) ? 0 : 1;
+ var low = (hi) ? 0 : 1;
+ for (var i=0, l=data.length; i < l; i++) {
+ bands.hiData.push([data[i][0], bd[hi][i]]);
+ bands.lowData.push([data[i][0], bd[low][i]]);
+ }
+ }
+
+ // we don't have proper data array, don't show bands.
+ else {
+ bands.show = false;
+ }
+ }
+
+ // if more than 2 arrays, have arrays of [ylow, yhi] values.
+ // note, can't distinguish case of [[ylow, yhi], [ylow, yhi]] from [[ylow, ylow], [yhi, yhi]]
+ // this is assumed to be of the latter form.
+ else if (bd.length > 2 && !$.isArray(bd[0][0])) {
+ var hi = (bd[0][0] > bd[0][1]) ? 0 : 1;
+ var low = (hi) ? 0 : 1;
+ for (var i=0, l=bd.length; i<l; i++) {
+ bands.hiData.push([data[i][0], bd[i][hi]]);
+ bands.lowData.push([data[i][0], bd[i][low]]);
+ }
+ }
+
+ // don't have proper data, auto calculate
+ else {
+ var intrv = bands.interval;
+ var a = null;
+ var b = null;
+ var afunc = null;
+ var bfunc = null;
+
+ if ($.isArray(intrv)) {
+ a = intrv[0];
+ b = intrv[1];
+ }
+ else {
+ a = intrv;
+ }
+
+ if (isNaN(a)) {
+ // we have a string
+ if (a.charAt(a.length - 1) === '%') {
+ afunc = 'multiply';
+ a = parseFloat(a)/100 + 1;
+ }
+ }
+
+ else {
+ a = parseFloat(a);
+ afunc = 'add';
+ }
+
+ if (b !== null && isNaN(b)) {
+ // we have a string
+ if (b.charAt(b.length - 1) === '%') {
+ bfunc = 'multiply';
+ b = parseFloat(b)/100 + 1;
+ }
+ }
+
+ else if (b !== null) {
+ b = parseFloat(b);
+ bfunc = 'add';
+ }
+
+ if (a !== null) {
+ if (b === null) {
+ b = -a;
+ bfunc = afunc;
+ if (bfunc === 'multiply') {
+ b += 2;
+ }
+ }
+
+ // make sure a always applies to hi band.
+ if (a < b) {
+ var temp = a;
+ a = b;
+ b = temp;
+ temp = afunc;
+ afunc = bfunc;
+ bfunc = temp;
+ }
+
+ for (var i=0, l = data.length; i < l; i++) {
+ switch (afunc) {
+ case 'add':
+ bands.hiData.push([data[i][0], data[i][1] + a]);
+ break;
+ case 'multiply':
+ bands.hiData.push([data[i][0], data[i][1] * a]);
+ break;
+ }
+ switch (bfunc) {
+ case 'add':
+ bands.lowData.push([data[i][0], data[i][1] + b]);
+ break;
+ case 'multiply':
+ bands.lowData.push([data[i][0], data[i][1] * b]);
+ break;
+ }
+ }
+ }
+
+ else {
+ bands.show = false;
+ }
+ }
+
+ var hd = bands.hiData;
+ var ld = bands.lowData;
+ for (var i = 0, l = hd.length; i<l; i++) {
+ if ((hd[i][1] != null && hd[i][1] > bands._max) || bands._max == null) {
+ bands._max = hd[i][1];
+ }
+ }
+ for (var i = 0, l = ld.length; i<l; i++) {
+ if ((ld[i][1] != null && ld[i][1] < bands._min) || bands._min == null) {
+ bands._min = ld[i][1];
+ }
+ }
+
+ // one last check for proper data
+ // these don't apply any more since allowing arbitrary x,y values
+ // if (bands.hiData.length != bands.lowData.length) {
+ // bands.show = false;
+ // }
+
+ // if (bands.hiData.length != this.data.length) {
+ // bands.show = false;
+ // }
+
+ if (bands.fillColor === null) {
+ var c = $.jqplot.getColorComponents(bands.color);
+ // now adjust alpha to differentiate fill
+ c[3] = c[3] * 0.5;
+ bands.fillColor = 'rgba(' + c[0] +', '+ c[1] +', '+ c[2] +', '+ c[3] + ')';
+ }
+ };
+
+ function getSteps (d, f) {
+ return (3.4182054+f) * Math.pow(d, -0.3534992);
+ }
+
+ function computeSteps (d1, d2) {
+ var s = Math.sqrt(Math.pow((d2[0]- d1[0]), 2) + Math.pow ((d2[1] - d1[1]), 2));
+ return 5.7648 * Math.log(s) + 7.4456;
+ }
+
+ function tanh (x) {
+ var a = (Math.exp(2*x) - 1) / (Math.exp(2*x) + 1);
+ return a;
+ }
+
+ //////////
+ // computeConstrainedSmoothedData
+ // An implementation of the constrained cubic spline interpolation
+ // method as presented in:
+ //
+ // Kruger, CJC, Constrained Cubic Spine Interpolation for Chemical Engineering Applications
+ // http://www.korf.co.uk/spline.pdf
+ //
+ // The implementation below borrows heavily from the sample Visual Basic
+ // implementation by CJC Kruger found in http://www.korf.co.uk/spline.xls
+ //
+ /////////
+
+ // called with scope of series
+ function computeConstrainedSmoothedData (gd) {
+ var smooth = this.renderer.smooth;
+ var dim = this.canvas.getWidth();
+ var xp = this._xaxis.series_p2u;
+ var yp = this._yaxis.series_p2u;
+ var steps =null;
+ var _steps = null;
+ var dist = gd.length/dim;
+ var _smoothedData = [];
+ var _smoothedPlotData = [];
+
+ if (!isNaN(parseFloat(smooth))) {
+ steps = parseFloat(smooth);
+ }
+ else {
+ steps = getSteps(dist, 0.5);
+ }
+
+ var yy = [];
+ var xx = [];
+
+ for (var i=0, l = gd.length; i<l; i++) {
+ yy.push(gd[i][1]);
+ xx.push(gd[i][0]);
+ }
+
+ function dxx(x1, x0) {
+ if (x1 - x0 == 0) {
+ return Math.pow(10,10);
+ }
+ else {
+ return x1 - x0;
+ }
+ }
+
+ var A, B, C, D;
+ // loop through each line segment. Have # points - 1 line segments. Nmber segments starting at 1.
+ var nmax = gd.length - 1;
+ for (var num = 1, gdl = gd.length; num<gdl; num++) {
+ var gxx = [];
+ var ggxx = [];
+ // point at each end of segment.
+ for (var j = 0; j < 2; j++) {
+ var i = num - 1 + j; // point number, 0 to # points.
+
+ if (i == 0 || i == nmax) {
+ gxx[j] = Math.pow(10, 10);
+ }
+ else if (yy[i+1] - yy[i] == 0 || yy[i] - yy[i-1] == 0) {
+ gxx[j] = 0;
+ }
+ else if (((xx[i+1] - xx[i]) / (yy[i+1] - yy[i]) + (xx[i] - xx[i-1]) / (yy[i] - yy[i-1])) == 0 ) {
+ gxx[j] = 0;
+ }
+ else if ( (yy[i+1] - yy[i]) * (yy[i] - yy[i-1]) < 0 ) {
+ gxx[j] = 0;
+ }
+
+ else {
+ gxx[j] = 2 / (dxx(xx[i + 1], xx[i]) / (yy[i + 1] - yy[i]) + dxx(xx[i], xx[i - 1]) / (yy[i] - yy[i - 1]));
+ }
+ }
+
+ // Reset first derivative (slope) at first and last point
+ if (num == 1) {
+ // First point has 0 2nd derivative
+ gxx[0] = 3 / 2 * (yy[1] - yy[0]) / dxx(xx[1], xx[0]) - gxx[1] / 2;
+ }
+ else if (num == nmax) {
+ // Last point has 0 2nd derivative
+ gxx[1] = 3 / 2 * (yy[nmax] - yy[nmax - 1]) / dxx(xx[nmax], xx[nmax - 1]) - gxx[0] / 2;
+ }
+
+ // Calc second derivative at points
+ ggxx[0] = -2 * (gxx[1] + 2 * gxx[0]) / dxx(xx[num], xx[num - 1]) + 6 * (yy[num] - yy[num - 1]) / Math.pow(dxx(xx[num], xx[num - 1]), 2);
+ ggxx[1] = 2 * (2 * gxx[1] + gxx[0]) / dxx(xx[num], xx[num - 1]) - 6 * (yy[num] - yy[num - 1]) / Math.pow(dxx(xx[num], xx[num - 1]), 2);
+
+ // Calc constants for cubic interpolation
+ D = 1 / 6 * (ggxx[1] - ggxx[0]) / dxx(xx[num], xx[num - 1]);
+ C = 1 / 2 * (xx[num] * ggxx[0] - xx[num - 1] * ggxx[1]) / dxx(xx[num], xx[num - 1]);
+ B = (yy[num] - yy[num - 1] - C * (Math.pow(xx[num], 2) - Math.pow(xx[num - 1], 2)) - D * (Math.pow(xx[num], 3) - Math.pow(xx[num - 1], 3))) / dxx(xx[num], xx[num - 1]);
+ A = yy[num - 1] - B * xx[num - 1] - C * Math.pow(xx[num - 1], 2) - D * Math.pow(xx[num - 1], 3);
+
+ var increment = (xx[num] - xx[num - 1]) / steps;
+ var temp, tempx;
+
+ for (var j = 0, l = steps; j < l; j++) {
+ temp = [];
+ tempx = xx[num - 1] + j * increment;
+ temp.push(tempx);
+ temp.push(A + B * tempx + C * Math.pow(tempx, 2) + D * Math.pow(tempx, 3));
+ _smoothedData.push(temp);
+ _smoothedPlotData.push([xp(temp[0]), yp(temp[1])]);
+ }
+ }
+
+ _smoothedData.push(gd[i]);
+ _smoothedPlotData.push([xp(gd[i][0]), yp(gd[i][1])]);
+
+ return [_smoothedData, _smoothedPlotData];
+ }
+
+ ///////
+ // computeHermiteSmoothedData
+ // A hermite spline smoothing of the plot data.
+ // This implementation is derived from the one posted
+ // by krypin on the jqplot-users mailing list:
+ //
+ // http://groups.google.com/group/jqplot-users/browse_thread/thread/748be6a445723cea?pli=1
+ //
+ // with a blog post:
+ //
+ // http://blog.statscollector.com/a-plugin-renderer-for-jqplot-to-draw-a-hermite-spline/
+ //
+ // and download of the original plugin:
+ //
+ // http://blog.statscollector.com/wp-content/uploads/2010/02/jqplot.hermiteSplineRenderer.js
+ //////////
+
+ // called with scope of series
+ function computeHermiteSmoothedData (gd) {
+ var smooth = this.renderer.smooth;
+ var tension = this.renderer.tension;
+ var dim = this.canvas.getWidth();
+ var xp = this._xaxis.series_p2u;
+ var yp = this._yaxis.series_p2u;
+ var steps =null;
+ var _steps = null;
+ var a = null;
+ var a1 = null;
+ var a2 = null;
+ var slope = null;
+ var slope2 = null;
+ var temp = null;
+ var t, s, h1, h2, h3, h4;
+ var TiX, TiY, Ti1X, Ti1Y;
+ var pX, pY, p;
+ var sd = [];
+ var spd = [];
+ var dist = gd.length/dim;
+ var min, max, stretch, scale, shift;
+ var _smoothedData = [];
+ var _smoothedPlotData = [];
+ if (!isNaN(parseFloat(smooth))) {
+ steps = parseFloat(smooth);
+ }
+ else {
+ steps = getSteps(dist, 0.5);
+ }
+ if (!isNaN(parseFloat(tension))) {
+ tension = parseFloat(tension);
+ }
+
+ for (var i=0, l = gd.length-1; i < l; i++) {
+
+ if (tension === null) {
+ slope = Math.abs((gd[i+1][1] - gd[i][1]) / (gd[i+1][0] - gd[i][0]));
+
+ min = 0.3;
+ max = 0.6;
+ stretch = (max - min)/2.0;
+ scale = 2.5;
+ shift = -1.4;
+
+ temp = slope/scale + shift;
+
+ a1 = stretch * tanh(temp) - stretch * tanh(shift) + min;
+
+ // if have both left and right line segments, will use minimum tension.
+ if (i > 0) {
+ slope2 = Math.abs((gd[i][1] - gd[i-1][1]) / (gd[i][0] - gd[i-1][0]));
+ }
+ temp = slope2/scale + shift;
+
+ a2 = stretch * tanh(temp) - stretch * tanh(shift) + min;
+
+ a = (a1 + a2)/2.0;
+
+ }
+ else {
+ a = tension;
+ }
+ for (t=0; t < steps; t++) {
+ s = t / steps;
+ h1 = (1 + 2*s)*Math.pow((1-s),2);
+ h2 = s*Math.pow((1-s),2);
+ h3 = Math.pow(s,2)*(3-2*s);
+ h4 = Math.pow(s,2)*(s-1);
+
+ if (gd[i-1]) {
+ TiX = a * (gd[i+1][0] - gd[i-1][0]);
+ TiY = a * (gd[i+1][1] - gd[i-1][1]);
+ } else {
+ TiX = a * (gd[i+1][0] - gd[i][0]);
+ TiY = a * (gd[i+1][1] - gd[i][1]);
+ }
+ if (gd[i+2]) {
+ Ti1X = a * (gd[i+2][0] - gd[i][0]);
+ Ti1Y = a * (gd[i+2][1] - gd[i][1]);
+ } else {
+ Ti1X = a * (gd[i+1][0] - gd[i][0]);
+ Ti1Y = a * (gd[i+1][1] - gd[i][1]);
+ }
+
+ pX = h1*gd[i][0] + h3*gd[i+1][0] + h2*TiX + h4*Ti1X;
+ pY = h1*gd[i][1] + h3*gd[i+1][1] + h2*TiY + h4*Ti1Y;
+ p = [pX, pY];
+
+ _smoothedData.push(p);
+ _smoothedPlotData.push([xp(pX), yp(pY)]);
+ }
+ }
+ _smoothedData.push(gd[l]);
+ _smoothedPlotData.push([xp(gd[l][0]), yp(gd[l][1])]);
+
+ return [_smoothedData, _smoothedPlotData];
+ }
+
+ // setGridData
+ // converts the user data values to grid coordinates and stores them
+ // in the gridData array.
+ // Called with scope of a series.
+ $.jqplot.LineRenderer.prototype.setGridData = function(plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var data = this._plotData;
+ var pdata = this._prevPlotData;
+ this.gridData = [];
+ this._prevGridData = [];
+ this.renderer._smoothedData = [];
+ this.renderer._smoothedPlotData = [];
+ this.renderer._hiBandGridData = [];
+ this.renderer._lowBandGridData = [];
+ this.renderer._hiBandSmoothedData = [];
+ this.renderer._lowBandSmoothedData = [];
+ var bands = this.renderer.bands;
+ var hasNull = false;
+ for (var i=0, l=data.length; i < l; i++) {
+ // if not a line series or if no nulls in data, push the converted point onto the array.
+ if (data[i][0] != null && data[i][1] != null) {
+ this.gridData.push([xp.call(this._xaxis, data[i][0]), yp.call(this._yaxis, data[i][1])]);
+ }
+ // else if there is a null, preserve it.
+ else if (data[i][0] == null) {
+ hasNull = true;
+ this.gridData.push([null, yp.call(this._yaxis, data[i][1])]);
+ }
+ else if (data[i][1] == null) {
+ hasNull = true;
+ this.gridData.push([xp.call(this._xaxis, data[i][0]), null]);
+ }
+ // if not a line series or if no nulls in data, push the converted point onto the array.
+ if (pdata[i] != null && pdata[i][0] != null && pdata[i][1] != null) {
+ this._prevGridData.push([xp.call(this._xaxis, pdata[i][0]), yp.call(this._yaxis, pdata[i][1])]);
+ }
+ // else if there is a null, preserve it.
+ else if (pdata[i] != null && pdata[i][0] == null) {
+ this._prevGridData.push([null, yp.call(this._yaxis, pdata[i][1])]);
+ }
+ else if (pdata[i] != null && pdata[i][0] != null && pdata[i][1] == null) {
+ this._prevGridData.push([xp.call(this._xaxis, pdata[i][0]), null]);
+ }
+ }
+
+ // don't do smoothing or bands on broken lines.
+ if (hasNull) {
+ this.renderer.smooth = false;
+ if (this._type === 'line') {
+ bands.show = false;
+ }
+ }
+
+ if (this._type === 'line' && bands.show) {
+ for (var i=0, l=bands.hiData.length; i<l; i++) {
+ this.renderer._hiBandGridData.push([xp.call(this._xaxis, bands.hiData[i][0]), yp.call(this._yaxis, bands.hiData[i][1])]);
+ }
+ for (var i=0, l=bands.lowData.length; i<l; i++) {
+ this.renderer._lowBandGridData.push([xp.call(this._xaxis, bands.lowData[i][0]), yp.call(this._yaxis, bands.lowData[i][1])]);
+ }
+ }
+
+ // calculate smoothed data if enough points and no nulls
+ if (this._type === 'line' && this.renderer.smooth && this.gridData.length > 2) {
+ var ret;
+ if (this.renderer.constrainSmoothing) {
+ ret = computeConstrainedSmoothedData.call(this, this.gridData);
+ this.renderer._smoothedData = ret[0];
+ this.renderer._smoothedPlotData = ret[1];
+
+ if (bands.show) {
+ ret = computeConstrainedSmoothedData.call(this, this.renderer._hiBandGridData);
+ this.renderer._hiBandSmoothedData = ret[0];
+ ret = computeConstrainedSmoothedData.call(this, this.renderer._lowBandGridData);
+ this.renderer._lowBandSmoothedData = ret[0];
+ }
+
+ ret = null;
+ }
+ else {
+ ret = computeHermiteSmoothedData.call(this, this.gridData);
+ this.renderer._smoothedData = ret[0];
+ this.renderer._smoothedPlotData = ret[1];
+
+ if (bands.show) {
+ ret = computeHermiteSmoothedData.call(this, this.renderer._hiBandGridData);
+ this.renderer._hiBandSmoothedData = ret[0];
+ ret = computeHermiteSmoothedData.call(this, this.renderer._lowBandGridData);
+ this.renderer._lowBandSmoothedData = ret[0];
+ }
+
+ ret = null;
+ }
+ }
+ };
+
+ // makeGridData
+ // converts any arbitrary data values to grid coordinates and
+ // returns them. This method exists so that plugins can use a series'
+ // linerenderer to generate grid data points without overwriting the
+ // grid data associated with that series.
+ // Called with scope of a series.
+ $.jqplot.LineRenderer.prototype.makeGridData = function(data, plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var gd = [];
+ var pgd = [];
+ this.renderer._smoothedData = [];
+ this.renderer._smoothedPlotData = [];
+ this.renderer._hiBandGridData = [];
+ this.renderer._lowBandGridData = [];
+ this.renderer._hiBandSmoothedData = [];
+ this.renderer._lowBandSmoothedData = [];
+ var bands = this.renderer.bands;
+ var hasNull = false;
+ for (var i=0; i<data.length; i++) {
+ // if not a line series or if no nulls in data, push the converted point onto the array.
+ if (data[i][0] != null && data[i][1] != null) {
+ gd.push([xp.call(this._xaxis, data[i][0]), yp.call(this._yaxis, data[i][1])]);
+ }
+ // else if there is a null, preserve it.
+ else if (data[i][0] == null) {
+ hasNull = true;
+ gd.push([null, yp.call(this._yaxis, data[i][1])]);
+ }
+ else if (data[i][1] == null) {
+ hasNull = true;
+ gd.push([xp.call(this._xaxis, data[i][0]), null]);
+ }
+ }
+
+ // don't do smoothing or bands on broken lines.
+ if (hasNull) {
+ this.renderer.smooth = false;
+ if (this._type === 'line') {
+ bands.show = false;
+ }
+ }
+
+ if (this._type === 'line' && bands.show) {
+ for (var i=0, l=bands.hiData.length; i<l; i++) {
+ this.renderer._hiBandGridData.push([xp.call(this._xaxis, bands.hiData[i][0]), yp.call(this._yaxis, bands.hiData[i][1])]);
+ }
+ for (var i=0, l=bands.lowData.length; i<l; i++) {
+ this.renderer._lowBandGridData.push([xp.call(this._xaxis, bands.lowData[i][0]), yp.call(this._yaxis, bands.lowData[i][1])]);
+ }
+ }
+
+ if (this._type === 'line' && this.renderer.smooth && gd.length > 2) {
+ var ret;
+ if (this.renderer.constrainSmoothing) {
+ ret = computeConstrainedSmoothedData.call(this, gd);
+ this.renderer._smoothedData = ret[0];
+ this.renderer._smoothedPlotData = ret[1];
+
+ if (bands.show) {
+ ret = computeConstrainedSmoothedData.call(this, this.renderer._hiBandGridData);
+ this.renderer._hiBandSmoothedData = ret[0];
+ ret = computeConstrainedSmoothedData.call(this, this.renderer._lowBandGridData);
+ this.renderer._lowBandSmoothedData = ret[0];
+ }
+
+ ret = null;
+ }
+ else {
+ ret = computeHermiteSmoothedData.call(this, gd);
+ this.renderer._smoothedData = ret[0];
+ this.renderer._smoothedPlotData = ret[1];
+
+ if (bands.show) {
+ ret = computeHermiteSmoothedData.call(this, this.renderer._hiBandGridData);
+ this.renderer._hiBandSmoothedData = ret[0];
+ ret = computeHermiteSmoothedData.call(this, this.renderer._lowBandGridData);
+ this.renderer._lowBandSmoothedData = ret[0];
+ }
+
+ ret = null;
+ }
+ }
+ return gd;
+ };
+
+
+ // called within scope of series.
+ $.jqplot.LineRenderer.prototype.draw = function(ctx, gd, options, plot) {
+ var i;
+ // get a copy of the options, so we don't modify the original object.
+ var opts = $.extend(true, {}, options);
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var fillAndStroke = (opts.fillAndStroke != undefined) ? opts.fillAndStroke : this.fillAndStroke;
+ var xmin, ymin, xmax, ymax;
+ ctx.save();
+ if (gd.length) {
+ if (showLine) {
+ // if we fill, we'll have to add points to close the curve.
+ if (fill) {
+ if (this.fillToZero) {
+ // have to break line up into shapes at axis crossings
+ var negativeColor = this.negativeColor;
+ if (! this.useNegativeColors) {
+ negativeColor = opts.fillStyle;
+ }
+ var isnegative = false;
+ var posfs = opts.fillStyle;
+
+ // if stoking line as well as filling, get a copy of line data.
+ if (fillAndStroke) {
+ var fasgd = gd.slice(0);
+ }
+ // if not stacked, fill down to axis
+ if (this.index == 0 || !this._stack) {
+
+ var tempgd = [];
+ var pd = (this.renderer.smooth) ? this.renderer._smoothedPlotData : this._plotData;
+ this._areaPoints = [];
+ var pyzero = this._yaxis.series_u2p(this.fillToValue);
+ var pxzero = this._xaxis.series_u2p(this.fillToValue);
+
+ opts.closePath = true;
+
+ if (this.fillAxis == 'y') {
+ tempgd.push([gd[0][0], pyzero]);
+ this._areaPoints.push([gd[0][0], pyzero]);
+
+ for (var i=0; i<gd.length-1; i++) {
+ tempgd.push(gd[i]);
+ this._areaPoints.push(gd[i]);
+ // do we have an axis crossing?
+ if (pd[i][1] * pd[i+1][1] <= 0) {
+ if (pd[i][1] < 0) {
+ isnegative = true;
+ opts.fillStyle = negativeColor;
+ }
+ else {
+ isnegative = false;
+ opts.fillStyle = posfs;
+ }
+
+ var xintercept = gd[i][0] + (gd[i+1][0] - gd[i][0]) * (pyzero-gd[i][1])/(gd[i+1][1] - gd[i][1]);
+ tempgd.push([xintercept, pyzero]);
+ this._areaPoints.push([xintercept, pyzero]);
+ // now draw this shape and shadow.
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, tempgd, opts);
+ }
+ this.renderer.shapeRenderer.draw(ctx, tempgd, opts);
+ // now empty temp array and continue
+ tempgd = [[xintercept, pyzero]];
+ // this._areaPoints = [[xintercept, pyzero]];
+ }
+ }
+ if (pd[gd.length-1][1] < 0) {
+ isnegative = true;
+ opts.fillStyle = negativeColor;
+ }
+ else {
+ isnegative = false;
+ opts.fillStyle = posfs;
+ }
+ tempgd.push(gd[gd.length-1]);
+ this._areaPoints.push(gd[gd.length-1]);
+ tempgd.push([gd[gd.length-1][0], pyzero]);
+ this._areaPoints.push([gd[gd.length-1][0], pyzero]);
+ }
+ // now draw the last area.
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, tempgd, opts);
+ }
+ this.renderer.shapeRenderer.draw(ctx, tempgd, opts);
+
+
+ // var gridymin = this._yaxis.series_u2p(0);
+ // // IE doesn't return new length on unshift
+ // gd.unshift([gd[0][0], gridymin]);
+ // len = gd.length;
+ // gd.push([gd[len - 1][0], gridymin]);
+ }
+ // if stacked, fill to line below
+ else {
+ var prev = this._prevGridData;
+ for (var i=prev.length; i>0; i--) {
+ gd.push(prev[i-1]);
+ // this._areaPoints.push(prev[i-1]);
+ }
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, gd, opts);
+ }
+ this._areaPoints = gd;
+ this.renderer.shapeRenderer.draw(ctx, gd, opts);
+ }
+ }
+ /////////////////////////
+ // Not filled to zero
+ ////////////////////////
+ else {
+ // if stoking line as well as filling, get a copy of line data.
+ if (fillAndStroke) {
+ var fasgd = gd.slice(0);
+ }
+ // if not stacked, fill down to axis
+ if (this.index == 0 || !this._stack) {
+ // var gridymin = this._yaxis.series_u2p(this._yaxis.min) - this.gridBorderWidth / 2;
+ var gridymin = ctx.canvas.height;
+ // IE doesn't return new length on unshift
+ gd.unshift([gd[0][0], gridymin]);
+ var len = gd.length;
+ gd.push([gd[len - 1][0], gridymin]);
+ }
+ // if stacked, fill to line below
+ else {
+ var prev = this._prevGridData;
+ for (var i=prev.length; i>0; i--) {
+ gd.push(prev[i-1]);
+ }
+ }
+ this._areaPoints = gd;
+
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, gd, opts);
+ }
+
+ this.renderer.shapeRenderer.draw(ctx, gd, opts);
+ }
+ if (fillAndStroke) {
+ var fasopts = $.extend(true, {}, opts, {fill:false, closePath:false});
+ this.renderer.shapeRenderer.draw(ctx, fasgd, fasopts);
+ //////////
+ // TODO: figure out some way to do shadows nicely
+ // if (shadow) {
+ // this.renderer.shadowRenderer.draw(ctx, fasgd, fasopts);
+ // }
+ // now draw the markers
+ if (this.markerRenderer.show) {
+ if (this.renderer.smooth) {
+ fasgd = this.gridData;
+ }
+ for (i=0; i<fasgd.length; i++) {
+ this.markerRenderer.draw(fasgd[i][0], fasgd[i][1], ctx, opts.markerOptions);
+ }
+ }
+ }
+ }
+ else {
+
+ if (this.renderer.bands.show) {
+ var bdat;
+ var bopts = $.extend(true, {}, opts);
+ if (this.renderer.bands.showLines) {
+ bdat = (this.renderer.smooth) ? this.renderer._hiBandSmoothedData : this.renderer._hiBandGridData;
+ this.renderer.shapeRenderer.draw(ctx, bdat, opts);
+ bdat = (this.renderer.smooth) ? this.renderer._lowBandSmoothedData : this.renderer._lowBandGridData;
+ this.renderer.shapeRenderer.draw(ctx, bdat, bopts);
+ }
+
+ if (this.renderer.bands.fill) {
+ if (this.renderer.smooth) {
+ bdat = this.renderer._hiBandSmoothedData.concat(this.renderer._lowBandSmoothedData.reverse());
+ }
+ else {
+ bdat = this.renderer._hiBandGridData.concat(this.renderer._lowBandGridData.reverse());
+ }
+ this._areaPoints = bdat;
+ bopts.closePath = true;
+ bopts.fill = true;
+ bopts.fillStyle = this.renderer.bands.fillColor;
+ this.renderer.shapeRenderer.draw(ctx, bdat, bopts);
+ }
+ }
+
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, gd, opts);
+ }
+
+ this.renderer.shapeRenderer.draw(ctx, gd, opts);
+ }
+ }
+ // calculate the bounding box
+ var xmin = xmax = ymin = ymax = null;
+ for (i=0; i<this._areaPoints.length; i++) {
+ var p = this._areaPoints[i];
+ if (xmin > p[0] || xmin == null) {
+ xmin = p[0];
+ }
+ if (ymax < p[1] || ymax == null) {
+ ymax = p[1];
+ }
+ if (xmax < p[0] || xmax == null) {
+ xmax = p[0];
+ }
+ if (ymin > p[1] || ymin == null) {
+ ymin = p[1];
+ }
+ }
+
+ if (this.type === 'line' && this.renderer.bands.show) {
+ ymax = this._yaxis.series_u2p(this.renderer.bands._min);
+ ymin = this._yaxis.series_u2p(this.renderer.bands._max);
+ }
+
+ this._boundingBox = [[xmin, ymax], [xmax, ymin]];
+
+ // now draw the markers
+ if (this.markerRenderer.show && !fill) {
+ if (this.renderer.smooth) {
+ gd = this.gridData;
+ }
+ for (i=0; i<gd.length; i++) {
+ if (gd[i][0] != null && gd[i][1] != null) {
+ this.markerRenderer.draw(gd[i][0], gd[i][1], ctx, opts.markerOptions);
+ }
+ }
+ }
+ }
+
+ ctx.restore();
+ };
+
+ $.jqplot.LineRenderer.prototype.drawShadow = function(ctx, gd, options) {
+ // This is a no-op, shadows drawn with lines.
+ };
+
+ // called with scope of plot.
+ // make sure to not leave anything highlighted.
+ function postInit(target, data, options) {
+ for (var i=0; i<this.series.length; i++) {
+ if (this.series[i].renderer.constructor == $.jqplot.LineRenderer) {
+ // don't allow mouseover and mousedown at same time.
+ if (this.series[i].highlightMouseOver) {
+ this.series[i].highlightMouseDown = false;
+ }
+ }
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.lineRenderer && this.plugins.lineRenderer.highlightCanvas) {
+ this.plugins.lineRenderer.highlightCanvas.resetCanvas();
+ this.plugins.lineRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.lineRenderer.highlightedSeriesIndex = null;
+ this.plugins.lineRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+
+ this.eventCanvas._elem.before(this.plugins.lineRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-lineRenderer-highlight-canvas', this._plotDimensions, this));
+ this.plugins.lineRenderer.highlightCanvas.setContext();
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
+ }
+
+ function highlight (plot, sidx, pidx, points) {
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.lineRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.lineRenderer.highlightedSeriesIndex = sidx;
+ var opts = {fillStyle: s.highlightColor};
+ if (s.type === 'line' && s.renderer.bands.show) {
+ opts.fill = true;
+ opts.closePath = true;
+ }
+ s.renderer.shapeRenderer.draw(canvas._ctx, points, opts);
+ canvas = null;
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.lineRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.lineRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ canvas = null;
+ }
+
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.lineRenderer.highlightedSeriesIndex)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.lineRenderer.highlightedSeriesIndex)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
+ var idx = plot.plugins.lineRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ }
+
+ function handleClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt = jQuery.Event('jqplotDataClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var idx = plot.plugins.lineRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ var evt = jQuery.Event('jqplotDataRightClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+
+ // class: $.jqplot.LinearAxisRenderer
+ // The default jqPlot axis renderer, creating a numeric axis.
+ $.jqplot.LinearAxisRenderer = function() {
+ };
+
+ // called with scope of axis object.
+ $.jqplot.LinearAxisRenderer.prototype.init = function(options){
+ // prop: breakPoints
+ // EXPERIMENTAL!! Use at your own risk!
+ // Works only with linear axes and the default tick renderer.
+ // Array of [start, stop] points to create a broken axis.
+ // Broken axes have a "jump" in them, which is an immediate
+ // transition from a smaller value to a larger value.
+ // Currently, axis ticks MUST be manually assigned if using breakPoints
+ // by using the axis ticks array option.
+ this.breakPoints = null;
+ // prop: breakTickLabel
+ // Label to use at the axis break if breakPoints are specified.
+ this.breakTickLabel = "&asymp;";
+ // prop: drawBaseline
+ // True to draw the axis baseline.
+ this.drawBaseline = true;
+ // prop: baselineWidth
+ // width of the baseline in pixels.
+ this.baselineWidth = null;
+ // prop: baselineColor
+ // CSS color spec for the baseline.
+ this.baselineColor = null;
+ // prop: forceTickAt0
+ // This will ensure that there is always a tick mark at 0.
+ // If data range is strictly positive or negative,
+ // this will force 0 to be inside the axis bounds unless
+ // the appropriate axis pad (pad, padMin or padMax) is set
+ // to 0, then this will force an axis min or max value at 0.
+ // This has know effect when any of the following options
+ // are set: autoscale, min, max, numberTicks or tickInterval.
+ this.forceTickAt0 = false;
+ // prop: forceTickAt100
+ // This will ensure that there is always a tick mark at 100.
+ // If data range is strictly above or below 100,
+ // this will force 100 to be inside the axis bounds unless
+ // the appropriate axis pad (pad, padMin or padMax) is set
+ // to 0, then this will force an axis min or max value at 100.
+ // This has know effect when any of the following options
+ // are set: autoscale, min, max, numberTicks or tickInterval.
+ this.forceTickAt100 = false;
+ // prop: tickInset
+ // Controls the amount to inset the first and last ticks from
+ // the edges of the grid, in multiples of the tick interval.
+ // 0 is no inset, 0.5 is one half a tick interval, 1 is a full
+ // tick interval, etc.
+ this.tickInset = 0;
+ // prop: minorTicks
+ // Number of ticks to add between "major" ticks.
+ // Major ticks are ticks supplied by user or auto computed.
+ // Minor ticks cannot be created by user.
+ this.minorTicks = 0;
+ // prop: alignTicks
+ // true to align tick marks across opposed axes
+ // such as from the y2axis to yaxis.
+ this.alignTicks = false;
+ this._autoFormatString = '';
+ this._overrideFormatString = false;
+ this._scalefact = 1.0;
+ $.extend(true, this, options);
+ if (this.breakPoints) {
+ if (!$.isArray(this.breakPoints)) {
+ this.breakPoints = null;
+ }
+ else if (this.breakPoints.length < 2 || this.breakPoints[1] <= this.breakPoints[0]) {
+ this.breakPoints = null;
+ }
+ }
+ if (this.numberTicks != null && this.numberTicks < 2) {
+ this.numberTicks = 2;
+ }
+ this.resetDataBounds();
+ };
+
+ // called with scope of axis
+ $.jqplot.LinearAxisRenderer.prototype.draw = function(ctx, plot) {
+ if (this.show) {
+ // populate the axis label and value properties.
+ // createTicks is a method on the renderer, but
+ // call it within the scope of the axis.
+ this.renderer.createTicks.call(this, plot);
+ // fill a div with axes labels in the right direction.
+ // Need to pregenerate each axis to get its bounds and
+ // position it and the labels correctly on the plot.
+ var dim=0;
+ var temp;
+ // Added for theming.
+ if (this._elem) {
+ // Memory Leaks patch
+ //this._elem.empty();
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ this._elem = $(document.createElement('div'));
+ this._elem.addClass('jqplot-axis jqplot-'+this.name);
+ this._elem.css('position', 'absolute');
+
+
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ this._elem.width(this._plotDimensions.width);
+ }
+ else {
+ this._elem.height(this._plotDimensions.height);
+ }
+
+ // create a _label object.
+ this.labelOptions.axis = this.name;
+ this._label = new this.labelRenderer(this.labelOptions);
+ if (this._label.show) {
+ var elem = this._label.draw(ctx, plot);
+ elem.appendTo(this._elem);
+ elem = null;
+ }
+
+ var t = this._ticks;
+ var tick;
+ for (var i=0; i<t.length; i++) {
+ tick = t[i];
+ if (tick.show && tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
+ this._elem.append(tick.draw(ctx, plot));
+ }
+ }
+ tick = null;
+ t = null;
+ }
+ return this._elem;
+ };
+
+ // called with scope of an axis
+ $.jqplot.LinearAxisRenderer.prototype.reset = function() {
+ this.min = this._options.min;
+ this.max = this._options.max;
+ this.tickInterval = this._options.tickInterval;
+ this.numberTicks = this._options.numberTicks;
+ this._autoFormatString = '';
+ if (this._overrideFormatString && this.tickOptions && this.tickOptions.formatString) {
+ this.tickOptions.formatString = '';
+ }
+
+ // this._ticks = this.__ticks;
+ };
+
+ // called with scope of axis
+ $.jqplot.LinearAxisRenderer.prototype.set = function() {
+ var dim = 0;
+ var temp;
+ var w = 0;
+ var h = 0;
+ var lshow = (this._label == null) ? false : this._label.show;
+ if (this.show) {
+ var t = this._ticks;
+ var tick;
+ for (var i=0; i<t.length; i++) {
+ tick = t[i];
+ if (!tick._breakTick && tick.show && tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ temp = tick._elem.outerHeight(true);
+ }
+ else {
+ temp = tick._elem.outerWidth(true);
+ }
+ if (temp > dim) {
+ dim = temp;
+ }
+ }
+ }
+ tick = null;
+ t = null;
+
+ if (lshow) {
+ w = this._label._elem.outerWidth(true);
+ h = this._label._elem.outerHeight(true);
+ }
+ if (this.name == 'xaxis') {
+ dim = dim + h;
+ this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
+ }
+ else if (this.name == 'x2axis') {
+ dim = dim + h;
+ this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
+ }
+ else if (this.name == 'yaxis') {
+ dim = dim + w;
+ this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ else {
+ dim = dim + w;
+ this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.LinearAxisRenderer.prototype.createTicks = function(plot) {
+ // we're are operating on an axis here
+ var ticks = this._ticks;
+ var userTicks = this.ticks;
+ var name = this.name;
+ // databounds were set on axis initialization.
+ var db = this._dataBounds;
+ var dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
+ var interval;
+ var min, max;
+ var pos1, pos2;
+ var tt, i;
+ // get a copy of user's settings for min/max.
+ var userMin = this.min;
+ var userMax = this.max;
+ var userNT = this.numberTicks;
+ var userTI = this.tickInterval;
+
+ var threshold = 30;
+ this._scalefact = (Math.max(dim, threshold+1) - threshold)/300.0;
+
+ // if we already have ticks, use them.
+ // ticks must be in order of increasing value.
+
+ if (userTicks.length) {
+ // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+ for (i=0; i<userTicks.length; i++){
+ var ut = userTicks[i];
+ var t = new this.tickRenderer(this.tickOptions);
+ if ($.isArray(ut)) {
+ t.value = ut[0];
+ if (this.breakPoints) {
+ if (ut[0] == this.breakPoints[0]) {
+ t.label = this.breakTickLabel;
+ t._breakTick = true;
+ t.showGridline = false;
+ t.showMark = false;
+ }
+ else if (ut[0] > this.breakPoints[0] && ut[0] <= this.breakPoints[1]) {
+ t.show = false;
+ t.showGridline = false;
+ t.label = ut[1];
+ }
+ else {
+ t.label = ut[1];
+ }
+ }
+ else {
+ t.label = ut[1];
+ }
+ t.setTick(ut[0], this.name);
+ this._ticks.push(t);
+ }
+
+ else if ($.isPlainObject(ut)) {
+ $.extend(true, t, ut);
+ t.axis = this.name;
+ this._ticks.push(t);
+ }
+
+ else {
+ t.value = ut;
+ if (this.breakPoints) {
+ if (ut == this.breakPoints[0]) {
+ t.label = this.breakTickLabel;
+ t._breakTick = true;
+ t.showGridline = false;
+ t.showMark = false;
+ }
+ else if (ut > this.breakPoints[0] && ut <= this.breakPoints[1]) {
+ t.show = false;
+ t.showGridline = false;
+ }
+ }
+ t.setTick(ut, this.name);
+ this._ticks.push(t);
+ }
+ }
+ this.numberTicks = userTicks.length;
+ this.min = this._ticks[0].value;
+ this.max = this._ticks[this.numberTicks-1].value;
+ this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
+ }
+
+ // we don't have any ticks yet, let's make some!
+ else {
+ if (name == 'xaxis' || name == 'x2axis') {
+ dim = this._plotDimensions.width;
+ }
+ else {
+ dim = this._plotDimensions.height;
+ }
+
+ var _numberTicks = this.numberTicks;
+
+ // if aligning this axis, use number of ticks from previous axis.
+ // Do I need to reset somehow if alignTicks is changed and then graph is replotted??
+ if (this.alignTicks) {
+ if (this.name === 'x2axis' && plot.axes.xaxis.show) {
+ _numberTicks = plot.axes.xaxis.numberTicks;
+ }
+ else if (this.name.charAt(0) === 'y' && this.name !== 'yaxis' && this.name !== 'yMidAxis' && plot.axes.yaxis.show) {
+ _numberTicks = plot.axes.yaxis.numberTicks;
+ }
+ }
+
+ min = ((this.min != null) ? this.min : db.min);
+ max = ((this.max != null) ? this.max : db.max);
+
+ var range = max - min;
+ var rmin, rmax;
+ var temp;
+
+ if (this.tickOptions == null || !this.tickOptions.formatString) {
+ this._overrideFormatString = true;
+ }
+
+ // Doing complete autoscaling
+ if (this.min == null || this.max == null && this.tickInterval == null && !this.autoscale) {
+ // Check if user must have tick at 0 or 100 and ensure they are in range.
+ // The autoscaling algorithm will always place ticks at 0 and 100 if they are in range.
+ if (this.forceTickAt0) {
+ if (min > 0) {
+ min = 0;
+ }
+ if (max < 0) {
+ max = 0;
+ }
+ }
+
+ if (this.forceTickAt100) {
+ if (min > 100) {
+ min = 100;
+ }
+ if (max < 100) {
+ max = 100;
+ }
+ }
+
+ var keepMin = false,
+ keepMax = false;
+
+ if (this.min != null) {
+ keepMin = true;
+ }
+
+ else if (this.max != null) {
+ keepMax = true;
+ }
+
+ // var threshold = 30;
+ // var tdim = Math.max(dim, threshold+1);
+ // this._scalefact = (tdim-threshold)/300.0;
+ var ret = $.jqplot.LinearTickGenerator(min, max, this._scalefact, _numberTicks, keepMin, keepMax);
+ // calculate a padded max and min, points should be less than these
+ // so that they aren't too close to the edges of the plot.
+ // User can adjust how much padding is allowed with pad, padMin and PadMax options.
+ // If min or max is set, don't pad that end of axis.
+ var tumin = (this.min != null) ? min : min + range*(this.padMin - 1);
+ var tumax = (this.max != null) ? max : max - range*(this.padMax - 1);
+
+ // if they're equal, we shouldn't have to do anything, right?
+ // if (min <=tumin || max >= tumax) {
+ if (min <tumin || max > tumax) {
+ tumin = (this.min != null) ? min : min - range*(this.padMin - 1);
+ tumax = (this.max != null) ? max : max + range*(this.padMax - 1);
+ ret = $.jqplot.LinearTickGenerator(tumin, tumax, this._scalefact, _numberTicks, keepMin, keepMax);
+ }
+
+ this.min = ret[0];
+ this.max = ret[1];
+ // if numberTicks specified, it should return the same.
+ this.numberTicks = ret[2];
+ this._autoFormatString = ret[3];
+ this.tickInterval = ret[4];
+ }
+
+ // User has specified some axis scale related option, can use auto algorithm
+ else {
+
+ // if min and max are same, space them out a bit
+ if (min == max) {
+ var adj = 0.05;
+ if (min > 0) {
+ adj = Math.max(Math.log(min)/Math.LN10, 0.05);
+ }
+ min -= adj;
+ max += adj;
+ }
+
+ // autoscale. Can't autoscale if min or max is supplied.
+ // Will use numberTicks and tickInterval if supplied. Ticks
+ // across multiple axes may not line up depending on how
+ // bars are to be plotted.
+ if (this.autoscale && this.min == null && this.max == null) {
+ var rrange, ti, margin;
+ var forceMinZero = false;
+ var forceZeroLine = false;
+ var intervals = {min:null, max:null, average:null, stddev:null};
+ // if any series are bars, or if any are fill to zero, and if this
+ // is the axis to fill toward, check to see if we can start axis at zero.
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ var faname = (s.fillAxis == 'x') ? s._xaxis.name : s._yaxis.name;
+ // check to see if this is the fill axis
+ if (this.name == faname) {
+ var vals = s._plotValues[s.fillAxis];
+ var vmin = vals[0];
+ var vmax = vals[0];
+ for (var j=1; j<vals.length; j++) {
+ if (vals[j] < vmin) {
+ vmin = vals[j];
+ }
+ else if (vals[j] > vmax) {
+ vmax = vals[j];
+ }
+ }
+ var dp = (vmax - vmin) / vmax;
+ // is this sries a bar?
+ if (s.renderer.constructor == $.jqplot.BarRenderer) {
+ // if no negative values and could also check range.
+ if (vmin >= 0 && (s.fillToZero || dp > 0.1)) {
+ forceMinZero = true;
+ }
+ else {
+ forceMinZero = false;
+ if (s.fill && s.fillToZero && vmin < 0 && vmax > 0) {
+ forceZeroLine = true;
+ }
+ else {
+ forceZeroLine = false;
+ }
+ }
+ }
+
+ // if not a bar and filling, use appropriate method.
+ else if (s.fill) {
+ if (vmin >= 0 && (s.fillToZero || dp > 0.1)) {
+ forceMinZero = true;
+ }
+ else if (vmin < 0 && vmax > 0 && s.fillToZero) {
+ forceMinZero = false;
+ forceZeroLine = true;
+ }
+ else {
+ forceMinZero = false;
+ forceZeroLine = false;
+ }
+ }
+
+ // if not a bar and not filling, only change existing state
+ // if it doesn't make sense
+ else if (vmin < 0) {
+ forceMinZero = false;
+ }
+ }
+ }
+
+ // check if we need make axis min at 0.
+ if (forceMinZero) {
+ // compute number of ticks
+ this.numberTicks = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
+ this.min = 0;
+ userMin = 0;
+ // what order is this range?
+ // what tick interval does that give us?
+ ti = max/(this.numberTicks-1);
+ temp = Math.pow(10, Math.abs(Math.floor(Math.log(ti)/Math.LN10)));
+ if (ti/temp == parseInt(ti/temp, 10)) {
+ ti += temp;
+ }
+ this.tickInterval = Math.ceil(ti/temp) * temp;
+ this.max = this.tickInterval * (this.numberTicks - 1);
+ }
+
+ // check if we need to make sure there is a tick at 0.
+ else if (forceZeroLine) {
+ // compute number of ticks
+ this.numberTicks = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
+ var ntmin = Math.ceil(Math.abs(min)/range*(this.numberTicks-1));
+ var ntmax = this.numberTicks - 1 - ntmin;
+ ti = Math.max(Math.abs(min/ntmin), Math.abs(max/ntmax));
+ temp = Math.pow(10, Math.abs(Math.floor(Math.log(ti)/Math.LN10)));
+ this.tickInterval = Math.ceil(ti/temp) * temp;
+ this.max = this.tickInterval * ntmax;
+ this.min = -this.tickInterval * ntmin;
+ }
+
+ // if nothing else, do autoscaling which will try to line up ticks across axes.
+ else {
+ if (this.numberTicks == null){
+ if (this.tickInterval) {
+ this.numberTicks = 3 + Math.ceil(range / this.tickInterval);
+ }
+ else {
+ this.numberTicks = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
+ }
+ }
+
+ if (this.tickInterval == null) {
+ // get a tick interval
+ ti = range/(this.numberTicks - 1);
+
+ if (ti < 1) {
+ temp = Math.pow(10, Math.abs(Math.floor(Math.log(ti)/Math.LN10)));
+ }
+ else {
+ temp = 1;
+ }
+ this.tickInterval = Math.ceil(ti*temp*this.pad)/temp;
+ }
+ else {
+ temp = 1 / this.tickInterval;
+ }
+
+ // try to compute a nicer, more even tick interval
+ // temp = Math.pow(10, Math.floor(Math.log(ti)/Math.LN10));
+ // this.tickInterval = Math.ceil(ti/temp) * temp;
+ rrange = this.tickInterval * (this.numberTicks - 1);
+ margin = (rrange - range)/2;
+
+ if (this.min == null) {
+ this.min = Math.floor(temp*(min-margin))/temp;
+ }
+ if (this.max == null) {
+ this.max = this.min + rrange;
+ }
+ }
+
+ // Compute a somewhat decent format string if it is needed.
+ // get precision of interval and determine a format string.
+ var sf = $.jqplot.getSignificantFigures(this.tickInterval);
+
+ var fstr;
+
+ // if we have only a whole number, use integer formatting
+ if (sf.digitsLeft >= sf.significantDigits) {
+ fstr = '%d';
+ }
+
+ else {
+ var temp = Math.max(0, 5 - sf.digitsLeft);
+ temp = Math.min(temp, sf.digitsRight);
+ fstr = '%.'+ temp + 'f';
+ }
+
+ this._autoFormatString = fstr;
+ }
+
+ // Use the default algorithm which pads each axis to make the chart
+ // centered nicely on the grid.
+ else {
+
+ rmin = (this.min != null) ? this.min : min - range*(this.padMin - 1);
+ rmax = (this.max != null) ? this.max : max + range*(this.padMax - 1);
+ range = rmax - rmin;
+
+ if (this.numberTicks == null){
+ // if tickInterval is specified by user, we will ignore computed maximum.
+ // max will be equal or greater to fit even # of ticks.
+ if (this.tickInterval != null) {
+ this.numberTicks = Math.ceil((rmax - rmin)/this.tickInterval)+1;
+ }
+ else if (dim > 100) {
+ this.numberTicks = parseInt(3+(dim-100)/75, 10);
+ }
+ else {
+ this.numberTicks = 2;
+ }
+ }
+
+ if (this.tickInterval == null) {
+ this.tickInterval = range / (this.numberTicks-1);
+ }
+
+ if (this.max == null) {
+ rmax = rmin + this.tickInterval*(this.numberTicks - 1);
+ }
+ if (this.min == null) {
+ rmin = rmax - this.tickInterval*(this.numberTicks - 1);
+ }
+
+ // get precision of interval and determine a format string.
+ var sf = $.jqplot.getSignificantFigures(this.tickInterval);
+
+ var fstr;
+
+ // if we have only a whole number, use integer formatting
+ if (sf.digitsLeft >= sf.significantDigits) {
+ fstr = '%d';
+ }
+
+ else {
+ var temp = Math.max(0, 5 - sf.digitsLeft);
+ temp = Math.min(temp, sf.digitsRight);
+ fstr = '%.'+ temp + 'f';
+ }
+
+
+ this._autoFormatString = fstr;
+
+ this.min = rmin;
+ this.max = rmax;
+ }
+
+ if (this.renderer.constructor == $.jqplot.LinearAxisRenderer && this._autoFormatString == '') {
+ // fix for misleading tick display with small range and low precision.
+ range = this.max - this.min;
+ // figure out precision
+ var temptick = new this.tickRenderer(this.tickOptions);
+ // use the tick formatString or, the default.
+ var fs = temptick.formatString || $.jqplot.config.defaultTickFormatString;
+ var fs = fs.match($.jqplot.sprintf.regex)[0];
+ var precision = 0;
+ if (fs) {
+ if (fs.search(/[fFeEgGpP]/) > -1) {
+ var m = fs.match(/\%\.(\d{0,})?[eEfFgGpP]/);
+ if (m) {
+ precision = parseInt(m[1], 10);
+ }
+ else {
+ precision = 6;
+ }
+ }
+ else if (fs.search(/[di]/) > -1) {
+ precision = 0;
+ }
+ // fact will be <= 1;
+ var fact = Math.pow(10, -precision);
+ if (this.tickInterval < fact) {
+ // need to correct underrange
+ if (userNT == null && userTI == null) {
+ this.tickInterval = fact;
+ if (userMax == null && userMin == null) {
+ // this.min = Math.floor((this._dataBounds.min - this.tickInterval)/fact) * fact;
+ this.min = Math.floor(this._dataBounds.min/fact) * fact;
+ if (this.min == this._dataBounds.min) {
+ this.min = this._dataBounds.min - this.tickInterval;
+ }
+ // this.max = Math.ceil((this._dataBounds.max + this.tickInterval)/fact) * fact;
+ this.max = Math.ceil(this._dataBounds.max/fact) * fact;
+ if (this.max == this._dataBounds.max) {
+ this.max = this._dataBounds.max + this.tickInterval;
+ }
+ var n = (this.max - this.min)/this.tickInterval;
+ n = n.toFixed(11);
+ n = Math.ceil(n);
+ this.numberTicks = n + 1;
+ }
+ else if (userMax == null) {
+ // add one tick for top of range.
+ var n = (this._dataBounds.max - this.min) / this.tickInterval;
+ n = n.toFixed(11);
+ this.numberTicks = Math.ceil(n) + 2;
+ this.max = this.min + this.tickInterval * (this.numberTicks-1);
+ }
+ else if (userMin == null) {
+ // add one tick for bottom of range.
+ var n = (this.max - this._dataBounds.min) / this.tickInterval;
+ n = n.toFixed(11);
+ this.numberTicks = Math.ceil(n) + 2;
+ this.min = this.max - this.tickInterval * (this.numberTicks-1);
+ }
+ else {
+ // calculate a number of ticks so max is within axis scale
+ this.numberTicks = Math.ceil((userMax - userMin)/this.tickInterval) + 1;
+ // if user's min and max don't fit evenly in ticks, adjust.
+ // This takes care of cases such as user min set to 0, max set to 3.5 but tick
+ // format string set to %d (integer ticks)
+ this.min = Math.floor(userMin*Math.pow(10, precision))/Math.pow(10, precision);
+ this.max = Math.ceil(userMax*Math.pow(10, precision))/Math.pow(10, precision);
+ // this.max = this.min + this.tickInterval*(this.numberTicks-1);
+ this.numberTicks = Math.ceil((this.max - this.min)/this.tickInterval) + 1;
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ this.tickOptions = this.tickOptions || {};
+ this.tickOptions.formatString = this._autoFormatString;
+ }
+
+ var t, to;
+ for (var i=0; i<this.numberTicks; i++){
+ tt = this.min + i * this.tickInterval;
+ t = new this.tickRenderer(this.tickOptions);
+ // var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
+
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+
+ if (i < this.numberTicks - 1) {
+ for (var j=0; j<this.minorTicks; j++) {
+ tt += this.tickInterval/(this.minorTicks+1);
+ to = $.extend(true, {}, this.tickOptions, {name:this.name, value:tt, label:'', isMinorTick:true});
+ t = new this.tickRenderer(to);
+ this._ticks.push(t);
+ }
+ }
+ t = null;
+ }
+ }
+
+ if (this.tickInset) {
+ this.min = this.min - this.tickInset * this.tickInterval;
+ this.max = this.max + this.tickInset * this.tickInterval;
+ }
+
+ ticks = null;
+ };
+
+ // Used to reset just the values of the ticks and then repack, which will
+ // recalculate the positioning functions. It is assuemd that the
+ // number of ticks is the same and the values of the new array are at the
+ // proper interval.
+ // This method needs to be called with the scope of an axis object, like:
+ //
+ // > plot.axes.yaxis.renderer.resetTickValues.call(plot.axes.yaxis, yarr);
+ //
+ $.jqplot.LinearAxisRenderer.prototype.resetTickValues = function(opts) {
+ if ($.isArray(opts) && opts.length == this._ticks.length) {
+ var t;
+ for (var i=0; i<opts.length; i++) {
+ t = this._ticks[i];
+ t.value = opts[i];
+ t.label = t.formatter(t.formatString, opts[i]);
+ t.label = t.prefix + t.label;
+ t._elem.html(t.label);
+ }
+ t = null;
+ this.min = $.jqplot.arrayMin(opts);
+ this.max = $.jqplot.arrayMax(opts);
+ this.pack();
+ }
+ // Not implemented yet.
+ // else if ($.isPlainObject(opts)) {
+ //
+ // }
+ };
+
+ // called with scope of axis
+ $.jqplot.LinearAxisRenderer.prototype.pack = function(pos, offsets) {
+ // Add defaults for repacking from resetTickValues function.
+ pos = pos || {};
+ offsets = offsets || this._offsets;
+
+ var ticks = this._ticks;
+ var max = this.max;
+ var min = this.min;
+ var offmax = offsets.max;
+ var offmin = offsets.min;
+ var lshow = (this._label == null) ? false : this._label.show;
+
+ for (var p in pos) {
+ this._elem.css(p, pos[p]);
+ }
+
+ this._offsets = offsets;
+ // pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
+ var pixellength = offmax - offmin;
+ var unitlength = max - min;
+
+ // point to unit and unit to point conversions references to Plot DOM element top left corner.
+ if (this.breakPoints) {
+ unitlength = unitlength - this.breakPoints[1] + this.breakPoints[0];
+
+ this.p2u = function(p){
+ return (p - offmin) * unitlength / pixellength + min;
+ };
+
+ this.u2p = function(u){
+ if (u > this.breakPoints[0] && u < this.breakPoints[1]){
+ u = this.breakPoints[0];
+ }
+ if (u <= this.breakPoints[0]) {
+ return (u - min) * pixellength / unitlength + offmin;
+ }
+ else {
+ return (u - this.breakPoints[1] + this.breakPoints[0] - min) * pixellength / unitlength + offmin;
+ }
+ };
+
+ if (this.name.charAt(0) == 'x'){
+ this.series_u2p = function(u){
+ if (u > this.breakPoints[0] && u < this.breakPoints[1]){
+ u = this.breakPoints[0];
+ }
+ if (u <= this.breakPoints[0]) {
+ return (u - min) * pixellength / unitlength;
+ }
+ else {
+ return (u - this.breakPoints[1] + this.breakPoints[0] - min) * pixellength / unitlength;
+ }
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+
+ else {
+ this.series_u2p = function(u){
+ if (u > this.breakPoints[0] && u < this.breakPoints[1]){
+ u = this.breakPoints[0];
+ }
+ if (u >= this.breakPoints[1]) {
+ return (u - max) * pixellength / unitlength;
+ }
+ else {
+ return (u + this.breakPoints[1] - this.breakPoints[0] - max) * pixellength / unitlength;
+ }
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + max;
+ };
+ }
+ }
+ else {
+ this.p2u = function(p){
+ return (p - offmin) * unitlength / pixellength + min;
+ };
+
+ this.u2p = function(u){
+ return (u - min) * pixellength / unitlength + offmin;
+ };
+
+ if (this.name == 'xaxis' || this.name == 'x2axis'){
+ this.series_u2p = function(u){
+ return (u - min) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+
+ else {
+ this.series_u2p = function(u){
+ return (u - max) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + max;
+ };
+ }
+ }
+
+ if (this.show) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'xaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ if (temp * t.angle < 0) {
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ }
+ // position at start
+ else {
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'end':
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ case 'start':
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ break;
+ case 'middle':
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ default:
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getWidth()/2;
+ }
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('left', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var w = this._label._elem.outerWidth(true);
+ this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
+ if (this.name == 'xaxis') {
+ this._label._elem.css('bottom', '0px');
+ }
+ else {
+ this._label._elem.css('top', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ else {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'yaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ case 'end':
+ if (temp * t.angle < 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'start':
+ if (t.angle > 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'middle':
+ // if (t.angle > 0) {
+ // shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ // }
+ // else {
+ // shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ // }
+ shim = -t.getHeight()/2;
+ break;
+ default:
+ shim = -t.getHeight()/2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getHeight()/2;
+ }
+
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('top', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var h = this._label._elem.outerHeight(true);
+ this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
+ if (this.name == 'yaxis') {
+ this._label._elem.css('left', '0px');
+ }
+ else {
+ this._label._elem.css('right', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ }
+
+ ticks = null;
+ };
+
+
+ /**
+ * The following code was generaously given to me a while back by Scott Prahl.
+ * He did a good job at computing axes min, max and number of ticks for the
+ * case where the user has not set any scale related parameters (tickInterval,
+ * numberTicks, min or max). I had ignored this use case for a long time,
+ * focusing on the more difficult case where user has set some option controlling
+ * tick generation. Anyway, about time I got this into jqPlot.
+ * Thanks Scott!!
+ */
+
+ /**
+ * Copyright (c) 2010 Scott Prahl
+ * The next three routines are currently available for use in all personal
+ * or commercial projects under both the MIT and GPL version 2.0 licenses.
+ * This means that you can choose the license that best suits your project
+ * and use it accordingly.
+ */
+
+ // A good format string depends on the interval. If the interval is greater
+ // than 1 then there is no need to show any decimal digits. If it is < 1.0, then
+ // use the magnitude of the interval to determine the number of digits to show.
+ function bestFormatString (interval)
+ {
+ var fstr;
+ interval = Math.abs(interval);
+ if (interval >= 10) {
+ fstr = '%d';
+ }
+
+ else if (interval > 1) {
+ if (interval === parseInt(interval, 10)) {
+ fstr = '%d';
+ }
+ else {
+ fstr = '%.1f';
+ }
+ }
+
+ else {
+ var expv = -Math.floor(Math.log(interval)/Math.LN10);
+ fstr = '%.' + expv + 'f';
+ }
+
+ return fstr;
+ }
+
+ var _factors = [0.1, 0.2, 0.3, 0.4, 0.5, 0.8, 1, 2, 3, 4, 5];
+
+ var _getLowerFactor = function(f) {
+ var i = _factors.indexOf(f);
+ if (i > 0) {
+ return _factors[i-1];
+ }
+ else {
+ return _factors[_factors.length - 1] / 100;
+ }
+ };
+
+ var _getHigherFactor = function(f) {
+ var i = _factors.indexOf(f);
+ if (i < _factors.length-1) {
+ return _factors[i+1];
+ }
+ else {
+ return _factors[0] * 100;
+ }
+ };
+
+ // Given a fixed minimum and maximum and a target number ot ticks
+ // figure out the best interval and
+ // return min, max, number ticks, format string and tick interval
+ function bestConstrainedInterval(min, max, nttarget) {
+ // run through possible number to ticks and see which interval is best
+ var low = Math.floor(nttarget/2);
+ var hi = Math.ceil(nttarget*1.5);
+ var badness = Number.MAX_VALUE;
+ var r = (max - min);
+ var temp;
+ var sd;
+ var bestNT;
+ var gsf = $.jqplot.getSignificantFigures;
+ var fsd;
+ var fs;
+ var currentNT;
+ var bestPrec;
+
+ for (var i=0, l=hi-low+1; i<l; i++) {
+ currentNT = low + i;
+ temp = r/(currentNT-1);
+ sd = gsf(temp);
+
+ temp = Math.abs(nttarget - currentNT) + sd.digitsRight;
+ if (temp < badness) {
+ badness = temp;
+ bestNT = currentNT;
+ bestPrec = sd.digitsRight;
+ }
+ else if (temp === badness) {
+ // let nicer ticks trump number ot ticks
+ if (sd.digitsRight < bestPrec) {
+ bestNT = currentNT;
+ bestPrec = sd.digitsRight;
+ }
+ }
+
+ }
+
+ fsd = Math.max(bestPrec, Math.max(gsf(min).digitsRight, gsf(max).digitsRight));
+ if (fsd === 0) {
+ fs = '%d';
+ }
+ else {
+ fs = '%.' + fsd + 'f';
+ }
+ temp = r / (bestNT - 1);
+ // min, max, number ticks, format string, tick interval
+ return [min, max, bestNT, fs, temp];
+ }
+
+ // This will return an interval of form 2 * 10^n, 5 * 10^n or 10 * 10^n
+ // it is based soley on the range and number of ticks. So if user specifies
+ // number of ticks, use this.
+ function bestInterval(range, numberTicks) {
+ numberTicks = numberTicks || 7;
+ var minimum = range / (numberTicks - 1);
+ var magnitude = Math.pow(10, Math.floor(Math.log(minimum) / Math.LN10));
+ var residual = minimum / magnitude;
+ var interval;
+ // "nicest" ranges are 1, 2, 5 or powers of these.
+ // for magnitudes below 1, only allow these.
+ if (magnitude < 1) {
+ if (residual > 5) {
+ interval = 10 * magnitude;
+ }
+ else if (residual > 2) {
+ interval = 5 * magnitude;
+ }
+ else if (residual > 1) {
+ interval = 2 * magnitude;
+ }
+ else {
+ interval = magnitude;
+ }
+ }
+ // for large ranges (whole integers), allow intervals like 3, 4 or powers of these.
+ // this helps a lot with poor choices for number of ticks.
+ else {
+ if (residual > 5) {
+ interval = 10 * magnitude;
+ }
+ else if (residual > 4) {
+ interval = 5 * magnitude;
+ }
+ else if (residual > 3) {
+ interval = 4 * magnitude;
+ }
+ else if (residual > 2) {
+ interval = 3 * magnitude;
+ }
+ else if (residual > 1) {
+ interval = 2 * magnitude;
+ }
+ else {
+ interval = magnitude;
+ }
+ }
+
+ return interval;
+ }
+
+ // This will return an interval of form 2 * 10^n, 5 * 10^n or 10 * 10^n
+ // it is based soley on the range of data, number of ticks must be computed later.
+ function bestLinearInterval(range, scalefact) {
+ scalefact = scalefact || 1;
+ var expv = Math.floor(Math.log(range)/Math.LN10);
+ var magnitude = Math.pow(10, expv);
+ // 0 < f < 10
+ var f = range / magnitude;
+ var fact;
+ // for large plots, scalefact will decrease f and increase number of ticks.
+ // for small plots, scalefact will increase f and decrease number of ticks.
+ f = f/scalefact;
+
+ // for large plots, smaller interval, more ticks.
+ if (f<=0.38) {
+ fact = 0.1;
+ }
+ else if (f<=1.6) {
+ fact = 0.2;
+ }
+ else if (f<=4.0) {
+ fact = 0.5;
+ }
+ else if (f<=8.0) {
+ fact = 1.0;
+ }
+ // for very small plots, larger interval, less ticks in number ticks
+ else if (f<=16.0) {
+ fact = 2;
+ }
+ else {
+ fact = 5;
+ }
+
+ return fact*magnitude;
+ }
+
+ function bestLinearComponents(range, scalefact) {
+ var expv = Math.floor(Math.log(range)/Math.LN10);
+ var magnitude = Math.pow(10, expv);
+ // 0 < f < 10
+ var f = range / magnitude;
+ var interval;
+ var fact;
+ // for large plots, scalefact will decrease f and increase number of ticks.
+ // for small plots, scalefact will increase f and decrease number of ticks.
+ f = f/scalefact;
+
+ // for large plots, smaller interval, more ticks.
+ if (f<=0.38) {
+ fact = 0.1;
+ }
+ else if (f<=1.6) {
+ fact = 0.2;
+ }
+ else if (f<=4.0) {
+ fact = 0.5;
+ }
+ else if (f<=8.0) {
+ fact = 1.0;
+ }
+ // for very small plots, larger interval, less ticks in number ticks
+ else if (f<=16.0) {
+ fact = 2;
+ }
+ // else if (f<=20.0) {
+ // fact = 3;
+ // }
+ // else if (f<=24.0) {
+ // fact = 4;
+ // }
+ else {
+ fact = 5;
+ }
+
+ interval = fact * magnitude;
+
+ return [interval, fact, magnitude];
+ }
+
+ // Given the min and max for a dataset, return suitable endpoints
+ // for the graphing, a good number for the number of ticks, and a
+ // format string so that extraneous digits are not displayed.
+ // returned is an array containing [min, max, nTicks, format]
+ $.jqplot.LinearTickGenerator = function(axis_min, axis_max, scalefact, numberTicks, keepMin, keepMax) {
+ // Set to preserve EITHER min OR max.
+ // If min is preserved, max must be free.
+ keepMin = (keepMin === null) ? false : keepMin;
+ keepMax = (keepMax === null || keepMin) ? false : keepMax;
+ // if endpoints are equal try to include zero otherwise include one
+ if (axis_min === axis_max) {
+ axis_max = (axis_max) ? 0 : 1;
+ }
+
+ scalefact = scalefact || 1.0;
+
+ // make sure range is positive
+ if (axis_max < axis_min) {
+ var a = axis_max;
+ axis_max = axis_min;
+ axis_min = a;
+ }
+
+ var r = [];
+ var ss = bestLinearInterval(axis_max - axis_min, scalefact);
+
+ var gsf = $.jqplot.getSignificantFigures;
+
+ if (numberTicks == null) {
+
+ // Figure out the axis min, max and number of ticks
+ // the min and max will be some multiple of the tick interval,
+ // 1*10^n, 2*10^n or 5*10^n. This gaurantees that, if the
+ // axis min is negative, 0 will be a tick.
+ if (!keepMin && !keepMax) {
+ r[0] = Math.floor(axis_min / ss) * ss; // min
+ r[1] = Math.ceil(axis_max / ss) * ss; // max
+ r[2] = Math.round((r[1]-r[0])/ss+1.0); // number of ticks
+ r[3] = bestFormatString(ss); // format string
+ r[4] = ss; // tick Interval
+ }
+
+ else if (keepMin) {
+ r[0] = axis_min; // min
+ r[2] = Math.ceil((axis_max - axis_min) / ss + 1.0); // number of ticks
+ r[1] = axis_min + (r[2] - 1) * ss; // max
+ var digitsMin = gsf(axis_min).digitsRight;
+ var digitsSS = gsf(ss).digitsRight;
+ if (digitsMin < digitsSS) {
+ r[3] = bestFormatString(ss); // format string
+ }
+ else {
+ r[3] = '%.' + digitsMin + 'f';
+ }
+ r[4] = ss; // tick Interval
+ }
+
+ else if (keepMax) {
+ r[1] = axis_max; // max
+ r[2] = Math.ceil((axis_max - axis_min) / ss + 1.0); // number of ticks
+ r[0] = axis_max - (r[2] - 1) * ss; // min
+ var digitsMax = gsf(axis_max).digitsRight;
+ var digitsSS = gsf(ss).digitsRight;
+ if (digitsMax < digitsSS) {
+ r[3] = bestFormatString(ss); // format string
+ }
+ else {
+ r[3] = '%.' + digitsMax + 'f';
+ }
+ r[4] = ss; // tick Interval
+ }
+ }
+
+ else {
+ var tempr = [];
+
+ // Figure out the axis min, max and number of ticks
+ // the min and max will be some multiple of the tick interval,
+ // 1*10^n, 2*10^n or 5*10^n. This gaurantees that, if the
+ // axis min is negative, 0 will be a tick.
+ tempr[0] = Math.floor(axis_min / ss) * ss; // min
+ tempr[1] = Math.ceil(axis_max / ss) * ss; // max
+ tempr[2] = Math.round((tempr[1]-tempr[0])/ss+1.0); // number of ticks
+ tempr[3] = bestFormatString(ss); // format string
+ tempr[4] = ss; // tick Interval
+
+ // first, see if we happen to get the right number of ticks
+ if (tempr[2] === numberTicks) {
+ r = tempr;
+ }
+
+ else {
+
+ var newti = bestInterval(tempr[1] - tempr[0], numberTicks);
+
+ r[0] = tempr[0]; // min
+ r[2] = numberTicks; // number of ticks
+ r[4] = newti; // tick interval
+ r[3] = bestFormatString(newti); // format string
+ r[1] = r[0] + (r[2] - 1) * r[4]; // max
+ }
+ }
+
+ return r;
+ };
+
+ $.jqplot.LinearTickGenerator.bestLinearInterval = bestLinearInterval;
+ $.jqplot.LinearTickGenerator.bestInterval = bestInterval;
+ $.jqplot.LinearTickGenerator.bestLinearComponents = bestLinearComponents;
+ $.jqplot.LinearTickGenerator.bestConstrainedInterval = bestConstrainedInterval;
+
+
+ // class: $.jqplot.MarkerRenderer
+ // The default jqPlot marker renderer, rendering the points on the line.
+ $.jqplot.MarkerRenderer = function(options){
+ // Group: Properties
+
+ // prop: show
+ // whether or not to show the marker.
+ this.show = true;
+ // prop: style
+ // One of diamond, circle, square, x, plus, dash, filledDiamond, filledCircle, filledSquare
+ this.style = 'filledCircle';
+ // prop: lineWidth
+ // size of the line for non-filled markers.
+ this.lineWidth = 2;
+ // prop: size
+ // Size of the marker (diameter or circle, length of edge of square, etc.)
+ this.size = 9.0;
+ // prop: color
+ // color of marker. Will be set to color of series by default on init.
+ this.color = '#666666';
+ // prop: shadow
+ // whether or not to draw a shadow on the line
+ this.shadow = true;
+ // prop: shadowAngle
+ // Shadow angle in degrees
+ this.shadowAngle = 45;
+ // prop: shadowOffset
+ // Shadow offset from line in pixels
+ this.shadowOffset = 1;
+ // prop: shadowDepth
+ // Number of times shadow is stroked, each stroke offset shadowOffset from the last.
+ this.shadowDepth = 3;
+ // prop: shadowAlpha
+ // Alpha channel transparency of shadow. 0 = transparent.
+ this.shadowAlpha = '0.07';
+ // prop: shadowRenderer
+ // Renderer that will draws the shadows on the marker.
+ this.shadowRenderer = new $.jqplot.ShadowRenderer();
+ // prop: shapeRenderer
+ // Renderer that will draw the marker.
+ this.shapeRenderer = new $.jqplot.ShapeRenderer();
+
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ var sdopt = {angle:this.shadowAngle, offset:this.shadowOffset, alpha:this.shadowAlpha, lineWidth:this.lineWidth, depth:this.shadowDepth, closePath:true};
+ if (this.style.indexOf('filled') != -1) {
+ sdopt.fill = true;
+ }
+ if (this.style.indexOf('ircle') != -1) {
+ sdopt.isarc = true;
+ sdopt.closePath = false;
+ }
+ this.shadowRenderer.init(sdopt);
+
+ var shopt = {fill:false, isarc:false, strokeStyle:this.color, fillStyle:this.color, lineWidth:this.lineWidth, closePath:true};
+ if (this.style.indexOf('filled') != -1) {
+ shopt.fill = true;
+ }
+ if (this.style.indexOf('ircle') != -1) {
+ shopt.isarc = true;
+ shopt.closePath = false;
+ }
+ this.shapeRenderer.init(shopt);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawDiamond = function(x, y, ctx, fill, options) {
+ var stretch = 1.2;
+ var dx = this.size/2/stretch;
+ var dy = this.size/2*stretch;
+ var points = [[x-dx, y], [x, y+dy], [x+dx, y], [x, y-dy]];
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points);
+ }
+ this.shapeRenderer.draw(ctx, points, options);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawPlus = function(x, y, ctx, fill, options) {
+ var stretch = 1.0;
+ var dx = this.size/2*stretch;
+ var dy = this.size/2*stretch;
+ var points1 = [[x, y-dy], [x, y+dy]];
+ var points2 = [[x+dx, y], [x-dx, y]];
+ var opts = $.extend(true, {}, this.options, {closePath:false});
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points1, {closePath:false});
+ this.shadowRenderer.draw(ctx, points2, {closePath:false});
+ }
+ this.shapeRenderer.draw(ctx, points1, opts);
+ this.shapeRenderer.draw(ctx, points2, opts);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawX = function(x, y, ctx, fill, options) {
+ var stretch = 1.0;
+ var dx = this.size/2*stretch;
+ var dy = this.size/2*stretch;
+ var opts = $.extend(true, {}, this.options, {closePath:false});
+ var points1 = [[x-dx, y-dy], [x+dx, y+dy]];
+ var points2 = [[x-dx, y+dy], [x+dx, y-dy]];
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points1, {closePath:false});
+ this.shadowRenderer.draw(ctx, points2, {closePath:false});
+ }
+ this.shapeRenderer.draw(ctx, points1, opts);
+ this.shapeRenderer.draw(ctx, points2, opts);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawDash = function(x, y, ctx, fill, options) {
+ var stretch = 1.0;
+ var dx = this.size/2*stretch;
+ var dy = this.size/2*stretch;
+ var points = [[x-dx, y], [x+dx, y]];
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points);
+ }
+ this.shapeRenderer.draw(ctx, points, options);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawLine = function(p1, p2, ctx, fill, options) {
+ var points = [p1, p2];
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points);
+ }
+ this.shapeRenderer.draw(ctx, points, options);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawSquare = function(x, y, ctx, fill, options) {
+ var stretch = 1.0;
+ var dx = this.size/2/stretch;
+ var dy = this.size/2*stretch;
+ var points = [[x-dx, y-dy], [x-dx, y+dy], [x+dx, y+dy], [x+dx, y-dy]];
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points);
+ }
+ this.shapeRenderer.draw(ctx, points, options);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.drawCircle = function(x, y, ctx, fill, options) {
+ var radius = this.size/2;
+ var end = 2*Math.PI;
+ var points = [x, y, radius, 0, end, true];
+ if (this.shadow) {
+ this.shadowRenderer.draw(ctx, points);
+ }
+ this.shapeRenderer.draw(ctx, points, options);
+ };
+
+ $.jqplot.MarkerRenderer.prototype.draw = function(x, y, ctx, options) {
+ options = options || {};
+ // hack here b/c shape renderer uses canvas based color style options
+ // and marker uses css style names.
+ if (options.show == null || options.show != false) {
+ if (options.color && !options.fillStyle) {
+ options.fillStyle = options.color;
+ }
+ if (options.color && !options.strokeStyle) {
+ options.strokeStyle = options.color;
+ }
+ switch (this.style) {
+ case 'diamond':
+ this.drawDiamond(x,y,ctx, false, options);
+ break;
+ case 'filledDiamond':
+ this.drawDiamond(x,y,ctx, true, options);
+ break;
+ case 'circle':
+ this.drawCircle(x,y,ctx, false, options);
+ break;
+ case 'filledCircle':
+ this.drawCircle(x,y,ctx, true, options);
+ break;
+ case 'square':
+ this.drawSquare(x,y,ctx, false, options);
+ break;
+ case 'filledSquare':
+ this.drawSquare(x,y,ctx, true, options);
+ break;
+ case 'x':
+ this.drawX(x,y,ctx, true, options);
+ break;
+ case 'plus':
+ this.drawPlus(x,y,ctx, true, options);
+ break;
+ case 'dash':
+ this.drawDash(x,y,ctx, true, options);
+ break;
+ case 'line':
+ this.drawLine(x, y, ctx, false, options);
+ break;
+ default:
+ this.drawDiamond(x,y,ctx, false, options);
+ break;
+ }
+ }
+ };
+
+ // class: $.jqplot.shadowRenderer
+ // The default jqPlot shadow renderer, rendering shadows behind shapes.
+ $.jqplot.ShadowRenderer = function(options){
+ // Group: Properties
+
+ // prop: angle
+ // Angle of the shadow in degrees. Measured counter-clockwise from the x axis.
+ this.angle = 45;
+ // prop: offset
+ // Pixel offset at the given shadow angle of each shadow stroke from the last stroke.
+ this.offset = 1;
+ // prop: alpha
+ // alpha transparency of shadow stroke.
+ this.alpha = 0.07;
+ // prop: lineWidth
+ // width of the shadow line stroke.
+ this.lineWidth = 1.5;
+ // prop: lineJoin
+ // How line segments of the shadow are joined.
+ this.lineJoin = 'miter';
+ // prop: lineCap
+ // how ends of the shadow line are rendered.
+ this.lineCap = 'round';
+ // prop; closePath
+ // whether line path segment is closed upon itself.
+ this.closePath = false;
+ // prop: fill
+ // whether to fill the shape.
+ this.fill = false;
+ // prop: depth
+ // how many times the shadow is stroked. Each stroke will be offset by offset at angle degrees.
+ this.depth = 3;
+ this.strokeStyle = 'rgba(0,0,0,0.1)';
+ // prop: isarc
+ // whether the shadow is an arc or not.
+ this.isarc = false;
+
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.ShadowRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ };
+
+ // function: draw
+ // draws an transparent black (i.e. gray) shadow.
+ //
+ // ctx - canvas drawing context
+ // points - array of points or [x, y, radius, start angle (rad), end angle (rad)]
+ $.jqplot.ShadowRenderer.prototype.draw = function(ctx, points, options) {
+ ctx.save();
+ var opts = (options != null) ? options : {};
+ var fill = (opts.fill != null) ? opts.fill : this.fill;
+ var fillRect = (opts.fillRect != null) ? opts.fillRect : this.fillRect;
+ var closePath = (opts.closePath != null) ? opts.closePath : this.closePath;
+ var offset = (opts.offset != null) ? opts.offset : this.offset;
+ var alpha = (opts.alpha != null) ? opts.alpha : this.alpha;
+ var depth = (opts.depth != null) ? opts.depth : this.depth;
+ var isarc = (opts.isarc != null) ? opts.isarc : this.isarc;
+ var linePattern = (opts.linePattern != null) ? opts.linePattern : this.linePattern;
+ ctx.lineWidth = (opts.lineWidth != null) ? opts.lineWidth : this.lineWidth;
+ ctx.lineJoin = (opts.lineJoin != null) ? opts.lineJoin : this.lineJoin;
+ ctx.lineCap = (opts.lineCap != null) ? opts.lineCap : this.lineCap;
+ ctx.strokeStyle = opts.strokeStyle || this.strokeStyle || 'rgba(0,0,0,'+alpha+')';
+ ctx.fillStyle = opts.fillStyle || this.fillStyle || 'rgba(0,0,0,'+alpha+')';
+ for (var j=0; j<depth; j++) {
+ var ctxPattern = $.jqplot.LinePattern(ctx, linePattern);
+ ctx.translate(Math.cos(this.angle*Math.PI/180)*offset, Math.sin(this.angle*Math.PI/180)*offset);
+ ctxPattern.beginPath();
+ if (isarc) {
+ ctx.arc(points[0], points[1], points[2], points[3], points[4], true);
+ }
+ else if (fillRect) {
+ if (fillRect) {
+ ctx.fillRect(points[0], points[1], points[2], points[3]);
+ }
+ }
+ else if (points && points.length){
+ var move = true;
+ for (var i=0; i<points.length; i++) {
+ // skip to the first non-null point and move to it.
+ if (points[i][0] != null && points[i][1] != null) {
+ if (move) {
+ ctxPattern.moveTo(points[i][0], points[i][1]);
+ move = false;
+ }
+ else {
+ ctxPattern.lineTo(points[i][0], points[i][1]);
+ }
+ }
+ else {
+ move = true;
+ }
+ }
+
+ }
+ if (closePath) {
+ ctxPattern.closePath();
+ }
+ if (fill) {
+ ctx.fill();
+ }
+ else {
+ ctx.stroke();
+ }
+ }
+ ctx.restore();
+ };
+
+ // class: $.jqplot.shapeRenderer
+ // The default jqPlot shape renderer. Given a set of points will
+ // plot them and either stroke a line (fill = false) or fill them (fill = true).
+ // If a filled shape is desired, closePath = true must also be set to close
+ // the shape.
+ $.jqplot.ShapeRenderer = function(options){
+
+ this.lineWidth = 1.5;
+ // prop: linePattern
+ // line pattern 'dashed', 'dotted', 'solid', some combination
+ // of '-' and '.' characters such as '.-.' or a numerical array like
+ // [draw, skip, draw, skip, ...] such as [1, 10] to draw a dotted line,
+ // [1, 10, 20, 10] to draw a dot-dash line, and so on.
+ this.linePattern = 'solid';
+ // prop: lineJoin
+ // How line segments of the shadow are joined.
+ this.lineJoin = 'miter';
+ // prop: lineCap
+ // how ends of the shadow line are rendered.
+ this.lineCap = 'round';
+ // prop; closePath
+ // whether line path segment is closed upon itself.
+ this.closePath = false;
+ // prop: fill
+ // whether to fill the shape.
+ this.fill = false;
+ // prop: isarc
+ // whether the shadow is an arc or not.
+ this.isarc = false;
+ // prop: fillRect
+ // true to draw shape as a filled rectangle.
+ this.fillRect = false;
+ // prop: strokeRect
+ // true to draw shape as a stroked rectangle.
+ this.strokeRect = false;
+ // prop: clearRect
+ // true to cear a rectangle.
+ this.clearRect = false;
+ // prop: strokeStyle
+ // css color spec for the stoke style
+ this.strokeStyle = '#999999';
+ // prop: fillStyle
+ // css color spec for the fill style.
+ this.fillStyle = '#999999';
+
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.ShapeRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ };
+
+ // function: draw
+ // draws the shape.
+ //
+ // ctx - canvas drawing context
+ // points - array of points for shapes or
+ // [x, y, width, height] for rectangles or
+ // [x, y, radius, start angle (rad), end angle (rad)] for circles and arcs.
+ $.jqplot.ShapeRenderer.prototype.draw = function(ctx, points, options) {
+ ctx.save();
+ var opts = (options != null) ? options : {};
+ var fill = (opts.fill != null) ? opts.fill : this.fill;
+ var closePath = (opts.closePath != null) ? opts.closePath : this.closePath;
+ var fillRect = (opts.fillRect != null) ? opts.fillRect : this.fillRect;
+ var strokeRect = (opts.strokeRect != null) ? opts.strokeRect : this.strokeRect;
+ var clearRect = (opts.clearRect != null) ? opts.clearRect : this.clearRect;
+ var isarc = (opts.isarc != null) ? opts.isarc : this.isarc;
+ var linePattern = (opts.linePattern != null) ? opts.linePattern : this.linePattern;
+ var ctxPattern = $.jqplot.LinePattern(ctx, linePattern);
+ ctx.lineWidth = opts.lineWidth || this.lineWidth;
+ ctx.lineJoin = opts.lineJoin || this.lineJoin;
+ ctx.lineCap = opts.lineCap || this.lineCap;
+ ctx.strokeStyle = (opts.strokeStyle || opts.color) || this.strokeStyle;
+ ctx.fillStyle = opts.fillStyle || this.fillStyle;
+ ctx.beginPath();
+ if (isarc) {
+ ctx.arc(points[0], points[1], points[2], points[3], points[4], true);
+ if (closePath) {
+ ctx.closePath();
+ }
+ if (fill) {
+ ctx.fill();
+ }
+ else {
+ ctx.stroke();
+ }
+ ctx.restore();
+ return;
+ }
+ else if (clearRect) {
+ ctx.clearRect(points[0], points[1], points[2], points[3]);
+ ctx.restore();
+ return;
+ }
+ else if (fillRect || strokeRect) {
+ if (fillRect) {
+ ctx.fillRect(points[0], points[1], points[2], points[3]);
+ }
+ if (strokeRect) {
+ ctx.strokeRect(points[0], points[1], points[2], points[3]);
+ ctx.restore();
+ return;
+ }
+ }
+ else if (points && points.length){
+ var move = true;
+ for (var i=0; i<points.length; i++) {
+ // skip to the first non-null point and move to it.
+ if (points[i][0] != null && points[i][1] != null) {
+ if (move) {
+ ctxPattern.moveTo(points[i][0], points[i][1]);
+ move = false;
+ }
+ else {
+ ctxPattern.lineTo(points[i][0], points[i][1]);
+ }
+ }
+ else {
+ move = true;
+ }
+ }
+ if (closePath) {
+ ctxPattern.closePath();
+ }
+ if (fill) {
+ ctx.fill();
+ }
+ else {
+ ctx.stroke();
+ }
+ }
+ ctx.restore();
+ };
+
+ // class $.jqplot.TableLegendRenderer
+ // The default legend renderer for jqPlot.
+ $.jqplot.TableLegendRenderer = function(){
+ //
+ };
+
+ $.jqplot.TableLegendRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.TableLegendRenderer.prototype.addrow = function (label, color, pad, reverse) {
+ var rs = (pad) ? this.rowSpacing+'px' : '0px';
+ var tr;
+ var td;
+ var elem;
+ var div0;
+ var div1;
+ elem = document.createElement('tr');
+ tr = $(elem);
+ tr.addClass('jqplot-table-legend');
+ elem = null;
+
+ if (reverse){
+ tr.prependTo(this._elem);
+ }
+
+ else{
+ tr.appendTo(this._elem);
+ }
+
+ if (this.showSwatches) {
+ td = $(document.createElement('td'));
+ td.addClass('jqplot-table-legend jqplot-table-legend-swatch');
+ td.css({textAlign: 'center', paddingTop: rs});
+
+ div0 = $(document.createElement('div'));
+ div0.addClass('jqplot-table-legend-swatch-outline');
+ div1 = $(document.createElement('div'));
+ div1.addClass('jqplot-table-legend-swatch');
+ div1.css({backgroundColor: color, borderColor: color});
+
+ tr.append(td.append(div0.append(div1)));
+
+ // $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
+ // '<div><div class="jqplot-table-legend-swatch" style="background-color:'+color+';border-color:'+color+';"></div>'+
+ // '</div></td>').appendTo(tr);
+ }
+ if (this.showLabels) {
+ td = $(document.createElement('td'));
+ td.addClass('jqplot-table-legend jqplot-table-legend-label');
+ td.css('paddingTop', rs);
+ tr.append(td);
+
+ // elem = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
+ // elem.appendTo(tr);
+ if (this.escapeHtml) {
+ td.text(label);
+ }
+ else {
+ td.html(label);
+ }
+ }
+ td = null;
+ div0 = null;
+ div1 = null;
+ tr = null;
+ elem = null;
+ };
+
+ // called with scope of legend
+ $.jqplot.TableLegendRenderer.prototype.draw = function() {
+ if (this._elem) {
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ if (this.show) {
+ var series = this._series;
+ // make a table. one line label per row.
+ var elem = document.createElement('table');
+ this._elem = $(elem);
+ this._elem.addClass('jqplot-table-legend');
+
+ var ss = {position:'absolute'};
+ if (this.background) {
+ ss['background'] = this.background;
+ }
+ if (this.border) {
+ ss['border'] = this.border;
+ }
+ if (this.fontSize) {
+ ss['fontSize'] = this.fontSize;
+ }
+ if (this.fontFamily) {
+ ss['fontFamily'] = this.fontFamily;
+ }
+ if (this.textColor) {
+ ss['textColor'] = this.textColor;
+ }
+ if (this.marginTop != null) {
+ ss['marginTop'] = this.marginTop;
+ }
+ if (this.marginBottom != null) {
+ ss['marginBottom'] = this.marginBottom;
+ }
+ if (this.marginLeft != null) {
+ ss['marginLeft'] = this.marginLeft;
+ }
+ if (this.marginRight != null) {
+ ss['marginRight'] = this.marginRight;
+ }
+
+
+ var pad = false,
+ reverse = false,
+ s;
+ for (var i = 0; i< series.length; i++) {
+ s = series[i];
+ if (s._stack || s.renderer.constructor == $.jqplot.BezierCurveRenderer){
+ reverse = true;
+ }
+ if (s.show && s.showLabel) {
+ var lt = this.labels[i] || s.label.toString();
+ if (lt) {
+ var color = s.color;
+ if (reverse && i < series.length - 1){
+ pad = true;
+ }
+ else if (reverse && i == series.length - 1){
+ pad = false;
+ }
+ this.renderer.addrow.call(this, lt, color, pad, reverse);
+ pad = true;
+ }
+ // let plugins add more rows to legend. Used by trend line plugin.
+ for (var j=0; j<$.jqplot.addLegendRowHooks.length; j++) {
+ var item = $.jqplot.addLegendRowHooks[j].call(this, s);
+ if (item) {
+ this.renderer.addrow.call(this, item.label, item.color, pad);
+ pad = true;
+ }
+ }
+ lt = null;
+ }
+ }
+ }
+ return this._elem;
+ };
+
+ $.jqplot.TableLegendRenderer.prototype.pack = function(offsets) {
+ if (this.show) {
+ if (this.placement == 'insideGrid') {
+ switch (this.location) {
+ case 'nw':
+ var a = offsets.left;
+ var b = offsets.top;
+ this._elem.css('left', a);
+ this._elem.css('top', b);
+ break;
+ case 'n':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = offsets.top;
+ this._elem.css('left', a);
+ this._elem.css('top', b);
+ break;
+ case 'ne':
+ var a = offsets.right;
+ var b = offsets.top;
+ this._elem.css({right:a, top:b});
+ break;
+ case 'e':
+ var a = offsets.right;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({right:a, top:b});
+ break;
+ case 'se':
+ var a = offsets.right;
+ var b = offsets.bottom;
+ this._elem.css({right:a, bottom:b});
+ break;
+ case 's':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = offsets.bottom;
+ this._elem.css({left:a, bottom:b});
+ break;
+ case 'sw':
+ var a = offsets.left;
+ var b = offsets.bottom;
+ this._elem.css({left:a, bottom:b});
+ break;
+ case 'w':
+ var a = offsets.left;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({left:a, top:b});
+ break;
+ default: // same as 'se'
+ var a = offsets.right;
+ var b = offsets.bottom;
+ this._elem.css({right:a, bottom:b});
+ break;
+ }
+
+ }
+ else if (this.placement == 'outside'){
+ switch (this.location) {
+ case 'nw':
+ var a = this._plotDimensions.width - offsets.left;
+ var b = offsets.top;
+ this._elem.css('right', a);
+ this._elem.css('top', b);
+ break;
+ case 'n':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = this._plotDimensions.height - offsets.top;
+ this._elem.css('left', a);
+ this._elem.css('bottom', b);
+ break;
+ case 'ne':
+ var a = this._plotDimensions.width - offsets.right;
+ var b = offsets.top;
+ this._elem.css({left:a, top:b});
+ break;
+ case 'e':
+ var a = this._plotDimensions.width - offsets.right;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({left:a, top:b});
+ break;
+ case 'se':
+ var a = this._plotDimensions.width - offsets.right;
+ var b = offsets.bottom;
+ this._elem.css({left:a, bottom:b});
+ break;
+ case 's':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = this._plotDimensions.height - offsets.bottom;
+ this._elem.css({left:a, top:b});
+ break;
+ case 'sw':
+ var a = this._plotDimensions.width - offsets.left;
+ var b = offsets.bottom;
+ this._elem.css({right:a, bottom:b});
+ break;
+ case 'w':
+ var a = this._plotDimensions.width - offsets.left;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({right:a, top:b});
+ break;
+ default: // same as 'se'
+ var a = offsets.right;
+ var b = offsets.bottom;
+ this._elem.css({right:a, bottom:b});
+ break;
+ }
+ }
+ else {
+ switch (this.location) {
+ case 'nw':
+ this._elem.css({left:0, top:offsets.top});
+ break;
+ case 'n':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ this._elem.css({left: a, top:offsets.top});
+ break;
+ case 'ne':
+ this._elem.css({right:0, top:offsets.top});
+ break;
+ case 'e':
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({right:offsets.right, top:b});
+ break;
+ case 'se':
+ this._elem.css({right:offsets.right, bottom:offsets.bottom});
+ break;
+ case 's':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ this._elem.css({left: a, bottom:offsets.bottom});
+ break;
+ case 'sw':
+ this._elem.css({left:offsets.left, bottom:offsets.bottom});
+ break;
+ case 'w':
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({left:offsets.left, top:b});
+ break;
+ default: // same as 'se'
+ this._elem.css({right:offsets.right, bottom:offsets.bottom});
+ break;
+ }
+ }
+ }
+ };
+
+ /**
+ * Class: $.jqplot.ThemeEngine
+ * Theme Engine provides a programatic way to change some of the more
+ * common jqplot styling options such as fonts, colors and grid options.
+ * A theme engine instance is created with each plot. The theme engine
+ * manages a collection of themes which can be modified, added to, or
+ * applied to the plot.
+ *
+ * The themeEngine class is not instantiated directly.
+ * When a plot is initialized, the current plot options are scanned
+ * an a default theme named "Default" is created. This theme is
+ * used as the basis for other themes added to the theme engine and
+ * is always available.
+ *
+ * A theme is a simple javascript object with styling parameters for
+ * various entities of the plot. A theme has the form:
+ *
+ *
+ * > {
+ * > _name:f "Default",
+ * > target: {
+ * > backgroundColor: "transparent"
+ * > },
+ * > legend: {
+ * > textColor: null,
+ * > fontFamily: null,
+ * > fontSize: null,
+ * > border: null,
+ * > background: null
+ * > },
+ * > title: {
+ * > textColor: "rgb(102, 102, 102)",
+ * > fontFamily: "'Trebuchet MS',Arial,Helvetica,sans-serif",
+ * > fontSize: "19.2px",
+ * > textAlign: "center"
+ * > },
+ * > seriesStyles: {},
+ * > series: [{
+ * > color: "#4bb2c5",
+ * > lineWidth: 2.5,
+ * > linePattern: "solid",
+ * > shadow: true,
+ * > fillColor: "#4bb2c5",
+ * > showMarker: true,
+ * > markerOptions: {
+ * > color: "#4bb2c5",
+ * > show: true,
+ * > style: 'filledCircle',
+ * > lineWidth: 1.5,
+ * > size: 4,
+ * > shadow: true
+ * > }
+ * > }],
+ * > grid: {
+ * > drawGridlines: true,
+ * > gridLineColor: "#cccccc",
+ * > gridLineWidth: 1,
+ * > backgroundColor: "#fffdf6",
+ * > borderColor: "#999999",
+ * > borderWidth: 2,
+ * > shadow: true
+ * > },
+ * > axesStyles: {
+ * > label: {},
+ * > ticks: {}
+ * > },
+ * > axes: {
+ * > xaxis: {
+ * > borderColor: "#999999",
+ * > borderWidth: 2,
+ * > ticks: {
+ * > show: true,
+ * > showGridline: true,
+ * > showLabel: true,
+ * > showMark: true,
+ * > size: 4,
+ * > textColor: "",
+ * > whiteSpace: "nowrap",
+ * > fontSize: "12px",
+ * > fontFamily: "'Trebuchet MS',Arial,Helvetica,sans-serif"
+ * > },
+ * > label: {
+ * > textColor: "rgb(102, 102, 102)",
+ * > whiteSpace: "normal",
+ * > fontSize: "14.6667px",
+ * > fontFamily: "'Trebuchet MS',Arial,Helvetica,sans-serif",
+ * > fontWeight: "400"
+ * > }
+ * > },
+ * > yaxis: {
+ * > borderColor: "#999999",
+ * > borderWidth: 2,
+ * > ticks: {
+ * > show: true,
+ * > showGridline: true,
+ * > showLabel: true,
+ * > showMark: true,
+ * > size: 4,
+ * > textColor: "",
+ * > whiteSpace: "nowrap",
+ * > fontSize: "12px",
+ * > fontFamily: "'Trebuchet MS',Arial,Helvetica,sans-serif"
+ * > },
+ * > label: {
+ * > textColor: null,
+ * > whiteSpace: null,
+ * > fontSize: null,
+ * > fontFamily: null,
+ * > fontWeight: null
+ * > }
+ * > },
+ * > x2axis: {...
+ * > },
+ * > ...
+ * > y9axis: {...
+ * > }
+ * > }
+ * > }
+ *
+ * "seriesStyles" is a style object that will be applied to all series in the plot.
+ * It will forcibly override any styles applied on the individual series. "axesStyles" is
+ * a style object that will be applied to all axes in the plot. It will also forcibly
+ * override any styles on the individual axes.
+ *
+ * The example shown above has series options for a line series. Options for other
+ * series types are shown below:
+ *
+ * Bar Series:
+ *
+ * > {
+ * > color: "#4bb2c5",
+ * > seriesColors: ["#4bb2c5", "#EAA228", "#c5b47f", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc", "#c747a3", "#cddf54", "#FBD178", "#26B4E3", "#bd70c7"],
+ * > lineWidth: 2.5,
+ * > shadow: true,
+ * > barPadding: 2,
+ * > barMargin: 10,
+ * > barWidth: 15.09375,
+ * > highlightColors: ["rgb(129,201,214)", "rgb(129,201,214)", "rgb(129,201,214)", "rgb(129,201,214)", "rgb(129,201,214)", "rgb(129,201,214)", "rgb(129,201,214)", "rgb(129,201,214)"]
+ * > }
+ *
+ * Pie Series:
+ *
+ * > {
+ * > seriesColors: ["#4bb2c5", "#EAA228", "#c5b47f", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc", "#c747a3", "#cddf54", "#FBD178", "#26B4E3", "#bd70c7"],
+ * > padding: 20,
+ * > sliceMargin: 0,
+ * > fill: true,
+ * > shadow: true,
+ * > startAngle: 0,
+ * > lineWidth: 2.5,
+ * > highlightColors: ["rgb(129,201,214)", "rgb(240,189,104)", "rgb(214,202,165)", "rgb(137,180,158)", "rgb(168,180,137)", "rgb(180,174,89)", "rgb(180,113,161)", "rgb(129,141,236)", "rgb(227,205,120)", "rgb(255,138,76)", "rgb(76,169,219)", "rgb(215,126,190)", "rgb(220,232,135)", "rgb(200,167,96)", "rgb(103,202,235)", "rgb(208,154,215)"]
+ * > }
+ *
+ * Funnel Series:
+ *
+ * > {
+ * > color: "#4bb2c5",
+ * > lineWidth: 2,
+ * > shadow: true,
+ * > padding: {
+ * > top: 20,
+ * > right: 20,
+ * > bottom: 20,
+ * > left: 20
+ * > },
+ * > sectionMargin: 6,
+ * > seriesColors: ["#4bb2c5", "#EAA228", "#c5b47f", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc", "#c747a3", "#cddf54", "#FBD178", "#26B4E3", "#bd70c7"],
+ * > highlightColors: ["rgb(147,208,220)", "rgb(242,199,126)", "rgb(220,210,178)", "rgb(154,191,172)", "rgb(180,191,154)", "rgb(191,186,112)", "rgb(191,133,174)", "rgb(147,157,238)", "rgb(231,212,139)", "rgb(255,154,102)", "rgb(102,181,224)", "rgb(221,144,199)", "rgb(225,235,152)", "rgb(200,167,96)", "rgb(124,210,238)", "rgb(215,169,221)"]
+ * > }
+ *
+ */
+ $.jqplot.ThemeEngine = function(){
+ // Group: Properties
+ //
+ // prop: themes
+ // hash of themes managed by the theme engine.
+ // Indexed by theme name.
+ this.themes = {};
+ // prop: activeTheme
+ // Pointer to currently active theme
+ this.activeTheme=null;
+
+ };
+
+ // called with scope of plot
+ $.jqplot.ThemeEngine.prototype.init = function() {
+ // get the Default theme from the current plot settings.
+ var th = new $.jqplot.Theme({_name:'Default'});
+ var n, i, nn;
+
+ for (n in th.target) {
+ if (n == "textColor") {
+ th.target[n] = this.target.css('color');
+ }
+ else {
+ th.target[n] = this.target.css(n);
+ }
+ }
+
+ if (this.title.show && this.title._elem) {
+ for (n in th.title) {
+ if (n == "textColor") {
+ th.title[n] = this.title._elem.css('color');
+ }
+ else {
+ th.title[n] = this.title._elem.css(n);
+ }
+ }
+ }
+
+ for (n in th.grid) {
+ th.grid[n] = this.grid[n];
+ }
+ if (th.grid.backgroundColor == null && this.grid.background != null) {
+ th.grid.backgroundColor = this.grid.background;
+ }
+ if (this.legend.show && this.legend._elem) {
+ for (n in th.legend) {
+ if (n == 'textColor') {
+ th.legend[n] = this.legend._elem.css('color');
+ }
+ else {
+ th.legend[n] = this.legend._elem.css(n);
+ }
+ }
+ }
+ var s;
+
+ for (i=0; i<this.series.length; i++) {
+ s = this.series[i];
+ if (s.renderer.constructor == $.jqplot.LineRenderer) {
+ th.series.push(new LineSeriesProperties());
+ }
+ else if (s.renderer.constructor == $.jqplot.BarRenderer) {
+ th.series.push(new BarSeriesProperties());
+ }
+ else if (s.renderer.constructor == $.jqplot.PieRenderer) {
+ th.series.push(new PieSeriesProperties());
+ }
+ else if (s.renderer.constructor == $.jqplot.DonutRenderer) {
+ th.series.push(new DonutSeriesProperties());
+ }
+ else if (s.renderer.constructor == $.jqplot.FunnelRenderer) {
+ th.series.push(new FunnelSeriesProperties());
+ }
+ else if (s.renderer.constructor == $.jqplot.MeterGaugeRenderer) {
+ th.series.push(new MeterSeriesProperties());
+ }
+ else {
+ th.series.push({});
+ }
+ for (n in th.series[i]) {
+ th.series[i][n] = s[n];
+ }
+ }
+ var a, ax;
+ for (n in this.axes) {
+ ax = this.axes[n];
+ a = th.axes[n] = new AxisProperties();
+ a.borderColor = ax.borderColor;
+ a.borderWidth = ax.borderWidth;
+ if (ax._ticks && ax._ticks[0]) {
+ for (nn in a.ticks) {
+ if (ax._ticks[0].hasOwnProperty(nn)) {
+ a.ticks[nn] = ax._ticks[0][nn];
+ }
+ else if (ax._ticks[0]._elem){
+ a.ticks[nn] = ax._ticks[0]._elem.css(nn);
+ }
+ }
+ }
+ if (ax._label && ax._label.show) {
+ for (nn in a.label) {
+ // a.label[nn] = ax._label._elem.css(nn);
+ if (ax._label[nn]) {
+ a.label[nn] = ax._label[nn];
+ }
+ else if (ax._label._elem){
+ if (nn == 'textColor') {
+ a.label[nn] = ax._label._elem.css('color');
+ }
+ else {
+ a.label[nn] = ax._label._elem.css(nn);
+ }
+ }
+ }
+ }
+ }
+ this.themeEngine._add(th);
+ this.themeEngine.activeTheme = this.themeEngine.themes[th._name];
+ };
+ /**
+ * Group: methods
+ *
+ * method: get
+ *
+ * Get and return the named theme or the active theme if no name given.
+ *
+ * parameter:
+ *
+ * name - name of theme to get.
+ *
+ * returns:
+ *
+ * Theme instance of given name.
+ */
+ $.jqplot.ThemeEngine.prototype.get = function(name) {
+ if (!name) {
+ // return the active theme
+ return this.activeTheme;
+ }
+ else {
+ return this.themes[name];
+ }
+ };
+
+ function numericalOrder(a,b) { return a-b; }
+
+ /**
+ * method: getThemeNames
+ *
+ * Return the list of theme names in this manager in alpha-numerical order.
+ *
+ * parameter:
+ *
+ * None
+ *
+ * returns:
+ *
+ * A the list of theme names in this manager in alpha-numerical order.
+ */
+ $.jqplot.ThemeEngine.prototype.getThemeNames = function() {
+ var tn = [];
+ for (var n in this.themes) {
+ tn.push(n);
+ }
+ return tn.sort(numericalOrder);
+ };
+
+ /**
+ * method: getThemes
+ *
+ * Return a list of themes in alpha-numerical order by name.
+ *
+ * parameter:
+ *
+ * None
+ *
+ * returns:
+ *
+ * A list of themes in alpha-numerical order by name.
+ */
+ $.jqplot.ThemeEngine.prototype.getThemes = function() {
+ var tn = [];
+ var themes = [];
+ for (var n in this.themes) {
+ tn.push(n);
+ }
+ tn.sort(numericalOrder);
+ for (var i=0; i<tn.length; i++) {
+ themes.push(this.themes[tn[i]]);
+ }
+ return themes;
+ };
+
+ $.jqplot.ThemeEngine.prototype.activate = function(plot, name) {
+ // sometimes need to redraw whole plot.
+ var redrawPlot = false;
+ if (!name && this.activeTheme && this.activeTheme._name) {
+ name = this.activeTheme._name;
+ }
+ if (!this.themes.hasOwnProperty(name)) {
+ throw new Error("No theme of that name");
+ }
+ else {
+ var th = this.themes[name];
+ this.activeTheme = th;
+ var val, checkBorderColor = false, checkBorderWidth = false;
+ var arr = ['xaxis', 'x2axis', 'yaxis', 'y2axis'];
+
+ for (i=0; i<arr.length; i++) {
+ var ax = arr[i];
+ if (th.axesStyles.borderColor != null) {
+ plot.axes[ax].borderColor = th.axesStyles.borderColor;
+ }
+ if (th.axesStyles.borderWidth != null) {
+ plot.axes[ax].borderWidth = th.axesStyles.borderWidth;
+ }
+ }
+
+ for (var axname in plot.axes) {
+ var axis = plot.axes[axname];
+ if (axis.show) {
+ var thaxis = th.axes[axname] || {};
+ var thaxstyle = th.axesStyles;
+ var thax = $.jqplot.extend(true, {}, thaxis, thaxstyle);
+ val = (th.axesStyles.borderColor != null) ? th.axesStyles.borderColor : thax.borderColor;
+ if (thax.borderColor != null) {
+ axis.borderColor = thax.borderColor;
+ redrawPlot = true;
+ }
+ val = (th.axesStyles.borderWidth != null) ? th.axesStyles.borderWidth : thax.borderWidth;
+ if (thax.borderWidth != null) {
+ axis.borderWidth = thax.borderWidth;
+ redrawPlot = true;
+ }
+ if (axis._ticks && axis._ticks[0]) {
+ for (var nn in thax.ticks) {
+ // val = null;
+ // if (th.axesStyles.ticks && th.axesStyles.ticks[nn] != null) {
+ // val = th.axesStyles.ticks[nn];
+ // }
+ // else if (thax.ticks[nn] != null){
+ // val = thax.ticks[nn]
+ // }
+ val = thax.ticks[nn];
+ if (val != null) {
+ axis.tickOptions[nn] = val;
+ axis._ticks = [];
+ redrawPlot = true;
+ }
+ }
+ }
+ if (axis._label && axis._label.show) {
+ for (var nn in thax.label) {
+ // val = null;
+ // if (th.axesStyles.label && th.axesStyles.label[nn] != null) {
+ // val = th.axesStyles.label[nn];
+ // }
+ // else if (thax.label && thax.label[nn] != null){
+ // val = thax.label[nn]
+ // }
+ val = thax.label[nn];
+ if (val != null) {
+ axis.labelOptions[nn] = val;
+ redrawPlot = true;
+ }
+ }
+ }
+
+ }
+ }
+
+ for (var n in th.grid) {
+ if (th.grid[n] != null) {
+ plot.grid[n] = th.grid[n];
+ }
+ }
+ if (!redrawPlot) {
+ plot.grid.draw();
+ }
+
+ if (plot.legend.show) {
+ for (n in th.legend) {
+ if (th.legend[n] != null) {
+ plot.legend[n] = th.legend[n];
+ }
+ }
+ }
+ if (plot.title.show) {
+ for (n in th.title) {
+ if (th.title[n] != null) {
+ plot.title[n] = th.title[n];
+ }
+ }
+ }
+
+ var i;
+ for (i=0; i<th.series.length; i++) {
+ var opts = {};
+ var redrawSeries = false;
+ for (n in th.series[i]) {
+ val = (th.seriesStyles[n] != null) ? th.seriesStyles[n] : th.series[i][n];
+ if (val != null) {
+ opts[n] = val;
+ if (n == 'color') {
+ plot.series[i].renderer.shapeRenderer.fillStyle = val;
+ plot.series[i].renderer.shapeRenderer.strokeStyle = val;
+ plot.series[i][n] = val;
+ }
+ else if ((n == 'lineWidth') || (n == 'linePattern')) {
+ plot.series[i].renderer.shapeRenderer[n] = val;
+ plot.series[i][n] = val;
+ }
+ else if (n == 'markerOptions') {
+ merge (plot.series[i].markerOptions, val);
+ merge (plot.series[i].markerRenderer, val);
+ }
+ else {
+ plot.series[i][n] = val;
+ }
+ redrawPlot = true;
+ }
+ }
+ }
+
+ if (redrawPlot) {
+ plot.target.empty();
+ plot.draw();
+ }
+
+ for (n in th.target) {
+ if (th.target[n] != null) {
+ plot.target.css(n, th.target[n]);
+ }
+ }
+ }
+
+ };
+
+ $.jqplot.ThemeEngine.prototype._add = function(theme, name) {
+ if (name) {
+ theme._name = name;
+ }
+ if (!theme._name) {
+ theme._name = Date.parse(new Date());
+ }
+ if (!this.themes.hasOwnProperty(theme._name)) {
+ this.themes[theme._name] = theme;
+ }
+ else {
+ throw new Error("jqplot.ThemeEngine Error: Theme already in use");
+ }
+ };
+
+ // method remove
+ // Delete the named theme, return true on success, false on failure.
+
+
+ /**
+ * method: remove
+ *
+ * Remove the given theme from the themeEngine.
+ *
+ * parameters:
+ *
+ * name - name of the theme to remove.
+ *
+ * returns:
+ *
+ * true on success, false on failure.
+ */
+ $.jqplot.ThemeEngine.prototype.remove = function(name) {
+ if (name == 'Default') {
+ return false;
+ }
+ return delete this.themes[name];
+ };
+
+ /**
+ * method: newTheme
+ *
+ * Create a new theme based on the default theme, adding it the themeEngine.
+ *
+ * parameters:
+ *
+ * name - name of the new theme.
+ * obj - optional object of styles to be applied to this new theme.
+ *
+ * returns:
+ *
+ * new Theme object.
+ */
+ $.jqplot.ThemeEngine.prototype.newTheme = function(name, obj) {
+ if (typeof(name) == 'object') {
+ obj = obj || name;
+ name = null;
+ }
+ if (obj && obj._name) {
+ name = obj._name;
+ }
+ else {
+ name = name || Date.parse(new Date());
+ }
+ // var th = new $.jqplot.Theme(name);
+ var th = this.copy(this.themes['Default']._name, name);
+ $.jqplot.extend(th, obj);
+ return th;
+ };
+
+ // function clone(obj) {
+ // return eval(obj.toSource());
+ // }
+
+ function clone(obj){
+ if(obj == null || typeof(obj) != 'object'){
+ return obj;
+ }
+
+ var temp = new obj.constructor();
+ for(var key in obj){
+ temp[key] = clone(obj[key]);
+ }
+ return temp;
+ }
+
+ $.jqplot.clone = clone;
+
+ function merge(obj1, obj2) {
+ if (obj2 == null || typeof(obj2) != 'object') {
+ return;
+ }
+ for (var key in obj2) {
+ if (key == 'highlightColors') {
+ obj1[key] = clone(obj2[key]);
+ }
+ if (obj2[key] != null && typeof(obj2[key]) == 'object') {
+ if (!obj1.hasOwnProperty(key)) {
+ obj1[key] = {};
+ }
+ merge(obj1[key], obj2[key]);
+ }
+ else {
+ obj1[key] = obj2[key];
+ }
+ }
+ }
+
+ $.jqplot.merge = merge;
+
+ // Use the jQuery 1.3.2 extend function since behaviour in jQuery 1.4 seems problematic
+ $.jqplot.extend = function() {
+ // copy reference to target object
+ var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !toString.call(target) === "[object Function]" ) {
+ target = {};
+ }
+
+ for ( ; i < length; i++ ){
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( var name in options ) {
+ var src = target[ name ], copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging object values
+ if ( deep && copy && typeof copy === "object" && !copy.nodeType ) {
+ target[ name ] = $.jqplot.extend( deep,
+ // Never move original objects, clone them
+ src || ( copy.length != null ? [ ] : { } )
+ , copy );
+ }
+ // Don't bring in undefined values
+ else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+ // Return the modified object
+ return target;
+ };
+
+ /**
+ * method: rename
+ *
+ * Rename a theme.
+ *
+ * parameters:
+ *
+ * oldName - current name of the theme.
+ * newName - desired name of the theme.
+ *
+ * returns:
+ *
+ * new Theme object.
+ */
+ $.jqplot.ThemeEngine.prototype.rename = function (oldName, newName) {
+ if (oldName == 'Default' || newName == 'Default') {
+ throw new Error ("jqplot.ThemeEngine Error: Cannot rename from/to Default");
+ }
+ if (this.themes.hasOwnProperty(newName)) {
+ throw new Error ("jqplot.ThemeEngine Error: New name already in use.");
+ }
+ else if (this.themes.hasOwnProperty(oldName)) {
+ var th = this.copy (oldName, newName);
+ this.remove(oldName);
+ return th;
+ }
+ throw new Error("jqplot.ThemeEngine Error: Old name or new name invalid");
+ };
+
+ /**
+ * method: copy
+ *
+ * Create a copy of an existing theme in the themeEngine, adding it the themeEngine.
+ *
+ * parameters:
+ *
+ * sourceName - name of the existing theme.
+ * targetName - name of the copy.
+ * obj - optional object of style parameter to apply to the new theme.
+ *
+ * returns:
+ *
+ * new Theme object.
+ */
+ $.jqplot.ThemeEngine.prototype.copy = function (sourceName, targetName, obj) {
+ if (targetName == 'Default') {
+ throw new Error ("jqplot.ThemeEngine Error: Cannot copy over Default theme");
+ }
+ if (!this.themes.hasOwnProperty(sourceName)) {
+ var s = "jqplot.ThemeEngine Error: Source name invalid";
+ throw new Error(s);
+ }
+ if (this.themes.hasOwnProperty(targetName)) {
+ var s = "jqplot.ThemeEngine Error: Target name invalid";
+ throw new Error(s);
+ }
+ else {
+ var th = clone(this.themes[sourceName]);
+ th._name = targetName;
+ $.jqplot.extend(true, th, obj);
+ this._add(th);
+ return th;
+ }
+ };
+
+
+ $.jqplot.Theme = function(name, obj) {
+ if (typeof(name) == 'object') {
+ obj = obj || name;
+ name = null;
+ }
+ name = name || Date.parse(new Date());
+ this._name = name;
+ this.target = {
+ backgroundColor: null
+ };
+ this.legend = {
+ textColor: null,
+ fontFamily: null,
+ fontSize: null,
+ border: null,
+ background: null
+ };
+ this.title = {
+ textColor: null,
+ fontFamily: null,
+ fontSize: null,
+ textAlign: null
+ };
+ this.seriesStyles = {};
+ this.series = [];
+ this.grid = {
+ drawGridlines: null,
+ gridLineColor: null,
+ gridLineWidth: null,
+ backgroundColor: null,
+ borderColor: null,
+ borderWidth: null,
+ shadow: null
+ };
+ this.axesStyles = {label:{}, ticks:{}};
+ this.axes = {};
+ if (typeof(obj) == 'string') {
+ this._name = obj;
+ }
+ else if(typeof(obj) == 'object') {
+ $.jqplot.extend(true, this, obj);
+ }
+ };
+
+ var AxisProperties = function() {
+ this.borderColor = null;
+ this.borderWidth = null;
+ this.ticks = new AxisTicks();
+ this.label = new AxisLabel();
+ };
+
+ var AxisTicks = function() {
+ this.show = null;
+ this.showGridline = null;
+ this.showLabel = null;
+ this.showMark = null;
+ this.size = null;
+ this.textColor = null;
+ this.whiteSpace = null;
+ this.fontSize = null;
+ this.fontFamily = null;
+ };
+
+ var AxisLabel = function() {
+ this.textColor = null;
+ this.whiteSpace = null;
+ this.fontSize = null;
+ this.fontFamily = null;
+ this.fontWeight = null;
+ };
+
+ var LineSeriesProperties = function() {
+ this.color=null;
+ this.lineWidth=null;
+ this.linePattern=null;
+ this.shadow=null;
+ this.fillColor=null;
+ this.showMarker=null;
+ this.markerOptions = new MarkerOptions();
+ };
+
+ var MarkerOptions = function() {
+ this.show = null;
+ this.style = null;
+ this.lineWidth = null;
+ this.size = null;
+ this.color = null;
+ this.shadow = null;
+ };
+
+ var BarSeriesProperties = function() {
+ this.color=null;
+ this.seriesColors=null;
+ this.lineWidth=null;
+ this.shadow=null;
+ this.barPadding=null;
+ this.barMargin=null;
+ this.barWidth=null;
+ this.highlightColors=null;
+ };
+
+ var PieSeriesProperties = function() {
+ this.seriesColors=null;
+ this.padding=null;
+ this.sliceMargin=null;
+ this.fill=null;
+ this.shadow=null;
+ this.startAngle=null;
+ this.lineWidth=null;
+ this.highlightColors=null;
+ };
+
+ var DonutSeriesProperties = function() {
+ this.seriesColors=null;
+ this.padding=null;
+ this.sliceMargin=null;
+ this.fill=null;
+ this.shadow=null;
+ this.startAngle=null;
+ this.lineWidth=null;
+ this.innerDiameter=null;
+ this.thickness=null;
+ this.ringMargin=null;
+ this.highlightColors=null;
+ };
+
+ var FunnelSeriesProperties = function() {
+ this.color=null;
+ this.lineWidth=null;
+ this.shadow=null;
+ this.padding=null;
+ this.sectionMargin=null;
+ this.seriesColors=null;
+ this.highlightColors=null;
+ };
+
+ var MeterSeriesProperties = function() {
+ this.padding=null;
+ this.backgroundColor=null;
+ this.ringColor=null;
+ this.tickColor=null;
+ this.ringWidth=null;
+ this.intervalColors=null;
+ this.intervalInnerRadius=null;
+ this.intervalOuterRadius=null;
+ this.hubRadius=null;
+ this.needleThickness=null;
+ this.needlePad=null;
+ };
+
+
+
+
+ $.fn.jqplotChildText = function() {
+ return $(this).contents().filter(function() {
+ return this.nodeType == 3; // Node.TEXT_NODE not defined in I7
+ }).text();
+ };
+
+ // Returns font style as abbreviation for "font" property.
+ $.fn.jqplotGetComputedFontStyle = function() {
+ var css = window.getComputedStyle ? window.getComputedStyle(this[0], "") : this[0].currentStyle;
+ var attrs = css['font-style'] ? ['font-style', 'font-weight', 'font-size', 'font-family'] : ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'];
+ var style = [];
+
+ for (var i=0 ; i < attrs.length; ++i) {
+ var attr = String(css[attrs[i]]);
+
+ if (attr && attr != 'normal') {
+ style.push(attr);
+ }
+ }
+ return style.join(' ');
+ };
+
+ /**
+ * Namespace: $.fn
+ * jQuery namespace to attach functions to jQuery elements.
+ *
+ */
+
+ $.fn.jqplotToImageCanvas = function(options) {
+
+ options = options || {};
+ var x_offset = (options.x_offset == null) ? 0 : options.x_offset;
+ var y_offset = (options.y_offset == null) ? 0 : options.y_offset;
+ var backgroundColor = (options.backgroundColor == null) ? 'rgb(255,255,255)' : options.backgroundColor;
+
+ if ($(this).width() == 0 || $(this).height() == 0) {
+ return null;
+ }
+
+ // excanvas and hence IE < 9 do not support toDataURL and cannot export images.
+ if ($.jqplot.use_excanvas) {
+ return null;
+ }
+
+ var newCanvas = document.createElement("canvas");
+ var h = $(this).outerHeight(true);
+ var w = $(this).outerWidth(true);
+ var offs = $(this).offset();
+ var plotleft = offs.left;
+ var plottop = offs.top;
+ var transx = 0, transy = 0;
+
+ // have to check if any elements are hanging outside of plot area before rendering,
+ // since changing width of canvas will erase canvas.
+
+ var clses = ['jqplot-table-legend', 'jqplot-xaxis-tick', 'jqplot-x2axis-tick', 'jqplot-yaxis-tick', 'jqplot-y2axis-tick', 'jqplot-y3axis-tick',
+ 'jqplot-y4axis-tick', 'jqplot-y5axis-tick', 'jqplot-y6axis-tick', 'jqplot-y7axis-tick', 'jqplot-y8axis-tick', 'jqplot-y9axis-tick',
+ 'jqplot-xaxis-label', 'jqplot-x2axis-label', 'jqplot-yaxis-label', 'jqplot-y2axis-label', 'jqplot-y3axis-label', 'jqplot-y4axis-label',
+ 'jqplot-y5axis-label', 'jqplot-y6axis-label', 'jqplot-y7axis-label', 'jqplot-y8axis-label', 'jqplot-y9axis-label' ];
+
+ var temptop, templeft, tempbottom, tempright;
+
+ for (var i = 0; i < clses.length; i++) {
+ $(this).find('.'+clses[i]).each(function() {
+ temptop = $(this).offset().top - plottop;
+ templeft = $(this).offset().left - plotleft;
+ tempright = templeft + $(this).outerWidth(true) + transx;
+ tempbottom = temptop + $(this).outerHeight(true) + transy;
+ if (templeft < -transx) {
+ w = w - transx - templeft;
+ transx = -templeft;
+ }
+ if (temptop < -transy) {
+ h = h - transy - temptop;
+ transy = - temptop;
+ }
+ if (tempright > w) {
+ w = tempright;
+ }
+ if (tempbottom > h) {
+ h = tempbottom;
+ }
+ });
+ }
+
+ newCanvas.width = w + Number(x_offset);
+ newCanvas.height = h + Number(y_offset);
+
+ var newContext = newCanvas.getContext("2d");
+
+ newContext.save();
+ newContext.fillStyle = backgroundColor;
+ newContext.fillRect(0,0, newCanvas.width, newCanvas.height);
+ newContext.restore();
+
+ newContext.translate(transx, transy);
+ newContext.textAlign = 'left';
+ newContext.textBaseline = 'top';
+
+ function getLineheight(el) {
+ var lineheight = parseInt($(el).css('line-height'), 10);
+
+ if (isNaN(lineheight)) {
+ lineheight = parseInt($(el).css('font-size'), 10) * 1.2;
+ }
+ return lineheight;
+ }
+
+ function writeWrappedText (el, context, text, left, top, canvasWidth) {
+ var lineheight = getLineheight(el);
+ var tagwidth = $(el).innerWidth();
+ var tagheight = $(el).innerHeight();
+ var words = text.split(/\s+/);
+ var wl = words.length;
+ var w = '';
+ var breaks = [];
+ var temptop = top;
+ var templeft = left;
+
+ for (var i=0; i<wl; i++) {
+ w += words[i];
+ if (context.measureText(w).width > tagwidth) {
+ breaks.push(i);
+ w = '';
+ i--;
+ }
+ }
+ if (breaks.length === 0) {
+ // center text if necessary
+ if ($(el).css('textAlign') === 'center') {
+ templeft = left + (canvasWidth - context.measureText(w).width)/2 - transx;
+ }
+ context.fillText(text, templeft, top);
+ }
+ else {
+ w = words.slice(0, breaks[0]).join(' ');
+ // center text if necessary
+ if ($(el).css('textAlign') === 'center') {
+ templeft = left + (canvasWidth - context.measureText(w).width)/2 - transx;
+ }
+ context.fillText(w, templeft, temptop);
+ temptop += lineheight;
+ for (var i=1, l=breaks.length; i<l; i++) {
+ w = words.slice(breaks[i-1], breaks[i]).join(' ');
+ // center text if necessary
+ if ($(el).css('textAlign') === 'center') {
+ templeft = left + (canvasWidth - context.measureText(w).width)/2 - transx;
+ }
+ context.fillText(w, templeft, temptop);
+ temptop += lineheight;
+ }
+ w = words.slice(breaks[i-1], words.length).join(' ');
+ // center text if necessary
+ if ($(el).css('textAlign') === 'center') {
+ templeft = left + (canvasWidth - context.measureText(w).width)/2 - transx;
+ }
+ context.fillText(w, templeft, temptop);
+ }
+
+ }
+
+ function _jqpToImage(el, x_offset, y_offset) {
+ var tagname = el.tagName.toLowerCase();
+ var p = $(el).position();
+ var css = window.getComputedStyle ? window.getComputedStyle(el, "") : el.currentStyle; // for IE < 9
+ var left = x_offset + p.left + parseInt(css.marginLeft, 10) + parseInt(css.borderLeftWidth, 10) + parseInt(css.paddingLeft, 10);
+ var top = y_offset + p.top + parseInt(css.marginTop, 10) + parseInt(css.borderTopWidth, 10)+ parseInt(css.paddingTop, 10);
+ var w = newCanvas.width;
+ // var left = x_offset + p.left + $(el).css('marginLeft') + $(el).css('borderLeftWidth')
+
+ // somehow in here, for divs within divs, the width of the inner div should be used instead of the canvas.
+
+ if ((tagname == 'div' || tagname == 'span') && !$(el).hasClass('jqplot-highlighter-tooltip')) {
+ $(el).children().each(function() {
+ _jqpToImage(this, left, top);
+ });
+ var text = $(el).jqplotChildText();
+
+ if (text) {
+ newContext.font = $(el).jqplotGetComputedFontStyle();
+ newContext.fillStyle = $(el).css('color');
+
+ writeWrappedText(el, newContext, text, left, top, w);
+ }
+ }
+
+ // handle the standard table legend
+
+ else if (tagname === 'table' && $(el).hasClass('jqplot-table-legend')) {
+ newContext.strokeStyle = $(el).css('border-top-color');
+ newContext.fillStyle = $(el).css('background-color');
+ newContext.fillRect(left, top, $(el).innerWidth(), $(el).innerHeight());
+ if (parseInt($(el).css('border-top-width'), 10) > 0) {
+ newContext.strokeRect(left, top, $(el).innerWidth(), $(el).innerHeight());
+ }
+
+ // find all the swatches
+ $(el).find('div.jqplot-table-legend-swatch-outline').each(function() {
+ // get the first div and stroke it
+ var elem = $(this);
+ newContext.strokeStyle = elem.css('border-top-color');
+ var l = left + elem.position().left;
+ var t = top + elem.position().top;
+ newContext.strokeRect(l, t, elem.innerWidth(), elem.innerHeight());
+
+ // now fill the swatch
+
+ l += parseInt(elem.css('padding-left'), 10);
+ t += parseInt(elem.css('padding-top'), 10);
+ var h = elem.innerHeight() - 2 * parseInt(elem.css('padding-top'), 10);
+ var w = elem.innerWidth() - 2 * parseInt(elem.css('padding-left'), 10);
+
+ var swatch = elem.children('div.jqplot-table-legend-swatch');
+ newContext.fillStyle = swatch.css('background-color');
+ newContext.fillRect(l, t, w, h);
+ });
+
+ // now add text
+
+ $(el).find('td.jqplot-table-legend-label').each(function(){
+ var elem = $(this);
+ var l = left + elem.position().left;
+ var t = top + elem.position().top + parseInt(elem.css('padding-top'), 10);
+ newContext.font = elem.jqplotGetComputedFontStyle();
+ newContext.fillStyle = elem.css('color');
+ writeWrappedText(elem, newContext, elem.text(), l, t, w);
+ });
+
+ var elem = null;
+ }
+
+ else if (tagname == 'canvas') {
+ newContext.drawImage(el, left, top);
+ }
+ }
+ $(this).children().each(function() {
+ _jqpToImage(this, x_offset, y_offset);
+ });
+ return newCanvas;
+ };
+
+ // return the raw image data string.
+ // Should work on canvas supporting browsers.
+ $.fn.jqplotToImageStr = function(options) {
+ var imgCanvas = $(this).jqplotToImageCanvas(options);
+ if (imgCanvas) {
+ return imgCanvas.toDataURL("image/png");
+ }
+ else {
+ return null;
+ }
+ };
+
+ // return a DOM <img> element and return it.
+ // Should work on canvas supporting browsers.
+ $.fn.jqplotToImageElem = function(options) {
+ var elem = document.createElement("img");
+ var str = $(this).jqplotToImageStr(options);
+ elem.src = str;
+ return elem;
+ };
+
+ // return a string for an <img> element and return it.
+ // Should work on canvas supporting browsers.
+ $.fn.jqplotToImageElemStr = function(options) {
+ var str = '<img src='+$(this).jqplotToImageStr(options)+' />';
+ return str;
+ };
+
+ // Not guaranteed to work, even on canvas supporting browsers due to
+ // limitations with location.href and browser support.
+ $.fn.jqplotSaveImage = function() {
+ var imgData = $(this).jqplotToImageStr({});
+ if (imgData) {
+ window.location.href = imgData.replace("image/png", "image/octet-stream");
+ }
+
+ };
+
+ // Not guaranteed to work, even on canvas supporting browsers due to
+ // limitations with window.open and arbitrary data.
+ $.fn.jqplotViewImage = function() {
+ var imgStr = $(this).jqplotToImageElemStr({});
+ var imgData = $(this).jqplotToImageStr({});
+ if (imgStr) {
+ var w = window.open('');
+ w.document.open("image/png");
+ w.document.write(imgStr);
+ w.document.close();
+ w = null;
+ }
+ };
+
+
+
+
+ /**
+ * @description
+ * <p>Object with extended date parsing and formatting capabilities.
+ * This library borrows many concepts and ideas from the Date Instance
+ * Methods by Ken Snyder along with some parts of Ken's actual code.</p>
+ *
+ * <p>jsDate takes a different approach by not extending the built-in
+ * Date Object, improving date parsing, allowing for multiple formatting
+ * syntaxes and multiple and more easily expandable localization.</p>
+ *
+ * @author Chris Leonello
+ * @date #date#
+ * @version #VERSION#
+ * @copyright (c) 2010-2013 Chris Leonello
+ * jsDate is currently available for use in all personal or commercial projects
+ * under both the MIT and GPL version 2.0 licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * <p>Ken's original Date Instance Methods and copyright notice:</p>
+ * <pre>
+ * Ken Snyder (ken d snyder at gmail dot com)
+ * 2008-09-10
+ * version 2.0.2 (http://kendsnyder.com/sandbox/date/)
+ * Creative Commons Attribution License 3.0 (http://creativecommons.org/licenses/by/3.0/)
+ * </pre>
+ *
+ * @class
+ * @name jsDate
+ * @param {String | Number | Array | Date&nbsp;Object | Options&nbsp;Object} arguments Optional arguments, either a parsable date/time string,
+ * a JavaScript timestamp, an array of numbers of form [year, month, day, hours, minutes, seconds, milliseconds],
+ * a Date object, or an options object of form {syntax: "perl", date:some Date} where all options are optional.
+ */
+
+ var jsDate = function () {
+
+ this.syntax = jsDate.config.syntax;
+ this._type = "jsDate";
+ this.proxy = new Date();
+ this.options = {};
+ this.locale = jsDate.regional.getLocale();
+ this.formatString = '';
+ this.defaultCentury = jsDate.config.defaultCentury;
+
+ switch ( arguments.length ) {
+ case 0:
+ break;
+ case 1:
+ // other objects either won't have a _type property or,
+ // if they do, it shouldn't be set to "jsDate", so
+ // assume it is an options argument.
+ if (get_type(arguments[0]) == "[object Object]" && arguments[0]._type != "jsDate") {
+ var opts = this.options = arguments[0];
+ this.syntax = opts.syntax || this.syntax;
+ this.defaultCentury = opts.defaultCentury || this.defaultCentury;
+ this.proxy = jsDate.createDate(opts.date);
+ }
+ else {
+ this.proxy = jsDate.createDate(arguments[0]);
+ }
+ break;
+ default:
+ var a = [];
+ for ( var i=0; i<arguments.length; i++ ) {
+ a.push(arguments[i]);
+ }
+ // this should be the current date/time?
+ this.proxy = new Date();
+ this.proxy.setFullYear.apply( this.proxy, a.slice(0,3) );
+ if ( a.slice(3).length ) {
+ this.proxy.setHours.apply( this.proxy, a.slice(3) );
+ }
+ break;
+ }
+ };
+
+ /**
+ * @namespace Configuration options that will be used as defaults for all instances on the page.
+ * @property {String} defaultLocale The default locale to use [en].
+ * @property {String} syntax The default syntax to use [perl].
+ * @property {Number} defaultCentury The default centry for 2 digit dates.
+ */
+ jsDate.config = {
+ defaultLocale: 'en',
+ syntax: 'perl',
+ defaultCentury: 1900
+ };
+
+ /**
+ * Add an arbitrary amount to the currently stored date
+ *
+ * @param {Number} number
+ * @param {String} unit
+ * @returns {jsDate}
+ */
+
+ jsDate.prototype.add = function(number, unit) {
+ var factor = multipliers[unit] || multipliers.day;
+ if (typeof factor == 'number') {
+ this.proxy.setTime(this.proxy.getTime() + (factor * number));
+ } else {
+ factor.add(this, number);
+ }
+ return this;
+ };
+
+ /**
+ * Create a new jqplot.date object with the same date
+ *
+ * @returns {jsDate}
+ */
+
+ jsDate.prototype.clone = function() {
+ return new jsDate(this.proxy.getTime());
+ };
+
+ /**
+ * Get the UTC TimeZone Offset of this date in milliseconds.
+ *
+ * @returns {Number}
+ */
+
+ jsDate.prototype.getUtcOffset = function() {
+ return this.proxy.getTimezoneOffset() * 60000;
+ };
+
+ /**
+ * Find the difference between this jsDate and another date.
+ *
+ * @param {String| Number| Array| jsDate&nbsp;Object| Date&nbsp;Object} dateObj
+ * @param {String} unit
+ * @param {Boolean} allowDecimal
+ * @returns {Number} Number of units difference between dates.
+ */
+
+ jsDate.prototype.diff = function(dateObj, unit, allowDecimal) {
+ // ensure we have a Date object
+ dateObj = new jsDate(dateObj);
+ if (dateObj === null) {
+ return null;
+ }
+ // get the multiplying factor integer or factor function
+ var factor = multipliers[unit] || multipliers.day;
+ if (typeof factor == 'number') {
+ // multiply
+ var unitDiff = (this.proxy.getTime() - dateObj.proxy.getTime()) / factor;
+ } else {
+ // run function
+ var unitDiff = factor.diff(this.proxy, dateObj.proxy);
+ }
+ // if decimals are not allowed, round toward zero
+ return (allowDecimal ? unitDiff : Math[unitDiff > 0 ? 'floor' : 'ceil'](unitDiff));
+ };
+
+ /**
+ * Get the abbreviated name of the current week day
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getAbbrDayName = function() {
+ return jsDate.regional[this.locale]["dayNamesShort"][this.proxy.getDay()];
+ };
+
+ /**
+ * Get the abbreviated name of the current month
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getAbbrMonthName = function() {
+ return jsDate.regional[this.locale]["monthNamesShort"][this.proxy.getMonth()];
+ };
+
+ /**
+ * Get UPPER CASE AM or PM for the current time
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getAMPM = function() {
+ return this.proxy.getHours() >= 12 ? 'PM' : 'AM';
+ };
+
+ /**
+ * Get lower case am or pm for the current time
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getAmPm = function() {
+ return this.proxy.getHours() >= 12 ? 'pm' : 'am';
+ };
+
+ /**
+ * Get the century (19 for 20th Century)
+ *
+ * @returns {Integer} Century (19 for 20th century).
+ */
+ jsDate.prototype.getCentury = function() {
+ return parseInt(this.proxy.getFullYear()/100, 10);
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getDate = function() {
+ return this.proxy.getDate();
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getDay = function() {
+ return this.proxy.getDay();
+ };
+
+ /**
+ * Get the Day of week 1 (Monday) thru 7 (Sunday)
+ *
+ * @returns {Integer} Day of week 1 (Monday) thru 7 (Sunday)
+ */
+ jsDate.prototype.getDayOfWeek = function() {
+ var dow = this.proxy.getDay();
+ return dow===0?7:dow;
+ };
+
+ /**
+ * Get the day of the year
+ *
+ * @returns {Integer} 1 - 366, day of the year
+ */
+ jsDate.prototype.getDayOfYear = function() {
+ var d = this.proxy;
+ var ms = d - new Date('' + d.getFullYear() + '/1/1 GMT');
+ ms += d.getTimezoneOffset()*60000;
+ d = null;
+ return parseInt(ms/60000/60/24, 10)+1;
+ };
+
+ /**
+ * Get the name of the current week day
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getDayName = function() {
+ return jsDate.regional[this.locale]["dayNames"][this.proxy.getDay()];
+ };
+
+ /**
+ * Get the week number of the given year, starting with the first Sunday as the first week
+ * @returns {Integer} Week number (13 for the 13th full week of the year).
+ */
+ jsDate.prototype.getFullWeekOfYear = function() {
+ var d = this.proxy;
+ var doy = this.getDayOfYear();
+ var rdow = 6-d.getDay();
+ var woy = parseInt((doy+rdow)/7, 10);
+ return woy;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getFullYear = function() {
+ return this.proxy.getFullYear();
+ };
+
+ /**
+ * Get the GMT offset in hours and minutes (e.g. +06:30)
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getGmtOffset = function() {
+ // divide the minutes offset by 60
+ var hours = this.proxy.getTimezoneOffset() / 60;
+ // decide if we are ahead of or behind GMT
+ var prefix = hours < 0 ? '+' : '-';
+ // remove the negative sign if any
+ hours = Math.abs(hours);
+ // add the +/- to the padded number of hours to : to the padded minutes
+ return prefix + addZeros(Math.floor(hours), 2) + ':' + addZeros((hours % 1) * 60, 2);
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getHours = function() {
+ return this.proxy.getHours();
+ };
+
+ /**
+ * Get the current hour on a 12-hour scheme
+ *
+ * @returns {Integer}
+ */
+
+ jsDate.prototype.getHours12 = function() {
+ var hours = this.proxy.getHours();
+ return hours > 12 ? hours - 12 : (hours == 0 ? 12 : hours);
+ };
+
+
+ jsDate.prototype.getIsoWeek = function() {
+ var d = this.proxy;
+ var woy = this.getWeekOfYear();
+ var dow1_1 = (new Date('' + d.getFullYear() + '/1/1')).getDay();
+ // First week is 01 and not 00 as in the case of %U and %W,
+ // so we add 1 to the final result except if day 1 of the year
+ // is a Monday (then %W returns 01).
+ // We also need to subtract 1 if the day 1 of the year is
+ // Friday-Sunday, so the resulting equation becomes:
+ var idow = woy + (dow1_1 > 4 || dow1_1 <= 1 ? 0 : 1);
+ if(idow == 53 && (new Date('' + d.getFullYear() + '/12/31')).getDay() < 4)
+ {
+ idow = 1;
+ }
+ else if(idow === 0)
+ {
+ d = new jsDate(new Date('' + (d.getFullYear()-1) + '/12/31'));
+ idow = d.getIsoWeek();
+ }
+ d = null;
+ return idow;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getMilliseconds = function() {
+ return this.proxy.getMilliseconds();
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getMinutes = function() {
+ return this.proxy.getMinutes();
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getMonth = function() {
+ return this.proxy.getMonth();
+ };
+
+ /**
+ * Get the name of the current month
+ *
+ * @returns {String}
+ */
+
+ jsDate.prototype.getMonthName = function() {
+ return jsDate.regional[this.locale]["monthNames"][this.proxy.getMonth()];
+ };
+
+ /**
+ * Get the number of the current month, 1-12
+ *
+ * @returns {Integer}
+ */
+
+ jsDate.prototype.getMonthNumber = function() {
+ return this.proxy.getMonth() + 1;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getSeconds = function() {
+ return this.proxy.getSeconds();
+ };
+
+ /**
+ * Return a proper two-digit year integer
+ *
+ * @returns {Integer}
+ */
+
+ jsDate.prototype.getShortYear = function() {
+ return this.proxy.getYear() % 100;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getTime = function() {
+ return this.proxy.getTime();
+ };
+
+ /**
+ * Get the timezone abbreviation
+ *
+ * @returns {String} Abbreviation for the timezone
+ */
+ jsDate.prototype.getTimezoneAbbr = function() {
+ return this.proxy.toString().replace(/^.*\(([^)]+)\)$/, '$1');
+ };
+
+ /**
+ * Get the browser-reported name for the current timezone (e.g. MDT, Mountain Daylight Time)
+ *
+ * @returns {String}
+ */
+ jsDate.prototype.getTimezoneName = function() {
+ var match = /(?:\((.+)\)$| ([A-Z]{3}) )/.exec(this.toString());
+ return match[1] || match[2] || 'GMT' + this.getGmtOffset();
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getTimezoneOffset = function() {
+ return this.proxy.getTimezoneOffset();
+ };
+
+
+ /**
+ * Get the week number of the given year, starting with the first Monday as the first week
+ * @returns {Integer} Week number (13 for the 13th week of the year).
+ */
+ jsDate.prototype.getWeekOfYear = function() {
+ var doy = this.getDayOfYear();
+ var rdow = 7 - this.getDayOfWeek();
+ var woy = parseInt((doy+rdow)/7, 10);
+ return woy;
+ };
+
+ /**
+ * Get the current date as a Unix timestamp
+ *
+ * @returns {Integer}
+ */
+
+ jsDate.prototype.getUnix = function() {
+ return Math.round(this.proxy.getTime() / 1000, 0);
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.getYear = function() {
+ return this.proxy.getYear();
+ };
+
+ /**
+ * Return a date one day ahead (or any other unit)
+ *
+ * @param {String} unit Optional, year | month | day | week | hour | minute | second | millisecond
+ * @returns {jsDate}
+ */
+
+ jsDate.prototype.next = function(unit) {
+ unit = unit || 'day';
+ return this.clone().add(1, unit);
+ };
+
+ /**
+ * Set the jsDate instance to a new date.
+ *
+ * @param {String | Number | Array | Date Object | jsDate Object | Options Object} arguments Optional arguments,
+ * either a parsable date/time string,
+ * a JavaScript timestamp, an array of numbers of form [year, month, day, hours, minutes, seconds, milliseconds],
+ * a Date object, jsDate Object or an options object of form {syntax: "perl", date:some Date} where all options are optional.
+ */
+ jsDate.prototype.set = function() {
+ switch ( arguments.length ) {
+ case 0:
+ this.proxy = new Date();
+ break;
+ case 1:
+ // other objects either won't have a _type property or,
+ // if they do, it shouldn't be set to "jsDate", so
+ // assume it is an options argument.
+ if (get_type(arguments[0]) == "[object Object]" && arguments[0]._type != "jsDate") {
+ var opts = this.options = arguments[0];
+ this.syntax = opts.syntax || this.syntax;
+ this.defaultCentury = opts.defaultCentury || this.defaultCentury;
+ this.proxy = jsDate.createDate(opts.date);
+ }
+ else {
+ this.proxy = jsDate.createDate(arguments[0]);
+ }
+ break;
+ default:
+ var a = [];
+ for ( var i=0; i<arguments.length; i++ ) {
+ a.push(arguments[i]);
+ }
+ // this should be the current date/time
+ this.proxy = new Date();
+ this.proxy.setFullYear.apply( this.proxy, a.slice(0,3) );
+ if ( a.slice(3).length ) {
+ this.proxy.setHours.apply( this.proxy, a.slice(3) );
+ }
+ break;
+ }
+ return this;
+ };
+
+ /**
+ * Sets the day of the month for a specified date according to local time.
+ * @param {Integer} dayValue An integer from 1 to 31, representing the day of the month.
+ */
+ jsDate.prototype.setDate = function(n) {
+ this.proxy.setDate(n);
+ return this;
+ };
+
+ /**
+ * Sets the full year for a specified date according to local time.
+ * @param {Integer} yearValue The numeric value of the year, for example, 1995.
+ * @param {Integer} monthValue Optional, between 0 and 11 representing the months January through December.
+ * @param {Integer} dayValue Optional, between 1 and 31 representing the day of the month. If you specify the dayValue parameter, you must also specify the monthValue.
+ */
+ jsDate.prototype.setFullYear = function() {
+ this.proxy.setFullYear.apply(this.proxy, arguments);
+ return this;
+ };
+
+ /**
+ * Sets the hours for a specified date according to local time.
+ *
+ * @param {Integer} hoursValue An integer between 0 and 23, representing the hour.
+ * @param {Integer} minutesValue Optional, An integer between 0 and 59, representing the minutes.
+ * @param {Integer} secondsValue Optional, An integer between 0 and 59, representing the seconds.
+ * If you specify the secondsValue parameter, you must also specify the minutesValue.
+ * @param {Integer} msValue Optional, A number between 0 and 999, representing the milliseconds.
+ * If you specify the msValue parameter, you must also specify the minutesValue and secondsValue.
+ */
+ jsDate.prototype.setHours = function() {
+ this.proxy.setHours.apply(this.proxy, arguments);
+ return this;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.setMilliseconds = function(n) {
+ this.proxy.setMilliseconds(n);
+ return this;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.setMinutes = function() {
+ this.proxy.setMinutes.apply(this.proxy, arguments);
+ return this;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.setMonth = function() {
+ this.proxy.setMonth.apply(this.proxy, arguments);
+ return this;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.setSeconds = function() {
+ this.proxy.setSeconds.apply(this.proxy, arguments);
+ return this;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.setTime = function(n) {
+ this.proxy.setTime(n);
+ return this;
+ };
+
+ /**
+ * Implements Date functionality
+ */
+ jsDate.prototype.setYear = function() {
+ this.proxy.setYear.apply(this.proxy, arguments);
+ return this;
+ };
+
+ /**
+ * Provide a formatted string representation of this date.
+ *
+ * @param {String} formatString A format string.
+ * See: {@link jsDate.formats}.
+ * @returns {String} Date String.
+ */
+
+ jsDate.prototype.strftime = function(formatString) {
+ formatString = formatString || this.formatString || jsDate.regional[this.locale]['formatString'];
+ return jsDate.strftime(this, formatString, this.syntax);
+ };
+
+ /**
+ * Return a String representation of this jsDate object.
+ * @returns {String} Date string.
+ */
+
+ jsDate.prototype.toString = function() {
+ return this.proxy.toString();
+ };
+
+ /**
+ * Convert the current date to an 8-digit integer (%Y%m%d)
+ *
+ * @returns {Integer}
+ */
+
+ jsDate.prototype.toYmdInt = function() {
+ return (this.proxy.getFullYear() * 10000) + (this.getMonthNumber() * 100) + this.proxy.getDate();
+ };
+
+ /**
+ * @namespace Holds localizations for month/day names.
+ * <p>jsDate attempts to detect locale when loaded and defaults to 'en'.
+ * If a localization is detected which is not available, jsDate defaults to 'en'.
+ * Additional localizations can be added after jsDate loads. After adding a localization,
+ * call the jsDate.regional.getLocale() method. Currently, en, fr and de are defined.</p>
+ *
+ * <p>Localizations must be an object and have the following properties defined: monthNames, monthNamesShort, dayNames, dayNamesShort and Localizations are added like:</p>
+ * <pre class="code">
+ * jsDate.regional['en'] = {
+ * monthNames : 'January February March April May June July August September October November December'.split(' '),
+ * monthNamesShort : 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '),
+ * dayNames : 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' '),
+ * dayNamesShort : 'Sun Mon Tue Wed Thu Fri Sat'.split(' ')
+ * };
+ * </pre>
+ * <p>After adding localizations, call <code>jsDate.regional.getLocale();</code> to update the locale setting with the
+ * new localizations.</p>
+ */
+
+ jsDate.regional = {
+ 'en': {
+ monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
+ monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'fr': {
+ monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'],
+ monthNamesShort: ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'],
+ dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
+ dayNamesShort: ['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'de': {
+ monthNames: ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'],
+ monthNamesShort: ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'],
+ dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'],
+ dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'es': {
+ monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio', 'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
+ monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun', 'Jul','Ago','Sep','Oct','Nov','Dic'],
+ dayNames: ['Domingo','Lunes','Martes','Mi&eacute;rcoles','Jueves','Viernes','S&aacute;bado'],
+ dayNamesShort: ['Dom','Lun','Mar','Mi&eacute;','Juv','Vie','S&aacute;b'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'ru': {
+ monthNames: ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'],
+ monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
+ dayNames: ['воскресенье','понедельник','вторник','среда','четверг','пятница','суббота'],
+ dayNamesShort: ['вск','пнд','втр','срд','чтв','птн','сбт'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'ar': {
+ monthNames: ['كانون الثاني', 'شباط', 'آذار', 'نيسان', 'آذار', 'حزيران','تموز', 'آب', 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'],
+ monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'],
+ dayNames: ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة'],
+ dayNamesShort: ['سبت', 'أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'pt': {
+ monthNames: ['Janeiro','Fevereiro','Mar&ccedil;o','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
+ monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
+ dayNames: ['Domingo','Segunda-feira','Ter&ccedil;a-feira','Quarta-feira','Quinta-feira','Sexta-feira','S&aacute;bado'],
+ dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','S&aacute;b'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'pt-BR': {
+ monthNames: ['Janeiro','Fevereiro','Mar&ccedil;o','Abril','Maio','Junho', 'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
+ monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
+ dayNames: ['Domingo','Segunda-feira','Ter&ccedil;a-feira','Quarta-feira','Quinta-feira','Sexta-feira','S&aacute;bado'],
+ dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','S&aacute;b'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'pl': {
+ monthNames: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec','Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'],
+ monthNamesShort: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze','Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
+ dayNames: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'],
+ dayNamesShort: ['Ni', 'Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'Sb'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'nl': {
+ monthNames: ['Januari','Februari','Maart','April','Mei','Juni','July','Augustus','September','Oktober','November','December'],
+ monthNamesShort: ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Aug','Sep','Okt','Nov','Dec'],
+ dayNames:','['Zondag','Maandag','Dinsdag','Woensdag','Donderdag','Vrijdag','Zaterdag'],
+ dayNamesShort: ['Zo','Ma','Di','Wo','Do','Vr','Za'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ },
+
+ 'sv': {
+ monthNames: ['januari','februari','mars','april','maj','juni','juli','augusti','september','oktober','november','december'],
+ monthNamesShort: ['jan','feb','mar','apr','maj','jun','jul','aug','sep','okt','nov','dec'],
+ dayNames: ['söndag','måndag','tisdag','onsdag','torsdag','fredag','lördag'],
+ dayNamesShort: ['sön','mån','tis','ons','tor','fre','lör'],
+ formatString: '%Y-%m-%d %H:%M:%S'
+ }
+
+ };
+
+ // Set english variants to 'en'
+ jsDate.regional['en-US'] = jsDate.regional['en-GB'] = jsDate.regional['en'];
+
+ /**
+ * Try to determine the users locale based on the lang attribute of the html page. Defaults to 'en'
+ * if it cannot figure out a locale of if the locale does not have a localization defined.
+ * @returns {String} locale
+ */
+
+ jsDate.regional.getLocale = function () {
+ var l = jsDate.config.defaultLocale;
+
+ if ( document && document.getElementsByTagName('html') && document.getElementsByTagName('html')[0].lang ) {
+ l = document.getElementsByTagName('html')[0].lang;
+ if (!jsDate.regional.hasOwnProperty(l)) {
+ l = jsDate.config.defaultLocale;
+ }
+ }
+
+ return l;
+ };
+
+ // ms in day
+ var day = 24 * 60 * 60 * 1000;
+
+ // padd a number with zeros
+ var addZeros = function(num, digits) {
+ num = String(num);
+ var i = digits - num.length;
+ var s = String(Math.pow(10, i)).slice(1);
+ return s.concat(num);
+ };
+
+ // representations used for calculating differences between dates.
+ // This borrows heavily from Ken Snyder's work.
+ var multipliers = {
+ millisecond: 1,
+ second: 1000,
+ minute: 60 * 1000,
+ hour: 60 * 60 * 1000,
+ day: day,
+ week: 7 * day,
+ month: {
+ // add a number of months
+ add: function(d, number) {
+ // add any years needed (increments of 12)
+ multipliers.year.add(d, Math[number > 0 ? 'floor' : 'ceil'](number / 12));
+ // ensure that we properly wrap betwen December and January
+ // 11 % 12 = 11
+ // 12 % 12 = 0
+ var prevMonth = d.getMonth() + (number % 12);
+ if (prevMonth == 12) {
+ prevMonth = 0;
+ d.setYear(d.getFullYear() + 1);
+ } else if (prevMonth == -1) {
+ prevMonth = 11;
+ d.setYear(d.getFullYear() - 1);
+ }
+ d.setMonth(prevMonth);
+ },
+ // get the number of months between two Date objects (decimal to the nearest day)
+ diff: function(d1, d2) {
+ // get the number of years
+ var diffYears = d1.getFullYear() - d2.getFullYear();
+ // get the number of remaining months
+ var diffMonths = d1.getMonth() - d2.getMonth() + (diffYears * 12);
+ // get the number of remaining days
+ var diffDays = d1.getDate() - d2.getDate();
+ // return the month difference with the days difference as a decimal
+ return diffMonths + (diffDays / 30);
+ }
+ },
+ year: {
+ // add a number of years
+ add: function(d, number) {
+ d.setYear(d.getFullYear() + Math[number > 0 ? 'floor' : 'ceil'](number));
+ },
+ // get the number of years between two Date objects (decimal to the nearest day)
+ diff: function(d1, d2) {
+ return multipliers.month.diff(d1, d2) / 12;
+ }
+ }
+ };
+ //
+ // Alias each multiplier with an 's' to allow 'year' and 'years' for example.
+ // This comes from Ken Snyders work.
+ //
+ for (var unit in multipliers) {
+ if (unit.substring(unit.length - 1) != 's') { // IE will iterate newly added properties :|
+ multipliers[unit + 's'] = multipliers[unit];
+ }
+ }
+
+ //
+ // take a jsDate instance and a format code and return the formatted value.
+ // This is a somewhat modified version of Ken Snyder's method.
+ //
+ var format = function(d, code, syntax) {
+ // if shorcut codes are used, recursively expand those.
+ if (jsDate.formats[syntax]["shortcuts"][code]) {
+ return jsDate.strftime(d, jsDate.formats[syntax]["shortcuts"][code], syntax);
+ } else {
+ // get the format code function and addZeros() argument
+ var getter = (jsDate.formats[syntax]["codes"][code] || '').split('.');
+ var nbr = d['get' + getter[0]] ? d['get' + getter[0]]() : '';
+ if (getter[1]) {
+ nbr = addZeros(nbr, getter[1]);
+ }
+ return nbr;
+ }
+ };
+
+ /**
+ * @static
+ * Static function for convert a date to a string according to a given format. Also acts as namespace for strftime format codes.
+ * <p>strftime formatting can be accomplished without creating a jsDate object by calling jsDate.strftime():</p>
+ * <pre class="code">
+ * var formattedDate = jsDate.strftime('Feb 8, 2006 8:48:32', '%Y-%m-%d %H:%M:%S');
+ * </pre>
+ * @param {String | Number | Array | jsDate&nbsp;Object | Date&nbsp;Object} date A parsable date string, JavaScript time stamp, Array of form [year, month, day, hours, minutes, seconds, milliseconds], jsDate Object or Date object.
+ * @param {String} formatString String with embedded date formatting codes.
+ * See: {@link jsDate.formats}.
+ * @param {String} syntax Optional syntax to use [default perl].
+ * @param {String} locale Optional locale to use.
+ * @returns {String} Formatted representation of the date.
+ */
+ //
+ // Logic as implemented here is very similar to Ken Snyder's Date Instance Methods.
+ //
+ jsDate.strftime = function(d, formatString, syntax, locale) {
+ var syn = 'perl';
+ var loc = jsDate.regional.getLocale();
+
+ // check if syntax and locale are available or reversed
+ if (syntax && jsDate.formats.hasOwnProperty(syntax)) {
+ syn = syntax;
+ }
+ else if (syntax && jsDate.regional.hasOwnProperty(syntax)) {
+ loc = syntax;
+ }
+
+ if (locale && jsDate.formats.hasOwnProperty(locale)) {
+ syn = locale;
+ }
+ else if (locale && jsDate.regional.hasOwnProperty(locale)) {
+ loc = locale;
+ }
+
+ if (get_type(d) != "[object Object]" || d._type != "jsDate") {
+ d = new jsDate(d);
+ d.locale = loc;
+ }
+ if (!formatString) {
+ formatString = d.formatString || jsDate.regional[loc]['formatString'];
+ }
+ // default the format string to year-month-day
+ var source = formatString || '%Y-%m-%d',
+ result = '',
+ match;
+ // replace each format code
+ while (source.length > 0) {
+ if (match = source.match(jsDate.formats[syn].codes.matcher)) {
+ result += source.slice(0, match.index);
+ result += (match[1] || '') + format(d, match[2], syn);
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source;
+ source = '';
+ }
+ }
+ return result;
+ };
+
+ /**
+ * @namespace
+ * Namespace to hold format codes and format shortcuts. "perl" and "php" format codes
+ * and shortcuts are defined by default. Additional codes and shortcuts can be
+ * added like:
+ *
+ * <pre class="code">
+ * jsDate.formats["perl"] = {
+ * "codes": {
+ * matcher: /someregex/,
+ * Y: "fullYear", // name of "get" method without the "get",
+ * ..., // more codes
+ * },
+ * "shortcuts": {
+ * F: '%Y-%m-%d',
+ * ..., // more shortcuts
+ * }
+ * };
+ * </pre>
+ *
+ * <p>Additionally, ISO and SQL shortcuts are defined and can be accesses via:
+ * <code>jsDate.formats.ISO</code> and <code>jsDate.formats.SQL</code>
+ */
+
+ jsDate.formats = {
+ ISO:'%Y-%m-%dT%H:%M:%S.%N%G',
+ SQL:'%Y-%m-%d %H:%M:%S'
+ };
+
+ /**
+ * Perl format codes and shortcuts for strftime.
+ *
+ * A hash (object) of codes where each code must be an array where the first member is
+ * the name of a Date.prototype or jsDate.prototype function to call
+ * and optionally a second member indicating the number to pass to addZeros()
+ *
+ * <p>The following format codes are defined:</p>
+ *
+ * <pre class="code">
+ * Code Result Description
+ * == Years ==
+ * %Y 2008 Four-digit year
+ * %y 08 Two-digit year
+ *
+ * == Months ==
+ * %m 09 Two-digit month
+ * %#m 9 One or two-digit month
+ * %B September Full month name
+ * %b Sep Abbreviated month name
+ *
+ * == Days ==
+ * %d 05 Two-digit day of month
+ * %#d 5 One or two-digit day of month
+ * %e 5 One or two-digit day of month
+ * %A Sunday Full name of the day of the week
+ * %a Sun Abbreviated name of the day of the week
+ * %w 0 Number of the day of the week (0 = Sunday, 6 = Saturday)
+ *
+ * == Hours ==
+ * %H 23 Hours in 24-hour format (two digits)
+ * %#H 3 Hours in 24-hour integer format (one or two digits)
+ * %I 11 Hours in 12-hour format (two digits)
+ * %#I 3 Hours in 12-hour integer format (one or two digits)
+ * %p PM AM or PM
+ *
+ * == Minutes ==
+ * %M 09 Minutes (two digits)
+ * %#M 9 Minutes (one or two digits)
+ *
+ * == Seconds ==
+ * %S 02 Seconds (two digits)
+ * %#S 2 Seconds (one or two digits)
+ * %s 1206567625723 Unix timestamp (Seconds past 1970-01-01 00:00:00)
+ *
+ * == Milliseconds ==
+ * %N 008 Milliseconds (three digits)
+ * %#N 8 Milliseconds (one to three digits)
+ *
+ * == Timezone ==
+ * %O 360 difference in minutes between local time and GMT
+ * %Z Mountain Standard Time Name of timezone as reported by browser
+ * %G 06:00 Hours and minutes between GMT
+ *
+ * == Shortcuts ==
+ * %F 2008-03-26 %Y-%m-%d
+ * %T 05:06:30 %H:%M:%S
+ * %X 05:06:30 %H:%M:%S
+ * %x 03/26/08 %m/%d/%y
+ * %D 03/26/08 %m/%d/%y
+ * %#c Wed Mar 26 15:31:00 2008 %a %b %e %H:%M:%S %Y
+ * %v 3-Sep-2008 %e-%b-%Y
+ * %R 15:31 %H:%M
+ * %r 03:31:00 PM %I:%M:%S %p
+ *
+ * == Characters ==
+ * %n \n Newline
+ * %t \t Tab
+ * %% % Percent Symbol
+ * </pre>
+ *
+ * <p>Formatting shortcuts that will be translated into their longer version.
+ * Be sure that format shortcuts do not refer to themselves: this will cause an infinite loop.</p>
+ *
+ * <p>Format codes and format shortcuts can be redefined after the jsDate
+ * module is imported.</p>
+ *
+ * <p>Note that if you redefine the whole hash (object), you must supply a "matcher"
+ * regex for the parser. The default matcher is:</p>
+ *
+ * <code>/()%(#?(%|[a-z]))/i</code>
+ *
+ * <p>which corresponds to the Perl syntax used by default.</p>
+ *
+ * <p>By customizing the matcher and format codes, nearly any strftime functionality is possible.</p>
+ */
+
+ jsDate.formats.perl = {
+ codes: {
+ //
+ // 2-part regex matcher for format codes
+ //
+ // first match must be the character before the code (to account for escaping)
+ // second match must be the format code character(s)
+ //
+ matcher: /()%(#?(%|[a-z]))/i,
+ // year
+ Y: 'FullYear',
+ y: 'ShortYear.2',
+ // month
+ m: 'MonthNumber.2',
+ '#m': 'MonthNumber',
+ B: 'MonthName',
+ b: 'AbbrMonthName',
+ // day
+ d: 'Date.2',
+ '#d': 'Date',
+ e: 'Date',
+ A: 'DayName',
+ a: 'AbbrDayName',
+ w: 'Day',
+ // hours
+ H: 'Hours.2',
+ '#H': 'Hours',
+ I: 'Hours12.2',
+ '#I': 'Hours12',
+ p: 'AMPM',
+ // minutes
+ M: 'Minutes.2',
+ '#M': 'Minutes',
+ // seconds
+ S: 'Seconds.2',
+ '#S': 'Seconds',
+ s: 'Unix',
+ // milliseconds
+ N: 'Milliseconds.3',
+ '#N': 'Milliseconds',
+ // timezone
+ O: 'TimezoneOffset',
+ Z: 'TimezoneName',
+ G: 'GmtOffset'
+ },
+
+ shortcuts: {
+ // date
+ F: '%Y-%m-%d',
+ // time
+ T: '%H:%M:%S',
+ X: '%H:%M:%S',
+ // local format date
+ x: '%m/%d/%y',
+ D: '%m/%d/%y',
+ // local format extended
+ '#c': '%a %b %e %H:%M:%S %Y',
+ // local format short
+ v: '%e-%b-%Y',
+ R: '%H:%M',
+ r: '%I:%M:%S %p',
+ // tab and newline
+ t: '\t',
+ n: '\n',
+ '%': '%'
+ }
+ };
+
+ /**
+ * PHP format codes and shortcuts for strftime.
+ *
+ * A hash (object) of codes where each code must be an array where the first member is
+ * the name of a Date.prototype or jsDate.prototype function to call
+ * and optionally a second member indicating the number to pass to addZeros()
+ *
+ * <p>The following format codes are defined:</p>
+ *
+ * <pre class="code">
+ * Code Result Description
+ * === Days ===
+ * %a Sun through Sat An abbreviated textual representation of the day
+ * %A Sunday - Saturday A full textual representation of the day
+ * %d 01 to 31 Two-digit day of the month (with leading zeros)
+ * %e 1 to 31 Day of the month, with a space preceding single digits.
+ * %j 001 to 366 Day of the year, 3 digits with leading zeros
+ * %u 1 - 7 (Mon - Sun) ISO-8601 numeric representation of the day of the week
+ * %w 0 - 6 (Sun - Sat) Numeric representation of the day of the week
+ *
+ * === Week ===
+ * %U 13 Full Week number, starting with the first Sunday as the first week
+ * %V 01 through 53 ISO-8601:1988 week number, starting with the first week of the year
+ * with at least 4 weekdays, with Monday being the start of the week
+ * %W 46 A numeric representation of the week of the year,
+ * starting with the first Monday as the first week
+ * === Month ===
+ * %b Jan through Dec Abbreviated month name, based on the locale
+ * %B January - December Full month name, based on the locale
+ * %h Jan through Dec Abbreviated month name, based on the locale (an alias of %b)
+ * %m 01 - 12 (Jan - Dec) Two digit representation of the month
+ *
+ * === Year ===
+ * %C 19 Two digit century (year/100, truncated to an integer)
+ * %y 09 for 2009 Two digit year
+ * %Y 2038 Four digit year
+ *
+ * === Time ===
+ * %H 00 through 23 Two digit representation of the hour in 24-hour format
+ * %I 01 through 12 Two digit representation of the hour in 12-hour format
+ * %l 1 through 12 Hour in 12-hour format, with a space preceeding single digits
+ * %M 00 through 59 Two digit representation of the minute
+ * %p AM/PM UPPER-CASE 'AM' or 'PM' based on the given time
+ * %P am/pm lower-case 'am' or 'pm' based on the given time
+ * %r 09:34:17 PM Same as %I:%M:%S %p
+ * %R 00:35 Same as %H:%M
+ * %S 00 through 59 Two digit representation of the second
+ * %T 21:34:17 Same as %H:%M:%S
+ * %X 03:59:16 Preferred time representation based on locale, without the date
+ * %z -0500 or EST Either the time zone offset from UTC or the abbreviation
+ * %Z -0500 or EST The time zone offset/abbreviation option NOT given by %z
+ *
+ * === Time and Date ===
+ * %D 02/05/09 Same as %m/%d/%y
+ * %F 2009-02-05 Same as %Y-%m-%d (commonly used in database datestamps)
+ * %s 305815200 Unix Epoch Time timestamp (same as the time() function)
+ * %x 02/05/09 Preferred date representation, without the time
+ *
+ * === Miscellaneous ===
+ * %n --- A newline character (\n)
+ * %t --- A Tab character (\t)
+ * %% --- A literal percentage character (%)
+ * </pre>
+ */
+
+ jsDate.formats.php = {
+ codes: {
+ //
+ // 2-part regex matcher for format codes
+ //
+ // first match must be the character before the code (to account for escaping)
+ // second match must be the format code character(s)
+ //
+ matcher: /()%((%|[a-z]))/i,
+ // day
+ a: 'AbbrDayName',
+ A: 'DayName',
+ d: 'Date.2',
+ e: 'Date',
+ j: 'DayOfYear.3',
+ u: 'DayOfWeek',
+ w: 'Day',
+ // week
+ U: 'FullWeekOfYear.2',
+ V: 'IsoWeek.2',
+ W: 'WeekOfYear.2',
+ // month
+ b: 'AbbrMonthName',
+ B: 'MonthName',
+ m: 'MonthNumber.2',
+ h: 'AbbrMonthName',
+ // year
+ C: 'Century.2',
+ y: 'ShortYear.2',
+ Y: 'FullYear',
+ // time
+ H: 'Hours.2',
+ I: 'Hours12.2',
+ l: 'Hours12',
+ p: 'AMPM',
+ P: 'AmPm',
+ M: 'Minutes.2',
+ S: 'Seconds.2',
+ s: 'Unix',
+ O: 'TimezoneOffset',
+ z: 'GmtOffset',
+ Z: 'TimezoneAbbr'
+ },
+
+ shortcuts: {
+ D: '%m/%d/%y',
+ F: '%Y-%m-%d',
+ T: '%H:%M:%S',
+ X: '%H:%M:%S',
+ x: '%m/%d/%y',
+ R: '%H:%M',
+ r: '%I:%M:%S %p',
+ t: '\t',
+ n: '\n',
+ '%': '%'
+ }
+ };
+ //
+ // Conceptually, the logic implemented here is similar to Ken Snyder's Date Instance Methods.
+ // I use his idea of a set of parsers which can be regular expressions or functions,
+ // iterating through those, and then seeing if Date.parse() will create a date.
+ // The parser expressions and functions are a little different and some bugs have been
+ // worked out. Also, a lot of "pre-parsing" is done to fix implementation
+ // variations of Date.parse() between browsers.
+ //
+ jsDate.createDate = function(date) {
+ // if passing in multiple arguments, try Date constructor
+ if (date == null) {
+ return new Date();
+ }
+ // If the passed value is already a date object, return it
+ if (date instanceof Date) {
+ return date;
+ }
+ // if (typeof date == 'number') return new Date(date * 1000);
+ // If the passed value is an integer, interpret it as a javascript timestamp
+ if (typeof date == 'number') {
+ return new Date(date);
+ }
+
+ // Before passing strings into Date.parse(), have to normalize them for certain conditions.
+ // If strings are not formatted staccording to the EcmaScript spec, results from Date parse will be implementation dependent.
+ //
+ // For example:
+ // * FF and Opera assume 2 digit dates are pre y2k, Chome assumes <50 is pre y2k, 50+ is 21st century.
+ // * Chrome will correctly parse '1984-1-25' into localtime, FF and Opera will not parse.
+ // * Both FF, Chrome and Opera will parse '1984/1/25' into localtime.
+
+ // remove leading and trailing spaces
+ var parsable = String(date).replace(/^\s*(.+)\s*$/g, '$1');
+
+ // replace dahses (-) with slashes (/) in dates like n[nnn]/n[n]/n[nnn]
+ parsable = parsable.replace(/^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,4})/, "$1/$2/$3");
+
+ /////////
+ // Need to check for '15-Dec-09' also.
+ // FF will not parse, but Chrome will.
+ // Chrome will set date to 2009 as well.
+ /////////
+
+ // first check for 'dd-mmm-yyyy' or 'dd/mmm/yyyy' like '15-Dec-2010'
+ parsable = parsable.replace(/^(3[01]|[0-2]?\d)[-\/]([a-z]{3,})[-\/](\d{4})/i, "$1 $2 $3");
+
+ // Now check for 'dd-mmm-yy' or 'dd/mmm/yy' and normalize years to default century.
+ var match = parsable.match(/^(3[01]|[0-2]?\d)[-\/]([a-z]{3,})[-\/](\d{2})\D*/i);
+ if (match && match.length > 3) {
+ var m3 = parseFloat(match[3]);
+ var ny = jsDate.config.defaultCentury + m3;
+ ny = String(ny);
+
+ // now replace 2 digit year with 4 digit year
+ parsable = parsable.replace(/^(3[01]|[0-2]?\d)[-\/]([a-z]{3,})[-\/](\d{2})\D*/i, match[1] +' '+ match[2] +' '+ ny);
+
+ }
+
+ // Check for '1/19/70 8:14PM'
+ // where starts with mm/dd/yy or yy/mm/dd and have something after
+ // Check if 1st postiion is greater than 31, assume it is year.
+ // Assme all 2 digit years are 1900's.
+ // Finally, change them into US style mm/dd/yyyy representations.
+ match = parsable.match(/^([0-9]{1,2})[-\/]([0-9]{1,2})[-\/]([0-9]{1,2})[^0-9]/);
+
+ function h1(parsable, match) {
+ var m1 = parseFloat(match[1]);
+ var m2 = parseFloat(match[2]);
+ var m3 = parseFloat(match[3]);
+ var cent = jsDate.config.defaultCentury;
+ var ny, nd, nm, str;
+
+ if (m1 > 31) { // first number is a year
+ nd = m3;
+ nm = m2;
+ ny = cent + m1;
+ }
+
+ else { // last number is the year
+ nd = m2;
+ nm = m1;
+ ny = cent + m3;
+ }
+
+ str = nm+'/'+nd+'/'+ny;
+
+ // now replace 2 digit year with 4 digit year
+ return parsable.replace(/^([0-9]{1,2})[-\/]([0-9]{1,2})[-\/]([0-9]{1,2})/, str);
+
+ }
+
+ if (match && match.length > 3) {
+ parsable = h1(parsable, match);
+ }
+
+ // Now check for '1/19/70' with nothing after and do as above
+ var match = parsable.match(/^([0-9]{1,2})[-\/]([0-9]{1,2})[-\/]([0-9]{1,2})$/);
+
+ if (match && match.length > 3) {
+ parsable = h1(parsable, match);
+ }
+
+
+ var i = 0;
+ var length = jsDate.matchers.length;
+ var pattern,
+ ms,
+ current = parsable,
+ obj;
+ while (i < length) {
+ ms = Date.parse(current);
+ if (!isNaN(ms)) {
+ return new Date(ms);
+ }
+ pattern = jsDate.matchers[i];
+ if (typeof pattern == 'function') {
+ obj = pattern.call(jsDate, current);
+ if (obj instanceof Date) {
+ return obj;
+ }
+ } else {
+ current = parsable.replace(pattern[0], pattern[1]);
+ }
+ i++;
+ }
+ return NaN;
+ };
+
+
+ /**
+ * @static
+ * Handy static utility function to return the number of days in a given month.
+ * @param {Integer} year Year
+ * @param {Integer} month Month (1-12)
+ * @returns {Integer} Number of days in the month.
+ */
+ //
+ // handy utility method Borrowed right from Ken Snyder's Date Instance Mehtods.
+ //
+ jsDate.daysInMonth = function(year, month) {
+ if (month == 2) {
+ return new Date(year, 1, 29).getDate() == 29 ? 29 : 28;
+ }
+ return [undefined,31,undefined,31,30,31,30,31,31,30,31,30,31][month];
+ };
+
+
+ //
+ // An Array of regular expressions or functions that will attempt to match the date string.
+ // Functions are called with scope of a jsDate instance.
+ //
+ jsDate.matchers = [
+ // convert dd.mmm.yyyy to mm/dd/yyyy (world date to US date).
+ [/(3[01]|[0-2]\d)\s*\.\s*(1[0-2]|0\d)\s*\.\s*([1-9]\d{3})/, '$2/$1/$3'],
+ // convert yyyy-mm-dd to mm/dd/yyyy (ISO date to US date).
+ [/([1-9]\d{3})\s*-\s*(1[0-2]|0\d)\s*-\s*(3[01]|[0-2]\d)/, '$2/$3/$1'],
+ // Handle 12 hour or 24 hour time with milliseconds am/pm and optional date part.
+ function(str) {
+ var match = str.match(/^(?:(.+)\s+)?([012]?\d)(?:\s*\:\s*(\d\d))?(?:\s*\:\s*(\d\d(\.\d*)?))?\s*(am|pm)?\s*$/i);
+ // opt. date hour opt. minute opt. second opt. msec opt. am or pm
+ if (match) {
+ if (match[1]) {
+ var d = this.createDate(match[1]);
+ if (isNaN(d)) {
+ return;
+ }
+ } else {
+ var d = new Date();
+ d.setMilliseconds(0);
+ }
+ var hour = parseFloat(match[2]);
+ if (match[6]) {
+ hour = match[6].toLowerCase() == 'am' ? (hour == 12 ? 0 : hour) : (hour == 12 ? 12 : hour + 12);
+ }
+ d.setHours(hour, parseInt(match[3] || 0, 10), parseInt(match[4] || 0, 10), ((parseFloat(match[5] || 0)) || 0)*1000);
+ return d;
+ }
+ else {
+ return str;
+ }
+ },
+ // Handle ISO timestamp with time zone.
+ function(str) {
+ var match = str.match(/^(?:(.+))[T|\s+]([012]\d)(?:\:(\d\d))(?:\:(\d\d))(?:\.\d+)([\+\-]\d\d\:\d\d)$/i);
+ if (match) {
+ if (match[1]) {
+ var d = this.createDate(match[1]);
+ if (isNaN(d)) {
+ return;
+ }
+ } else {
+ var d = new Date();
+ d.setMilliseconds(0);
+ }
+ var hour = parseFloat(match[2]);
+ d.setHours(hour, parseInt(match[3], 10), parseInt(match[4], 10), parseFloat(match[5])*1000);
+ return d;
+ }
+ else {
+ return str;
+ }
+ },
+ // Try to match ambiguous strings like 12/8/22.
+ // Use FF date assumption that 2 digit years are 20th century (i.e. 1900's).
+ // This may be redundant with pre processing of date already performed.
+ function(str) {
+ var match = str.match(/^([0-3]?\d)\s*[-\/.\s]{1}\s*([a-zA-Z]{3,9})\s*[-\/.\s]{1}\s*([0-3]?\d)$/);
+ if (match) {
+ var d = new Date();
+ var cent = jsDate.config.defaultCentury;
+ var m1 = parseFloat(match[1]);
+ var m3 = parseFloat(match[3]);
+ var ny, nd, nm;
+ if (m1 > 31) { // first number is a year
+ nd = m3;
+ ny = cent + m1;
+ }
+
+ else { // last number is the year
+ nd = m1;
+ ny = cent + m3;
+ }
+
+ var nm = inArray(match[2], jsDate.regional[jsDate.regional.getLocale()]["monthNamesShort"]);
+
+ if (nm == -1) {
+ nm = inArray(match[2], jsDate.regional[jsDate.regional.getLocale()]["monthNames"]);
+ }
+
+ d.setFullYear(ny, nm, nd);
+ d.setHours(0,0,0,0);
+ return d;
+ }
+
+ else {
+ return str;
+ }
+ }
+ ];
+
+ //
+ // I think John Reisig published this method on his blog, ejohn.
+ //
+ function inArray( elem, array ) {
+ if ( array.indexOf ) {
+ return array.indexOf( elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ //
+ // Thanks to Kangax, Christian Sciberras and Stack Overflow for this method.
+ //
+ function get_type(thing){
+ if(thing===null) return "[object Null]"; // special case
+ return Object.prototype.toString.call(thing);
+ }
+
+ $.jsDate = jsDate;
+
+
+ /**
+ * JavaScript printf/sprintf functions.
+ *
+ * This code has been adapted from the publicly available sprintf methods
+ * by Ash Searle. His original header follows:
+ *
+ * This code is unrestricted: you are free to use it however you like.
+ *
+ * The functions should work as expected, performing left or right alignment,
+ * truncating strings, outputting numbers with a required precision etc.
+ *
+ * For complex cases, these functions follow the Perl implementations of
+ * (s)printf, allowing arguments to be passed out-of-order, and to set the
+ * precision or length of the output based on arguments instead of fixed
+ * numbers.
+ *
+ * See http://perldoc.perl.org/functions/sprintf.html for more information.
+ *
+ * Implemented:
+ * - zero and space-padding
+ * - right and left-alignment,
+ * - base X prefix (binary, octal and hex)
+ * - positive number prefix
+ * - (minimum) width
+ * - precision / truncation / maximum width
+ * - out of order arguments
+ *
+ * Not implemented (yet):
+ * - vector flag
+ * - size (bytes, words, long-words etc.)
+ *
+ * Will not implement:
+ * - %n or %p (no pass-by-reference in JavaScript)
+ *
+ * @version 2007.04.27
+ * @author Ash Searle
+ *
+ * You can see the original work and comments on his blog:
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ */
+
+ /**
+ * @Modifications 2009.05.26
+ * @author Chris Leonello
+ *
+ * Added %p %P specifier
+ * Acts like %g or %G but will not add more significant digits to the output than present in the input.
+ * Example:
+ * Format: '%.3p', Input: 0.012, Output: 0.012
+ * Format: '%.3g', Input: 0.012, Output: 0.0120
+ * Format: '%.4p', Input: 12.0, Output: 12.0
+ * Format: '%.4g', Input: 12.0, Output: 12.00
+ * Format: '%.4p', Input: 4.321e-5, Output: 4.321e-5
+ * Format: '%.4g', Input: 4.321e-5, Output: 4.3210e-5
+ *
+ * Example:
+ * >>> $.jqplot.sprintf('%.2f, %d', 23.3452, 43.23)
+ * "23.35, 43"
+ * >>> $.jqplot.sprintf("no value: %n, decimal with thousands separator: %'d", 23.3452, 433524)
+ * "no value: , decimal with thousands separator: 433,524"
+ */
+ $.jqplot.sprintf = function() {
+ function pad(str, len, chr, leftJustify) {
+ var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
+ return leftJustify ? str + padding : padding + str;
+
+ }
+
+ function thousand_separate(value) {
+ var value_str = new String(value);
+ for (var i=10; i>0; i--) {
+ if (value_str == (value_str = value_str.replace(/^(\d+)(\d{3})/, "$1"+$.jqplot.sprintf.thousandsSeparator+"$2"))) break;
+ }
+ return value_str;
+ }
+
+ function justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpace) {
+ var diff = minWidth - value.length;
+ if (diff > 0) {
+ var spchar = ' ';
+ if (htmlSpace) { spchar = '&nbsp;'; }
+ if (leftJustify || !zeroPad) {
+ value = pad(value, minWidth, spchar, leftJustify);
+ } else {
+ value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
+ }
+ }
+ return value;
+ }
+
+ function formatBaseX(value, base, prefix, leftJustify, minWidth, precision, zeroPad, htmlSpace) {
+ // Note: casts negative numbers to positive ones
+ var number = value >>> 0;
+ prefix = prefix && number && {'2': '0b', '8': '0', '16': '0x'}[base] || '';
+ value = prefix + pad(number.toString(base), precision || 0, '0', false);
+ return justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpace);
+ }
+
+ function formatString(value, leftJustify, minWidth, precision, zeroPad, htmlSpace) {
+ if (precision != null) {
+ value = value.slice(0, precision);
+ }
+ return justify(value, '', leftJustify, minWidth, zeroPad, htmlSpace);
+ }
+
+ var a = arguments, i = 0, format = a[i++];
+
+ return format.replace($.jqplot.sprintf.regex, function(substring, valueIndex, flags, minWidth, _, precision, type) {
+ if (substring == '%%') { return '%'; }
+
+ // parse flags
+ var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false, htmlSpace = false, thousandSeparation = false;
+ for (var j = 0; flags && j < flags.length; j++) switch (flags.charAt(j)) {
+ case ' ': positivePrefix = ' '; break;
+ case '+': positivePrefix = '+'; break;
+ case '-': leftJustify = true; break;
+ case '0': zeroPad = true; break;
+ case '#': prefixBaseX = true; break;
+ case '&': htmlSpace = true; break;
+ case '\'': thousandSeparation = true; break;
+ }
+
+ // parameters may be null, undefined, empty-string or real valued
+ // we want to ignore null, undefined and empty-string values
+
+ if (!minWidth) {
+ minWidth = 0;
+ }
+ else if (minWidth == '*') {
+ minWidth = +a[i++];
+ }
+ else if (minWidth.charAt(0) == '*') {
+ minWidth = +a[minWidth.slice(1, -1)];
+ }
+ else {
+ minWidth = +minWidth;
+ }
+
+ // Note: undocumented perl feature:
+ if (minWidth < 0) {
+ minWidth = -minWidth;
+ leftJustify = true;
+ }
+
+ if (!isFinite(minWidth)) {
+ throw new Error('$.jqplot.sprintf: (minimum-)width must be finite');
+ }
+
+ if (!precision) {
+ precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0);
+ }
+ else if (precision == '*') {
+ precision = +a[i++];
+ }
+ else if (precision.charAt(0) == '*') {
+ precision = +a[precision.slice(1, -1)];
+ }
+ else {
+ precision = +precision;
+ }
+
+ // grab value using valueIndex if required?
+ var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];
+
+ switch (type) {
+ case 's': {
+ if (value == null) {
+ return '';
+ }
+ return formatString(String(value), leftJustify, minWidth, precision, zeroPad, htmlSpace);
+ }
+ case 'c': return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad, htmlSpace);
+ case 'b': return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad,htmlSpace);
+ case 'o': return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad, htmlSpace);
+ case 'x': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad, htmlSpace);
+ case 'X': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad, htmlSpace).toUpperCase();
+ case 'u': return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad, htmlSpace);
+ case 'i': {
+ var number = parseInt(+value, 10);
+ if (isNaN(number)) {
+ return '';
+ }
+ var prefix = number < 0 ? '-' : positivePrefix;
+ var number_str = thousandSeparation ? thousand_separate(String(Math.abs(number))): String(Math.abs(number));
+ value = prefix + pad(number_str, precision, '0', false);
+ //value = prefix + pad(String(Math.abs(number)), precision, '0', false);
+ return justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpace);
+ }
+ case 'd': {
+ var number = Math.round(+value);
+ if (isNaN(number)) {
+ return '';
+ }
+ var prefix = number < 0 ? '-' : positivePrefix;
+ var number_str = thousandSeparation ? thousand_separate(String(Math.abs(number))): String(Math.abs(number));
+ value = prefix + pad(number_str, precision, '0', false);
+ return justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpace);
+ }
+ case 'e':
+ case 'E':
+ case 'f':
+ case 'F':
+ case 'g':
+ case 'G':
+ {
+ var number = +value;
+ if (isNaN(number)) {
+ return '';
+ }
+ var prefix = number < 0 ? '-' : positivePrefix;
+ var method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
+ var textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2];
+ var number_str = Math.abs(number)[method](precision);
+
+ // Apply the decimal mark properly by splitting the number by the
+ // decimalMark, applying thousands separator, and then placing it
+ // back in.
+ var parts = number_str.toString().split('.');
+ parts[0] = thousandSeparation ? thousand_separate(parts[0]) : parts[0];
+ number_str = parts.join($.jqplot.sprintf.decimalMark);
+
+ value = prefix + number_str;
+ var justified = justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpace)[textTransform]();
+
+ return justified;
+ }
+ case 'p':
+ case 'P':
+ {
+ // make sure number is a number
+ var number = +value;
+ if (isNaN(number)) {
+ return '';
+ }
+ var prefix = number < 0 ? '-' : positivePrefix;
+
+ var parts = String(Number(Math.abs(number)).toExponential()).split(/e|E/);
+ var sd = (parts[0].indexOf('.') != -1) ? parts[0].length - 1 : String(number).length;
+ var zeros = (parts[1] < 0) ? -parts[1] - 1 : 0;
+
+ if (Math.abs(number) < 1) {
+ if (sd + zeros <= precision) {
+ value = prefix + Math.abs(number).toPrecision(sd);
+ }
+ else {
+ if (sd <= precision - 1) {
+ value = prefix + Math.abs(number).toExponential(sd-1);
+ }
+ else {
+ value = prefix + Math.abs(number).toExponential(precision-1);
+ }
+ }
+ }
+ else {
+ var prec = (sd <= precision) ? sd : precision;
+ value = prefix + Math.abs(number).toPrecision(prec);
+ }
+ var textTransform = ['toString', 'toUpperCase']['pP'.indexOf(type) % 2];
+ return justify(value, prefix, leftJustify, minWidth, zeroPad, htmlSpace)[textTransform]();
+ }
+ case 'n': return '';
+ default: return substring;
+ }
+ });
+ };
+
+ $.jqplot.sprintf.thousandsSeparator = ',';
+ // Specifies the decimal mark for floating point values. By default a period '.'
+ // is used. If you change this value to for example a comma be sure to also
+ // change the thousands separator or else this won't work since a simple String
+ // replace is used (replacing all periods with the mark specified here).
+ $.jqplot.sprintf.decimalMark = '.';
+
+ $.jqplot.sprintf.regex = /%%|%(\d+\$)?([-+#0&\' ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([nAscboxXuidfegpEGP])/g;
+
+ $.jqplot.getSignificantFigures = function(number) {
+ var parts = String(Number(Math.abs(number)).toExponential()).split(/e|E/);
+ // total significant digits
+ var sd = (parts[0].indexOf('.') != -1) ? parts[0].length - 1 : parts[0].length;
+ var zeros = (parts[1] < 0) ? -parts[1] - 1 : 0;
+ // exponent
+ var expn = parseInt(parts[1], 10);
+ // digits to the left of the decimal place
+ var dleft = (expn + 1 > 0) ? expn + 1 : 0;
+ // digits to the right of the decimal place
+ var dright = (sd <= dleft) ? 0 : sd - expn - 1;
+ return {significantDigits: sd, digitsLeft: dleft, digitsRight: dright, zeros: zeros, exponent: expn} ;
+ };
+
+ $.jqplot.getPrecision = function(number) {
+ return $.jqplot.getSignificantFigures(number).digitsRight;
+ };
+
+
+
+
+ var backCompat = $.uiBackCompat !== false;
+
+ $.jqplot.effects = {
+ effect: {}
+ };
+
+ // prefix used for storing data on .data()
+ var dataSpace = "jqplot.storage.";
+
+ /******************************************************************************/
+ /*********************************** EFFECTS **********************************/
+ /******************************************************************************/
+
+ $.extend( $.jqplot.effects, {
+ version: "1.9pre",
+
+ // Saves a set of properties in a data storage
+ save: function( element, set ) {
+ for( var i=0; i < set.length; i++ ) {
+ if ( set[ i ] !== null ) {
+ element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] );
+ }
+ }
+ },
+
+ // Restores a set of previously saved properties from a data storage
+ restore: function( element, set ) {
+ for( var i=0; i < set.length; i++ ) {
+ if ( set[ i ] !== null ) {
+ element.css( set[ i ], element.data( dataSpace + set[ i ] ) );
+ }
+ }
+ },
+
+ setMode: function( el, mode ) {
+ if (mode === "toggle") {
+ mode = el.is( ":hidden" ) ? "show" : "hide";
+ }
+ return mode;
+ },
+
+ // Wraps the element around a wrapper that copies position properties
+ createWrapper: function( element ) {
+
+ // if the element is already wrapped, return it
+ if ( element.parent().is( ".ui-effects-wrapper" )) {
+ return element.parent();
+ }
+
+ // wrap the element
+ var props = {
+ width: element.outerWidth(true),
+ height: element.outerHeight(true),
+ "float": element.css( "float" )
+ },
+ wrapper = $( "<div></div>" )
+ .addClass( "ui-effects-wrapper" )
+ .css({
+ fontSize: "100%",
+ background: "transparent",
+ border: "none",
+ margin: 0,
+ padding: 0
+ }),
+ // Store the size in case width/height are defined in % - Fixes #5245
+ size = {
+ width: element.width(),
+ height: element.height()
+ },
+ active = document.activeElement;
+
+ element.wrap( wrapper );
+
+ // Fixes #7595 - Elements lose focus when wrapped.
+ if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+ $( active ).focus();
+ }
+
+ wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually loose the reference to the wrapped element
+
+ // transfer positioning properties to the wrapper
+ if ( element.css( "position" ) === "static" ) {
+ wrapper.css({ position: "relative" });
+ element.css({ position: "relative" });
+ } else {
+ $.extend( props, {
+ position: element.css( "position" ),
+ zIndex: element.css( "z-index" )
+ });
+ $.each([ "top", "left", "bottom", "right" ], function(i, pos) {
+ props[ pos ] = element.css( pos );
+ if ( isNaN( parseInt( props[ pos ], 10 ) ) ) {
+ props[ pos ] = "auto";
+ }
+ });
+ element.css({
+ position: "relative",
+ top: 0,
+ left: 0,
+ right: "auto",
+ bottom: "auto"
+ });
+ }
+ element.css(size);
+
+ return wrapper.css( props ).show();
+ },
+
+ removeWrapper: function( element ) {
+ var active = document.activeElement;
+
+ if ( element.parent().is( ".ui-effects-wrapper" ) ) {
+ element.parent().replaceWith( element );
+
+ // Fixes #7595 - Elements lose focus when wrapped.
+ if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+ $( active ).focus();
+ }
+ }
+
+
+ return element;
+ }
+ });
+
+ // return an effect options object for the given parameters:
+ function _normalizeArguments( effect, options, speed, callback ) {
+
+ // short path for passing an effect options object:
+ if ( $.isPlainObject( effect ) ) {
+ return effect;
+ }
+
+ // convert to an object
+ effect = { effect: effect };
+
+ // catch (effect)
+ if ( options === undefined ) {
+ options = {};
+ }
+
+ // catch (effect, callback)
+ if ( $.isFunction( options ) ) {
+ callback = options;
+ speed = null;
+ options = {};
+ }
+
+ // catch (effect, speed, ?)
+ if ( $.type( options ) === "number" || $.fx.speeds[ options ]) {
+ callback = speed;
+ speed = options;
+ options = {};
+ }
+
+ // catch (effect, options, callback)
+ if ( $.isFunction( speed ) ) {
+ callback = speed;
+ speed = null;
+ }
+
+ // add options to effect
+ if ( options ) {
+ $.extend( effect, options );
+ }
+
+ speed = speed || options.duration;
+ effect.duration = $.fx.off ? 0 : typeof speed === "number"
+ ? speed : speed in $.fx.speeds ? $.fx.speeds[ speed ] : $.fx.speeds._default;
+
+ effect.complete = callback || options.complete;
+
+ return effect;
+ }
+
+ function standardSpeed( speed ) {
+ // valid standard speeds
+ if ( !speed || typeof speed === "number" || $.fx.speeds[ speed ] ) {
+ return true;
+ }
+
+ // invalid strings - treat as "normal" speed
+ if ( typeof speed === "string" && !$.jqplot.effects.effect[ speed ] ) {
+ // TODO: remove in 2.0 (#7115)
+ if ( backCompat && $.jqplot.effects[ speed ] ) {
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ $.fn.extend({
+ jqplotEffect: function( effect, options, speed, callback ) {
+ var args = _normalizeArguments.apply( this, arguments ),
+ mode = args.mode,
+ queue = args.queue,
+ effectMethod = $.jqplot.effects.effect[ args.effect ],
+
+ // DEPRECATED: remove in 2.0 (#7115)
+ oldEffectMethod = !effectMethod && backCompat && $.jqplot.effects[ args.effect ];
+
+ if ( $.fx.off || !( effectMethod || oldEffectMethod ) ) {
+ // delegate to the original method (e.g., .show()) if possible
+ if ( mode ) {
+ return this[ mode ]( args.duration, args.complete );
+ } else {
+ return this.each( function() {
+ if ( args.complete ) {
+ args.complete.call( this );
+ }
+ });
+ }
+ }
+
+ function run( next ) {
+ var elem = $( this ),
+ complete = args.complete,
+ mode = args.mode;
+
+ function done() {
+ if ( $.isFunction( complete ) ) {
+ complete.call( elem[0] );
+ }
+ if ( $.isFunction( next ) ) {
+ next();
+ }
+ }
+
+ // if the element is hiddden and mode is hide,
+ // or element is visible and mode is show
+ if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) {
+ done();
+ } else {
+ effectMethod.call( elem[0], args, done );
+ }
+ }
+
+ // TODO: remove this check in 2.0, effectMethod will always be true
+ if ( effectMethod ) {
+ return queue === false ? this.each( run ) : this.queue( queue || "fx", run );
+ } else {
+ // DEPRECATED: remove in 2.0 (#7115)
+ return oldEffectMethod.call(this, {
+ options: args,
+ duration: args.duration,
+ callback: args.complete,
+ mode: args.mode
+ });
+ }
+ }
+ });
+
+
+
+
+ var rvertical = /up|down|vertical/,
+ rpositivemotion = /up|left|vertical|horizontal/;
+
+ $.jqplot.effects.effect.blind = function( o, done ) {
+ // Create element
+ var el = $( this ),
+ props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
+ mode = $.jqplot.effects.setMode( el, o.mode || "hide" ),
+ direction = o.direction || "up",
+ vertical = rvertical.test( direction ),
+ ref = vertical ? "height" : "width",
+ ref2 = vertical ? "top" : "left",
+ motion = rpositivemotion.test( direction ),
+ animation = {},
+ show = mode === "show",
+ wrapper, distance, top;
+
+ // // if already wrapped, the wrapper's properties are my property. #6245
+ if ( el.parent().is( ".ui-effects-wrapper" ) ) {
+ $.jqplot.effects.save( el.parent(), props );
+ } else {
+ $.jqplot.effects.save( el, props );
+ }
+ el.show();
+ top = parseInt(el.css('top'), 10);
+ wrapper = $.jqplot.effects.createWrapper( el ).css({
+ overflow: "hidden"
+ });
+
+ distance = vertical ? wrapper[ ref ]() + top : wrapper[ ref ]();
+
+ animation[ ref ] = show ? String(distance) : '0';
+ if ( !motion ) {
+ el
+ .css( vertical ? "bottom" : "right", 0 )
+ .css( vertical ? "top" : "left", "" )
+ .css({ position: "absolute" });
+ animation[ ref2 ] = show ? '0' : String(distance);
+ }
+
+ // // start at 0 if we are showing
+ if ( show ) {
+ wrapper.css( ref, 0 );
+ if ( ! motion ) {
+ wrapper.css( ref2, distance );
+ }
+ }
+
+ // // Animate
+ wrapper.animate( animation, {
+ duration: o.duration,
+ easing: o.easing,
+ queue: false,
+ complete: function() {
+ if ( mode === "hide" ) {
+ el.hide();
+ }
+ $.jqplot.effects.restore( el, props );
+ $.jqplot.effects.removeWrapper( el );
+ done();
+ }
+ });
+
+ };
+
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.BezierCurveRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.BezierCurveRenderer.js
new file mode 100644
index 0000000..1c7b031
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.BezierCurveRenderer.js
@@ -0,0 +1,314 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ // Class: $.jqplot.BezierCurveRenderer.js
+ // Renderer which draws lines as stacked bezier curves.
+ // Data for the line will not be specified as an array of
+ // [x, y] data point values, but as a an array of [start piont, bezier curve]
+ // So, the line is specified as: [[xstart, ystart], [cp1x, cp1y, cp2x, cp2y, xend, yend]].
+ $.jqplot.BezierCurveRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.BezierCurveRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.BezierCurveRenderer.prototype.constructor = $.jqplot.BezierCurveRenderer;
+
+
+ // Method: setGridData
+ // converts the user data values to grid coordinates and stores them
+ // in the gridData array.
+ // Called with scope of a series.
+ $.jqplot.BezierCurveRenderer.prototype.setGridData = function(plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ // this._plotData should be same as this.data
+ var data = this.data;
+ this.gridData = [];
+ this._prevGridData = [];
+ // if seriesIndex = 0, fill to x axis.
+ // if seriesIndex > 0, fill to previous series data.
+ var idx = this.index;
+ if (data.length == 2) {
+ if (idx == 0) {
+ this.gridData = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
+ xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
+ [xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, this._yaxis.min)],
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
+ ];
+ }
+ else {
+ var psd = plot.series[idx-1].data;
+ this.gridData = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
+ xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
+ [xp.call(this._xaxis, psd[1][4]), yp.call(this._yaxis, psd[1][5])],
+ [xp.call(this._xaxis, psd[1][2]), yp.call(this._yaxis, psd[1][3]),
+ xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
+ xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
+ ];
+ }
+ }
+ else {
+ if (idx == 0) {
+ this.gridData = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
+ xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
+ [xp.call(this._xaxis, data[3][1]), yp.call(this._yaxis, this._yaxis.min)],
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
+ ];
+ }
+ else {
+ var psd = plot.series[idx-1].data;
+ this.gridData = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
+ xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
+ [xp.call(this._xaxis, psd[3][0]), yp.call(this._yaxis, psd[3][1])],
+ [xp.call(this._xaxis, psd[2][0]), yp.call(this._yaxis, psd[2][1]),
+ xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
+ xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
+ ];
+ }
+ }
+ };
+
+ // Method: makeGridData
+ // converts any arbitrary data values to grid coordinates and
+ // returns them. This method exists so that plugins can use a series'
+ // linerenderer to generate grid data points without overwriting the
+ // grid data associated with that series.
+ // Called with scope of a series.
+ $.jqplot.BezierCurveRenderer.prototype.makeGridData = function(data, plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var gd = [];
+ var pgd = [];
+ // if seriesIndex = 0, fill to x axis.
+ // if seriesIndex > 0, fill to previous series data.
+ var idx = this.index;
+ if (data.length == 2) {
+ if (idx == 0) {
+ gd = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
+ xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
+ [xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, this._yaxis.min)],
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
+ ];
+ }
+ else {
+ var psd = plot.series[idx-1].data;
+ gd = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
+ xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
+ [xp.call(this._xaxis, psd[1][4]), yp.call(this._yaxis, psd[1][5])],
+ [xp.call(this._xaxis, psd[1][2]), yp.call(this._yaxis, psd[1][3]),
+ xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
+ xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
+ ];
+ }
+ }
+ else {
+ if (idx == 0) {
+ gd = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
+ xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
+ [xp.call(this._xaxis, data[3][1]), yp.call(this._yaxis, this._yaxis.min)],
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
+ ];
+ }
+ else {
+ var psd = plot.series[idx-1].data;
+ gd = [
+ [xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
+ [xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
+ xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
+ xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
+ [xp.call(this._xaxis, psd[3][0]), yp.call(this._yaxis, psd[3][1])],
+ [xp.call(this._xaxis, psd[2][0]), yp.call(this._yaxis, psd[2][1]),
+ xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
+ xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
+ ];
+ }
+ }
+ return gd;
+ };
+
+
+ // called within scope of series.
+ $.jqplot.BezierCurveRenderer.prototype.draw = function(ctx, gd, options) {
+ var i;
+ ctx.save();
+ if (gd.length) {
+ if (this.showLine) {
+ ctx.save();
+ var opts = (options != null) ? options : {};
+ ctx.fillStyle = opts.fillStyle || this.color;
+ ctx.beginPath();
+ ctx.moveTo(gd[0][0], gd[0][1]);
+ ctx.bezierCurveTo(gd[1][0], gd[1][1], gd[1][2], gd[1][3], gd[1][4], gd[1][5]);
+ ctx.lineTo(gd[2][0], gd[2][1]);
+ if (gd[3].length == 2) {
+ ctx.lineTo(gd[3][0], gd[3][1]);
+ }
+ else {
+ ctx.bezierCurveTo(gd[3][0], gd[3][1], gd[3][2], gd[3][3], gd[3][4], gd[3][5]);
+ }
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+ }
+ }
+
+ ctx.restore();
+ };
+
+ $.jqplot.BezierCurveRenderer.prototype.drawShadow = function(ctx, gd, options) {
+ // This is a no-op, shadows drawn with lines.
+ };
+
+ $.jqplot.BezierAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.BezierAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.BezierAxisRenderer.prototype.constructor = $.jqplot.BezierAxisRenderer;
+
+
+ // Axes on a plot with Bezier Curves
+ $.jqplot.BezierAxisRenderer.prototype.init = function(options){
+ $.extend(true, this, options);
+ var db = this._dataBounds;
+ // Go through all the series attached to this axis and find
+ // the min/max bounds for this axis.
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ var d = s.data;
+ if (d.length == 4) {
+ for (var j=0; j<d.length; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ if (d[j][0] < db.min || db.min == null) {
+ db.min = d[j][0];
+ }
+ if (d[j][0] > db.max || db.max == null) {
+ db.max = d[j][0];
+ }
+ }
+ else {
+ if (d[j][1] < db.min || db.min == null) {
+ db.min = d[j][1];
+ }
+ if (d[j][1] > db.max || db.max == null) {
+ db.max = d[j][1];
+ }
+ }
+ }
+ }
+ else {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ if (d[0][0] < db.min || db.min == null) {
+ db.min = d[0][0];
+ }
+ if (d[0][0] > db.max || db.max == null) {
+ db.max = d[0][0];
+ }
+ for (var j=0; j<5; j+=2) {
+ if (d[1][j] < db.min || db.min == null) {
+ db.min = d[1][j];
+ }
+ if (d[1][j] > db.max || db.max == null) {
+ db.max = d[1][j];
+ }
+ }
+ }
+ else {
+ if (d[0][1] < db.min || db.min == null) {
+ db.min = d[0][1];
+ }
+ if (d[0][1] > db.max || db.max == null) {
+ db.max = d[0][1];
+ }
+ for (var j=1; j<6; j+=2) {
+ if (d[1][j] < db.min || db.min == null) {
+ db.min = d[1][j];
+ }
+ if (d[1][j] > db.max || db.max == null) {
+ db.max = d[1][j];
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = $.extend(true, {pad:0}, options.axesDefaults);
+ options.seriesDefaults = options.seriesDefaults || {};
+ options.legend = $.extend(true, {placement:'outside'}, options.legend);
+ // only set these if there is a pie series
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.BezierCurveRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.BezierCurveRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.BezierAxisRenderer;
+ }
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.barRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.barRenderer.js
new file mode 100644
index 0000000..9b714e9
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.barRenderer.js
@@ -0,0 +1,801 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+
+ // Class: $.jqplot.BarRenderer
+ // A plugin renderer for jqPlot to draw a bar plot.
+ // Draws series as a line.
+
+ $.jqplot.BarRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.BarRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.BarRenderer.prototype.constructor = $.jqplot.BarRenderer;
+
+ // called with scope of series.
+ $.jqplot.BarRenderer.prototype.init = function(options, plot) {
+ // Group: Properties
+ //
+ // prop: barPadding
+ // Number of pixels between adjacent bars at the same axis value.
+ this.barPadding = 8;
+ // prop: barMargin
+ // Number of pixels between groups of bars at adjacent axis values.
+ this.barMargin = 10;
+ // prop: barDirection
+ // 'vertical' = up and down bars, 'horizontal' = side to side bars
+ this.barDirection = 'vertical';
+ // prop: barWidth
+ // Width of the bar in pixels (auto by devaul). null = calculated automatically.
+ this.barWidth = null;
+ // prop: shadowOffset
+ // offset of the shadow from the slice and offset of
+ // each succesive stroke of the shadow from the last.
+ this.shadowOffset = 2;
+ // prop: shadowDepth
+ // number of strokes to apply to the shadow,
+ // each stroke offset shadowOffset from the last.
+ this.shadowDepth = 5;
+ // prop: shadowAlpha
+ // transparency of the shadow (0 = transparent, 1 = opaque)
+ this.shadowAlpha = 0.08;
+ // prop: waterfall
+ // true to enable waterfall plot.
+ this.waterfall = false;
+ // prop: groups
+ // group bars into this many groups
+ this.groups = 1;
+ // prop: varyBarColor
+ // true to color each bar of a series separately rather than
+ // have every bar of a given series the same color.
+ // If used for non-stacked multiple series bar plots, user should
+ // specify a separate 'seriesColors' array for each series.
+ // Otherwise, each series will set their bars to the same color array.
+ // This option has no Effect for stacked bar charts and is disabled.
+ this.varyBarColor = false;
+ // prop: highlightMouseOver
+ // True to highlight slice when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on a slice.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over a slice.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColors
+ // an array of colors to use when highlighting a bar.
+ this.highlightColors = [];
+ // prop: transposedData
+ // NOT IMPLEMENTED YET. True if this is a horizontal bar plot and
+ // x and y values are "transposed". Tranposed, or "swapped", data is
+ // required prior to rev. 894 builds of jqPlot with horizontal bars.
+ // Allows backward compatability of bar renderer horizontal bars with
+ // old style data sets.
+ this.transposedData = true;
+ this.renderer.animation = {
+ show: false,
+ direction: 'down',
+ speed: 3000,
+ _supported: true
+ };
+ this._type = 'bar';
+
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (options.highlightMouseDown && options.highlightMouseOver == null) {
+ options.highlightMouseOver = false;
+ }
+
+ //////
+ // This is probably wrong here.
+ // After going back and forth on whether renderer should be the thing
+ // or extend the thing, it seems that it it best if it is a property
+ // on the thing. This should be something that is commonized
+ // among series renderers in the future.
+ //////
+ $.extend(true, this, options);
+
+ // really should probably do this
+ $.extend(true, this.renderer, options);
+ // fill is still needed to properly draw the legend.
+ // bars have to be filled.
+ this.fill = true;
+
+ // if horizontal bar and animating, reset the default direction
+ if (this.barDirection === 'horizontal' && this.rendererOptions.animation && this.rendererOptions.animation.direction == null) {
+ this.renderer.animation.direction = 'left';
+ }
+
+ if (this.waterfall) {
+ this.fillToZero = false;
+ this.disableStack = true;
+ }
+
+ if (this.barDirection == 'vertical' ) {
+ this._primaryAxis = '_xaxis';
+ this._stackAxis = 'y';
+ this.fillAxis = 'y';
+ }
+ else {
+ this._primaryAxis = '_yaxis';
+ this._stackAxis = 'x';
+ this.fillAxis = 'x';
+ }
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+ // total number of values for all bar series, total number of bar series, and position of this series
+ this._plotSeriesInfo = null;
+ // Array of actual data colors used for each data point.
+ this._dataColors = [];
+ this._barPoints = [];
+
+ // set the shape renderer options
+ var opts = {lineJoin:'miter', lineCap:'round', fill:true, isarc:false, strokeStyle:this.color, fillStyle:this.color, closePath:this.fill};
+ this.renderer.shapeRenderer.init(opts);
+ // set the shadow renderer options
+ var sopts = {lineJoin:'miter', lineCap:'round', fill:true, isarc:false, angle:this.shadowAngle, offset:this.shadowOffset, alpha:this.shadowAlpha, depth:this.shadowDepth, closePath:this.fill};
+ this.renderer.shadowRenderer.init(sopts);
+
+ plot.postInitHooks.addOnce(postInit);
+ plot.postDrawHooks.addOnce(postPlotDraw);
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+ plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
+ plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
+ plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
+ plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
+ };
+
+ // called with scope of series
+ function barPreInit(target, data, seriesDefaults, options) {
+ if (this.rendererOptions.barDirection == 'horizontal') {
+ this._stackAxis = 'x';
+ this._primaryAxis = '_yaxis';
+ }
+ if (this.rendererOptions.waterfall == true) {
+ this._data = $.extend(true, [], this.data);
+ var sum = 0;
+ var pos = (!this.rendererOptions.barDirection || this.rendererOptions.barDirection === 'vertical' || this.transposedData === false) ? 1 : 0;
+ for(var i=0; i<this.data.length; i++) {
+ sum += this.data[i][pos];
+ if (i>0) {
+ this.data[i][pos] += this.data[i-1][pos];
+ }
+ }
+ this.data[this.data.length] = (pos == 1) ? [this.data.length+1, sum] : [sum, this.data.length+1];
+ this._data[this._data.length] = (pos == 1) ? [this._data.length+1, sum] : [sum, this._data.length+1];
+ }
+ if (this.rendererOptions.groups > 1) {
+ this.breakOnNull = true;
+ var l = this.data.length;
+ var skip = parseInt(l/this.rendererOptions.groups, 10);
+ var count = 0;
+ for (var i=skip; i<l; i+=skip) {
+ this.data.splice(i+count, 0, [null, null]);
+ this._plotData.splice(i+count, 0, [null, null]);
+ this._stackData.splice(i+count, 0, [null, null]);
+ count++;
+ }
+ for (i=0; i<this.data.length; i++) {
+ if (this._primaryAxis == '_xaxis') {
+ this.data[i][0] = i+1;
+ this._plotData[i][0] = i+1;
+ this._stackData[i][0] = i+1;
+ }
+ else {
+ this.data[i][1] = i+1;
+ this._plotData[i][1] = i+1;
+ this._stackData[i][1] = i+1;
+ }
+ }
+ }
+ }
+
+ $.jqplot.preSeriesInitHooks.push(barPreInit);
+
+ // needs to be called with scope of series, not renderer.
+ $.jqplot.BarRenderer.prototype.calcSeriesNumbers = function() {
+ var nvals = 0;
+ var nseries = 0;
+ var paxis = this[this._primaryAxis];
+ var s, series, pos;
+ // loop through all series on this axis
+ for (var i=0; i < paxis._series.length; i++) {
+ series = paxis._series[i];
+ if (series === this) {
+ pos = i;
+ }
+ // is the series rendered as a bar?
+ if (series.renderer.constructor == $.jqplot.BarRenderer) {
+ // gridData may not be computed yet, use data length insted
+ nvals += series.data.length;
+ nseries += 1;
+ }
+ }
+ // return total number of values for all bar series, total number of bar series, and position of this series
+ return [nvals, nseries, pos];
+ };
+
+ $.jqplot.BarRenderer.prototype.setBarWidth = function() {
+ // need to know how many data values we have on the approprate axis and figure it out.
+ var i;
+ var nvals = 0;
+ var nseries = 0;
+ var paxis = this[this._primaryAxis];
+ var s, series, pos;
+ var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
+ nvals = temp[0];
+ nseries = temp[1];
+ var nticks = paxis.numberTicks;
+ var nbins = (nticks-1)/2;
+ // so, now we have total number of axis values.
+ if (paxis.name == 'xaxis' || paxis.name == 'x2axis') {
+ if (this._stack) {
+ this.barWidth = (paxis._offsets.max - paxis._offsets.min) / nvals * nseries - this.barMargin;
+ }
+ else {
+ this.barWidth = ((paxis._offsets.max - paxis._offsets.min)/nbins - this.barPadding * (nseries-1) - this.barMargin*2)/nseries;
+ // this.barWidth = (paxis._offsets.max - paxis._offsets.min) / nvals - this.barPadding - this.barMargin/nseries;
+ }
+ }
+ else {
+ if (this._stack) {
+ this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals * nseries - this.barMargin;
+ }
+ else {
+ this.barWidth = ((paxis._offsets.min - paxis._offsets.max)/nbins - this.barPadding * (nseries-1) - this.barMargin*2)/nseries;
+ // this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals - this.barPadding - this.barMargin/nseries;
+ }
+ }
+ return [nvals, nseries];
+ };
+
+ function computeHighlightColors (colors) {
+ var ret = [];
+ for (var i=0; i<colors.length; i++){
+ var rgba = $.jqplot.getColorComponents(colors[i]);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
+ newrgb[j] = parseInt(newrgb[j], 10);
+ }
+ ret.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
+ }
+ return ret;
+ }
+
+ function getStart(sidx, didx, comp, plot, axis) {
+ // check if sign change
+ var seriesIndex = sidx,
+ prevSeriesIndex = sidx - 1,
+ start,
+ prevVal,
+ aidx = (axis === 'x') ? 0 : 1;
+
+ // is this not the first series?
+ if (seriesIndex > 0) {
+ prevVal = plot.series[prevSeriesIndex]._plotData[didx][aidx];
+
+ // is there a sign change
+ if ((comp * prevVal) < 0) {
+ start = getStart(prevSeriesIndex, didx, comp, plot, axis);
+ }
+
+ // no sign change.
+ else {
+ start = plot.series[prevSeriesIndex].gridData[didx][aidx];
+ }
+
+ }
+
+ // if first series, return value at 0
+ else {
+
+ start = (aidx === 0) ? plot.series[seriesIndex]._xaxis.series_u2p(0) : plot.series[seriesIndex]._yaxis.series_u2p(0);
+ }
+
+ return start;
+ }
+
+
+ $.jqplot.BarRenderer.prototype.draw = function(ctx, gridData, options, plot) {
+ var i;
+ // Ughhh, have to make a copy of options b/c it may be modified later.
+ var opts = $.extend({}, options);
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var xaxis = this.xaxis;
+ var yaxis = this.yaxis;
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var pointx, pointy;
+ // clear out data colors.
+ this._dataColors = [];
+ this._barPoints = [];
+
+ if (this.barWidth == null) {
+ this.renderer.setBarWidth.call(this);
+ }
+
+ var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
+ var nvals = temp[0];
+ var nseries = temp[1];
+ var pos = temp[2];
+ var points = [];
+
+ if (this._stack) {
+ this._barNudge = 0;
+ }
+ else {
+ this._barNudge = (-Math.abs(nseries/2 - 0.5) + pos) * (this.barWidth + this.barPadding);
+ }
+ if (showLine) {
+ var negativeColors = new $.jqplot.ColorGenerator(this.negativeSeriesColors);
+ var positiveColors = new $.jqplot.ColorGenerator(this.seriesColors);
+ var negativeColor = negativeColors.get(this.index);
+ if (! this.useNegativeColors) {
+ negativeColor = opts.fillStyle;
+ }
+ var positiveColor = opts.fillStyle;
+ var base;
+ var xstart;
+ var ystart;
+
+ if (this.barDirection == 'vertical') {
+ for (var i=0; i<gridData.length; i++) {
+ if (!this._stack && this.data[i][1] == null) {
+ continue;
+ }
+ points = [];
+ base = gridData[i][0] + this._barNudge;
+
+ // stacked
+ if (this._stack && this._prevGridData.length) {
+ ystart = getStart(this.index, i, this._plotData[i][1], plot, 'y');
+ }
+
+ // not stacked
+ else {
+ if (this.fillToZero) {
+ ystart = this._yaxis.series_u2p(0);
+ }
+ else if (this.waterfall && i > 0 && i < this.gridData.length-1) {
+ ystart = this.gridData[i-1][1];
+ }
+ else if (this.waterfall && i == 0 && i < this.gridData.length-1) {
+ if (this._yaxis.min <= 0 && this._yaxis.max >= 0) {
+ ystart = this._yaxis.series_u2p(0);
+ }
+ else if (this._yaxis.min > 0) {
+ ystart = ctx.canvas.height;
+ }
+ else {
+ ystart = 0;
+ }
+ }
+ else if (this.waterfall && i == this.gridData.length - 1) {
+ if (this._yaxis.min <= 0 && this._yaxis.max >= 0) {
+ ystart = this._yaxis.series_u2p(0);
+ }
+ else if (this._yaxis.min > 0) {
+ ystart = ctx.canvas.height;
+ }
+ else {
+ ystart = 0;
+ }
+ }
+ else {
+ ystart = ctx.canvas.height;
+ }
+ }
+ if ((this.fillToZero && this._plotData[i][1] < 0) || (this.waterfall && this._data[i][1] < 0)) {
+ if (this.varyBarColor && !this._stack) {
+ if (this.useNegativeColors) {
+ opts.fillStyle = negativeColors.next();
+ }
+ else {
+ opts.fillStyle = positiveColors.next();
+ }
+ }
+ else {
+ opts.fillStyle = negativeColor;
+ }
+ }
+ else {
+ if (this.varyBarColor && !this._stack) {
+ opts.fillStyle = positiveColors.next();
+ }
+ else {
+ opts.fillStyle = positiveColor;
+ }
+ }
+
+ if (!this.fillToZero || this._plotData[i][1] >= 0) {
+ points.push([base-this.barWidth/2, ystart]);
+ points.push([base-this.barWidth/2, gridData[i][1]]);
+ points.push([base+this.barWidth/2, gridData[i][1]]);
+ points.push([base+this.barWidth/2, ystart]);
+ }
+ // for negative bars make sure points are always ordered clockwise
+ else {
+ points.push([base-this.barWidth/2, gridData[i][1]]);
+ points.push([base-this.barWidth/2, ystart]);
+ points.push([base+this.barWidth/2, ystart]);
+ points.push([base+this.barWidth/2, gridData[i][1]]);
+ }
+ this._barPoints.push(points);
+ // now draw the shadows if not stacked.
+ // for stacked plots, they are predrawn by drawShadow
+ if (shadow && !this._stack) {
+ var sopts = $.extend(true, {}, opts);
+ // need to get rid of fillStyle on shadow.
+ delete sopts.fillStyle;
+ this.renderer.shadowRenderer.draw(ctx, points, sopts);
+ }
+ var clr = opts.fillStyle || this.color;
+ this._dataColors.push(clr);
+ this.renderer.shapeRenderer.draw(ctx, points, opts);
+ }
+ }
+
+ else if (this.barDirection == 'horizontal'){
+ for (var i=0; i<gridData.length; i++) {
+ if (!this._stack && this.data[i][0] == null) {
+ continue;
+ }
+ points = [];
+ base = gridData[i][1] - this._barNudge;
+ xstart;
+
+ if (this._stack && this._prevGridData.length) {
+ xstart = getStart(this.index, i, this._plotData[i][0], plot, 'x');
+ }
+ // not stacked
+ else {
+ if (this.fillToZero) {
+ xstart = this._xaxis.series_u2p(0);
+ }
+ else if (this.waterfall && i > 0 && i < this.gridData.length-1) {
+ xstart = this.gridData[i-1][0];
+ }
+ else if (this.waterfall && i == 0 && i < this.gridData.length-1) {
+ if (this._xaxis.min <= 0 && this._xaxis.max >= 0) {
+ xstart = this._xaxis.series_u2p(0);
+ }
+ else if (this._xaxis.min > 0) {
+ xstart = 0;
+ }
+ else {
+ xstart = 0;
+ }
+ }
+ else if (this.waterfall && i == this.gridData.length - 1) {
+ if (this._xaxis.min <= 0 && this._xaxis.max >= 0) {
+ xstart = this._xaxis.series_u2p(0);
+ }
+ else if (this._xaxis.min > 0) {
+ xstart = 0;
+ }
+ else {
+ xstart = ctx.canvas.width;
+ }
+ }
+ else {
+ xstart = 0;
+ }
+ }
+ if ((this.fillToZero && this._plotData[i][0] < 0) || (this.waterfall && this._data[i][0] < 0)) {
+ if (this.varyBarColor && !this._stack) {
+ if (this.useNegativeColors) {
+ opts.fillStyle = negativeColors.next();
+ }
+ else {
+ opts.fillStyle = positiveColors.next();
+ }
+ }
+ else {
+ opts.fillStyle = negativeColor;
+ }
+ }
+ else {
+ if (this.varyBarColor && !this._stack) {
+ opts.fillStyle = positiveColors.next();
+ }
+ else {
+ opts.fillStyle = positiveColor;
+ }
+ }
+
+
+ if (!this.fillToZero || this._plotData[i][0] >= 0) {
+ points.push([xstart, base + this.barWidth / 2]);
+ points.push([xstart, base - this.barWidth / 2]);
+ points.push([gridData[i][0], base - this.barWidth / 2]);
+ points.push([gridData[i][0], base + this.barWidth / 2]);
+ }
+ else {
+ points.push([gridData[i][0], base + this.barWidth / 2]);
+ points.push([gridData[i][0], base - this.barWidth / 2]);
+ points.push([xstart, base - this.barWidth / 2]);
+ points.push([xstart, base + this.barWidth / 2]);
+ }
+
+ this._barPoints.push(points);
+ // now draw the shadows if not stacked.
+ // for stacked plots, they are predrawn by drawShadow
+ if (shadow && !this._stack) {
+ var sopts = $.extend(true, {}, opts);
+ delete sopts.fillStyle;
+ this.renderer.shadowRenderer.draw(ctx, points, sopts);
+ }
+ var clr = opts.fillStyle || this.color;
+ this._dataColors.push(clr);
+ this.renderer.shapeRenderer.draw(ctx, points, opts);
+ }
+ }
+ }
+
+ if (this.highlightColors.length == 0) {
+ this.highlightColors = $.jqplot.computeHighlightColors(this._dataColors);
+ }
+
+ else if (typeof(this.highlightColors) == 'string') {
+ var temp = this.highlightColors;
+ this.highlightColors = [];
+ for (var i=0; i<this._dataColors.length; i++) {
+ this.highlightColors.push(temp);
+ }
+ }
+
+ };
+
+
+ // for stacked plots, shadows will be pre drawn by drawShadow.
+ $.jqplot.BarRenderer.prototype.drawShadow = function(ctx, gridData, options, plot) {
+ var i;
+ var opts = (options != undefined) ? options : {};
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var xaxis = this.xaxis;
+ var yaxis = this.yaxis;
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var pointx, points, pointy, nvals, nseries, pos;
+
+ if (this._stack && this.shadow) {
+ if (this.barWidth == null) {
+ this.renderer.setBarWidth.call(this);
+ }
+
+ var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
+ nvals = temp[0];
+ nseries = temp[1];
+ pos = temp[2];
+
+ if (this._stack) {
+ this._barNudge = 0;
+ }
+ else {
+ this._barNudge = (-Math.abs(nseries/2 - 0.5) + pos) * (this.barWidth + this.barPadding);
+ }
+ if (showLine) {
+
+ if (this.barDirection == 'vertical') {
+ for (var i=0; i<gridData.length; i++) {
+ if (this.data[i][1] == null) {
+ continue;
+ }
+ points = [];
+ var base = gridData[i][0] + this._barNudge;
+ var ystart;
+
+ if (this._stack && this._prevGridData.length) {
+ ystart = getStart(this.index, i, this._plotData[i][1], plot, 'y');
+ }
+ else {
+ if (this.fillToZero) {
+ ystart = this._yaxis.series_u2p(0);
+ }
+ else {
+ ystart = ctx.canvas.height;
+ }
+ }
+
+ points.push([base-this.barWidth/2, ystart]);
+ points.push([base-this.barWidth/2, gridData[i][1]]);
+ points.push([base+this.barWidth/2, gridData[i][1]]);
+ points.push([base+this.barWidth/2, ystart]);
+ this.renderer.shadowRenderer.draw(ctx, points, opts);
+ }
+ }
+
+ else if (this.barDirection == 'horizontal'){
+ for (var i=0; i<gridData.length; i++) {
+ if (this.data[i][0] == null) {
+ continue;
+ }
+ points = [];
+ var base = gridData[i][1] - this._barNudge;
+ var xstart;
+
+ if (this._stack && this._prevGridData.length) {
+ xstart = getStart(this.index, i, this._plotData[i][0], plot, 'x');
+ }
+ else {
+ if (this.fillToZero) {
+ xstart = this._xaxis.series_u2p(0);
+ }
+ else {
+ xstart = 0;
+ }
+ }
+
+ points.push([xstart, base+this.barWidth/2]);
+ points.push([gridData[i][0], base+this.barWidth/2]);
+ points.push([gridData[i][0], base-this.barWidth/2]);
+ points.push([xstart, base-this.barWidth/2]);
+ this.renderer.shadowRenderer.draw(ctx, points, opts);
+ }
+ }
+ }
+
+ }
+ };
+
+ function postInit(target, data, options) {
+ for (var i=0; i<this.series.length; i++) {
+ if (this.series[i].renderer.constructor == $.jqplot.BarRenderer) {
+ // don't allow mouseover and mousedown at same time.
+ if (this.series[i].highlightMouseOver) {
+ this.series[i].highlightMouseDown = false;
+ }
+ }
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.barRenderer && this.plugins.barRenderer.highlightCanvas) {
+
+ this.plugins.barRenderer.highlightCanvas.resetCanvas();
+ this.plugins.barRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.barRenderer = {highlightedSeriesIndex:null};
+ this.plugins.barRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+
+ this.eventCanvas._elem.before(this.plugins.barRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-barRenderer-highlight-canvas', this._plotDimensions, this));
+ this.plugins.barRenderer.highlightCanvas.setContext();
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
+ }
+
+ function highlight (plot, sidx, pidx, points) {
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.barRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.barRenderer.highlightedSeriesIndex = sidx;
+ var opts = {fillStyle: s.highlightColors[pidx]};
+ s.renderer.shapeRenderer.draw(canvas._ctx, points, opts);
+ canvas = null;
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.barRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.barRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ canvas = null;
+ }
+
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].show && plot.series[ins[0]].highlightMouseOver &&
+ !(ins[0] == plot.plugins.barRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.barRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
+ var idx = plot.plugins.barRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ }
+
+ function handleClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt = jQuery.Event('jqplotDataClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var idx = plot.plugins.barRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ var evt = jQuery.Event('jqplotDataRightClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.blockRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.blockRenderer.js
new file mode 100644
index 0000000..e7c782c
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.blockRenderer.js
@@ -0,0 +1,235 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.BlockRenderer
+ * Plugin renderer to draw a x-y block chart. A Block chart has data points displayed as
+ * colored squares with a text label inside. Data must be supplied in the form:
+ *
+ * > [[x1, y1, "label 1", {css}], [x2, y2, "label 2", {css}], ...]
+ *
+ * The label and css object are optional. If the label is ommitted, the
+ * box will collapse unless a css height and/or width is specified.
+ *
+ * The css object is an object specifying css properties
+ * such as:
+ *
+ * > {background:'#4f98a5', border:'3px solid gray', padding:'1px'}
+ *
+ * Note that css properties specified with the data point override defaults
+ * specified with the series.
+ *
+ */
+ $.jqplot.BlockRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.BlockRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.BlockRenderer.prototype.constructor = $.jqplot.BlockRenderer;
+
+ // called with scope of a series
+ $.jqplot.BlockRenderer.prototype.init = function(options) {
+ // Group: Properties
+ //
+ // prop: css
+ // default css styles that will be applied to all data blocks.
+ // these values will be overridden by css styles supplied with the
+ // individulal data points.
+ this.css = {padding:'2px', border:'1px solid #999', textAlign:'center'};
+ // prop: escapeHtml
+ // true to escape html in the box label.
+ this.escapeHtml = false;
+ // prop: insertBreaks
+ // true to turn spaces in data block label into html breaks <br />.
+ this.insertBreaks = true;
+ // prop: varyBlockColors
+ // true to vary the color of each block in this series according to
+ // the seriesColors array. False to set each block to the color
+ // specified on this series. This has no effect if a css background color
+ // option is specified in the renderer css options.
+ this.varyBlockColors = false;
+ $.extend(true, this, options);
+ if (this.css.backgroundColor) {
+ this.color = this.css.backgroundColor;
+ }
+ else if (this.css.background) {
+ this.color = this.css.background;
+ }
+ else if (!this.varyBlockColors) {
+ this.css.background = this.color;
+ }
+ this.canvas = new $.jqplot.BlockCanvas();
+ this.shadowCanvas = new $.jqplot.BlockCanvas();
+ this.canvas._plotDimensions = this._plotDimensions;
+ this.shadowCanvas._plotDimensions = this._plotDimensions;
+ this._type = 'block';
+
+ // group: Methods
+ //
+ // Method: moveBlock
+ // Moves an individual block. More efficient than redrawing
+ // the whole series by calling plot.drawSeries().
+ // Properties:
+ // idx - the 0 based index of the block or point in this series.
+ // x - the x coordinate in data units (value on x axis) to move the block to.
+ // y - the y coordinate in data units (value on the y axis) to move the block to.
+ // duration - optional parameter to create an animated movement. Can be a
+ // number (higher is slower animation) or 'fast', 'normal' or 'slow'. If not
+ // provided, the element is moved without any animation.
+ this.moveBlock = function (idx, x, y, duration) {
+ // update plotData, stackData, data and gridData
+ // x and y are in data coordinates.
+ var el = this.canvas._elem.children(':eq('+idx+')');
+ this.data[idx][0] = x;
+ this.data[idx][1] = y;
+ this._plotData[idx][0] = x;
+ this._plotData[idx][1] = y;
+ this._stackData[idx][0] = x;
+ this._stackData[idx][1] = y;
+ this.gridData[idx][0] = this._xaxis.series_u2p(x);
+ this.gridData[idx][1] = this._yaxis.series_u2p(y);
+ var w = el.outerWidth();
+ var h = el.outerHeight();
+ var left = this.gridData[idx][0] - w/2 + 'px';
+ var top = this.gridData[idx][1] - h/2 + 'px';
+ if (duration) {
+ if (parseInt(duration, 10)) {
+ duration = parseInt(duration, 10);
+ }
+ el.animate({left:left, top:top}, duration);
+ }
+ else {
+ el.css({left:left, top:top});
+ }
+ el = null;
+ };
+ };
+
+ // called with scope of series
+ $.jqplot.BlockRenderer.prototype.draw = function (ctx, gd, options) {
+ if (this.plugins.pointLabels) {
+ this.plugins.pointLabels.show = false;
+ }
+ var i, el, d, gd, t, css, w, h, left, top;
+ var opts = (options != undefined) ? options : {};
+ var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
+ this.canvas._elem.empty();
+ for (i=0; i<this.gridData.length; i++) {
+ d = this.data[i];
+ gd = this.gridData[i];
+ t = '';
+ css = {};
+ if (typeof d[2] == 'string') {
+ t = d[2];
+ }
+ else if (typeof d[2] == 'object') {
+ css = d[2];
+ }
+ if (typeof d[3] == 'object') {
+ css = d[3];
+ }
+ if (this.insertBreaks){
+ t = t.replace(/ /g, '<br />');
+ }
+ css = $.extend(true, {}, this.css, css);
+ // create a div
+ el = $('<div style="position:absolute;margin-left:auto;margin-right:auto;"></div>');
+ this.canvas._elem.append(el);
+ // set text
+ this.escapeHtml ? el.text(t) : el.html(t);
+ // style it
+ // remove styles we don't want overridden.
+ delete css.position;
+ delete css.marginRight;
+ delete css.marginLeft;
+ if (!css.background && !css.backgroundColor && !css.backgroundImage){
+ css.background = colorGenerator.next();
+ }
+ el.css(css);
+ w = el.outerWidth();
+ h = el.outerHeight();
+ left = gd[0] - w/2 + 'px';
+ top = gd[1] - h/2 + 'px';
+ el.css({left:left, top:top});
+ el = null;
+ }
+ };
+
+ $.jqplot.BlockCanvas = function() {
+ $.jqplot.ElemContainer.call(this);
+ this._ctx;
+ };
+
+ $.jqplot.BlockCanvas.prototype = new $.jqplot.ElemContainer();
+ $.jqplot.BlockCanvas.prototype.constructor = $.jqplot.BlockCanvas;
+
+ $.jqplot.BlockCanvas.prototype.createElement = function(offsets, clss, plotDimensions) {
+ this._offsets = offsets;
+ var klass = 'jqplot-blockCanvas';
+ if (clss != undefined) {
+ klass = clss;
+ }
+ var elem;
+ // if this canvas already has a dom element, don't make a new one.
+ if (this._elem) {
+ elem = this._elem.get(0);
+ }
+ else {
+ elem = document.createElement('div');
+ }
+ // if new plotDimensions supplied, use them.
+ if (plotDimensions != undefined) {
+ this._plotDimensions = plotDimensions;
+ }
+
+ var w = this._plotDimensions.width - this._offsets.left - this._offsets.right + 'px';
+ var h = this._plotDimensions.height - this._offsets.top - this._offsets.bottom + 'px';
+ this._elem = $(elem);
+ this._elem.css({ position: 'absolute', width:w, height:h, left: this._offsets.left, top: this._offsets.top });
+
+ this._elem.addClass(klass);
+ return this._elem;
+ };
+
+ $.jqplot.BlockCanvas.prototype.setContext = function() {
+ this._ctx = {
+ canvas:{
+ width:0,
+ height:0
+ },
+ clearRect:function(){return null;}
+ };
+ return this._ctx;
+ };
+
+})(jQuery);
+
+ \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.bubbleRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.bubbleRenderer.js
new file mode 100644
index 0000000..1629d85
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.bubbleRenderer.js
@@ -0,0 +1,759 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ var arrayMax = function( array ){
+ return Math.max.apply( Math, array );
+ };
+ var arrayMin = function( array ){
+ return Math.min.apply( Math, array );
+ };
+
+ /**
+ * Class: $.jqplot.BubbleRenderer
+ * Plugin renderer to draw a bubble chart. A Bubble chart has data points displayed as
+ * colored circles with an optional text label inside. To use
+ * the bubble renderer, you must include the bubble renderer like:
+ *
+ * > <script language="javascript" type="text/javascript" src="../src/plugins/jqplot.bubbleRenderer.js"></script>
+ *
+ * Data must be supplied in
+ * the form:
+ *
+ * > [[x1, y1, r1, <label or {label:'text', color:color}>], ...]
+ *
+ * where the label or options
+ * object is optional.
+ *
+ * Note that all bubble colors will be the same
+ * unless the "varyBubbleColors" option is set to true. Colors can be specified in the data array
+ * or in the seriesColors array option on the series. If no colors are defined, the default jqPlot
+ * series of 16 colors are used. Colors are automatically cycled around again if there are more
+ * bubbles than colors.
+ *
+ * Bubbles are autoscaled by default to fit within the chart area while maintaining
+ * relative sizes. If the "autoscaleBubbles" option is set to false, the r(adius) values
+ * in the data array a treated as literal pixel values for the radii of the bubbles.
+ *
+ * Properties are passed into the bubble renderer in the rendererOptions object of
+ * the series options like:
+ *
+ * > seriesDefaults: {
+ * > renderer: $.jqplot.BubbleRenderer,
+ * > rendererOptions: {
+ * > bubbleAlpha: 0.7,
+ * > varyBubbleColors: false
+ * > }
+ * > }
+ *
+ */
+ $.jqplot.BubbleRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.BubbleRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.BubbleRenderer.prototype.constructor = $.jqplot.BubbleRenderer;
+
+ // called with scope of a series
+ $.jqplot.BubbleRenderer.prototype.init = function(options, plot) {
+ // Group: Properties
+ //
+ // prop: varyBubbleColors
+ // True to vary the color of each bubble in this series according to
+ // the seriesColors array. False to set each bubble to the color
+ // specified on this series. This has no effect if a css background color
+ // option is specified in the renderer css options.
+ this.varyBubbleColors = true;
+ // prop: autoscaleBubbles
+ // True to scale the bubble radius based on plot size.
+ // False will use the radius value as provided as a raw pixel value for
+ // bubble radius.
+ this.autoscaleBubbles = true;
+ // prop: autoscaleMultiplier
+ // Multiplier the bubble size if autoscaleBubbles is true.
+ this.autoscaleMultiplier = 1.0;
+ // prop: autoscalePointsFactor
+ // Factor which decreases bubble size based on how many bubbles on on the chart.
+ // 0 means no adjustment for number of bubbles. Negative values will decrease
+ // size of bubbles as more bubbles are added. Values between 0 and -0.2
+ // should work well.
+ this.autoscalePointsFactor = -0.07;
+ // prop: escapeHtml
+ // True to escape html in bubble label text.
+ this.escapeHtml = true;
+ // prop: highlightMouseOver
+ // True to highlight bubbles when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on a slice.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over a bubble.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColors
+ // An array of colors to use when highlighting a slice. Calculated automatically
+ // if not supplied.
+ this.highlightColors = [];
+ // prop: bubbleAlpha
+ // Alpha transparency to apply to all bubbles in this series.
+ this.bubbleAlpha = 1.0;
+ // prop: highlightAlpha
+ // Alpha transparency to apply when highlighting bubble.
+ // Set to value of bubbleAlpha by default.
+ this.highlightAlpha = null;
+ // prop: bubbleGradients
+ // True to color the bubbles with gradient fills instead of flat colors.
+ // NOT AVAILABLE IN IE due to lack of excanvas support for radial gradient fills.
+ // will be ignored in IE.
+ this.bubbleGradients = false;
+ // prop: showLabels
+ // True to show labels on bubbles (if any), false to not show.
+ this.showLabels = true;
+ // array of [point index, radius] which will be sorted in descending order to plot
+ // largest points below smaller points.
+ this.radii = [];
+ this.maxRadius = 0;
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+ // array of jQuery labels.
+ this.labels = [];
+ this.bubbleCanvases = [];
+ this._type = 'bubble';
+
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (options.highlightMouseDown && options.highlightMouseOver == null) {
+ options.highlightMouseOver = false;
+ }
+
+ $.extend(true, this, options);
+
+ if (this.highlightAlpha == null) {
+ this.highlightAlpha = this.bubbleAlpha;
+ if (this.bubbleGradients) {
+ this.highlightAlpha = 0.35;
+ }
+ }
+
+ this.autoscaleMultiplier = this.autoscaleMultiplier * Math.pow(this.data.length, this.autoscalePointsFactor);
+
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+
+ // adjust the series colors for options colors passed in with data or for alpha.
+ // note, this can leave undefined holes in the seriesColors array.
+ var comps;
+ for (var i=0; i<this.data.length; i++) {
+ var color = null;
+ var d = this.data[i];
+ this.maxRadius = Math.max(this.maxRadius, d[2]);
+ if (d[3]) {
+ if (typeof(d[3]) == 'object') {
+ color = d[3]['color'];
+ }
+ }
+
+ if (color == null) {
+ if (this.seriesColors[i] != null) {
+ color = this.seriesColors[i];
+ }
+ }
+
+ if (color && this.bubbleAlpha < 1.0) {
+ comps = $.jqplot.getColorComponents(color);
+ color = 'rgba('+comps[0]+', '+comps[1]+', '+comps[2]+', '+this.bubbleAlpha+')';
+ }
+
+ if (color) {
+ this.seriesColors[i] = color;
+ }
+ }
+
+ if (!this.varyBubbleColors) {
+ this.seriesColors = [this.color];
+ }
+
+ this.colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
+
+ // set highlight colors if none provided
+ if (this.highlightColors.length == 0) {
+ for (var i=0; i<this.seriesColors.length; i++){
+ var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
+ newrgb[j] = parseInt(newrgb[j], 10);
+ }
+ this.highlightColors.push('rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+', '+this.highlightAlpha+')');
+ }
+ }
+
+ this.highlightColorGenerator = new $.jqplot.ColorGenerator(this.highlightColors);
+
+ var sopts = {fill:true, isarc:true, angle:this.shadowAngle, alpha:this.shadowAlpha, closePath:true};
+
+ this.renderer.shadowRenderer.init(sopts);
+
+ this.canvas = new $.jqplot.DivCanvas();
+ this.canvas._plotDimensions = this._plotDimensions;
+
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+ plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
+ plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
+ plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
+ plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
+ plot.postDrawHooks.addOnce(postPlotDraw);
+
+ };
+
+
+ // converts the user data values to grid coordinates and stores them
+ // in the gridData array.
+ // Called with scope of a series.
+ $.jqplot.BubbleRenderer.prototype.setGridData = function(plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var data = this._plotData;
+ this.gridData = [];
+ var radii = [];
+ this.radii = [];
+ var dim = Math.min(plot._height, plot._width);
+ for (var i=0; i<this.data.length; i++) {
+ if (data[i] != null) {
+ this.gridData.push([xp.call(this._xaxis, data[i][0]), yp.call(this._yaxis, data[i][1]), data[i][2]]);
+ this.radii.push([i, data[i][2]]);
+ radii.push(data[i][2]);
+ }
+ }
+ var r, val, maxr = this.maxRadius = arrayMax(radii);
+ var l = this.gridData.length;
+ if (this.autoscaleBubbles) {
+ for (var i=0; i<l; i++) {
+ val = radii[i]/maxr;
+ r = this.autoscaleMultiplier * dim / 6;
+ this.gridData[i][2] = r * val;
+ }
+ }
+
+ this.radii.sort(function(a, b) { return b[1] - a[1]; });
+ };
+
+ // converts any arbitrary data values to grid coordinates and
+ // returns them. This method exists so that plugins can use a series'
+ // linerenderer to generate grid data points without overwriting the
+ // grid data associated with that series.
+ // Called with scope of a series.
+ $.jqplot.BubbleRenderer.prototype.makeGridData = function(data, plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var gd = [];
+ var radii = [];
+ this.radii = [];
+ var dim = Math.min(plot._height, plot._width);
+ for (var i=0; i<data.length; i++) {
+ if (data[i] != null) {
+ gd.push([xp.call(this._xaxis, data[i][0]), yp.call(this._yaxis, data[i][1]), data[i][2]]);
+ radii.push(data[i][2]);
+ this.radii.push([i, data[i][2]]);
+ }
+ }
+ var r, val, maxr = this.maxRadius = arrayMax(radii);
+ var l = this.gridData.length;
+ if (this.autoscaleBubbles) {
+ for (var i=0; i<l; i++) {
+ val = radii[i]/maxr;
+ r = this.autoscaleMultiplier * dim / 6;
+ gd[i][2] = r * val;
+ }
+ }
+ this.radii.sort(function(a, b) { return b[1] - a[1]; });
+ return gd;
+ };
+
+ // called with scope of series
+ $.jqplot.BubbleRenderer.prototype.draw = function (ctx, gd, options) {
+ if (this.plugins.pointLabels) {
+ this.plugins.pointLabels.show = false;
+ }
+ var opts = (options != undefined) ? options : {};
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ this.canvas._elem.empty();
+ for (var i=0; i<this.radii.length; i++) {
+ var idx = this.radii[i][0];
+ var t=null;
+ var color = null;
+ var el = null;
+ var tel = null;
+ var d = this.data[idx];
+ var gd = this.gridData[idx];
+ if (d[3]) {
+ if (typeof(d[3]) == 'object') {
+ t = d[3]['label'];
+ }
+ else if (typeof(d[3]) == 'string') {
+ t = d[3];
+ }
+ }
+
+ // color = (this.varyBubbleColors) ? this.colorGenerator.get(idx) : this.color;
+ color = this.colorGenerator.get(idx);
+
+ // If we're drawing a shadow, expand the canvas dimensions to accomodate.
+ var canvasRadius = gd[2];
+ var offset, depth;
+ if (this.shadow) {
+ offset = (0.7 + gd[2]/40).toFixed(1);
+ depth = 1 + Math.ceil(gd[2]/15);
+ canvasRadius += offset*depth;
+ }
+ this.bubbleCanvases[idx] = new $.jqplot.BubbleCanvas();
+ this.canvas._elem.append(this.bubbleCanvases[idx].createElement(gd[0], gd[1], canvasRadius));
+ this.bubbleCanvases[idx].setContext();
+ var ctx = this.bubbleCanvases[idx]._ctx;
+ var x = ctx.canvas.width/2;
+ var y = ctx.canvas.height/2;
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [x, y, gd[2], 0, 2*Math.PI], {offset: offset, depth: depth});
+ }
+ this.bubbleCanvases[idx].draw(gd[2], color, this.bubbleGradients, this.shadowAngle/180*Math.PI);
+
+ // now draw label.
+ if (t && this.showLabels) {
+ tel = $('<div style="position:absolute;" class="jqplot-bubble-label"></div>');
+ if (this.escapeHtml) {
+ tel.text(t);
+ }
+ else {
+ tel.html(t);
+ }
+ this.canvas._elem.append(tel);
+ var h = $(tel).outerHeight();
+ var w = $(tel).outerWidth();
+ var top = gd[1] - 0.5*h;
+ var left = gd[0] - 0.5*w;
+ tel.css({top: top, left: left});
+ this.labels[idx] = $(tel);
+ }
+ }
+ };
+
+
+ $.jqplot.DivCanvas = function() {
+ $.jqplot.ElemContainer.call(this);
+ this._ctx;
+ };
+
+ $.jqplot.DivCanvas.prototype = new $.jqplot.ElemContainer();
+ $.jqplot.DivCanvas.prototype.constructor = $.jqplot.DivCanvas;
+
+ $.jqplot.DivCanvas.prototype.createElement = function(offsets, clss, plotDimensions) {
+ this._offsets = offsets;
+ var klass = 'jqplot-DivCanvas';
+ if (clss != undefined) {
+ klass = clss;
+ }
+ var elem;
+ // if this canvas already has a dom element, don't make a new one.
+ if (this._elem) {
+ elem = this._elem.get(0);
+ }
+ else {
+ elem = document.createElement('div');
+ }
+ // if new plotDimensions supplied, use them.
+ if (plotDimensions != undefined) {
+ this._plotDimensions = plotDimensions;
+ }
+
+ var w = this._plotDimensions.width - this._offsets.left - this._offsets.right + 'px';
+ var h = this._plotDimensions.height - this._offsets.top - this._offsets.bottom + 'px';
+ this._elem = $(elem);
+ this._elem.css({ position: 'absolute', width:w, height:h, left: this._offsets.left, top: this._offsets.top });
+
+ this._elem.addClass(klass);
+ return this._elem;
+ };
+
+ $.jqplot.DivCanvas.prototype.setContext = function() {
+ this._ctx = {
+ canvas:{
+ width:0,
+ height:0
+ },
+ clearRect:function(){return null;}
+ };
+ return this._ctx;
+ };
+
+ $.jqplot.BubbleCanvas = function() {
+ $.jqplot.ElemContainer.call(this);
+ this._ctx;
+ };
+
+ $.jqplot.BubbleCanvas.prototype = new $.jqplot.ElemContainer();
+ $.jqplot.BubbleCanvas.prototype.constructor = $.jqplot.BubbleCanvas;
+
+ // initialize with the x,y pont of bubble center and the bubble radius.
+ $.jqplot.BubbleCanvas.prototype.createElement = function(x, y, r) {
+ var klass = 'jqplot-bubble-point';
+
+ var elem;
+ // if this canvas already has a dom element, don't make a new one.
+ if (this._elem) {
+ elem = this._elem.get(0);
+ }
+ else {
+ elem = document.createElement('canvas');
+ }
+
+ elem.width = (r != null) ? 2*r : elem.width;
+ elem.height = (r != null) ? 2*r : elem.height;
+ this._elem = $(elem);
+ var l = (x != null && r != null) ? x - r : this._elem.css('left');
+ var t = (y != null && r != null) ? y - r : this._elem.css('top');
+ this._elem.css({ position: 'absolute', left: l, top: t });
+
+ this._elem.addClass(klass);
+ if ($.jqplot.use_excanvas) {
+ window.G_vmlCanvasManager.init_(document);
+ elem = window.G_vmlCanvasManager.initElement(elem);
+ }
+
+ return this._elem;
+ };
+
+ $.jqplot.BubbleCanvas.prototype.draw = function(r, color, gradients, angle) {
+ var ctx = this._ctx;
+ // r = Math.floor(r*1.04);
+ // var x = Math.round(ctx.canvas.width/2);
+ // var y = Math.round(ctx.canvas.height/2);
+ var x = ctx.canvas.width/2;
+ var y = ctx.canvas.height/2;
+ ctx.save();
+ if (gradients && !$.jqplot.use_excanvas) {
+ r = r*1.04;
+ var comps = $.jqplot.getColorComponents(color);
+ var colorinner = 'rgba('+Math.round(comps[0]+0.8*(255-comps[0]))+', '+Math.round(comps[1]+0.8*(255-comps[1]))+', '+Math.round(comps[2]+0.8*(255-comps[2]))+', '+comps[3]+')';
+ var colorend = 'rgba('+comps[0]+', '+comps[1]+', '+comps[2]+', 0)';
+ // var rinner = Math.round(0.35 * r);
+ // var xinner = Math.round(x - Math.cos(angle) * 0.33 * r);
+ // var yinner = Math.round(y - Math.sin(angle) * 0.33 * r);
+ var rinner = 0.35 * r;
+ var xinner = x - Math.cos(angle) * 0.33 * r;
+ var yinner = y - Math.sin(angle) * 0.33 * r;
+ var radgrad = ctx.createRadialGradient(xinner, yinner, rinner, x, y, r);
+ radgrad.addColorStop(0, colorinner);
+ radgrad.addColorStop(0.93, color);
+ radgrad.addColorStop(0.96, colorend);
+ radgrad.addColorStop(1, colorend);
+ // radgrad.addColorStop(.98, colorend);
+ ctx.fillStyle = radgrad;
+ ctx.fillRect(0,0, ctx.canvas.width, ctx.canvas.height);
+ }
+ else {
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ var ang = 2*Math.PI;
+ ctx.arc(x, y, r, 0, ang, 0);
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.restore();
+ };
+
+ $.jqplot.BubbleCanvas.prototype.setContext = function() {
+ this._ctx = this._elem.get(0).getContext("2d");
+ return this._ctx;
+ };
+
+ $.jqplot.BubbleAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.BubbleAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.BubbleAxisRenderer.prototype.constructor = $.jqplot.BubbleAxisRenderer;
+
+ // called with scope of axis object.
+ $.jqplot.BubbleAxisRenderer.prototype.init = function(options){
+ $.extend(true, this, options);
+ var db = this._dataBounds;
+ var minsidx = 0,
+ minpidx = 0,
+ maxsidx = 0,
+ maxpidx = 0,
+ maxr = 0,
+ minr = 0,
+ minMaxRadius = 0,
+ maxMaxRadius = 0,
+ maxMult = 0,
+ minMult = 0;
+ // Go through all the series attached to this axis and find
+ // the min/max bounds for this axis.
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ var d = s._plotData;
+
+ for (var j=0; j<d.length; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ if (d[j][0] < db.min || db.min == null) {
+ db.min = d[j][0];
+ minsidx=i;
+ minpidx=j;
+ minr = d[j][2];
+ minMaxRadius = s.maxRadius;
+ minMult = s.autoscaleMultiplier;
+ }
+ if (d[j][0] > db.max || db.max == null) {
+ db.max = d[j][0];
+ maxsidx=i;
+ maxpidx=j;
+ maxr = d[j][2];
+ maxMaxRadius = s.maxRadius;
+ maxMult = s.autoscaleMultiplier;
+ }
+ }
+ else {
+ if (d[j][1] < db.min || db.min == null) {
+ db.min = d[j][1];
+ minsidx=i;
+ minpidx=j;
+ minr = d[j][2];
+ minMaxRadius = s.maxRadius;
+ minMult = s.autoscaleMultiplier;
+ }
+ if (d[j][1] > db.max || db.max == null) {
+ db.max = d[j][1];
+ maxsidx=i;
+ maxpidx=j;
+ maxr = d[j][2];
+ maxMaxRadius = s.maxRadius;
+ maxMult = s.autoscaleMultiplier;
+ }
+ }
+ }
+ }
+
+ var minRatio = minr/minMaxRadius;
+ var maxRatio = maxr/maxMaxRadius;
+
+ // need to estimate the effect of the radius on total axis span and adjust axis accordingly.
+ var span = db.max - db.min;
+ // var dim = (this.name == 'xaxis' || this.name == 'x2axis') ? this._plotDimensions.width : this._plotDimensions.height;
+ var dim = Math.min(this._plotDimensions.width, this._plotDimensions.height);
+
+ var minfact = minRatio * minMult/3 * span;
+ var maxfact = maxRatio * maxMult/3 * span;
+ db.max += maxfact;
+ db.min -= minfact;
+ };
+
+ function highlight (plot, sidx, pidx) {
+ plot.plugins.bubbleRenderer.highlightLabelCanvas.empty();
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.bubbleRenderer.highlightCanvas;
+ var ctx = canvas._ctx;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.bubbleRenderer.highlightedSeriesIndex = sidx;
+
+ var color = s.highlightColorGenerator.get(pidx);
+ var x = s.gridData[pidx][0],
+ y = s.gridData[pidx][1],
+ r = s.gridData[pidx][2];
+ ctx.save();
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.arc(x, y, r, 0, 2*Math.PI, 0);
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+ // bring label to front
+ if (s.labels[pidx]) {
+ plot.plugins.bubbleRenderer.highlightLabel = s.labels[pidx].clone();
+ plot.plugins.bubbleRenderer.highlightLabel.appendTo(plot.plugins.bubbleRenderer.highlightLabelCanvas);
+ plot.plugins.bubbleRenderer.highlightLabel.addClass('jqplot-bubble-label-highlight');
+ }
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.bubbleRenderer.highlightCanvas;
+ var sidx = plot.plugins.bubbleRenderer.highlightedSeriesIndex;
+ plot.plugins.bubbleRenderer.highlightLabelCanvas.empty();
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.bubbleRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ }
+
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var si = neighbor.seriesIndex;
+ var pi = neighbor.pointIndex;
+ var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.bubbleRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var si = neighbor.seriesIndex;
+ var pi = neighbor.pointIndex;
+ var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
+ if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.bubbleRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
+ var idx = plot.plugins.bubbleRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ }
+
+ function handleClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var si = neighbor.seriesIndex;
+ var pi = neighbor.pointIndex;
+ var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
+ var evt = jQuery.Event('jqplotDataClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var si = neighbor.seriesIndex;
+ var pi = neighbor.pointIndex;
+ var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
+ var idx = plot.plugins.bubbleRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ var evt = jQuery.Event('jqplotDataRightClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.bubbleRenderer && this.plugins.bubbleRenderer.highlightCanvas) {
+ this.plugins.bubbleRenderer.highlightCanvas.resetCanvas();
+ this.plugins.bubbleRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.bubbleRenderer = {highlightedSeriesIndex:null};
+ this.plugins.bubbleRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+ this.plugins.bubbleRenderer.highlightLabel = null;
+ this.plugins.bubbleRenderer.highlightLabelCanvas = $('<div style="position:absolute;"></div>');
+ var top = this._gridPadding.top;
+ var left = this._gridPadding.left;
+ var width = this._plotDimensions.width - this._gridPadding.left - this._gridPadding.right;
+ var height = this._plotDimensions.height - this._gridPadding.top - this._gridPadding.bottom;
+ this.plugins.bubbleRenderer.highlightLabelCanvas.css({top:top, left:left, width:width+'px', height:height+'px'});
+
+ this.eventCanvas._elem.before(this.plugins.bubbleRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-bubbleRenderer-highlight-canvas', this._plotDimensions, this));
+ this.eventCanvas._elem.before(this.plugins.bubbleRenderer.highlightLabelCanvas);
+
+ var hctx = this.plugins.bubbleRenderer.highlightCanvas.setContext();
+ }
+
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ // only set these if there is a Bubble series
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.BubbleRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.BubbleRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.BubbleAxisRenderer;
+ options.sortData = false;
+ }
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+
+})(jQuery);
+
+ \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js
new file mode 100644
index 0000000..6313dc1
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js
@@ -0,0 +1,203 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.CanvasAxisLabelRenderer
+ * Renderer to draw axis labels with a canvas element to support advanced
+ * featrues such as rotated text. This renderer uses a separate rendering engine
+ * to draw the text on the canvas. Two modes of rendering the text are available.
+ * If the browser has native font support for canvas fonts (currently Mozila 3.5
+ * and Safari 4), you can enable text rendering with the canvas fillText method.
+ * You do so by setting the "enableFontSupport" option to true.
+ *
+ * Browsers lacking native font support will have the text drawn on the canvas
+ * using the Hershey font metrics. Even if the "enableFontSupport" option is true
+ * non-supporting browsers will still render with the Hershey font.
+ *
+ */
+ $.jqplot.CanvasAxisLabelRenderer = function(options) {
+ // Group: Properties
+
+ // prop: angle
+ // angle of text, measured clockwise from x axis.
+ this.angle = 0;
+ // name of the axis associated with this tick
+ this.axis;
+ // prop: show
+ // whether or not to show the tick (mark and label).
+ this.show = true;
+ // prop: showLabel
+ // whether or not to show the label.
+ this.showLabel = true;
+ // prop: label
+ // label for the axis.
+ this.label = '';
+ // prop: fontFamily
+ // CSS spec for the font-family css attribute.
+ // Applies only to browsers supporting native font rendering in the
+ // canvas tag. Currently Mozilla 3.5 and Safari 4.
+ this.fontFamily = '"Trebuchet MS", Arial, Helvetica, sans-serif';
+ // prop: fontSize
+ // CSS spec for font size.
+ this.fontSize = '11pt';
+ // prop: fontWeight
+ // CSS spec for fontWeight: normal, bold, bolder, lighter or a number 100 - 900
+ this.fontWeight = 'normal';
+ // prop: fontStretch
+ // Multiplier to condense or expand font width.
+ // Applies only to browsers which don't support canvas native font rendering.
+ this.fontStretch = 1.0;
+ // prop: textColor
+ // css spec for the color attribute.
+ this.textColor = '#666666';
+ // prop: enableFontSupport
+ // true to turn on native canvas font support in Mozilla 3.5+ and Safari 4+.
+ // If true, label will be drawn with canvas tag native support for fonts.
+ // If false, label will be drawn with Hershey font metrics.
+ this.enableFontSupport = true;
+ // prop: pt2px
+ // Point to pixel scaling factor, used for computing height of bounding box
+ // around a label. The labels text renderer has a default setting of 1.4, which
+ // should be suitable for most fonts. Leave as null to use default. If tops of
+ // letters appear clipped, increase this. If bounding box seems too big, decrease.
+ // This is an issue only with the native font renderering capabilities of Mozilla
+ // 3.5 and Safari 4 since they do not provide a method to determine the font height.
+ this.pt2px = null;
+
+ this._elem;
+ this._ctx;
+ this._plotWidth;
+ this._plotHeight;
+ this._plotDimensions = {height:null, width:null};
+
+ $.extend(true, this, options);
+
+ if (options.angle == null && this.axis != 'xaxis' && this.axis != 'x2axis') {
+ this.angle = -90;
+ }
+
+ var ropts = {fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily};
+ if (this.pt2px) {
+ ropts.pt2px = this.pt2px;
+ }
+
+ if (this.enableFontSupport) {
+ if ($.jqplot.support_canvas_text()) {
+ this._textRenderer = new $.jqplot.CanvasFontRenderer(ropts);
+ }
+
+ else {
+ this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
+ }
+ }
+ else {
+ this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
+ }
+ };
+
+ $.jqplot.CanvasAxisLabelRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ this._textRenderer.init({fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily});
+ };
+
+ // return width along the x axis
+ // will check first to see if an element exists.
+ // if not, will return the computed text box width.
+ $.jqplot.CanvasAxisLabelRenderer.prototype.getWidth = function(ctx) {
+ if (this._elem) {
+ return this._elem.outerWidth(true);
+ }
+ else {
+ var tr = this._textRenderer;
+ var l = tr.getWidth(ctx);
+ var h = tr.getHeight(ctx);
+ var w = Math.abs(Math.sin(tr.angle)*h) + Math.abs(Math.cos(tr.angle)*l);
+ return w;
+ }
+ };
+
+ // return height along the y axis.
+ $.jqplot.CanvasAxisLabelRenderer.prototype.getHeight = function(ctx) {
+ if (this._elem) {
+ return this._elem.outerHeight(true);
+ }
+ else {
+ var tr = this._textRenderer;
+ var l = tr.getWidth(ctx);
+ var h = tr.getHeight(ctx);
+ var w = Math.abs(Math.cos(tr.angle)*h) + Math.abs(Math.sin(tr.angle)*l);
+ return w;
+ }
+ };
+
+ $.jqplot.CanvasAxisLabelRenderer.prototype.getAngleRad = function() {
+ var a = this.angle * Math.PI/180;
+ return a;
+ };
+
+ $.jqplot.CanvasAxisLabelRenderer.prototype.draw = function(ctx, plot) {
+ // Memory Leaks patch
+ if (this._elem) {
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ window.G_vmlCanvasManager.uninitElement(this._elem.get(0));
+ }
+
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ // create a canvas here, but can't draw on it untill it is appended
+ // to dom for IE compatability.
+ var elem = plot.canvasManager.getCanvas();
+
+ this._textRenderer.setText(this.label, ctx);
+ var w = this.getWidth(ctx);
+ var h = this.getHeight(ctx);
+ elem.width = w;
+ elem.height = h;
+ elem.style.width = w;
+ elem.style.height = h;
+
+ elem = plot.canvasManager.initCanvas(elem);
+
+ this._elem = $(elem);
+ this._elem.css({ position: 'absolute'});
+ this._elem.addClass('jqplot-'+this.axis+'-label');
+
+ elem = null;
+ return this._elem;
+ };
+
+ $.jqplot.CanvasAxisLabelRenderer.prototype.pack = function() {
+ this._textRenderer.draw(this._elem.get(0).getContext("2d"), this.label);
+ };
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js
new file mode 100644
index 0000000..b6120d0
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js
@@ -0,0 +1,253 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.CanvasAxisTickRenderer
+ * Renderer to draw axis ticks with a canvas element to support advanced
+ * featrues such as rotated text. This renderer uses a separate rendering engine
+ * to draw the text on the canvas. Two modes of rendering the text are available.
+ * If the browser has native font support for canvas fonts (currently Mozila 3.5
+ * and Safari 4), you can enable text rendering with the canvas fillText method.
+ * You do so by setting the "enableFontSupport" option to true.
+ *
+ * Browsers lacking native font support will have the text drawn on the canvas
+ * using the Hershey font metrics. Even if the "enableFontSupport" option is true
+ * non-supporting browsers will still render with the Hershey font.
+ */
+ $.jqplot.CanvasAxisTickRenderer = function(options) {
+ // Group: Properties
+
+ // prop: mark
+ // tick mark on the axis. One of 'inside', 'outside', 'cross', '' or null.
+ this.mark = 'outside';
+ // prop: showMark
+ // whether or not to show the mark on the axis.
+ this.showMark = true;
+ // prop: showGridline
+ // whether or not to draw the gridline on the grid at this tick.
+ this.showGridline = true;
+ // prop: isMinorTick
+ // if this is a minor tick.
+ this.isMinorTick = false;
+ // prop: angle
+ // angle of text, measured clockwise from x axis.
+ this.angle = 0;
+ // prop: markSize
+ // Length of the tick marks in pixels. For 'cross' style, length
+ // will be stoked above and below axis, so total length will be twice this.
+ this.markSize = 4;
+ // prop: show
+ // whether or not to show the tick (mark and label).
+ this.show = true;
+ // prop: showLabel
+ // whether or not to show the label.
+ this.showLabel = true;
+ // prop: labelPosition
+ // 'auto', 'start', 'middle' or 'end'.
+ // Whether tick label should be positioned so the start, middle, or end
+ // of the tick mark.
+ this.labelPosition = 'auto';
+ this.label = '';
+ this.value = null;
+ this._styles = {};
+ // prop: formatter
+ // A class of a formatter for the tick text.
+ // The default $.jqplot.DefaultTickFormatter uses sprintf.
+ this.formatter = $.jqplot.DefaultTickFormatter;
+ // prop: formatString
+ // string passed to the formatter.
+ this.formatString = '';
+ // prop: prefix
+ // String to prepend to the tick label.
+ // Prefix is prepended to the formatted tick label.
+ this.prefix = '';
+ // prop: fontFamily
+ // css spec for the font-family css attribute.
+ this.fontFamily = '"Trebuchet MS", Arial, Helvetica, sans-serif';
+ // prop: fontSize
+ // CSS spec for font size.
+ this.fontSize = '10pt';
+ // prop: fontWeight
+ // CSS spec for fontWeight
+ this.fontWeight = 'normal';
+ // prop: fontStretch
+ // Multiplier to condense or expand font width.
+ // Applies only to browsers which don't support canvas native font rendering.
+ this.fontStretch = 1.0;
+ // prop: textColor
+ // css spec for the color attribute.
+ this.textColor = '#666666';
+ // prop: enableFontSupport
+ // true to turn on native canvas font support in Mozilla 3.5+ and Safari 4+.
+ // If true, tick label will be drawn with canvas tag native support for fonts.
+ // If false, tick label will be drawn with Hershey font metrics.
+ this.enableFontSupport = true;
+ // prop: pt2px
+ // Point to pixel scaling factor, used for computing height of bounding box
+ // around a label. The labels text renderer has a default setting of 1.4, which
+ // should be suitable for most fonts. Leave as null to use default. If tops of
+ // letters appear clipped, increase this. If bounding box seems too big, decrease.
+ // This is an issue only with the native font renderering capabilities of Mozilla
+ // 3.5 and Safari 4 since they do not provide a method to determine the font height.
+ this.pt2px = null;
+
+ this._elem;
+ this._ctx;
+ this._plotWidth;
+ this._plotHeight;
+ this._plotDimensions = {height:null, width:null};
+
+ $.extend(true, this, options);
+
+ var ropts = {fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily};
+ if (this.pt2px) {
+ ropts.pt2px = this.pt2px;
+ }
+
+ if (this.enableFontSupport) {
+ if ($.jqplot.support_canvas_text()) {
+ this._textRenderer = new $.jqplot.CanvasFontRenderer(ropts);
+ }
+
+ else {
+ this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
+ }
+ }
+ else {
+ this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
+ }
+ };
+
+ $.jqplot.CanvasAxisTickRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ this._textRenderer.init({fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily});
+ };
+
+ // return width along the x axis
+ // will check first to see if an element exists.
+ // if not, will return the computed text box width.
+ $.jqplot.CanvasAxisTickRenderer.prototype.getWidth = function(ctx) {
+ if (this._elem) {
+ return this._elem.outerWidth(true);
+ }
+ else {
+ var tr = this._textRenderer;
+ var l = tr.getWidth(ctx);
+ var h = tr.getHeight(ctx);
+ var w = Math.abs(Math.sin(tr.angle)*h) + Math.abs(Math.cos(tr.angle)*l);
+ return w;
+ }
+ };
+
+ // return height along the y axis.
+ $.jqplot.CanvasAxisTickRenderer.prototype.getHeight = function(ctx) {
+ if (this._elem) {
+ return this._elem.outerHeight(true);
+ }
+ else {
+ var tr = this._textRenderer;
+ var l = tr.getWidth(ctx);
+ var h = tr.getHeight(ctx);
+ var w = Math.abs(Math.cos(tr.angle)*h) + Math.abs(Math.sin(tr.angle)*l);
+ return w;
+ }
+ };
+
+ // return top.
+ $.jqplot.CanvasAxisTickRenderer.prototype.getTop = function(ctx) {
+ if (this._elem) {
+ return this._elem.position().top;
+ }
+ else {
+ return null;
+ }
+ };
+
+ $.jqplot.CanvasAxisTickRenderer.prototype.getAngleRad = function() {
+ var a = this.angle * Math.PI/180;
+ return a;
+ };
+
+
+ $.jqplot.CanvasAxisTickRenderer.prototype.setTick = function(value, axisName, isMinor) {
+ this.value = value;
+ if (isMinor) {
+ this.isMinorTick = true;
+ }
+ return this;
+ };
+
+ $.jqplot.CanvasAxisTickRenderer.prototype.draw = function(ctx, plot) {
+ if (!this.label) {
+ this.label = this.prefix + this.formatter(this.formatString, this.value);
+ }
+
+ // Memory Leaks patch
+ if (this._elem) {
+ if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
+ window.G_vmlCanvasManager.uninitElement(this._elem.get(0));
+ }
+
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ // create a canvas here, but can't draw on it untill it is appended
+ // to dom for IE compatability.
+
+ var elem = plot.canvasManager.getCanvas();
+
+ this._textRenderer.setText(this.label, ctx);
+ var w = this.getWidth(ctx);
+ var h = this.getHeight(ctx);
+ // canvases seem to need to have width and heigh attributes directly set.
+ elem.width = w;
+ elem.height = h;
+ elem.style.width = w;
+ elem.style.height = h;
+ elem.style.textAlign = 'left';
+ elem.style.position = 'absolute';
+
+ elem = plot.canvasManager.initCanvas(elem);
+
+ this._elem = $(elem);
+ this._elem.css(this._styles);
+ this._elem.addClass('jqplot-'+this.axis+'-tick');
+
+ elem = null;
+ return this._elem;
+ };
+
+ $.jqplot.CanvasAxisTickRenderer.prototype.pack = function() {
+ this._textRenderer.draw(this._elem.get(0).getContext("2d"), this.label);
+ };
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasOverlay.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasOverlay.js
new file mode 100644
index 0000000..4094413
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasOverlay.js
@@ -0,0 +1,1021 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ var objCounter = 0;
+ // class: $.jqplot.CanvasOverlay
+ $.jqplot.CanvasOverlay = function(opts){
+ var options = opts || {};
+ this.options = {
+ show: $.jqplot.config.enablePlugins,
+ deferDraw: false
+ };
+ // prop: objects
+ this.objects = [];
+ this.objectNames = [];
+ this.canvas = null;
+ this.markerRenderer = new $.jqplot.MarkerRenderer({style:'line'});
+ this.markerRenderer.init();
+ this.highlightObjectIndex = null;
+ if (options.objects) {
+ var objs = options.objects,
+ obj;
+ for (var i=0; i<objs.length; i++) {
+ obj = objs[i];
+ for (var n in obj) {
+ switch (n) {
+ case 'line':
+ this.addLine(obj[n]);
+ break;
+ case 'horizontalLine':
+ this.addHorizontalLine(obj[n]);
+ break;
+ case 'dashedHorizontalLine':
+ this.addDashedHorizontalLine(obj[n]);
+ break;
+ case 'verticalLine':
+ this.addVerticalLine(obj[n]);
+ break;
+ case 'dashedVerticalLine':
+ this.addDashedVerticalLine(obj[n]);
+ break;
+ case 'rectangle':
+ this.addRectangle(obj[n]);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+ $.extend(true, this.options, options);
+ };
+
+ // called with scope of a plot object
+ $.jqplot.CanvasOverlay.postPlotInit = function (target, data, opts) {
+ var options = opts || {};
+ // add a canvasOverlay attribute to the plot
+ this.plugins.canvasOverlay = new $.jqplot.CanvasOverlay(options.canvasOverlay);
+ };
+
+
+ function LineBase() {
+ this.uid = null;
+ this.type = null;
+ this.gridStart = null;
+ this.gridStop = null;
+ this.tooltipWidthFactor = 0;
+ this.options = {
+ // prop: name
+ // Optional name for the overlay object.
+ // Can be later used to retrieve the object by name.
+ name: null,
+ // prop: show
+ // true to show (draw), false to not draw.
+ show: true,
+ // prop: lineWidth
+ // Width of the line.
+ lineWidth: 2,
+ // prop: lineCap
+ // Type of ending placed on the line ['round', 'butt', 'square']
+ lineCap: 'round',
+ // prop: color
+ // color of the line
+ color: '#666666',
+ // prop: shadow
+ // whether or not to draw a shadow on the line
+ shadow: true,
+ // prop: shadowAngle
+ // Shadow angle in degrees
+ shadowAngle: 45,
+ // prop: shadowOffset
+ // Shadow offset from line in pixels
+ shadowOffset: 1,
+ // prop: shadowDepth
+ // Number of times shadow is stroked, each stroke offset shadowOffset from the last.
+ shadowDepth: 3,
+ // prop: shadowAlpha
+ // Alpha channel transparency of shadow. 0 = transparent.
+ shadowAlpha: '0.07',
+ // prop: xaxis
+ // X axis to use for positioning/scaling the line.
+ xaxis: 'xaxis',
+ // prop: yaxis
+ // Y axis to use for positioning/scaling the line.
+ yaxis: 'yaxis',
+ // prop: showTooltip
+ // Show a tooltip with data point values.
+ showTooltip: false,
+ // prop: showTooltipPrecision
+ // Controls how close to line cursor must be to show tooltip.
+ // Higher number = closer to line, lower number = farther from line.
+ // 1.0 = cursor must be over line.
+ showTooltipPrecision: 0.6,
+ // prop: tooltipLocation
+ // Where to position tooltip, 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
+ tooltipLocation: 'nw',
+ // prop: fadeTooltip
+ // true = fade in/out tooltip, flase = show/hide tooltip
+ fadeTooltip: true,
+ // prop: tooltipFadeSpeed
+ // 'slow', 'def', 'fast', or number of milliseconds.
+ tooltipFadeSpeed: "fast",
+ // prop: tooltipOffset
+ // Pixel offset of tooltip from the highlight.
+ tooltipOffset: 4,
+ // prop: tooltipFormatString
+ // Format string passed the x and y values of the cursor on the line.
+ // e.g., 'Dogs: %.2f, Cats: %d'.
+ tooltipFormatString: '%d, %d'
+ };
+ }
+
+
+ function Rectangle(options) {
+ LineBase.call(this);
+ this.type = 'rectangle';
+ var opts = {
+ // prop: xmin
+ // x value for the start of the line, null to scale to axis min.
+ xmin: null,
+ // prop: xmax
+ // x value for the end of the line, null to scale to axis max.
+ xmax: null,
+ // prop xOffset
+ // offset ends of the line inside the grid. Number
+ xOffset: '6px', // number or string. Number interpreted as units, string as pixels.
+ xminOffset: null,
+ xmaxOffset: null,
+
+ ymin: null,
+ ymax: null,
+ yOffset: '6px', // number or string. Number interpreted as units, string as pixels.
+ yminOffset: null,
+ ymaxOffset: null
+ };
+ $.extend(true, this.options, opts, options);
+
+ if (this.options.showTooltipPrecision < 0.01) {
+ this.options.showTooltipPrecision = 0.01;
+ }
+ }
+
+ Rectangle.prototype = new LineBase();
+ Rectangle.prototype.constructor = Rectangle;
+
+
+ /**
+ * Class: Line
+ * A straight line.
+ */
+ function Line(options) {
+ LineBase.call(this);
+ this.type = 'line';
+ var opts = {
+ // prop: start
+ // [x, y] coordinates for the start of the line.
+ start: [],
+ // prop: stop
+ // [x, y] coordinates for the end of the line.
+ stop: []
+ };
+ $.extend(true, this.options, opts, options);
+
+ if (this.options.showTooltipPrecision < 0.01) {
+ this.options.showTooltipPrecision = 0.01;
+ }
+ }
+
+ Line.prototype = new LineBase();
+ Line.prototype.constructor = Line;
+
+
+ /**
+ * Class: HorizontalLine
+ * A straight horizontal line.
+ */
+ function HorizontalLine(options) {
+ LineBase.call(this);
+ this.type = 'horizontalLine';
+ var opts = {
+ // prop: y
+ // y value to position the line
+ y: null,
+ // prop: xmin
+ // x value for the start of the line, null to scale to axis min.
+ xmin: null,
+ // prop: xmax
+ // x value for the end of the line, null to scale to axis max.
+ xmax: null,
+ // prop xOffset
+ // offset ends of the line inside the grid. Number
+ xOffset: '6px', // number or string. Number interpreted as units, string as pixels.
+ xminOffset: null,
+ xmaxOffset: null
+ };
+ $.extend(true, this.options, opts, options);
+
+ if (this.options.showTooltipPrecision < 0.01) {
+ this.options.showTooltipPrecision = 0.01;
+ }
+ }
+
+ HorizontalLine.prototype = new LineBase();
+ HorizontalLine.prototype.constructor = HorizontalLine;
+
+
+ /**
+ * Class: DashedHorizontalLine
+ * A straight dashed horizontal line.
+ */
+ function DashedHorizontalLine(options) {
+ LineBase.call(this);
+ this.type = 'dashedHorizontalLine';
+ var opts = {
+ y: null,
+ xmin: null,
+ xmax: null,
+ xOffset: '6px', // number or string. Number interpreted as units, string as pixels.
+ xminOffset: null,
+ xmaxOffset: null,
+ // prop: dashPattern
+ // Array of line, space settings in pixels.
+ // Default is 8 pixel of line, 8 pixel of space.
+ // Note, limit to a 2 element array b/c of bug with higher order arrays.
+ dashPattern: [8,8]
+ };
+ $.extend(true, this.options, opts, options);
+
+ if (this.options.showTooltipPrecision < 0.01) {
+ this.options.showTooltipPrecision = 0.01;
+ }
+ }
+
+ DashedHorizontalLine.prototype = new LineBase();
+ DashedHorizontalLine.prototype.constructor = DashedHorizontalLine;
+
+
+ /**
+ * Class: VerticalLine
+ * A straight vertical line.
+ */
+ function VerticalLine(options) {
+ LineBase.call(this);
+ this.type = 'verticalLine';
+ var opts = {
+ x: null,
+ ymin: null,
+ ymax: null,
+ yOffset: '6px', // number or string. Number interpreted as units, string as pixels.
+ yminOffset: null,
+ ymaxOffset: null
+ };
+ $.extend(true, this.options, opts, options);
+
+ if (this.options.showTooltipPrecision < 0.01) {
+ this.options.showTooltipPrecision = 0.01;
+ }
+ }
+
+ VerticalLine.prototype = new LineBase();
+ VerticalLine.prototype.constructor = VerticalLine;
+
+
+ /**
+ * Class: DashedVerticalLine
+ * A straight dashed vertical line.
+ */
+ function DashedVerticalLine(options) {
+ LineBase.call(this);
+ this.type = 'dashedVerticalLine';
+ this.start = null;
+ this.stop = null;
+ var opts = {
+ x: null,
+ ymin: null,
+ ymax: null,
+ yOffset: '6px', // number or string. Number interpreted as units, string as pixels.
+ yminOffset: null,
+ ymaxOffset: null,
+ // prop: dashPattern
+ // Array of line, space settings in pixels.
+ // Default is 8 pixel of line, 8 pixel of space.
+ // Note, limit to a 2 element array b/c of bug with higher order arrays.
+ dashPattern: [8,8]
+ };
+ $.extend(true, this.options, opts, options);
+
+ if (this.options.showTooltipPrecision < 0.01) {
+ this.options.showTooltipPrecision = 0.01;
+ }
+ }
+
+ DashedVerticalLine.prototype = new LineBase();
+ DashedVerticalLine.prototype.constructor = DashedVerticalLine;
+
+ $.jqplot.CanvasOverlay.prototype.addLine = function(opts) {
+ var line = new Line(opts);
+ line.uid = objCounter++;
+ this.objects.push(line);
+ this.objectNames.push(line.options.name);
+ };
+
+ $.jqplot.CanvasOverlay.prototype.addHorizontalLine = function(opts) {
+ var line = new HorizontalLine(opts);
+ line.uid = objCounter++;
+ this.objects.push(line);
+ this.objectNames.push(line.options.name);
+ };
+
+ $.jqplot.CanvasOverlay.prototype.addDashedHorizontalLine = function(opts) {
+ var line = new DashedHorizontalLine(opts);
+ line.uid = objCounter++;
+ this.objects.push(line);
+ this.objectNames.push(line.options.name);
+ };
+
+ $.jqplot.CanvasOverlay.prototype.addVerticalLine = function(opts) {
+ var line = new VerticalLine(opts);
+ line.uid = objCounter++;
+ this.objects.push(line);
+ this.objectNames.push(line.options.name);
+ };
+
+ $.jqplot.CanvasOverlay.prototype.addDashedVerticalLine = function(opts) {
+ var line = new DashedVerticalLine(opts);
+ line.uid = objCounter++;
+ this.objects.push(line);
+ this.objectNames.push(line.options.name);
+ };
+
+ $.jqplot.CanvasOverlay.prototype.addRectangle = function(opts) {
+ var line = new Rectangle(opts);
+ line.uid = objCounter++;
+ this.objects.push(line);
+ this.objectNames.push(line.options.name);
+ };
+
+ $.jqplot.CanvasOverlay.prototype.removeObject = function(idx) {
+ // check if integer, remove by index
+ if ($.type(idx) == 'number') {
+ this.objects.splice(idx, 1);
+ this.objectNames.splice(idx, 1);
+ }
+ // if string, remove by name
+ else {
+ var id = $.inArray(idx, this.objectNames);
+ if (id != -1) {
+ this.objects.splice(id, 1);
+ this.objectNames.splice(id, 1);
+ }
+ }
+ };
+
+ $.jqplot.CanvasOverlay.prototype.getObject = function(idx) {
+ // check if integer, remove by index
+ if ($.type(idx) == 'number') {
+ return this.objects[idx];
+ }
+ // if string, remove by name
+ else {
+ var id = $.inArray(idx, this.objectNames);
+ if (id != -1) {
+ return this.objects[id];
+ }
+ }
+ };
+
+ // Set get as alias for getObject.
+ $.jqplot.CanvasOverlay.prototype.get = $.jqplot.CanvasOverlay.prototype.getObject;
+
+ $.jqplot.CanvasOverlay.prototype.clear = function(plot) {
+ this.canvas._ctx.clearRect(0,0,this.canvas.getWidth(), this.canvas.getHeight());
+ };
+
+ $.jqplot.CanvasOverlay.prototype.draw = function(plot) {
+ var obj,
+ objs = this.objects,
+ mr = this.markerRenderer,
+ start,
+ stop;
+ if (this.options.show) {
+ this.canvas._ctx.clearRect(0,0,this.canvas.getWidth(), this.canvas.getHeight());
+ for (var k=0; k<objs.length; k++) {
+ obj = objs[k];
+ var opts = $.extend(true, {}, obj.options);
+ if (obj.options.show) {
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ mr.shadow = obj.options.shadow;
+ obj.tooltipWidthFactor = obj.options.lineWidth / obj.options.showTooltipPrecision;
+ switch (obj.type) {
+ case 'line':
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ mr.style = 'line';
+ opts.closePath = false;
+ start = [plot.axes[obj.options.xaxis].series_u2p(obj.options.start[0]), plot.axes[obj.options.yaxis].series_u2p(obj.options.start[1])];
+ stop = [plot.axes[obj.options.xaxis].series_u2p(obj.options.stop[0]), plot.axes[obj.options.yaxis].series_u2p(obj.options.stop[1])];
+ obj.gridStart = start;
+ obj.gridStop = stop;
+ mr.draw(start, stop, this.canvas._ctx, opts);
+ break;
+ case 'horizontalLine':
+
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ if (obj.options.y != null) {
+ mr.style = 'line';
+ opts.closePath = false;
+ var xaxis = plot.axes[obj.options.xaxis],
+ xstart,
+ xstop,
+ y = plot.axes[obj.options.yaxis].series_u2p(obj.options.y),
+ xminoff = obj.options.xminOffset || obj.options.xOffset,
+ xmaxoff = obj.options.xmaxOffset || obj.options.xOffset;
+ if (obj.options.xmin != null) {
+ xstart = xaxis.series_u2p(obj.options.xmin);
+ }
+ else if (xminoff != null) {
+ if ($.type(xminoff) == "number") {
+ xstart = xaxis.series_u2p(xaxis.min + xminoff);
+ }
+ else if ($.type(xminoff) == "string") {
+ xstart = xaxis.series_u2p(xaxis.min) + parseFloat(xminoff);
+ }
+ }
+ if (obj.options.xmax != null) {
+ xstop = xaxis.series_u2p(obj.options.xmax);
+ }
+ else if (xmaxoff != null) {
+ if ($.type(xmaxoff) == "number") {
+ xstop = xaxis.series_u2p(xaxis.max - xmaxoff);
+ }
+ else if ($.type(xmaxoff) == "string") {
+ xstop = xaxis.series_u2p(xaxis.max) - parseFloat(xmaxoff);
+ }
+ }
+ if (xstop != null && xstart != null) {
+ obj.gridStart = [xstart, y];
+ obj.gridStop = [xstop, y];
+ mr.draw([xstart, y], [xstop, y], this.canvas._ctx, opts);
+ }
+ }
+ break;
+
+ case 'dashedHorizontalLine':
+
+ var dashPat = obj.options.dashPattern;
+ var dashPatLen = 0;
+ for (var i=0; i<dashPat.length; i++) {
+ dashPatLen += dashPat[i];
+ }
+
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ if (obj.options.y != null) {
+ mr.style = 'line';
+ opts.closePath = false;
+ var xaxis = plot.axes[obj.options.xaxis],
+ xstart,
+ xstop,
+ y = plot.axes[obj.options.yaxis].series_u2p(obj.options.y),
+ xminoff = obj.options.xminOffset || obj.options.xOffset,
+ xmaxoff = obj.options.xmaxOffset || obj.options.xOffset;
+ if (obj.options.xmin != null) {
+ xstart = xaxis.series_u2p(obj.options.xmin);
+ }
+ else if (xminoff != null) {
+ if ($.type(xminoff) == "number") {
+ xstart = xaxis.series_u2p(xaxis.min + xminoff);
+ }
+ else if ($.type(xminoff) == "string") {
+ xstart = xaxis.series_u2p(xaxis.min) + parseFloat(xminoff);
+ }
+ }
+ if (obj.options.xmax != null) {
+ xstop = xaxis.series_u2p(obj.options.xmax);
+ }
+ else if (xmaxoff != null) {
+ if ($.type(xmaxoff) == "number") {
+ xstop = xaxis.series_u2p(xaxis.max - xmaxoff);
+ }
+ else if ($.type(xmaxoff) == "string") {
+ xstop = xaxis.series_u2p(xaxis.max) - parseFloat(xmaxoff);
+ }
+ }
+ if (xstop != null && xstart != null) {
+ obj.gridStart = [xstart, y];
+ obj.gridStop = [xstop, y];
+ var numDash = Math.ceil((xstop - xstart)/dashPatLen);
+ var b=xstart, e;
+ for (var i=0; i<numDash; i++) {
+ for (var j=0; j<dashPat.length; j+=2) {
+ e = b+dashPat[j];
+ mr.draw([b, y], [e, y], this.canvas._ctx, opts);
+ b += dashPat[j];
+ if (j < dashPat.length-1) {
+ b += dashPat[j+1];
+ }
+ }
+ }
+ }
+ }
+ break;
+
+ case 'verticalLine':
+
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ if (obj.options.x != null) {
+ mr.style = 'line';
+ opts.closePath = false;
+ var yaxis = plot.axes[obj.options.yaxis],
+ ystart,
+ ystop,
+ x = plot.axes[obj.options.xaxis].series_u2p(obj.options.x),
+ yminoff = obj.options.yminOffset || obj.options.yOffset,
+ ymaxoff = obj.options.ymaxOffset || obj.options.yOffset;
+ if (obj.options.ymin != null) {
+ ystart = yaxis.series_u2p(obj.options.ymin);
+ }
+ else if (yminoff != null) {
+ if ($.type(yminoff) == "number") {
+ ystart = yaxis.series_u2p(yaxis.min - yminoff);
+ }
+ else if ($.type(yminoff) == "string") {
+ ystart = yaxis.series_u2p(yaxis.min) - parseFloat(yminoff);
+ }
+ }
+ if (obj.options.ymax != null) {
+ ystop = yaxis.series_u2p(obj.options.ymax);
+ }
+ else if (ymaxoff != null) {
+ if ($.type(ymaxoff) == "number") {
+ ystop = yaxis.series_u2p(yaxis.max + ymaxoff);
+ }
+ else if ($.type(ymaxoff) == "string") {
+ ystop = yaxis.series_u2p(yaxis.max) + parseFloat(ymaxoff);
+ }
+ }
+ if (ystop != null && ystart != null) {
+ obj.gridStart = [x, ystart];
+ obj.gridStop = [x, ystop];
+ mr.draw([x, ystart], [x, ystop], this.canvas._ctx, opts);
+ }
+ }
+ break;
+
+ case 'dashedVerticalLine':
+
+ var dashPat = obj.options.dashPattern;
+ var dashPatLen = 0;
+ for (var i=0; i<dashPat.length; i++) {
+ dashPatLen += dashPat[i];
+ }
+
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ if (obj.options.x != null) {
+ mr.style = 'line';
+ opts.closePath = false;
+ var yaxis = plot.axes[obj.options.yaxis],
+ ystart,
+ ystop,
+ x = plot.axes[obj.options.xaxis].series_u2p(obj.options.x),
+ yminoff = obj.options.yminOffset || obj.options.yOffset,
+ ymaxoff = obj.options.ymaxOffset || obj.options.yOffset;
+ if (obj.options.ymin != null) {
+ ystart = yaxis.series_u2p(obj.options.ymin);
+ }
+ else if (yminoff != null) {
+ if ($.type(yminoff) == "number") {
+ ystart = yaxis.series_u2p(yaxis.min - yminoff);
+ }
+ else if ($.type(yminoff) == "string") {
+ ystart = yaxis.series_u2p(yaxis.min) - parseFloat(yminoff);
+ }
+ }
+ if (obj.options.ymax != null) {
+ ystop = yaxis.series_u2p(obj.options.ymax);
+ }
+ else if (ymaxoff != null) {
+ if ($.type(ymaxoff) == "number") {
+ ystop = yaxis.series_u2p(yaxis.max + ymaxoff);
+ }
+ else if ($.type(ymaxoff) == "string") {
+ ystop = yaxis.series_u2p(yaxis.max) + parseFloat(ymaxoff);
+ }
+ }
+
+
+ if (ystop != null && ystart != null) {
+ obj.gridStart = [x, ystart];
+ obj.gridStop = [x, ystop];
+ var numDash = Math.ceil((ystart - ystop)/dashPatLen);
+ var firstDashAdjust = ((numDash * dashPatLen) - (ystart - ystop))/2.0;
+ var b=ystart, e, bs, es;
+ for (var i=0; i<numDash; i++) {
+ for (var j=0; j<dashPat.length; j+=2) {
+ e = b - dashPat[j];
+ if (e < ystop) {
+ e = ystop;
+ }
+ if (b < ystop) {
+ b = ystop;
+ }
+ // es = e;
+ // if (i == 0) {
+ // es += firstDashAdjust;
+ // }
+ mr.draw([x, b], [x, e], this.canvas._ctx, opts);
+ b -= dashPat[j];
+ if (j < dashPat.length-1) {
+ b -= dashPat[j+1];
+ }
+ }
+ }
+ }
+ }
+ break;
+
+ case 'rectangle':
+ // style and shadow properties should be set before
+ // every draw of marker renderer.
+ mr.style = 'line';
+ opts.closePath = true;
+
+ var xaxis = plot.axes[obj.options.xaxis],
+ xstart,
+ xstop,
+ y = plot.axes[obj.options.yaxis].series_u2p(obj.options.y),
+ xminoff = obj.options.xminOffset || obj.options.xOffset,
+ xmaxoff = obj.options.xmaxOffset || obj.options.xOffset;
+ if (obj.options.xmin != null) {
+ xstart = xaxis.series_u2p(obj.options.xmin);
+ }
+ else if (xminoff != null) {
+ if ($.type(xminoff) == "number") {
+ xstart = xaxis.series_u2p(xaxis.min + xminoff);
+ }
+ else if ($.type(xminoff) == "string") {
+ xstart = xaxis.series_u2p(xaxis.min) + parseFloat(xminoff);
+ }
+ }
+ if (obj.options.xmax != null) {
+ xstop = xaxis.series_u2p(obj.options.xmax);
+ }
+ else if (xmaxoff != null) {
+ if ($.type(xmaxoff) == "number") {
+ xstop = xaxis.series_u2p(xaxis.max - xmaxoff);
+ }
+ else if ($.type(xmaxoff) == "string") {
+ xstop = xaxis.series_u2p(xaxis.max) - parseFloat(xmaxoff);
+ }
+ }
+
+ var yaxis = plot.axes[obj.options.yaxis],
+ ystart,
+ ystop,
+ x = plot.axes[obj.options.xaxis].series_u2p(obj.options.x),
+ yminoff = obj.options.yminOffset || obj.options.yOffset,
+ ymaxoff = obj.options.ymaxOffset || obj.options.yOffset;
+ if (obj.options.ymin != null) {
+ ystart = yaxis.series_u2p(obj.options.ymin);
+ }
+ else if (yminoff != null) {
+ if ($.type(yminoff) == "number") {
+ ystart = yaxis.series_u2p(yaxis.min - yminoff);
+ }
+ else if ($.type(yminoff) == "string") {
+ ystart = yaxis.series_u2p(yaxis.min) - parseFloat(yminoff);
+ }
+ }
+ if (obj.options.ymax != null) {
+ ystop = yaxis.series_u2p(obj.options.ymax);
+ }
+ else if (ymaxoff != null) {
+ if ($.type(ymaxoff) == "number") {
+ ystop = yaxis.series_u2p(yaxis.max + ymaxoff);
+ }
+ else if ($.type(ymaxoff) == "string") {
+ ystop = yaxis.series_u2p(yaxis.max) + parseFloat(ymaxoff);
+ }
+ }
+
+
+ if (xstop != null && xstart != null && ystop != null && ystart != null) {
+ obj.gridStart = [xstart, ystart];
+ obj.gridStop = [xstop, ystop];
+
+ this.canvas._ctx.fillStyle = obj.options.color;
+ this.canvas._ctx.fillRect(xstart, ystart, xstop - xstart, ystop - ystart);
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ $.jqplot.CanvasOverlay.postPlotDraw = function() {
+ var co = this.plugins.canvasOverlay;
+ // Memory Leaks patch
+ if (co && co.highlightCanvas) {
+ co.highlightCanvas.resetCanvas();
+ co.highlightCanvas = null;
+ }
+ co.canvas = new $.jqplot.GenericCanvas();
+
+ this.eventCanvas._elem.before(co.canvas.createElement(this._gridPadding, 'jqplot-overlayCanvas-canvas', this._plotDimensions, this));
+ co.canvas.setContext();
+ if (!co.deferDraw) {
+ co.draw(this);
+ }
+
+ var elem = document.createElement('div');
+ co._tooltipElem = $(elem);
+ elem = null;
+ co._tooltipElem.addClass('jqplot-canvasOverlay-tooltip');
+ co._tooltipElem.css({position:'absolute', display:'none'});
+
+ this.eventCanvas._elem.before(co._tooltipElem);
+ this.eventCanvas._elem.bind('mouseleave', { elem: co._tooltipElem }, function (ev) { ev.data.elem.hide(); });
+
+ var co = null;
+ };
+
+
+ function showTooltip(plot, obj, gridpos, datapos) {
+ var co = plot.plugins.canvasOverlay;
+ var elem = co._tooltipElem;
+
+ var opts = obj.options, x, y;
+
+ elem.html($.jqplot.sprintf(opts.tooltipFormatString, datapos[0], datapos[1]));
+
+ switch (opts.tooltipLocation) {
+ case 'nw':
+ x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
+ break;
+ case 'n':
+ x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true)/2;
+ y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
+ break;
+ case 'ne':
+ x = gridpos[0] + plot._gridPadding.left + opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
+ break;
+ case 'e':
+ x = gridpos[0] + plot._gridPadding.left + opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top - elem.outerHeight(true)/2;
+ break;
+ case 'se':
+ x = gridpos[0] + plot._gridPadding.left + opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top + opts.tooltipOffset;
+ break;
+ case 's':
+ x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true)/2;
+ y = gridpos[1] + plot._gridPadding.top + opts.tooltipOffset;
+ break;
+ case 'sw':
+ x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top + opts.tooltipOffset;
+ break;
+ case 'w':
+ x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top - elem.outerHeight(true)/2;
+ break;
+ default: // same as 'nw'
+ x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
+ y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
+ break;
+ }
+
+ elem.css('left', x);
+ elem.css('top', y);
+ if (opts.fadeTooltip) {
+ // Fix for stacked up animations. Thnanks Trevor!
+ elem.stop(true,true).fadeIn(opts.tooltipFadeSpeed);
+ }
+ else {
+ elem.show();
+ }
+ elem = null;
+ }
+
+
+ function isNearLine(point, lstart, lstop, width) {
+ // r is point to test, p and q are end points.
+ var rx = point[0];
+ var ry = point[1];
+ var px = Math.round(lstop[0]);
+ var py = Math.round(lstop[1]);
+ var qx = Math.round(lstart[0]);
+ var qy = Math.round(lstart[1]);
+
+ var l = Math.sqrt(Math.pow(px-qx, 2) + Math.pow(py-qy, 2));
+
+ // scale error term by length of line.
+ var eps = width*l;
+ var res = Math.abs((qx-px) * (ry-py) - (qy-py) * (rx-px));
+ var ret = (res < eps) ? true : false;
+ return ret;
+ }
+
+ function isNearRectangle(point, lstart, lstop, width) {
+ // r is point to test, p and q are end points.
+ var rx = point[0];
+ var ry = point[1];
+ var px = Math.round(lstop[0]);
+ var py = Math.round(lstop[1]);
+ var qx = Math.round(lstart[0]);
+ var qy = Math.round(lstart[1]);
+
+ var temp;
+ if (px > qx) { temp = px; px = qx; qx = temp; }
+ if (py > qy) { temp = py; py = qy; qy = temp; }
+
+ var ret = (rx >= px && rx <= qx && ry >= py && ry <= qy);
+
+ return ret;
+ }
+
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ var co = plot.plugins.canvasOverlay;
+ var objs = co.objects;
+ var l = objs.length;
+ var obj, haveHighlight=false;
+ var elem;
+ for (var i=0; i<l; i++) {
+ obj = objs[i];
+ if (obj.options.showTooltip) {
+ var n;
+ if (obj.type === 'rectangle') {
+ n = isNearRectangle([gridpos.x, gridpos.y], obj.gridStart, obj.gridStop, obj.tooltipWidthFactor);
+ } else {
+ n = isNearLine([gridpos.x, gridpos.y], obj.gridStart, obj.gridStop, obj.tooltipWidthFactor);
+ }
+ datapos = [plot.axes[obj.options.xaxis].series_p2u(gridpos.x), plot.axes[obj.options.yaxis].series_p2u(gridpos.y)];
+
+ // cases:
+ // near line, no highlighting
+ // near line, highliting on this line
+ // near line, highlighting another line
+ // not near any line, highlighting
+ // not near any line, no highlighting
+
+ // near line, not currently highlighting
+ if (n && co.highlightObjectIndex == null) {
+ switch (obj.type) {
+ case 'line':
+ showTooltip(plot, obj, [gridpos.x, gridpos.y], datapos);
+ break;
+
+ case 'horizontalLine':
+ case 'dashedHorizontalLine':
+ showTooltip(plot, obj, [gridpos.x, obj.gridStart[1]], [datapos[0], obj.options.y]);
+ break;
+
+ case 'verticalLine':
+ case 'dashedVerticalLine':
+ showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
+ break;
+
+ case 'rectangle':
+ showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
+ break;
+
+ default:
+ break;
+ }
+ co.highlightObjectIndex = i;
+ haveHighlight = true;
+ break;
+ }
+
+ // near line, highlighting another line.
+ else if (n && co.highlightObjectIndex !== i) {
+ // turn off tooltip.
+ elem = co._tooltipElem;
+ if (obj.fadeTooltip) {
+ elem.fadeOut(obj.tooltipFadeSpeed);
+ }
+ else {
+ elem.hide();
+ }
+
+ // turn on right tooltip.
+ switch (obj.type) {
+ case 'line':
+ showTooltip(plot, obj, [gridpos.x, gridpos.y], datapos);
+ break;
+
+ case 'horizontalLine':
+ case 'dashedHorizontalLine':
+ showTooltip(plot, obj, [gridpos.x, obj.gridStart[1]], [datapos[0], obj.options.y]);
+ break;
+
+ case 'verticalLine':
+ case 'dashedVerticalLine':
+ showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
+ break;
+
+ case 'rectangle':
+ showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
+ break;
+
+ default:
+ break;
+ }
+
+ co.highlightObjectIndex = i;
+ haveHighlight = true;
+ break;
+ }
+
+ // near line, already highlighting this line, update
+ else if (n) {
+ switch (obj.type) {
+ case 'line':
+ showTooltip(plot, obj, [gridpos.x, gridpos.y], datapos);
+ break;
+
+ case 'horizontalLine':
+ case 'dashedHorizontalLine':
+ showTooltip(plot, obj, [gridpos.x, obj.gridStart[1]], [datapos[0], obj.options.y]);
+ break;
+
+ case 'verticalLine':
+ case 'dashedVerticalLine':
+ showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
+ break;
+
+ case 'rectangle':
+ showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
+ break;
+
+ default:
+ break;
+ }
+
+ haveHighlight = true;
+ break;
+ }
+ }
+ }
+
+ // check if we are highlighting and not near a line, turn it off.
+ if (!haveHighlight && co.highlightObjectIndex !== null) {
+ elem = co._tooltipElem;
+ obj = co.getObject(co.highlightObjectIndex);
+ if (obj.fadeTooltip) {
+ elem.fadeOut(obj.tooltipFadeSpeed);
+ }
+ else {
+ elem.hide();
+ }
+ co.highlightObjectIndex = null;
+ }
+ }
+
+ $.jqplot.postInitHooks.push($.jqplot.CanvasOverlay.postPlotInit);
+ $.jqplot.postDrawHooks.push($.jqplot.CanvasOverlay.postPlotDraw);
+ $.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js
new file mode 100644
index 0000000..dc8434e
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js
@@ -0,0 +1,449 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ * included jsDate library by Chris Leonello:
+ *
+ * Copyright (c) 2010-2013 Chris Leonello
+ *
+ * jsDate is currently available for use in all personal or commercial projects
+ * under both the MIT and GPL version 2.0 licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * jsDate borrows many concepts and ideas from the Date Instance
+ * Methods by Ken Snyder along with some parts of Ken's actual code.
+ *
+ * Ken's original Date Instance Methods and copyright notice:
+ *
+ * Ken Snyder (ken d snyder at gmail dot com)
+ * 2008-09-10
+ * version 2.0.2 (http://kendsnyder.com/sandbox/date/)
+ * Creative Commons Attribution License 3.0 (http://creativecommons.org/licenses/by/3.0/)
+ *
+ * jqplotToImage function based on Larry Siden's export-jqplot-to-png.js.
+ * Larry has generously given permission to adapt his code for inclusion
+ * into jqPlot.
+ *
+ * Larry's original code can be found here:
+ *
+ * https://github.com/lsiden/export-jqplot-to-png
+ *
+ *
+ */
+
+(function($) {
+ // This code is a modified version of the canvastext.js code, copyright below:
+ //
+ // This code is released to the public domain by Jim Studt, 2007.
+ // He may keep some sort of up to date copy at http://www.federated.com/~jim/canvastext/
+ //
+ $.jqplot.CanvasTextRenderer = function(options){
+ this.fontStyle = 'normal'; // normal, italic, oblique [not implemented]
+ this.fontVariant = 'normal'; // normal, small caps [not implemented]
+ this.fontWeight = 'normal'; // normal, bold, bolder, lighter, 100 - 900
+ this.fontSize = '10px';
+ this.fontFamily = 'sans-serif';
+ this.fontStretch = 1.0;
+ this.fillStyle = '#666666';
+ this.angle = 0;
+ this.textAlign = 'start';
+ this.textBaseline = 'alphabetic';
+ this.text;
+ this.width;
+ this.height;
+ this.pt2px = 1.28;
+
+ $.extend(true, this, options);
+ this.normalizedFontSize = this.normalizeFontSize(this.fontSize);
+ this.setHeight();
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.init = function(options) {
+ $.extend(true, this, options);
+ this.normalizedFontSize = this.normalizeFontSize(this.fontSize);
+ this.setHeight();
+ };
+
+ // convert css spec into point size
+ // returns float
+ $.jqplot.CanvasTextRenderer.prototype.normalizeFontSize = function(sz) {
+ sz = String(sz);
+ var n = parseFloat(sz);
+ if (sz.indexOf('px') > -1) {
+ return n/this.pt2px;
+ }
+ else if (sz.indexOf('pt') > -1) {
+ return n;
+ }
+ else if (sz.indexOf('em') > -1) {
+ return n*12;
+ }
+ else if (sz.indexOf('%') > -1) {
+ return n*12/100;
+ }
+ // default to pixels;
+ else {
+ return n/this.pt2px;
+ }
+ };
+
+
+ $.jqplot.CanvasTextRenderer.prototype.fontWeight2Float = function(w) {
+ // w = normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
+ // return values adjusted for Hershey font.
+ if (Number(w)) {
+ return w/400;
+ }
+ else {
+ switch (w) {
+ case 'normal':
+ return 1;
+ break;
+ case 'bold':
+ return 1.75;
+ break;
+ case 'bolder':
+ return 2.25;
+ break;
+ case 'lighter':
+ return 0.75;
+ break;
+ default:
+ return 1;
+ break;
+ }
+ }
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.getText = function() {
+ return this.text;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.setText = function(t, ctx) {
+ this.text = t;
+ this.setWidth(ctx);
+ return this;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.getWidth = function(ctx) {
+ return this.width;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.setWidth = function(ctx, w) {
+ if (!w) {
+ this.width = this.measure(ctx, this.text);
+ }
+ else {
+ this.width = w;
+ }
+ return this;
+ };
+
+ // return height in pixels.
+ $.jqplot.CanvasTextRenderer.prototype.getHeight = function(ctx) {
+ return this.height;
+ };
+
+ // w - height in pt
+ // set heigh in px
+ $.jqplot.CanvasTextRenderer.prototype.setHeight = function(w) {
+ if (!w) {
+ //height = this.fontSize /0.75;
+ this.height = this.normalizedFontSize * this.pt2px;
+ }
+ else {
+ this.height = w;
+ }
+ return this;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.letter = function (ch)
+ {
+ return this.letters[ch];
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.ascent = function()
+ {
+ return this.normalizedFontSize;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.descent = function()
+ {
+ return 7.0*this.normalizedFontSize/25.0;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.measure = function(ctx, str)
+ {
+ var total = 0;
+ var len = str.length;
+
+ for (var i = 0; i < len; i++) {
+ var c = this.letter(str.charAt(i));
+ if (c) {
+ total += c.width * this.normalizedFontSize / 25.0 * this.fontStretch;
+ }
+ }
+ return total;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.draw = function(ctx,str)
+ {
+ var x = 0;
+ // leave room at bottom for descenders.
+ var y = this.height*0.72;
+ var total = 0;
+ var len = str.length;
+ var mag = this.normalizedFontSize / 25.0;
+
+ ctx.save();
+ var tx, ty;
+
+ // 1st quadrant
+ if ((-Math.PI/2 <= this.angle && this.angle <= 0) || (Math.PI*3/2 <= this.angle && this.angle <= Math.PI*2)) {
+ tx = 0;
+ ty = -Math.sin(this.angle) * this.width;
+ }
+ // 4th quadrant
+ else if ((0 < this.angle && this.angle <= Math.PI/2) || (-Math.PI*2 <= this.angle && this.angle <= -Math.PI*3/2)) {
+ tx = Math.sin(this.angle) * this.height;
+ ty = 0;
+ }
+ // 2nd quadrant
+ else if ((-Math.PI < this.angle && this.angle < -Math.PI/2) || (Math.PI <= this.angle && this.angle <= Math.PI*3/2)) {
+ tx = -Math.cos(this.angle) * this.width;
+ ty = -Math.sin(this.angle) * this.width - Math.cos(this.angle) * this.height;
+ }
+ // 3rd quadrant
+ else if ((-Math.PI*3/2 < this.angle && this.angle < Math.PI) || (Math.PI/2 < this.angle && this.angle < Math.PI)) {
+ tx = Math.sin(this.angle) * this.height - Math.cos(this.angle)*this.width;
+ ty = -Math.cos(this.angle) * this.height;
+ }
+
+ ctx.strokeStyle = this.fillStyle;
+ ctx.fillStyle = this.fillStyle;
+ ctx.translate(tx, ty);
+ ctx.rotate(this.angle);
+ ctx.lineCap = "round";
+ // multiplier was 2.0
+ var fact = (this.normalizedFontSize > 30) ? 2.0 : 2 + (30 - this.normalizedFontSize)/20;
+ ctx.lineWidth = fact * mag * this.fontWeight2Float(this.fontWeight);
+
+ for ( var i = 0; i < len; i++) {
+ var c = this.letter( str.charAt(i));
+ if ( !c) {
+ continue;
+ }
+
+ ctx.beginPath();
+
+ var penUp = 1;
+ var needStroke = 0;
+ for ( var j = 0; j < c.points.length; j++) {
+ var a = c.points[j];
+ if ( a[0] == -1 && a[1] == -1) {
+ penUp = 1;
+ continue;
+ }
+ if ( penUp) {
+ ctx.moveTo( x + a[0]*mag*this.fontStretch, y - a[1]*mag);
+ penUp = false;
+ } else {
+ ctx.lineTo( x + a[0]*mag*this.fontStretch, y - a[1]*mag);
+ }
+ }
+ ctx.stroke();
+ x += c.width*mag*this.fontStretch;
+ }
+ ctx.restore();
+ return total;
+ };
+
+ $.jqplot.CanvasTextRenderer.prototype.letters = {
+ ' ': { width: 16, points: [] },
+ '!': { width: 10, points: [[5,21],[5,7],[-1,-1],[5,2],[4,1],[5,0],[6,1],[5,2]] },
+ '"': { width: 16, points: [[4,21],[4,14],[-1,-1],[12,21],[12,14]] },
+ '#': { width: 21, points: [[11,25],[4,-7],[-1,-1],[17,25],[10,-7],[-1,-1],[4,12],[18,12],[-1,-1],[3,6],[17,6]] },
+ '$': { width: 20, points: [[8,25],[8,-4],[-1,-1],[12,25],[12,-4],[-1,-1],[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },
+ '%': { width: 24, points: [[21,21],[3,0],[-1,-1],[8,21],[10,19],[10,17],[9,15],[7,14],[5,14],[3,16],[3,18],[4,20],[6,21],[8,21],[10,20],[13,19],[16,19],[19,20],[21,21],[-1,-1],[17,7],[15,6],[14,4],[14,2],[16,0],[18,0],[20,1],[21,3],[21,5],[19,7],[17,7]] },
+ '&': { width: 26, points: [[23,12],[23,13],[22,14],[21,14],[20,13],[19,11],[17,6],[15,3],[13,1],[11,0],[7,0],[5,1],[4,2],[3,4],[3,6],[4,8],[5,9],[12,13],[13,14],[14,16],[14,18],[13,20],[11,21],[9,20],[8,18],[8,16],[9,13],[11,10],[16,3],[18,1],[20,0],[22,0],[23,1],[23,2]] },
+ '\'': { width: 10, points: [[5,19],[4,20],[5,21],[6,20],[6,18],[5,16],[4,15]] },
+ '(': { width: 14, points: [[11,25],[9,23],[7,20],[5,16],[4,11],[4,7],[5,2],[7,-2],[9,-5],[11,-7]] },
+ ')': { width: 14, points: [[3,25],[5,23],[7,20],[9,16],[10,11],[10,7],[9,2],[7,-2],[5,-5],[3,-7]] },
+ '*': { width: 16, points: [[8,21],[8,9],[-1,-1],[3,18],[13,12],[-1,-1],[13,18],[3,12]] },
+ '+': { width: 26, points: [[13,18],[13,0],[-1,-1],[4,9],[22,9]] },
+ ',': { width: 10, points: [[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },
+ '-': { width: 18, points: [[6,9],[12,9]] },
+ '.': { width: 10, points: [[5,2],[4,1],[5,0],[6,1],[5,2]] },
+ '/': { width: 22, points: [[20,25],[2,-7]] },
+ '0': { width: 20, points: [[9,21],[6,20],[4,17],[3,12],[3,9],[4,4],[6,1],[9,0],[11,0],[14,1],[16,4],[17,9],[17,12],[16,17],[14,20],[11,21],[9,21]] },
+ '1': { width: 20, points: [[6,17],[8,18],[11,21],[11,0]] },
+ '2': { width: 20, points: [[4,16],[4,17],[5,19],[6,20],[8,21],[12,21],[14,20],[15,19],[16,17],[16,15],[15,13],[13,10],[3,0],[17,0]] },
+ '3': { width: 20, points: [[5,21],[16,21],[10,13],[13,13],[15,12],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },
+ '4': { width: 20, points: [[13,21],[3,7],[18,7],[-1,-1],[13,21],[13,0]] },
+ '5': { width: 20, points: [[15,21],[5,21],[4,12],[5,13],[8,14],[11,14],[14,13],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },
+ '6': { width: 20, points: [[16,18],[15,20],[12,21],[10,21],[7,20],[5,17],[4,12],[4,7],[5,3],[7,1],[10,0],[11,0],[14,1],[16,3],[17,6],[17,7],[16,10],[14,12],[11,13],[10,13],[7,12],[5,10],[4,7]] },
+ '7': { width: 20, points: [[17,21],[7,0],[-1,-1],[3,21],[17,21]] },
+ '8': { width: 20, points: [[8,21],[5,20],[4,18],[4,16],[5,14],[7,13],[11,12],[14,11],[16,9],[17,7],[17,4],[16,2],[15,1],[12,0],[8,0],[5,1],[4,2],[3,4],[3,7],[4,9],[6,11],[9,12],[13,13],[15,14],[16,16],[16,18],[15,20],[12,21],[8,21]] },
+ '9': { width: 20, points: [[16,14],[15,11],[13,9],[10,8],[9,8],[6,9],[4,11],[3,14],[3,15],[4,18],[6,20],[9,21],[10,21],[13,20],[15,18],[16,14],[16,9],[15,4],[13,1],[10,0],[8,0],[5,1],[4,3]] },
+ ':': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],[-1,-1],[5,2],[4,1],[5,0],[6,1],[5,2]] },
+ ';': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],[-1,-1],[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },
+ '<': { width: 24, points: [[20,18],[4,9],[20,0]] },
+ '=': { width: 26, points: [[4,12],[22,12],[-1,-1],[4,6],[22,6]] },
+ '>': { width: 24, points: [[4,18],[20,9],[4,0]] },
+ '?': { width: 18, points: [[3,16],[3,17],[4,19],[5,20],[7,21],[11,21],[13,20],[14,19],[15,17],[15,15],[14,13],[13,12],[9,10],[9,7],[-1,-1],[9,2],[8,1],[9,0],[10,1],[9,2]] },
+ '@': { width: 27, points: [[18,13],[17,15],[15,16],[12,16],[10,15],[9,14],[8,11],[8,8],[9,6],[11,5],[14,5],[16,6],[17,8],[-1,-1],[12,16],[10,14],[9,11],[9,8],[10,6],[11,5],[-1,-1],[18,16],[17,8],[17,6],[19,5],[21,5],[23,7],[24,10],[24,12],[23,15],[22,17],[20,19],[18,20],[15,21],[12,21],[9,20],[7,19],[5,17],[4,15],[3,12],[3,9],[4,6],[5,4],[7,2],[9,1],[12,0],[15,0],[18,1],[20,2],[21,3],[-1,-1],[19,16],[18,8],[18,6],[19,5]] },
+ 'A': { width: 18, points: [[9,21],[1,0],[-1,-1],[9,21],[17,0],[-1,-1],[4,7],[14,7]] },
+ 'B': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[-1,-1],[4,11],[13,11],[16,10],[17,9],[18,7],[18,4],[17,2],[16,1],[13,0],[4,0]] },
+ 'C': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5]] },
+ 'D': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[11,21],[14,20],[16,18],[17,16],[18,13],[18,8],[17,5],[16,3],[14,1],[11,0],[4,0]] },
+ 'E': { width: 19, points: [[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11],[-1,-1],[4,0],[17,0]] },
+ 'F': { width: 18, points: [[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11]] },
+ 'G': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[18,8],[-1,-1],[13,8],[18,8]] },
+ 'H': { width: 22, points: [[4,21],[4,0],[-1,-1],[18,21],[18,0],[-1,-1],[4,11],[18,11]] },
+ 'I': { width: 8, points: [[4,21],[4,0]] },
+ 'J': { width: 16, points: [[12,21],[12,5],[11,2],[10,1],[8,0],[6,0],[4,1],[3,2],[2,5],[2,7]] },
+ 'K': { width: 21, points: [[4,21],[4,0],[-1,-1],[18,21],[4,7],[-1,-1],[9,12],[18,0]] },
+ 'L': { width: 17, points: [[4,21],[4,0],[-1,-1],[4,0],[16,0]] },
+ 'M': { width: 24, points: [[4,21],[4,0],[-1,-1],[4,21],[12,0],[-1,-1],[20,21],[12,0],[-1,-1],[20,21],[20,0]] },
+ 'N': { width: 22, points: [[4,21],[4,0],[-1,-1],[4,21],[18,0],[-1,-1],[18,21],[18,0]] },
+ 'O': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21]] },
+ 'P': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,14],[17,12],[16,11],[13,10],[4,10]] },
+ 'Q': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21],[-1,-1],[12,4],[18,-2]] },
+ 'R': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[4,11],[-1,-1],[11,11],[18,0]] },
+ 'S': { width: 20, points: [[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },
+ 'T': { width: 16, points: [[8,21],[8,0],[-1,-1],[1,21],[15,21]] },
+ 'U': { width: 22, points: [[4,21],[4,6],[5,3],[7,1],[10,0],[12,0],[15,1],[17,3],[18,6],[18,21]] },
+ 'V': { width: 18, points: [[1,21],[9,0],[-1,-1],[17,21],[9,0]] },
+ 'W': { width: 24, points: [[2,21],[7,0],[-1,-1],[12,21],[7,0],[-1,-1],[12,21],[17,0],[-1,-1],[22,21],[17,0]] },
+ 'X': { width: 20, points: [[3,21],[17,0],[-1,-1],[17,21],[3,0]] },
+ 'Y': { width: 18, points: [[1,21],[9,11],[9,0],[-1,-1],[17,21],[9,11]] },
+ 'Z': { width: 20, points: [[17,21],[3,0],[-1,-1],[3,21],[17,21],[-1,-1],[3,0],[17,0]] },
+ '[': { width: 14, points: [[4,25],[4,-7],[-1,-1],[5,25],[5,-7],[-1,-1],[4,25],[11,25],[-1,-1],[4,-7],[11,-7]] },
+ '\\': { width: 14, points: [[0,21],[14,-3]] },
+ ']': { width: 14, points: [[9,25],[9,-7],[-1,-1],[10,25],[10,-7],[-1,-1],[3,25],[10,25],[-1,-1],[3,-7],[10,-7]] },
+ '^': { width: 16, points: [[6,15],[8,18],[10,15],[-1,-1],[3,12],[8,17],[13,12],[-1,-1],[8,17],[8,0]] },
+ '_': { width: 16, points: [[0,-2],[16,-2]] },
+ '`': { width: 10, points: [[6,21],[5,20],[4,18],[4,16],[5,15],[6,16],[5,17]] },
+ 'a': { width: 19, points: [[15,14],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
+ 'b': { width: 19, points: [[4,21],[4,0],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },
+ 'c': { width: 18, points: [[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
+ 'd': { width: 19, points: [[15,21],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
+ 'e': { width: 18, points: [[3,8],[15,8],[15,10],[14,12],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
+ 'f': { width: 12, points: [[10,21],[8,21],[6,20],[5,17],[5,0],[-1,-1],[2,14],[9,14]] },
+ 'g': { width: 19, points: [[15,14],[15,-2],[14,-5],[13,-6],[11,-7],[8,-7],[6,-6],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
+ 'h': { width: 19, points: [[4,21],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },
+ 'i': { width: 8, points: [[3,21],[4,20],[5,21],[4,22],[3,21],[-1,-1],[4,14],[4,0]] },
+ 'j': { width: 10, points: [[5,21],[6,20],[7,21],[6,22],[5,21],[-1,-1],[6,14],[6,-3],[5,-6],[3,-7],[1,-7]] },
+ 'k': { width: 17, points: [[4,21],[4,0],[-1,-1],[14,14],[4,4],[-1,-1],[8,8],[15,0]] },
+ 'l': { width: 8, points: [[4,21],[4,0]] },
+ 'm': { width: 30, points: [[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0],[-1,-1],[15,10],[18,13],[20,14],[23,14],[25,13],[26,10],[26,0]] },
+ 'n': { width: 19, points: [[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },
+ 'o': { width: 19, points: [[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3],[16,6],[16,8],[15,11],[13,13],[11,14],[8,14]] },
+ 'p': { width: 19, points: [[4,14],[4,-7],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },
+ 'q': { width: 19, points: [[15,14],[15,-7],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
+ 'r': { width: 13, points: [[4,14],[4,0],[-1,-1],[4,8],[5,11],[7,13],[9,14],[12,14]] },
+ 's': { width: 17, points: [[14,11],[13,13],[10,14],[7,14],[4,13],[3,11],[4,9],[6,8],[11,7],[13,6],[14,4],[14,3],[13,1],[10,0],[7,0],[4,1],[3,3]] },
+ 't': { width: 12, points: [[5,21],[5,4],[6,1],[8,0],[10,0],[-1,-1],[2,14],[9,14]] },
+ 'u': { width: 19, points: [[4,14],[4,4],[5,1],[7,0],[10,0],[12,1],[15,4],[-1,-1],[15,14],[15,0]] },
+ 'v': { width: 16, points: [[2,14],[8,0],[-1,-1],[14,14],[8,0]] },
+ 'w': { width: 22, points: [[3,14],[7,0],[-1,-1],[11,14],[7,0],[-1,-1],[11,14],[15,0],[-1,-1],[19,14],[15,0]] },
+ 'x': { width: 17, points: [[3,14],[14,0],[-1,-1],[14,14],[3,0]] },
+ 'y': { width: 16, points: [[2,14],[8,0],[-1,-1],[14,14],[8,0],[6,-4],[4,-6],[2,-7],[1,-7]] },
+ 'z': { width: 17, points: [[14,14],[3,0],[-1,-1],[3,14],[14,14],[-1,-1],[3,0],[14,0]] },
+ '{': { width: 14, points: [[9,25],[7,24],[6,23],[5,21],[5,19],[6,17],[7,16],[8,14],[8,12],[6,10],[-1,-1],[7,24],[6,22],[6,20],[7,18],[8,17],[9,15],[9,13],[8,11],[4,9],[8,7],[9,5],[9,3],[8,1],[7,0],[6,-2],[6,-4],[7,-6],[-1,-1],[6,8],[8,6],[8,4],[7,2],[6,1],[5,-1],[5,-3],[6,-5],[7,-6],[9,-7]] },
+ '|': { width: 8, points: [[4,25],[4,-7]] },
+ '}': { width: 14, points: [[5,25],[7,24],[8,23],[9,21],[9,19],[8,17],[7,16],[6,14],[6,12],[8,10],[-1,-1],[7,24],[8,22],[8,20],[7,18],[6,17],[5,15],[5,13],[6,11],[10,9],[6,7],[5,5],[5,3],[6,1],[7,0],[8,-2],[8,-4],[7,-6],[-1,-1],[8,8],[6,6],[6,4],[7,2],[8,1],[9,-1],[9,-3],[8,-5],[7,-6],[5,-7]] },
+ '~': { width: 24, points: [[3,6],[3,8],[4,11],[6,12],[8,12],[10,11],[14,8],[16,7],[18,7],[20,8],[21,10],[-1,-1],[3,8],[4,10],[6,11],[8,11],[10,10],[14,7],[16,6],[18,6],[20,7],[21,10],[21,12]] }
+ };
+
+ $.jqplot.CanvasFontRenderer = function(options) {
+ options = options || {};
+ if (!options.pt2px) {
+ options.pt2px = 1.5;
+ }
+ $.jqplot.CanvasTextRenderer.call(this, options);
+ };
+
+ $.jqplot.CanvasFontRenderer.prototype = new $.jqplot.CanvasTextRenderer({});
+ $.jqplot.CanvasFontRenderer.prototype.constructor = $.jqplot.CanvasFontRenderer;
+
+ $.jqplot.CanvasFontRenderer.prototype.measure = function(ctx, str)
+ {
+ // var fstyle = this.fontStyle+' '+this.fontVariant+' '+this.fontWeight+' '+this.fontSize+' '+this.fontFamily;
+ var fstyle = this.fontSize+' '+this.fontFamily;
+ ctx.save();
+ ctx.font = fstyle;
+ var w = ctx.measureText(str).width;
+ ctx.restore();
+ return w;
+ };
+
+ $.jqplot.CanvasFontRenderer.prototype.draw = function(ctx, str)
+ {
+ var x = 0;
+ // leave room at bottom for descenders.
+ var y = this.height*0.72;
+ //var y = 12;
+
+ ctx.save();
+ var tx, ty;
+
+ // 1st quadrant
+ if ((-Math.PI/2 <= this.angle && this.angle <= 0) || (Math.PI*3/2 <= this.angle && this.angle <= Math.PI*2)) {
+ tx = 0;
+ ty = -Math.sin(this.angle) * this.width;
+ }
+ // 4th quadrant
+ else if ((0 < this.angle && this.angle <= Math.PI/2) || (-Math.PI*2 <= this.angle && this.angle <= -Math.PI*3/2)) {
+ tx = Math.sin(this.angle) * this.height;
+ ty = 0;
+ }
+ // 2nd quadrant
+ else if ((-Math.PI < this.angle && this.angle < -Math.PI/2) || (Math.PI <= this.angle && this.angle <= Math.PI*3/2)) {
+ tx = -Math.cos(this.angle) * this.width;
+ ty = -Math.sin(this.angle) * this.width - Math.cos(this.angle) * this.height;
+ }
+ // 3rd quadrant
+ else if ((-Math.PI*3/2 < this.angle && this.angle < Math.PI) || (Math.PI/2 < this.angle && this.angle < Math.PI)) {
+ tx = Math.sin(this.angle) * this.height - Math.cos(this.angle)*this.width;
+ ty = -Math.cos(this.angle) * this.height;
+ }
+ ctx.strokeStyle = this.fillStyle;
+ ctx.fillStyle = this.fillStyle;
+ // var fstyle = this.fontStyle+' '+this.fontVariant+' '+this.fontWeight+' '+this.fontSize+' '+this.fontFamily;
+ var fstyle = this.fontSize+' '+this.fontFamily;
+ ctx.font = fstyle;
+ ctx.translate(tx, ty);
+ ctx.rotate(this.angle);
+ ctx.fillText(str, x, y);
+ // ctx.strokeText(str, x, y);
+
+ ctx.restore();
+ };
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js
new file mode 100644
index 0000000..aa77120
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js
@@ -0,0 +1,679 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * class: $.jqplot.CategoryAxisRenderer
+ * A plugin for jqPlot to render a category style axis, with equal pixel spacing between y data values of a series.
+ *
+ * To use this renderer, include the plugin in your source
+ * > <script type="text/javascript" language="javascript" src="plugins/jqplot.categoryAxisRenderer.js"></script>
+ *
+ * and supply the appropriate options to your plot
+ *
+ * > {axes:{xaxis:{renderer:$.jqplot.CategoryAxisRenderer}}}
+ **/
+ $.jqplot.CategoryAxisRenderer = function(options) {
+ $.jqplot.LinearAxisRenderer.call(this);
+ // prop: sortMergedLabels
+ // True to sort tick labels when labels are created by merging
+ // x axis values from multiple series. That is, say you have
+ // two series like:
+ // > line1 = [[2006, 4], [2008, 9], [2009, 16]];
+ // > line2 = [[2006, 3], [2007, 7], [2008, 6]];
+ // If no label array is specified, tick labels will be collected
+ // from the x values of the series. With sortMergedLabels
+ // set to true, tick labels will be:
+ // > [2006, 2007, 2008, 2009]
+ // With sortMergedLabels set to false, tick labels will be:
+ // > [2006, 2008, 2009, 2007]
+ //
+ // Note, this property is specified on the renderOptions for the
+ // axes when creating a plot:
+ // > axes:{xaxis:{renderer:$.jqplot.CategoryAxisRenderer, rendererOptions:{sortMergedLabels:true}}}
+ this.sortMergedLabels = false;
+ };
+
+ $.jqplot.CategoryAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.CategoryAxisRenderer.prototype.constructor = $.jqplot.CategoryAxisRenderer;
+
+ $.jqplot.CategoryAxisRenderer.prototype.init = function(options){
+ this.groups = 1;
+ this.groupLabels = [];
+ this._groupLabels = [];
+ this._grouped = false;
+ this._barsPerGroup = null;
+ this.reverse = false;
+ // prop: tickRenderer
+ // A class of a rendering engine for creating the ticks labels displayed on the plot,
+ // See <$.jqplot.AxisTickRenderer>.
+ // this.tickRenderer = $.jqplot.AxisTickRenderer;
+ // this.labelRenderer = $.jqplot.AxisLabelRenderer;
+ $.extend(true, this, {tickOptions:{formatString:'%d'}}, options);
+ var db = this._dataBounds;
+ // Go through all the series attached to this axis and find
+ // the min/max bounds for this axis.
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ if (s.groups) {
+ this.groups = s.groups;
+ }
+ var d = s.data;
+
+ for (var j=0; j<d.length; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ if (d[j][0] < db.min || db.min == null) {
+ db.min = d[j][0];
+ }
+ if (d[j][0] > db.max || db.max == null) {
+ db.max = d[j][0];
+ }
+ }
+ else {
+ if (d[j][1] < db.min || db.min == null) {
+ db.min = d[j][1];
+ }
+ if (d[j][1] > db.max || db.max == null) {
+ db.max = d[j][1];
+ }
+ }
+ }
+ }
+
+ if (this.groupLabels.length) {
+ this.groups = this.groupLabels.length;
+ }
+ };
+
+
+ $.jqplot.CategoryAxisRenderer.prototype.createTicks = function() {
+ // we're are operating on an axis here
+ var ticks = this._ticks;
+ var userTicks = this.ticks;
+ var name = this.name;
+ // databounds were set on axis initialization.
+ var db = this._dataBounds;
+ var dim, interval;
+ var min, max;
+ var pos1, pos2;
+ var tt, i;
+
+ // if we already have ticks, use them.
+ if (userTicks.length) {
+ // adjust with blanks if we have groups
+ if (this.groups > 1 && !this._grouped) {
+ var l = userTicks.length;
+ var skip = parseInt(l/this.groups, 10);
+ var count = 0;
+ for (var i=skip; i<l; i+=skip) {
+ userTicks.splice(i+count, 0, ' ');
+ count++;
+ }
+ this._grouped = true;
+ }
+ this.min = 0.5;
+ this.max = userTicks.length + 0.5;
+ var range = this.max - this.min;
+ this.numberTicks = 2*userTicks.length + 1;
+ for (i=0; i<userTicks.length; i++){
+ tt = this.min + 2 * i * range / (this.numberTicks-1);
+ // need a marker before and after the tick
+ var t = new this.tickRenderer(this.tickOptions);
+ t.showLabel = false;
+ // t.showMark = true;
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+ var t = new this.tickRenderer(this.tickOptions);
+ t.label = userTicks[i];
+ // t.showLabel = true;
+ t.showMark = false;
+ t.showGridline = false;
+ t.setTick(tt+0.5, this.name);
+ this._ticks.push(t);
+ }
+ // now add the last tick at the end
+ var t = new this.tickRenderer(this.tickOptions);
+ t.showLabel = false;
+ // t.showMark = true;
+ t.setTick(tt+1, this.name);
+ this._ticks.push(t);
+ }
+
+ // we don't have any ticks yet, let's make some!
+ else {
+ if (name == 'xaxis' || name == 'x2axis') {
+ dim = this._plotDimensions.width;
+ }
+ else {
+ dim = this._plotDimensions.height;
+ }
+
+ // if min, max and number of ticks specified, user can't specify interval.
+ if (this.min != null && this.max != null && this.numberTicks != null) {
+ this.tickInterval = null;
+ }
+
+ // if max, min, and interval specified and interval won't fit, ignore interval.
+ if (this.min != null && this.max != null && this.tickInterval != null) {
+ if (parseInt((this.max-this.min)/this.tickInterval, 10) != (this.max-this.min)/this.tickInterval) {
+ this.tickInterval = null;
+ }
+ }
+
+ // find out how many categories are in the lines and collect labels
+ var labels = [];
+ var numcats = 0;
+ var min = 0.5;
+ var max, val;
+ var isMerged = false;
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ for (var j=0; j<s.data.length; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ val = s.data[j][0];
+ }
+ else {
+ val = s.data[j][1];
+ }
+ if ($.inArray(val, labels) == -1) {
+ isMerged = true;
+ numcats += 1;
+ labels.push(val);
+ }
+ }
+ }
+
+ if (isMerged && this.sortMergedLabels) {
+ if (typeof labels[0] == "string") {
+ labels.sort();
+ } else {
+ labels.sort(function(a,b) { return a - b; });
+ }
+ }
+
+ // keep a reference to these tick labels to use for redrawing plot (see bug #57)
+ this.ticks = labels;
+
+ // now bin the data values to the right lables.
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ for (var j=0; j<s.data.length; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ val = s.data[j][0];
+ }
+ else {
+ val = s.data[j][1];
+ }
+ // for category axis, force the values into category bins.
+ // we should have the value in the label array now.
+ var idx = $.inArray(val, labels)+1;
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ s.data[j][0] = idx;
+ }
+ else {
+ s.data[j][1] = idx;
+ }
+ }
+ }
+
+ // adjust with blanks if we have groups
+ if (this.groups > 1 && !this._grouped) {
+ var l = labels.length;
+ var skip = parseInt(l/this.groups, 10);
+ var count = 0;
+ for (var i=skip; i<l; i+=skip+1) {
+ labels[i] = ' ';
+ }
+ this._grouped = true;
+ }
+
+ max = numcats + 0.5;
+ if (this.numberTicks == null) {
+ this.numberTicks = 2*numcats + 1;
+ }
+
+ var range = max - min;
+ this.min = min;
+ this.max = max;
+ var track = 0;
+
+ // todo: adjust this so more ticks displayed.
+ var maxVisibleTicks = parseInt(3+dim/10, 10);
+ var skip = parseInt(numcats/maxVisibleTicks, 10);
+
+ if (this.tickInterval == null) {
+
+ this.tickInterval = range / (this.numberTicks-1);
+
+ }
+ // if tickInterval is specified, we will ignore any computed maximum.
+ for (var i=0; i<this.numberTicks; i++){
+ tt = this.min + i * this.tickInterval;
+ var t = new this.tickRenderer(this.tickOptions);
+ // if even tick, it isn't a category, it's a divider
+ if (i/2 == parseInt(i/2, 10)) {
+ t.showLabel = false;
+ t.showMark = true;
+ }
+ else {
+ if (skip>0 && track<skip) {
+ t.showLabel = false;
+ track += 1;
+ }
+ else {
+ t.showLabel = true;
+ track = 0;
+ }
+ t.label = t.formatter(t.formatString, labels[(i-1)/2]);
+ t.showMark = false;
+ t.showGridline = false;
+ }
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+ }
+ }
+
+ };
+
+ // called with scope of axis
+ $.jqplot.CategoryAxisRenderer.prototype.draw = function(ctx, plot) {
+ if (this.show) {
+ // populate the axis label and value properties.
+ // createTicks is a method on the renderer, but
+ // call it within the scope of the axis.
+ this.renderer.createTicks.call(this);
+ // fill a div with axes labels in the right direction.
+ // Need to pregenerate each axis to get its bounds and
+ // position it and the labels correctly on the plot.
+ var dim=0;
+ var temp;
+ // Added for theming.
+ if (this._elem) {
+ // this._elem.empty();
+ // Memory Leaks patch
+ this._elem.emptyForce();
+ }
+
+ this._elem = this._elem || $('<div class="jqplot-axis jqplot-'+this.name+'" style="position:absolute;"></div>');
+
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ this._elem.width(this._plotDimensions.width);
+ }
+ else {
+ this._elem.height(this._plotDimensions.height);
+ }
+
+ // create a _label object.
+ this.labelOptions.axis = this.name;
+ this._label = new this.labelRenderer(this.labelOptions);
+ if (this._label.show) {
+ var elem = this._label.draw(ctx, plot);
+ elem.appendTo(this._elem);
+ }
+
+ var t = this._ticks;
+ for (var i=0; i<t.length; i++) {
+ var tick = t[i];
+ if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
+ var elem = tick.draw(ctx, plot);
+ elem.appendTo(this._elem);
+ }
+ }
+
+ this._groupLabels = [];
+ // now make group labels
+ for (var i=0; i<this.groupLabels.length; i++)
+ {
+ var elem = $('<div style="position:absolute;" class="jqplot-'+this.name+'-groupLabel"></div>');
+ elem.html(this.groupLabels[i]);
+ this._groupLabels.push(elem);
+ elem.appendTo(this._elem);
+ }
+ }
+ return this._elem;
+ };
+
+ // called with scope of axis
+ $.jqplot.CategoryAxisRenderer.prototype.set = function() {
+ var dim = 0;
+ var temp;
+ var w = 0;
+ var h = 0;
+ var lshow = (this._label == null) ? false : this._label.show;
+ if (this.show) {
+ var t = this._ticks;
+ for (var i=0; i<t.length; i++) {
+ var tick = t[i];
+ if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ temp = tick._elem.outerHeight(true);
+ }
+ else {
+ temp = tick._elem.outerWidth(true);
+ }
+ if (temp > dim) {
+ dim = temp;
+ }
+ }
+ }
+
+ var dim2 = 0;
+ for (var i=0; i<this._groupLabels.length; i++) {
+ var l = this._groupLabels[i];
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ temp = l.outerHeight(true);
+ }
+ else {
+ temp = l.outerWidth(true);
+ }
+ if (temp > dim2) {
+ dim2 = temp;
+ }
+ }
+
+ if (lshow) {
+ w = this._label._elem.outerWidth(true);
+ h = this._label._elem.outerHeight(true);
+ }
+ if (this.name == 'xaxis') {
+ dim += dim2 + h;
+ this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
+ }
+ else if (this.name == 'x2axis') {
+ dim += dim2 + h;
+ this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
+ }
+ else if (this.name == 'yaxis') {
+ dim += dim2 + w;
+ this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ else {
+ dim += dim2 + w;
+ this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.CategoryAxisRenderer.prototype.pack = function(pos, offsets) {
+ var ticks = this._ticks;
+ var max = this.max;
+ var min = this.min;
+ var offmax = offsets.max;
+ var offmin = offsets.min;
+ var lshow = (this._label == null) ? false : this._label.show;
+ var i;
+
+ for (var p in pos) {
+ this._elem.css(p, pos[p]);
+ }
+
+ this._offsets = offsets;
+ // pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
+ var pixellength = offmax - offmin;
+ var unitlength = max - min;
+
+ if (!this.reverse) {
+ // point to unit and unit to point conversions references to Plot DOM element top left corner.
+
+ this.u2p = function(u){
+ return (u - min) * pixellength / unitlength + offmin;
+ };
+
+ this.p2u = function(p){
+ return (p - offmin) * unitlength / pixellength + min;
+ };
+
+ if (this.name == 'xaxis' || this.name == 'x2axis'){
+ this.series_u2p = function(u){
+ return (u - min) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+
+ else {
+ this.series_u2p = function(u){
+ return (u - max) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + max;
+ };
+ }
+ }
+
+ else {
+ // point to unit and unit to point conversions references to Plot DOM element top left corner.
+
+ this.u2p = function(u){
+ return offmin + (max - u) * pixellength / unitlength;
+ };
+
+ this.p2u = function(p){
+ return min + (p - offmin) * unitlength / pixellength;
+ };
+
+ if (this.name == 'xaxis' || this.name == 'x2axis'){
+ this.series_u2p = function(u){
+ return (max - u) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + max;
+ };
+ }
+
+ else {
+ this.series_u2p = function(u){
+ return (min - u) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+
+ }
+
+
+ if (this.show) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ for (i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'xaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ if (temp * t.angle < 0) {
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ }
+ // position at start
+ else {
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'end':
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ case 'start':
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ break;
+ case 'middle':
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ default:
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getWidth()/2;
+ }
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('left', val);
+ t.pack();
+ }
+ }
+
+ var labeledge=['bottom', 0];
+ if (lshow) {
+ var w = this._label._elem.outerWidth(true);
+ this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
+ if (this.name == 'xaxis') {
+ this._label._elem.css('bottom', '0px');
+ labeledge = ['bottom', this._label._elem.outerHeight(true)];
+ }
+ else {
+ this._label._elem.css('top', '0px');
+ labeledge = ['top', this._label._elem.outerHeight(true)];
+ }
+ this._label.pack();
+ }
+
+ // draw the group labels
+ var step = parseInt(this._ticks.length/this.groups, 10) + 1;
+ for (i=0; i<this._groupLabels.length; i++) {
+ var mid = 0;
+ var count = 0;
+ for (var j=i*step; j<(i+1)*step; j++) {
+ if (j >= this._ticks.length-1) continue; // the last tick does not exist as there is no other group in order to have an empty one.
+ if (this._ticks[j]._elem && this._ticks[j].label != " ") {
+ var t = this._ticks[j]._elem;
+ var p = t.position();
+ mid += p.left + t.outerWidth(true)/2;
+ count++;
+ }
+ }
+ mid = mid/count;
+ this._groupLabels[i].css({'left':(mid - this._groupLabels[i].outerWidth(true)/2)});
+ this._groupLabels[i].css(labeledge[0], labeledge[1]);
+ }
+ }
+ else {
+ for (i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'yaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ case 'end':
+ if (temp * t.angle < 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'start':
+ if (t.angle > 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'middle':
+ // if (t.angle > 0) {
+ // shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ // }
+ // else {
+ // shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ // }
+ shim = -t.getHeight()/2;
+ break;
+ default:
+ shim = -t.getHeight()/2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getHeight()/2;
+ }
+
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('top', val);
+ t.pack();
+ }
+ }
+
+ var labeledge=['left', 0];
+ if (lshow) {
+ var h = this._label._elem.outerHeight(true);
+ this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
+ if (this.name == 'yaxis') {
+ this._label._elem.css('left', '0px');
+ labeledge = ['left', this._label._elem.outerWidth(true)];
+ }
+ else {
+ this._label._elem.css('right', '0px');
+ labeledge = ['right', this._label._elem.outerWidth(true)];
+ }
+ this._label.pack();
+ }
+
+ // draw the group labels, position top here, do left after label position.
+ var step = parseInt(this._ticks.length/this.groups, 10) + 1; // step is one more than before as we don't want to have overlaps in loops
+ for (i=0; i<this._groupLabels.length; i++) {
+ var mid = 0;
+ var count = 0;
+ for (var j=i*step; j<(i+1)*step; j++) { // j must never reach (i+1)*step as we don't want to have overlap between loops
+ if (j >= this._ticks.length-1) continue; // the last tick does not exist as there is no other group in order to have an empty one.
+ if (this._ticks[j]._elem && this._ticks[j].label != " ") {
+ var t = this._ticks[j]._elem;
+ var p = t.position();
+ mid += p.top + t.outerHeight()/2;
+ count++;
+ }
+ }
+ mid = mid/count;
+ this._groupLabels[i].css({'top':mid - this._groupLabels[i].outerHeight()/2});
+ this._groupLabels[i].css(labeledge[0], labeledge[1]);
+
+ }
+ }
+ }
+ };
+
+
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.ciParser.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.ciParser.js
new file mode 100644
index 0000000..2f094fc
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.ciParser.js
@@ -0,0 +1,116 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.ciParser
+ * Data Renderer function which converts a custom JSON data object into jqPlot data format.
+ * Set this as a callable on the jqplot dataRenderer plot option:
+ *
+ * > plot = $.jqplot('mychart', [data], { dataRenderer: $.jqplot.ciParser, ... });
+ *
+ * Where data is an object in JSON format or a JSON encoded string conforming to the
+ * City Index API spec.
+ *
+ * Note that calling the renderer function is handled internally by jqPlot. The
+ * user does not have to call the function. The parameters described below will
+ * automatically be passed to the ciParser function.
+ *
+ * Parameters:
+ * data - JSON encoded string or object.
+ * plot - reference to jqPlot Plot object.
+ *
+ * Returns:
+ * data array in jqPlot format.
+ *
+ */
+ $.jqplot.ciParser = function (data, plot) {
+ var ret = [],
+ line,
+ temp,
+ i, j, k, kk;
+
+ if (typeof(data) == "string") {
+ data = $.jqplot.JSON.parse(data, handleStrings);
+ }
+
+ else if (typeof(data) == "object") {
+ for (k in data) {
+ for (i=0; i<data[k].length; i++) {
+ for (kk in data[k][i]) {
+ data[k][i][kk] = handleStrings(kk, data[k][i][kk]);
+ }
+ }
+ }
+ }
+
+ else {
+ return null;
+ }
+
+ // function handleStrings
+ // Checks any JSON encoded strings to see if they are
+ // encoded dates. If so, pull out the timestamp.
+ // Expects dates to be represented by js timestamps.
+
+ function handleStrings(key, value) {
+ var a;
+ if (value != null) {
+ if (value.toString().indexOf('Date') >= 0) {
+ //here we will try to extract the ticks from the Date string in the "value" fields of JSON returned data
+ a = /^\/Date\((-?[0-9]+)\)\/$/.exec(value);
+ if (a) {
+ return parseInt(a[1], 10);
+ }
+ }
+ return value;
+ }
+ }
+
+ for (var prop in data) {
+ line = [];
+ temp = data[prop];
+ switch (prop) {
+ case "PriceTicks":
+ for (i=0; i<temp.length; i++) {
+ line.push([temp[i]['TickDate'], temp[i]['Price']]);
+ }
+ break;
+ case "PriceBars":
+ for (i=0; i<temp.length; i++) {
+ line.push([temp[i]['BarDate'], temp[i]['Open'], temp[i]['High'], temp[i]['Low'], temp[i]['Close']]);
+ }
+ break;
+ }
+ ret.push(line);
+ }
+ return ret;
+ };
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.cursor.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.cursor.js
new file mode 100644
index 0000000..bf1c36f
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.cursor.js
@@ -0,0 +1,1108 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+
+ /**
+ * Class: $.jqplot.Cursor
+ * Plugin class representing the cursor as displayed on the plot.
+ */
+ $.jqplot.Cursor = function(options) {
+ // Group: Properties
+ //
+ // prop: style
+ // CSS spec for cursor style
+ this.style = 'crosshair';
+ this.previousCursor = 'auto';
+ // prop: show
+ // whether to show the cursor or not.
+ this.show = $.jqplot.config.enablePlugins;
+ // prop: showTooltip
+ // show a cursor position tooltip. Location of the tooltip
+ // will be controlled by followMouse and tooltipLocation.
+ this.showTooltip = true;
+ // prop: followMouse
+ // Tooltip follows the mouse, it is not at a fixed location.
+ // Tooltip will show on the grid at the location given by
+ // tooltipLocation, offset from the grid edge by tooltipOffset.
+ this.followMouse = false;
+ // prop: tooltipLocation
+ // Where to position tooltip. If followMouse is true, this is
+ // relative to the cursor, otherwise, it is relative to the grid.
+ // One of 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
+ this.tooltipLocation = 'se';
+ // prop: tooltipOffset
+ // Pixel offset of tooltip from the grid boudaries or cursor center.
+ this.tooltipOffset = 6;
+ // prop: showTooltipGridPosition
+ // show the grid pixel coordinates of the mouse.
+ this.showTooltipGridPosition = false;
+ // prop: showTooltipUnitPosition
+ // show the unit (data) coordinates of the mouse.
+ this.showTooltipUnitPosition = true;
+ // prop: showTooltipDataPosition
+ // Used with showVerticalLine to show intersecting data points in the tooltip.
+ this.showTooltipDataPosition = false;
+ // prop: tooltipFormatString
+ // sprintf format string for the tooltip.
+ // Uses Ash Searle's javascript sprintf implementation
+ // found here: http://hexmen.com/blog/2007/03/printf-sprintf/
+ // See http://perldoc.perl.org/functions/sprintf.html for reference
+ // Note, if showTooltipDataPosition is true, the default tooltipFormatString
+ // will be set to the cursorLegendFormatString, not the default given here.
+ this.tooltipFormatString = '%.4P, %.4P';
+ // prop: useAxesFormatters
+ // Use the x and y axes formatters to format the text in the tooltip.
+ this.useAxesFormatters = true;
+ // prop: tooltipAxisGroups
+ // Show position for the specified axes.
+ // This is an array like [['xaxis', 'yaxis'], ['xaxis', 'y2axis']]
+ // Default is to compute automatically for all visible axes.
+ this.tooltipAxisGroups = [];
+ // prop: zoom
+ // Enable plot zooming.
+ this.zoom = false;
+ // zoomProxy and zoomTarget properties are not directly set by user.
+ // They Will be set through call to zoomProxy method.
+ this.zoomProxy = false;
+ this.zoomTarget = false;
+ // prop: looseZoom
+ // Will expand zoom range to provide more rounded tick values.
+ // Works only with linear, log and date axes.
+ this.looseZoom = true;
+ // prop: clickReset
+ // Will reset plot zoom if single click on plot without drag.
+ this.clickReset = false;
+ // prop: dblClickReset
+ // Will reset plot zoom if double click on plot without drag.
+ this.dblClickReset = true;
+ // prop: showVerticalLine
+ // draw a vertical line across the plot which follows the cursor.
+ // When the line is near a data point, a special legend and/or tooltip can
+ // be updated with the data values.
+ this.showVerticalLine = false;
+ // prop: showHorizontalLine
+ // draw a horizontal line across the plot which follows the cursor.
+ this.showHorizontalLine = false;
+ // prop: constrainZoomTo
+ // 'none', 'x' or 'y'
+ this.constrainZoomTo = 'none';
+ // // prop: autoscaleConstraint
+ // // when a constrained axis is specified, true will
+ // // auatoscale the adjacent axis.
+ // this.autoscaleConstraint = true;
+ this.shapeRenderer = new $.jqplot.ShapeRenderer();
+ this._zoom = {start:[], end:[], started: false, zooming:false, isZoomed:false, axes:{start:{}, end:{}}, gridpos:{}, datapos:{}};
+ this._tooltipElem;
+ this.zoomCanvas;
+ this.cursorCanvas;
+ // prop: intersectionThreshold
+ // pixel distance from data point or marker to consider cursor lines intersecting with point.
+ // If data point markers are not shown, this should be >= 1 or will often miss point intersections.
+ this.intersectionThreshold = 2;
+ // prop: showCursorLegend
+ // Replace the plot legend with an enhanced legend displaying intersection information.
+ this.showCursorLegend = false;
+ // prop: cursorLegendFormatString
+ // Format string used in the cursor legend. If showTooltipDataPosition is true,
+ // this will also be the default format string used by tooltipFormatString.
+ this.cursorLegendFormatString = $.jqplot.Cursor.cursorLegendFormatString;
+ // whether the cursor is over the grid or not.
+ this._oldHandlers = {onselectstart: null, ondrag: null, onmousedown: null};
+ // prop: constrainOutsideZoom
+ // True to limit actual zoom area to edges of grid, even when zooming
+ // outside of plot area. That is, can't zoom out by mousing outside plot.
+ this.constrainOutsideZoom = true;
+ // prop: showTooltipOutsideZoom
+ // True will keep updating the tooltip when zooming of the grid.
+ this.showTooltipOutsideZoom = false;
+ // true if mouse is over grid, false if not.
+ this.onGrid = false;
+ $.extend(true, this, options);
+ };
+
+ $.jqplot.Cursor.cursorLegendFormatString = '%s x:%s, y:%s';
+
+ // called with scope of plot
+ $.jqplot.Cursor.init = function (target, data, opts){
+ // add a cursor attribute to the plot
+ var options = opts || {};
+ this.plugins.cursor = new $.jqplot.Cursor(options.cursor);
+ var c = this.plugins.cursor;
+
+ if (c.show) {
+ $.jqplot.eventListenerHooks.push(['jqplotMouseEnter', handleMouseEnter]);
+ $.jqplot.eventListenerHooks.push(['jqplotMouseLeave', handleMouseLeave]);
+ $.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMouseMove]);
+
+ if (c.showCursorLegend) {
+ opts.legend = opts.legend || {};
+ opts.legend.renderer = $.jqplot.CursorLegendRenderer;
+ opts.legend.formatString = this.plugins.cursor.cursorLegendFormatString;
+ opts.legend.show = true;
+ }
+
+ if (c.zoom) {
+ $.jqplot.eventListenerHooks.push(['jqplotMouseDown', handleMouseDown]);
+
+ if (c.clickReset) {
+ $.jqplot.eventListenerHooks.push(['jqplotClick', handleClick]);
+ }
+
+ if (c.dblClickReset) {
+ $.jqplot.eventListenerHooks.push(['jqplotDblClick', handleDblClick]);
+ }
+ }
+
+ this.resetZoom = function() {
+ var axes = this.axes;
+ if (!c.zoomProxy) {
+ for (var ax in axes) {
+ axes[ax].reset();
+ axes[ax]._ticks = [];
+ // fake out tick creation algorithm to make sure original auto
+ // computed format string is used if _overrideFormatString is true
+ if (c._zoom.axes[ax] !== undefined) {
+ axes[ax]._autoFormatString = c._zoom.axes[ax].tickFormatString;
+ }
+ }
+ this.redraw();
+ }
+ else {
+ var ctx = this.plugins.cursor.zoomCanvas._ctx;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ ctx = null;
+ }
+ this.plugins.cursor._zoom.isZoomed = false;
+ this.target.trigger('jqplotResetZoom', [this, this.plugins.cursor]);
+ };
+
+
+ if (c.showTooltipDataPosition) {
+ c.showTooltipUnitPosition = false;
+ c.showTooltipGridPosition = false;
+ if (options.cursor.tooltipFormatString == undefined) {
+ c.tooltipFormatString = $.jqplot.Cursor.cursorLegendFormatString;
+ }
+ }
+ }
+ };
+
+ // called with context of plot
+ $.jqplot.Cursor.postDraw = function() {
+ var c = this.plugins.cursor;
+
+ // Memory Leaks patch
+ if (c.zoomCanvas) {
+ c.zoomCanvas.resetCanvas();
+ c.zoomCanvas = null;
+ }
+
+ if (c.cursorCanvas) {
+ c.cursorCanvas.resetCanvas();
+ c.cursorCanvas = null;
+ }
+
+ if (c._tooltipElem) {
+ c._tooltipElem.emptyForce();
+ c._tooltipElem = null;
+ }
+
+
+ if (c.zoom) {
+ c.zoomCanvas = new $.jqplot.GenericCanvas();
+ this.eventCanvas._elem.before(c.zoomCanvas.createElement(this._gridPadding, 'jqplot-zoom-canvas', this._plotDimensions, this));
+ c.zoomCanvas.setContext();
+ }
+
+ var elem = document.createElement('div');
+ c._tooltipElem = $(elem);
+ elem = null;
+ c._tooltipElem.addClass('jqplot-cursor-tooltip');
+ c._tooltipElem.css({position:'absolute', display:'none'});
+
+
+ if (c.zoomCanvas) {
+ c.zoomCanvas._elem.before(c._tooltipElem);
+ }
+
+ else {
+ this.eventCanvas._elem.before(c._tooltipElem);
+ }
+
+ if (c.showVerticalLine || c.showHorizontalLine) {
+ c.cursorCanvas = new $.jqplot.GenericCanvas();
+ this.eventCanvas._elem.before(c.cursorCanvas.createElement(this._gridPadding, 'jqplot-cursor-canvas', this._plotDimensions, this));
+ c.cursorCanvas.setContext();
+ }
+
+ // if we are showing the positions in unit coordinates, and no axes groups
+ // were specified, create a default set.
+ if (c.showTooltipUnitPosition){
+ if (c.tooltipAxisGroups.length === 0) {
+ var series = this.series;
+ var s;
+ var temp = [];
+ for (var i=0; i<series.length; i++) {
+ s = series[i];
+ var ax = s.xaxis+','+s.yaxis;
+ if ($.inArray(ax, temp) == -1) {
+ temp.push(ax);
+ }
+ }
+ for (var i=0; i<temp.length; i++) {
+ c.tooltipAxisGroups.push(temp[i].split(','));
+ }
+ }
+ }
+ };
+
+ // Group: methods
+ //
+ // method: $.jqplot.Cursor.zoomProxy
+ // links targetPlot to controllerPlot so that plot zooming of
+ // targetPlot will be controlled by zooming on the controllerPlot.
+ // controllerPlot will not actually zoom, but acts as an
+ // overview plot. Note, the zoom options must be set to true for
+ // zoomProxy to work.
+ $.jqplot.Cursor.zoomProxy = function(targetPlot, controllerPlot) {
+ var tc = targetPlot.plugins.cursor;
+ var cc = controllerPlot.plugins.cursor;
+ tc.zoomTarget = true;
+ tc.zoom = true;
+ tc.style = 'auto';
+ tc.dblClickReset = false;
+ cc.zoom = true;
+ cc.zoomProxy = true;
+
+ controllerPlot.target.bind('jqplotZoom', plotZoom);
+ controllerPlot.target.bind('jqplotResetZoom', plotReset);
+
+ function plotZoom(ev, gridpos, datapos, plot, cursor) {
+ tc.doZoom(gridpos, datapos, targetPlot, cursor);
+ }
+
+ function plotReset(ev, plot, cursor) {
+ targetPlot.resetZoom();
+ }
+ };
+
+ $.jqplot.Cursor.prototype.resetZoom = function(plot, cursor) {
+ var axes = plot.axes;
+ var cax = cursor._zoom.axes;
+ if (!plot.plugins.cursor.zoomProxy && cursor._zoom.isZoomed) {
+ for (var ax in axes) {
+ // axes[ax]._ticks = [];
+ // axes[ax].min = cax[ax].min;
+ // axes[ax].max = cax[ax].max;
+ // axes[ax].numberTicks = cax[ax].numberTicks;
+ // axes[ax].tickInterval = cax[ax].tickInterval;
+ // // for date axes
+ // axes[ax].daTickInterval = cax[ax].daTickInterval;
+ axes[ax].reset();
+ axes[ax]._ticks = [];
+ // fake out tick creation algorithm to make sure original auto
+ // computed format string is used if _overrideFormatString is true
+ axes[ax]._autoFormatString = cax[ax].tickFormatString;
+ }
+ plot.redraw();
+ cursor._zoom.isZoomed = false;
+ }
+ else {
+ var ctx = cursor.zoomCanvas._ctx;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ ctx = null;
+ }
+ plot.target.trigger('jqplotResetZoom', [plot, cursor]);
+ };
+
+ $.jqplot.Cursor.resetZoom = function(plot) {
+ plot.resetZoom();
+ };
+
+ $.jqplot.Cursor.prototype.doZoom = function (gridpos, datapos, plot, cursor) {
+ var c = cursor;
+ var axes = plot.axes;
+ var zaxes = c._zoom.axes;
+ var start = zaxes.start;
+ var end = zaxes.end;
+ var min, max, dp, span,
+ newmin, newmax, curax, _numberTicks, ret;
+ var ctx = plot.plugins.cursor.zoomCanvas._ctx;
+ // don't zoom if zoom area is too small (in pixels)
+ if ((c.constrainZoomTo == 'none' && Math.abs(gridpos.x - c._zoom.start[0]) > 6 && Math.abs(gridpos.y - c._zoom.start[1]) > 6) || (c.constrainZoomTo == 'x' && Math.abs(gridpos.x - c._zoom.start[0]) > 6) || (c.constrainZoomTo == 'y' && Math.abs(gridpos.y - c._zoom.start[1]) > 6)) {
+ if (!plot.plugins.cursor.zoomProxy) {
+ for (var ax in datapos) {
+ // make a copy of the original axes to revert back.
+ if (c._zoom.axes[ax] == undefined) {
+ c._zoom.axes[ax] = {};
+ c._zoom.axes[ax].numberTicks = axes[ax].numberTicks;
+ c._zoom.axes[ax].tickInterval = axes[ax].tickInterval;
+ // for date axes...
+ c._zoom.axes[ax].daTickInterval = axes[ax].daTickInterval;
+ c._zoom.axes[ax].min = axes[ax].min;
+ c._zoom.axes[ax].max = axes[ax].max;
+ c._zoom.axes[ax].tickFormatString = (axes[ax].tickOptions != null) ? axes[ax].tickOptions.formatString : '';
+ }
+
+
+ if ((c.constrainZoomTo == 'none') || (c.constrainZoomTo == 'x' && ax.charAt(0) == 'x') || (c.constrainZoomTo == 'y' && ax.charAt(0) == 'y')) {
+ dp = datapos[ax];
+ if (dp != null) {
+ if (dp > start[ax]) {
+ newmin = start[ax];
+ newmax = dp;
+ }
+ else {
+ span = start[ax] - dp;
+ newmin = dp;
+ newmax = start[ax];
+ }
+
+ curax = axes[ax];
+
+ _numberTicks = null;
+
+ // if aligning this axis, use number of ticks from previous axis.
+ // Do I need to reset somehow if alignTicks is changed and then graph is replotted??
+ if (curax.alignTicks) {
+ if (curax.name === 'x2axis' && plot.axes.xaxis.show) {
+ _numberTicks = plot.axes.xaxis.numberTicks;
+ }
+ else if (curax.name.charAt(0) === 'y' && curax.name !== 'yaxis' && curax.name !== 'yMidAxis' && plot.axes.yaxis.show) {
+ _numberTicks = plot.axes.yaxis.numberTicks;
+ }
+ }
+
+ if (this.looseZoom && (axes[ax].renderer.constructor === $.jqplot.LinearAxisRenderer || axes[ax].renderer.constructor === $.jqplot.LogAxisRenderer )) { //} || axes[ax].renderer.constructor === $.jqplot.DateAxisRenderer)) {
+
+ ret = $.jqplot.LinearTickGenerator(newmin, newmax, curax._scalefact, _numberTicks);
+
+ // if new minimum is less than "true" minimum of axis display, adjust it
+ if (axes[ax].tickInset && ret[0] < axes[ax].min + axes[ax].tickInset * axes[ax].tickInterval) {
+ ret[0] += ret[4];
+ ret[2] -= 1;
+ }
+
+ // if new maximum is greater than "true" max of axis display, adjust it
+ if (axes[ax].tickInset && ret[1] > axes[ax].max - axes[ax].tickInset * axes[ax].tickInterval) {
+ ret[1] -= ret[4];
+ ret[2] -= 1;
+ }
+
+ // for log axes, don't fall below current minimum, this will look bad and can't have 0 in range anyway.
+ if (axes[ax].renderer.constructor === $.jqplot.LogAxisRenderer && ret[0] < axes[ax].min) {
+ // remove a tick and shift min up
+ ret[0] += ret[4];
+ ret[2] -= 1;
+ }
+
+ axes[ax].min = ret[0];
+ axes[ax].max = ret[1];
+ axes[ax]._autoFormatString = ret[3];
+ axes[ax].numberTicks = ret[2];
+ axes[ax].tickInterval = ret[4];
+ // for date axes...
+ axes[ax].daTickInterval = [ret[4]/1000, 'seconds'];
+ }
+ else {
+ axes[ax].min = newmin;
+ axes[ax].max = newmax;
+ axes[ax].tickInterval = null;
+ axes[ax].numberTicks = null;
+ // for date axes...
+ axes[ax].daTickInterval = null;
+ }
+
+ axes[ax]._ticks = [];
+ }
+ }
+
+ // if ((c.constrainZoomTo == 'x' && ax.charAt(0) == 'y' && c.autoscaleConstraint) || (c.constrainZoomTo == 'y' && ax.charAt(0) == 'x' && c.autoscaleConstraint)) {
+ // dp = datapos[ax];
+ // if (dp != null) {
+ // axes[ax].max == null;
+ // axes[ax].min = null;
+ // }
+ // }
+ }
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ plot.redraw();
+ c._zoom.isZoomed = true;
+ ctx = null;
+ }
+ plot.target.trigger('jqplotZoom', [gridpos, datapos, plot, cursor]);
+ }
+ };
+
+ $.jqplot.preInitHooks.push($.jqplot.Cursor.init);
+ $.jqplot.postDrawHooks.push($.jqplot.Cursor.postDraw);
+
+ function updateTooltip(gridpos, datapos, plot) {
+ var c = plot.plugins.cursor;
+ var s = '';
+ var addbr = false;
+ if (c.showTooltipGridPosition) {
+ s = gridpos.x+', '+gridpos.y;
+ addbr = true;
+ }
+ if (c.showTooltipUnitPosition) {
+ var g;
+ for (var i=0; i<c.tooltipAxisGroups.length; i++) {
+ g = c.tooltipAxisGroups[i];
+ if (addbr) {
+ s += '<br />';
+ }
+ if (c.useAxesFormatters) {
+ for (var j=0; j<g.length; j++) {
+ if (j) {
+ s += ', ';
+ }
+ var af = plot.axes[g[j]]._ticks[0].formatter;
+ var afstr = plot.axes[g[j]]._ticks[0].formatString;
+ s += af(afstr, datapos[g[j]]);
+ }
+ }
+ else {
+ s += $.jqplot.sprintf(c.tooltipFormatString, datapos[g[0]], datapos[g[1]]);
+ }
+ addbr = true;
+ }
+ }
+
+ if (c.showTooltipDataPosition) {
+ var series = plot.series;
+ var ret = getIntersectingPoints(plot, gridpos.x, gridpos.y);
+ var addbr = false;
+
+ for (var i = 0; i< series.length; i++) {
+ if (series[i].show) {
+ var idx = series[i].index;
+ var label = series[i].label.toString();
+ var cellid = $.inArray(idx, ret.indices);
+ var sx = undefined;
+ var sy = undefined;
+ if (cellid != -1) {
+ var data = ret.data[cellid].data;
+ if (c.useAxesFormatters) {
+ var xf = series[i]._xaxis._ticks[0].formatter;
+ var yf = series[i]._yaxis._ticks[0].formatter;
+ var xfstr = series[i]._xaxis._ticks[0].formatString;
+ var yfstr = series[i]._yaxis._ticks[0].formatString;
+ sx = xf(xfstr, data[0]);
+ sy = yf(yfstr, data[1]);
+ }
+ else {
+ sx = data[0];
+ sy = data[1];
+ }
+ if (addbr) {
+ s += '<br />';
+ }
+ s += $.jqplot.sprintf(c.tooltipFormatString, label, sx, sy);
+ addbr = true;
+ }
+ }
+ }
+
+ }
+ c._tooltipElem.html(s);
+ }
+
+ function moveLine(gridpos, plot) {
+ var c = plot.plugins.cursor;
+ var ctx = c.cursorCanvas._ctx;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ if (c.showVerticalLine) {
+ c.shapeRenderer.draw(ctx, [[gridpos.x, 0], [gridpos.x, ctx.canvas.height]]);
+ }
+ if (c.showHorizontalLine) {
+ c.shapeRenderer.draw(ctx, [[0, gridpos.y], [ctx.canvas.width, gridpos.y]]);
+ }
+ var ret = getIntersectingPoints(plot, gridpos.x, gridpos.y);
+ if (c.showCursorLegend) {
+ var cells = $(plot.targetId + ' td.jqplot-cursor-legend-label');
+ for (var i=0; i<cells.length; i++) {
+ var idx = $(cells[i]).data('seriesIndex');
+ var series = plot.series[idx];
+ var label = series.label.toString();
+ var cellid = $.inArray(idx, ret.indices);
+ var sx = undefined;
+ var sy = undefined;
+ if (cellid != -1) {
+ var data = ret.data[cellid].data;
+ if (c.useAxesFormatters) {
+ var xf = series._xaxis._ticks[0].formatter;
+ var yf = series._yaxis._ticks[0].formatter;
+ var xfstr = series._xaxis._ticks[0].formatString;
+ var yfstr = series._yaxis._ticks[0].formatString;
+ sx = xf(xfstr, data[0]);
+ sy = yf(yfstr, data[1]);
+ }
+ else {
+ sx = data[0];
+ sy = data[1];
+ }
+ }
+ if (plot.legend.escapeHtml) {
+ $(cells[i]).text($.jqplot.sprintf(c.cursorLegendFormatString, label, sx, sy));
+ }
+ else {
+ $(cells[i]).html($.jqplot.sprintf(c.cursorLegendFormatString, label, sx, sy));
+ }
+ }
+ }
+ ctx = null;
+ }
+
+ function getIntersectingPoints(plot, x, y) {
+ var ret = {indices:[], data:[]};
+ var s, i, d0, d, j, r, p;
+ var threshold;
+ var c = plot.plugins.cursor;
+ for (var i=0; i<plot.series.length; i++) {
+ s = plot.series[i];
+ r = s.renderer;
+ if (s.show) {
+ threshold = c.intersectionThreshold;
+ if (s.showMarker) {
+ threshold += s.markerRenderer.size/2;
+ }
+ for (var j=0; j<s.gridData.length; j++) {
+ p = s.gridData[j];
+ // check vertical line
+ if (c.showVerticalLine) {
+ if (Math.abs(x-p[0]) <= threshold) {
+ ret.indices.push(i);
+ ret.data.push({seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]});
+ }
+ }
+ }
+ }
+ }
+ return ret;
+ }
+
+ function moveTooltip(gridpos, plot) {
+ var c = plot.plugins.cursor;
+ var elem = c._tooltipElem;
+ switch (c.tooltipLocation) {
+ case 'nw':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top - c.tooltipOffset - elem.outerHeight(true);
+ break;
+ case 'n':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true)/2;
+ var y = gridpos.y + plot._gridPadding.top - c.tooltipOffset - elem.outerHeight(true);
+ break;
+ case 'ne':
+ var x = gridpos.x + plot._gridPadding.left + c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top - c.tooltipOffset - elem.outerHeight(true);
+ break;
+ case 'e':
+ var x = gridpos.x + plot._gridPadding.left + c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top - elem.outerHeight(true)/2;
+ break;
+ case 'se':
+ var x = gridpos.x + plot._gridPadding.left + c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top + c.tooltipOffset;
+ break;
+ case 's':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true)/2;
+ var y = gridpos.y + plot._gridPadding.top + c.tooltipOffset;
+ break;
+ case 'sw':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top + c.tooltipOffset;
+ break;
+ case 'w':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top - elem.outerHeight(true)/2;
+ break;
+ default:
+ var x = gridpos.x + plot._gridPadding.left + c.tooltipOffset;
+ var y = gridpos.y + plot._gridPadding.top + c.tooltipOffset;
+ break;
+ }
+
+ elem.css('left', x);
+ elem.css('top', y);
+ elem = null;
+ }
+
+ function positionTooltip(plot) {
+ // fake a grid for positioning
+ var grid = plot._gridPadding;
+ var c = plot.plugins.cursor;
+ var elem = c._tooltipElem;
+ switch (c.tooltipLocation) {
+ case 'nw':
+ var a = grid.left + c.tooltipOffset;
+ var b = grid.top + c.tooltipOffset;
+ elem.css('left', a);
+ elem.css('top', b);
+ break;
+ case 'n':
+ var a = (grid.left + (plot._plotDimensions.width - grid.right))/2 - elem.outerWidth(true)/2;
+ var b = grid.top + c.tooltipOffset;
+ elem.css('left', a);
+ elem.css('top', b);
+ break;
+ case 'ne':
+ var a = grid.right + c.tooltipOffset;
+ var b = grid.top + c.tooltipOffset;
+ elem.css({right:a, top:b});
+ break;
+ case 'e':
+ var a = grid.right + c.tooltipOffset;
+ var b = (grid.top + (plot._plotDimensions.height - grid.bottom))/2 - elem.outerHeight(true)/2;
+ elem.css({right:a, top:b});
+ break;
+ case 'se':
+ var a = grid.right + c.tooltipOffset;
+ var b = grid.bottom + c.tooltipOffset;
+ elem.css({right:a, bottom:b});
+ break;
+ case 's':
+ var a = (grid.left + (plot._plotDimensions.width - grid.right))/2 - elem.outerWidth(true)/2;
+ var b = grid.bottom + c.tooltipOffset;
+ elem.css({left:a, bottom:b});
+ break;
+ case 'sw':
+ var a = grid.left + c.tooltipOffset;
+ var b = grid.bottom + c.tooltipOffset;
+ elem.css({left:a, bottom:b});
+ break;
+ case 'w':
+ var a = grid.left + c.tooltipOffset;
+ var b = (grid.top + (plot._plotDimensions.height - grid.bottom))/2 - elem.outerHeight(true)/2;
+ elem.css({left:a, top:b});
+ break;
+ default: // same as 'se'
+ var a = grid.right - c.tooltipOffset;
+ var b = grid.bottom + c.tooltipOffset;
+ elem.css({right:a, bottom:b});
+ break;
+ }
+ elem = null;
+ }
+
+ function handleClick (ev, gridpos, datapos, neighbor, plot) {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ var c = plot.plugins.cursor;
+ if (c.clickReset) {
+ c.resetZoom(plot, c);
+ }
+ var sel = window.getSelection;
+ if (document.selection && document.selection.empty)
+ {
+ document.selection.empty();
+ }
+ else if (sel && !sel().isCollapsed) {
+ sel().collapse();
+ }
+ return false;
+ }
+
+ function handleDblClick (ev, gridpos, datapos, neighbor, plot) {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ var c = plot.plugins.cursor;
+ if (c.dblClickReset) {
+ c.resetZoom(plot, c);
+ }
+ var sel = window.getSelection;
+ if (document.selection && document.selection.empty)
+ {
+ document.selection.empty();
+ }
+ else if (sel && !sel().isCollapsed) {
+ sel().collapse();
+ }
+ return false;
+ }
+
+ function handleMouseLeave(ev, gridpos, datapos, neighbor, plot) {
+ var c = plot.plugins.cursor;
+ c.onGrid = false;
+ if (c.show) {
+ $(ev.target).css('cursor', c.previousCursor);
+ if (c.showTooltip && !(c._zoom.zooming && c.showTooltipOutsideZoom && !c.constrainOutsideZoom)) {
+ c._tooltipElem.empty();
+ c._tooltipElem.hide();
+ }
+ if (c.zoom) {
+ c._zoom.gridpos = gridpos;
+ c._zoom.datapos = datapos;
+ }
+ if (c.showVerticalLine || c.showHorizontalLine) {
+ var ctx = c.cursorCanvas._ctx;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ ctx = null;
+ }
+ if (c.showCursorLegend) {
+ var cells = $(plot.targetId + ' td.jqplot-cursor-legend-label');
+ for (var i=0; i<cells.length; i++) {
+ var idx = $(cells[i]).data('seriesIndex');
+ var series = plot.series[idx];
+ var label = series.label.toString();
+ if (plot.legend.escapeHtml) {
+ $(cells[i]).text($.jqplot.sprintf(c.cursorLegendFormatString, label, undefined, undefined));
+ }
+ else {
+ $(cells[i]).html($.jqplot.sprintf(c.cursorLegendFormatString, label, undefined, undefined));
+ }
+
+ }
+ }
+ }
+ }
+
+ function handleMouseEnter(ev, gridpos, datapos, neighbor, plot) {
+ var c = plot.plugins.cursor;
+ c.onGrid = true;
+ if (c.show) {
+ c.previousCursor = ev.target.style.cursor;
+ ev.target.style.cursor = c.style;
+ if (c.showTooltip) {
+ updateTooltip(gridpos, datapos, plot);
+ if (c.followMouse) {
+ moveTooltip(gridpos, plot);
+ }
+ else {
+ positionTooltip(plot);
+ }
+ c._tooltipElem.show();
+ }
+ if (c.showVerticalLine || c.showHorizontalLine) {
+ moveLine(gridpos, plot);
+ }
+ }
+
+ }
+
+ function handleMouseMove(ev, gridpos, datapos, neighbor, plot) {
+ var c = plot.plugins.cursor;
+ if (c.show) {
+ if (c.showTooltip) {
+ updateTooltip(gridpos, datapos, plot);
+ if (c.followMouse) {
+ moveTooltip(gridpos, plot);
+ }
+ }
+ if (c.showVerticalLine || c.showHorizontalLine) {
+ moveLine(gridpos, plot);
+ }
+ }
+ }
+
+ function getEventPosition(ev) {
+ var plot = ev.data.plot;
+ var go = plot.eventCanvas._elem.offset();
+ var gridPos = {x:ev.pageX - go.left, y:ev.pageY - go.top};
+ //////
+ // TO DO: handle yMidAxis
+ //////
+ var dataPos = {xaxis:null, yaxis:null, x2axis:null, y2axis:null, y3axis:null, y4axis:null, y5axis:null, y6axis:null, y7axis:null, y8axis:null, y9axis:null, yMidAxis:null};
+ var an = ['xaxis', 'yaxis', 'x2axis', 'y2axis', 'y3axis', 'y4axis', 'y5axis', 'y6axis', 'y7axis', 'y8axis', 'y9axis', 'yMidAxis'];
+ var ax = plot.axes;
+ var n, axis;
+ for (n=11; n>0; n--) {
+ axis = an[n-1];
+ if (ax[axis].show) {
+ dataPos[axis] = ax[axis].series_p2u(gridPos[axis.charAt(0)]);
+ }
+ }
+
+ return {offsets:go, gridPos:gridPos, dataPos:dataPos};
+ }
+
+ function handleZoomMove(ev) {
+ var plot = ev.data.plot;
+ var c = plot.plugins.cursor;
+ // don't do anything if not on grid.
+ if (c.show && c.zoom && c._zoom.started && !c.zoomTarget) {
+ ev.preventDefault();
+ var ctx = c.zoomCanvas._ctx;
+ var positions = getEventPosition(ev);
+ var gridpos = positions.gridPos;
+ var datapos = positions.dataPos;
+ c._zoom.gridpos = gridpos;
+ c._zoom.datapos = datapos;
+ c._zoom.zooming = true;
+ var xpos = gridpos.x;
+ var ypos = gridpos.y;
+ var height = ctx.canvas.height;
+ var width = ctx.canvas.width;
+ if (c.showTooltip && !c.onGrid && c.showTooltipOutsideZoom) {
+ updateTooltip(gridpos, datapos, plot);
+ if (c.followMouse) {
+ moveTooltip(gridpos, plot);
+ }
+ }
+ if (c.constrainZoomTo == 'x') {
+ c._zoom.end = [xpos, height];
+ }
+ else if (c.constrainZoomTo == 'y') {
+ c._zoom.end = [width, ypos];
+ }
+ else {
+ c._zoom.end = [xpos, ypos];
+ }
+ var sel = window.getSelection;
+ if (document.selection && document.selection.empty)
+ {
+ document.selection.empty();
+ }
+ else if (sel && !sel().isCollapsed) {
+ sel().collapse();
+ }
+ drawZoomBox.call(c);
+ ctx = null;
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ var c = plot.plugins.cursor;
+ if(plot.plugins.mobile){
+ $(document).one('vmouseup.jqplot_cursor', {plot:plot}, handleMouseUp);
+ } else {
+ $(document).one('mouseup.jqplot_cursor', {plot:plot}, handleMouseUp);
+ }
+ var axes = plot.axes;
+ if (document.onselectstart != undefined) {
+ c._oldHandlers.onselectstart = document.onselectstart;
+ document.onselectstart = function () { return false; };
+ }
+ if (document.ondrag != undefined) {
+ c._oldHandlers.ondrag = document.ondrag;
+ document.ondrag = function () { return false; };
+ }
+ if (document.onmousedown != undefined) {
+ c._oldHandlers.onmousedown = document.onmousedown;
+ document.onmousedown = function () { return false; };
+ }
+ if (c.zoom) {
+ if (!c.zoomProxy) {
+ var ctx = c.zoomCanvas._ctx;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ ctx = null;
+ }
+ if (c.constrainZoomTo == 'x') {
+ c._zoom.start = [gridpos.x, 0];
+ }
+ else if (c.constrainZoomTo == 'y') {
+ c._zoom.start = [0, gridpos.y];
+ }
+ else {
+ c._zoom.start = [gridpos.x, gridpos.y];
+ }
+ c._zoom.started = true;
+ for (var ax in datapos) {
+ // get zoom starting position.
+ c._zoom.axes.start[ax] = datapos[ax];
+ }
+ if(plot.plugins.mobile){
+ $(document).bind('vmousemove.jqplotCursor', {plot:plot}, handleZoomMove);
+ } else {
+ $(document).bind('mousemove.jqplotCursor', {plot:plot}, handleZoomMove);
+ }
+
+ }
+ }
+
+ function handleMouseUp(ev) {
+ var plot = ev.data.plot;
+ var c = plot.plugins.cursor;
+ if (c.zoom && c._zoom.zooming && !c.zoomTarget) {
+ var xpos = c._zoom.gridpos.x;
+ var ypos = c._zoom.gridpos.y;
+ var datapos = c._zoom.datapos;
+ var height = c.zoomCanvas._ctx.canvas.height;
+ var width = c.zoomCanvas._ctx.canvas.width;
+ var axes = plot.axes;
+
+ if (c.constrainOutsideZoom && !c.onGrid) {
+ if (xpos < 0) { xpos = 0; }
+ else if (xpos > width) { xpos = width; }
+ if (ypos < 0) { ypos = 0; }
+ else if (ypos > height) { ypos = height; }
+
+ for (var axis in datapos) {
+ if (datapos[axis]) {
+ if (axis.charAt(0) == 'x') {
+ datapos[axis] = axes[axis].series_p2u(xpos);
+ }
+ else {
+ datapos[axis] = axes[axis].series_p2u(ypos);
+ }
+ }
+ }
+ }
+
+ if (c.constrainZoomTo == 'x') {
+ ypos = height;
+ }
+ else if (c.constrainZoomTo == 'y') {
+ xpos = width;
+ }
+ c._zoom.end = [xpos, ypos];
+ c._zoom.gridpos = {x:xpos, y:ypos};
+
+ c.doZoom(c._zoom.gridpos, datapos, plot, c);
+ }
+ c._zoom.started = false;
+ c._zoom.zooming = false;
+
+ $(document).unbind('mousemove.jqplotCursor', handleZoomMove);
+
+ if (document.onselectstart != undefined && c._oldHandlers.onselectstart != null){
+ document.onselectstart = c._oldHandlers.onselectstart;
+ c._oldHandlers.onselectstart = null;
+ }
+ if (document.ondrag != undefined && c._oldHandlers.ondrag != null){
+ document.ondrag = c._oldHandlers.ondrag;
+ c._oldHandlers.ondrag = null;
+ }
+ if (document.onmousedown != undefined && c._oldHandlers.onmousedown != null){
+ document.onmousedown = c._oldHandlers.onmousedown;
+ c._oldHandlers.onmousedown = null;
+ }
+
+ }
+
+ function drawZoomBox() {
+ var start = this._zoom.start;
+ var end = this._zoom.end;
+ var ctx = this.zoomCanvas._ctx;
+ var l, t, h, w;
+ if (end[0] > start[0]) {
+ l = start[0];
+ w = end[0] - start[0];
+ }
+ else {
+ l = end[0];
+ w = start[0] - end[0];
+ }
+ if (end[1] > start[1]) {
+ t = start[1];
+ h = end[1] - start[1];
+ }
+ else {
+ t = end[1];
+ h = start[1] - end[1];
+ }
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
+ ctx.strokeStyle = '#999999';
+ ctx.lineWidth = 1.0;
+ ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
+ ctx.clearRect(l, t, w, h);
+ // IE won't show transparent fill rect, so stroke a rect also.
+ ctx.strokeRect(l,t,w,h);
+ ctx = null;
+ }
+
+ $.jqplot.CursorLegendRenderer = function(options) {
+ $.jqplot.TableLegendRenderer.call(this, options);
+ this.formatString = '%s';
+ };
+
+ $.jqplot.CursorLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
+ $.jqplot.CursorLegendRenderer.prototype.constructor = $.jqplot.CursorLegendRenderer;
+
+ // called in context of a Legend
+ $.jqplot.CursorLegendRenderer.prototype.draw = function() {
+ if (this._elem) {
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+ if (this.show) {
+ var series = this._series, s;
+ // make a table. one line label per row.
+ var elem = document.createElement('table');
+ this._elem = $(elem);
+ elem = null;
+ this._elem.addClass('jqplot-legend jqplot-cursor-legend');
+ this._elem.css('position', 'absolute');
+
+ var pad = false;
+ for (var i = 0; i< series.length; i++) {
+ s = series[i];
+ if (s.show && s.showLabel) {
+ var lt = $.jqplot.sprintf(this.formatString, s.label.toString());
+ if (lt) {
+ var color = s.color;
+ if (s._stack && !s.fill) {
+ color = '';
+ }
+ addrow.call(this, lt, color, pad, i);
+ pad = true;
+ }
+ // let plugins add more rows to legend. Used by trend line plugin.
+ for (var j=0; j<$.jqplot.addLegendRowHooks.length; j++) {
+ var item = $.jqplot.addLegendRowHooks[j].call(this, s);
+ if (item) {
+ addrow.call(this, item.label, item.color, pad);
+ pad = true;
+ }
+ }
+ }
+ }
+ series = s = null;
+ delete series;
+ delete s;
+ }
+
+ function addrow(label, color, pad, idx) {
+ var rs = (pad) ? this.rowSpacing : '0';
+ var tr = $('<tr class="jqplot-legend jqplot-cursor-legend"></tr>').appendTo(this._elem);
+ tr.data('seriesIndex', idx);
+ $('<td class="jqplot-legend jqplot-cursor-legend-swatch" style="padding-top:'+rs+';">'+
+ '<div style="border:1px solid #cccccc;padding:0.2em;">'+
+ '<div class="jqplot-cursor-legend-swatch" style="background-color:'+color+';"></div>'+
+ '</div></td>').appendTo(tr);
+ var td = $('<td class="jqplot-legend jqplot-cursor-legend-label" style="vertical-align:middle;padding-top:'+rs+';"></td>');
+ td.appendTo(tr);
+ td.data('seriesIndex', idx);
+ if (this.escapeHtml) {
+ td.text(label);
+ }
+ else {
+ td.html(label);
+ }
+ tr = null;
+ td = null;
+ }
+ return this._elem;
+ };
+
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js
new file mode 100644
index 0000000..e371a28
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js
@@ -0,0 +1,741 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.DateAxisRenderer
+ * A plugin for a jqPlot to render an axis as a series of date values.
+ * This renderer has no options beyond those supplied by the <Axis> class.
+ * It supplies its own tick formatter, so the tickOptions.formatter option
+ * should not be overridden.
+ *
+ * Thanks to Ken Synder for his enhanced Date instance methods which are
+ * included with this code <http://kendsnyder.com/sandbox/date/>.
+ *
+ * To use this renderer, include the plugin in your source
+ * > <script type="text/javascript" language="javascript" src="plugins/jqplot.dateAxisRenderer.js"></script>
+ *
+ * and supply the appropriate options to your plot
+ *
+ * > {axes:{xaxis:{renderer:$.jqplot.DateAxisRenderer}}}
+ *
+ * Dates can be passed into the axis in almost any recognizable value and
+ * will be parsed. They will be rendered on the axis in the format
+ * specified by tickOptions.formatString. e.g. tickOptions.formatString = '%Y-%m-%d'.
+ *
+ * Accecptable format codes
+ * are:
+ *
+ * > Code Result Description
+ * > == Years ==
+ * > %Y 2008 Four-digit year
+ * > %y 08 Two-digit year
+ * > == Months ==
+ * > %m 09 Two-digit month
+ * > %#m 9 One or two-digit month
+ * > %B September Full month name
+ * > %b Sep Abbreviated month name
+ * > == Days ==
+ * > %d 05 Two-digit day of month
+ * > %#d 5 One or two-digit day of month
+ * > %e 5 One or two-digit day of month
+ * > %A Sunday Full name of the day of the week
+ * > %a Sun Abbreviated name of the day of the week
+ * > %w 0 Number of the day of the week (0 = Sunday, 6 = Saturday)
+ * > %o th The ordinal suffix string following the day of the month
+ * > == Hours ==
+ * > %H 23 Hours in 24-hour format (two digits)
+ * > %#H 3 Hours in 24-hour integer format (one or two digits)
+ * > %I 11 Hours in 12-hour format (two digits)
+ * > %#I 3 Hours in 12-hour integer format (one or two digits)
+ * > %p PM AM or PM
+ * > == Minutes ==
+ * > %M 09 Minutes (two digits)
+ * > %#M 9 Minutes (one or two digits)
+ * > == Seconds ==
+ * > %S 02 Seconds (two digits)
+ * > %#S 2 Seconds (one or two digits)
+ * > %s 1206567625723 Unix timestamp (Seconds past 1970-01-01 00:00:00)
+ * > == Milliseconds ==
+ * > %N 008 Milliseconds (three digits)
+ * > %#N 8 Milliseconds (one to three digits)
+ * > == Timezone ==
+ * > %O 360 difference in minutes between local time and GMT
+ * > %Z Mountain Standard Time Name of timezone as reported by browser
+ * > %G -06:00 Hours and minutes between GMT
+ * > == Shortcuts ==
+ * > %F 2008-03-26 %Y-%m-%d
+ * > %T 05:06:30 %H:%M:%S
+ * > %X 05:06:30 %H:%M:%S
+ * > %x 03/26/08 %m/%d/%y
+ * > %D 03/26/08 %m/%d/%y
+ * > %#c Wed Mar 26 15:31:00 2008 %a %b %e %H:%M:%S %Y
+ * > %v 3-Sep-2008 %e-%b-%Y
+ * > %R 15:31 %H:%M
+ * > %r 3:31:00 PM %I:%M:%S %p
+ * > == Characters ==
+ * > %n \n Newline
+ * > %t \t Tab
+ * > %% % Percent Symbol
+ */
+ $.jqplot.DateAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ this.date = new $.jsDate();
+ };
+
+ var second = 1000;
+ var minute = 60 * second;
+ var hour = 60 * minute;
+ var day = 24 * hour;
+ var week = 7 * day;
+
+ // these are less definitive
+ var month = 30.4368499 * day;
+ var year = 365.242199 * day;
+
+ var daysInMonths = [31,28,31,30,31,30,31,30,31,30,31,30];
+ // array of consistent nice intervals. Longer intervals
+ // will depend on days in month, days in year, etc.
+ var niceFormatStrings = ['%M:%S.%#N', '%M:%S.%#N', '%M:%S.%#N', '%M:%S', '%M:%S', '%M:%S', '%M:%S', '%H:%M:%S', '%H:%M:%S', '%H:%M', '%H:%M', '%H:%M', '%H:%M', '%H:%M', '%H:%M', '%a %H:%M', '%a %H:%M', '%b %e %H:%M', '%b %e %H:%M', '%b %e %H:%M', '%b %e %H:%M', '%v', '%v', '%v', '%v', '%v', '%v', '%v'];
+ var niceIntervals = [0.1*second, 0.2*second, 0.5*second, second, 2*second, 5*second, 10*second, 15*second, 30*second, minute, 2*minute, 5*minute, 10*minute, 15*minute, 30*minute, hour, 2*hour, 4*hour, 6*hour, 8*hour, 12*hour, day, 2*day, 3*day, 4*day, 5*day, week, 2*week];
+
+ var niceMonthlyIntervals = [];
+
+ function bestDateInterval(min, max, titarget) {
+ // iterate through niceIntervals to find one closest to titarget
+ var badness = Number.MAX_VALUE;
+ var temp, bestTi, bestfmt;
+ for (var i=0, l=niceIntervals.length; i < l; i++) {
+ temp = Math.abs(titarget - niceIntervals[i]);
+ if (temp < badness) {
+ badness = temp;
+ bestTi = niceIntervals[i];
+ bestfmt = niceFormatStrings[i];
+ }
+ }
+
+ return [bestTi, bestfmt];
+ }
+
+ $.jqplot.DateAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.DateAxisRenderer.prototype.constructor = $.jqplot.DateAxisRenderer;
+
+ $.jqplot.DateTickFormatter = function(format, val) {
+ if (!format) {
+ format = '%Y/%m/%d';
+ }
+ return $.jsDate.strftime(val, format);
+ };
+
+ $.jqplot.DateAxisRenderer.prototype.init = function(options){
+ // prop: tickRenderer
+ // A class of a rendering engine for creating the ticks labels displayed on the plot,
+ // See <$.jqplot.AxisTickRenderer>.
+ // this.tickRenderer = $.jqplot.AxisTickRenderer;
+ // this.labelRenderer = $.jqplot.AxisLabelRenderer;
+ this.tickOptions.formatter = $.jqplot.DateTickFormatter;
+ // prop: tickInset
+ // Controls the amount to inset the first and last ticks from
+ // the edges of the grid, in multiples of the tick interval.
+ // 0 is no inset, 0.5 is one half a tick interval, 1 is a full
+ // tick interval, etc.
+ this.tickInset = 0;
+ // prop: drawBaseline
+ // True to draw the axis baseline.
+ this.drawBaseline = true;
+ // prop: baselineWidth
+ // width of the baseline in pixels.
+ this.baselineWidth = null;
+ // prop: baselineColor
+ // CSS color spec for the baseline.
+ this.baselineColor = null;
+ this.daTickInterval = null;
+ this._daTickInterval = null;
+
+ $.extend(true, this, options);
+
+ var db = this._dataBounds,
+ stats,
+ sum,
+ s,
+ d,
+ pd,
+ sd,
+ intv;
+
+ // Go through all the series attached to this axis and find
+ // the min/max bounds for this axis.
+ for (var i=0; i<this._series.length; i++) {
+ stats = {intervals:[], frequencies:{}, sortedIntervals:[], min:null, max:null, mean:null};
+ sum = 0;
+ s = this._series[i];
+ d = s.data;
+ pd = s._plotData;
+ sd = s._stackData;
+ intv = 0;
+
+ for (var j=0; j<d.length; j++) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ d[j][0] = new $.jsDate(d[j][0]).getTime();
+ pd[j][0] = new $.jsDate(d[j][0]).getTime();
+ sd[j][0] = new $.jsDate(d[j][0]).getTime();
+ if ((d[j][0] != null && d[j][0] < db.min) || db.min == null) {
+ db.min = d[j][0];
+ }
+ if ((d[j][0] != null && d[j][0] > db.max) || db.max == null) {
+ db.max = d[j][0];
+ }
+ if (j>0) {
+ intv = Math.abs(d[j][0] - d[j-1][0]);
+ stats.intervals.push(intv);
+ if (stats.frequencies.hasOwnProperty(intv)) {
+ stats.frequencies[intv] += 1;
+ }
+ else {
+ stats.frequencies[intv] = 1;
+ }
+ }
+ sum += intv;
+
+ }
+ else {
+ d[j][1] = new $.jsDate(d[j][1]).getTime();
+ pd[j][1] = new $.jsDate(d[j][1]).getTime();
+ sd[j][1] = new $.jsDate(d[j][1]).getTime();
+ if ((d[j][1] != null && d[j][1] < db.min) || db.min == null) {
+ db.min = d[j][1];
+ }
+ if ((d[j][1] != null && d[j][1] > db.max) || db.max == null) {
+ db.max = d[j][1];
+ }
+ if (j>0) {
+ intv = Math.abs(d[j][1] - d[j-1][1]);
+ stats.intervals.push(intv);
+ if (stats.frequencies.hasOwnProperty(intv)) {
+ stats.frequencies[intv] += 1;
+ }
+ else {
+ stats.frequencies[intv] = 1;
+ }
+ }
+ }
+ sum += intv;
+ }
+
+ if (s.renderer.bands) {
+ if (s.renderer.bands.hiData.length) {
+ var bd = s.renderer.bands.hiData;
+ for (var j=0, l=bd.length; j < l; j++) {
+ if (this.name === 'xaxis' || this.name === 'x2axis') {
+ bd[j][0] = new $.jsDate(bd[j][0]).getTime();
+ if ((bd[j][0] != null && bd[j][0] > db.max) || db.max == null) {
+ db.max = bd[j][0];
+ }
+ }
+ else {
+ bd[j][1] = new $.jsDate(bd[j][1]).getTime();
+ if ((bd[j][1] != null && bd[j][1] > db.max) || db.max == null) {
+ db.max = bd[j][1];
+ }
+ }
+ }
+ }
+ if (s.renderer.bands.lowData.length) {
+ var bd = s.renderer.bands.lowData;
+ for (var j=0, l=bd.length; j < l; j++) {
+ if (this.name === 'xaxis' || this.name === 'x2axis') {
+ bd[j][0] = new $.jsDate(bd[j][0]).getTime();
+ if ((bd[j][0] != null && bd[j][0] < db.min) || db.min == null) {
+ db.min = bd[j][0];
+ }
+ }
+ else {
+ bd[j][1] = new $.jsDate(bd[j][1]).getTime();
+ if ((bd[j][1] != null && bd[j][1] < db.min) || db.min == null) {
+ db.min = bd[j][1];
+ }
+ }
+ }
+ }
+ }
+
+ var tempf = 0,
+ tempn=0;
+ for (var n in stats.frequencies) {
+ stats.sortedIntervals.push({interval:n, frequency:stats.frequencies[n]});
+ }
+ stats.sortedIntervals.sort(function(a, b){
+ return b.frequency - a.frequency;
+ });
+
+ stats.min = $.jqplot.arrayMin(stats.intervals);
+ stats.max = $.jqplot.arrayMax(stats.intervals);
+ stats.mean = sum/d.length;
+ this._intervalStats.push(stats);
+ stats = sum = s = d = pd = sd = null;
+ }
+ db = null;
+
+ };
+
+ // called with scope of an axis
+ $.jqplot.DateAxisRenderer.prototype.reset = function() {
+ this.min = this._options.min;
+ this.max = this._options.max;
+ this.tickInterval = this._options.tickInterval;
+ this.numberTicks = this._options.numberTicks;
+ this._autoFormatString = '';
+ if (this._overrideFormatString && this.tickOptions && this.tickOptions.formatString) {
+ this.tickOptions.formatString = '';
+ }
+ this.daTickInterval = this._daTickInterval;
+ // this._ticks = this.__ticks;
+ };
+
+ $.jqplot.DateAxisRenderer.prototype.createTicks = function(plot) {
+ // we're are operating on an axis here
+ var ticks = this._ticks;
+ var userTicks = this.ticks;
+ var name = this.name;
+ // databounds were set on axis initialization.
+ var db = this._dataBounds;
+ var iv = this._intervalStats;
+ var dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
+ var interval;
+ var min, max;
+ var pos1, pos2;
+ var tt, i;
+ var threshold = 30;
+ var insetMult = 1;
+ var daTickInterval = null;
+
+ // if user specified a tick interval, convert to usable.
+ if (this.tickInterval != null)
+ {
+ // if interval is a number or can be converted to one, use it.
+ // Assume it is in SECONDS!!!
+ if (Number(this.tickInterval)) {
+ daTickInterval = [Number(this.tickInterval), 'seconds'];
+ }
+ // else, parse out something we can build from.
+ else if (typeof this.tickInterval == "string") {
+ var parts = this.tickInterval.split(' ');
+ if (parts.length == 1) {
+ daTickInterval = [1, parts[0]];
+ }
+ else if (parts.length == 2) {
+ daTickInterval = [parts[0], parts[1]];
+ }
+ }
+ }
+
+ var tickInterval = this.tickInterval;
+
+ // if we already have ticks, use them.
+ // ticks must be in order of increasing value.
+
+ min = new $.jsDate((this.min != null) ? this.min : db.min).getTime();
+ max = new $.jsDate((this.max != null) ? this.max : db.max).getTime();
+
+ // see if we're zooming. if we are, don't use the min and max we're given,
+ // but compute some nice ones. They will be reset later.
+
+ var cursor = plot.plugins.cursor;
+
+ if (cursor && cursor._zoom && cursor._zoom.zooming) {
+ this.min = null;
+ this.max = null;
+ }
+
+ var range = max - min;
+
+ if (this.tickOptions == null || !this.tickOptions.formatString) {
+ this._overrideFormatString = true;
+ }
+
+ if (userTicks.length) {
+ // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+ for (i=0; i<userTicks.length; i++){
+ var ut = userTicks[i];
+ var t = new this.tickRenderer(this.tickOptions);
+ if (ut.constructor == Array) {
+ t.value = new $.jsDate(ut[0]).getTime();
+ t.label = ut[1];
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(t.value, this.name);
+ this._ticks.push(t);
+ }
+
+ else {
+ t.value = new $.jsDate(ut).getTime();
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(t.value, this.name);
+ this._ticks.push(t);
+ }
+ }
+ this.numberTicks = userTicks.length;
+ this.min = this._ticks[0].value;
+ this.max = this._ticks[this.numberTicks-1].value;
+ this.daTickInterval = [(this.max - this.min) / (this.numberTicks - 1)/1000, 'seconds'];
+ }
+
+ ////////
+ // We don't have any ticks yet, let's make some!
+ ////////
+
+ // special case when there is only one point, make three tick marks to center the point
+ else if (this.min == null && this.max == null && db.min == db.max)
+ {
+ var onePointOpts = $.extend(true, {}, this.tickOptions, {name: this.name, value: null});
+ var delta = 300000;
+ this.min = db.min - delta;
+ this.max = db.max + delta;
+ this.numberTicks = 3;
+
+ for(var i=this.min;i<=this.max;i+= delta)
+ {
+ onePointOpts.value = i;
+
+ var t = new this.tickRenderer(onePointOpts);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+
+ t.showLabel = false;
+ t.showMark = false;
+
+ this._ticks.push(t);
+ }
+
+ if(this.showTicks) {
+ this._ticks[1].showLabel = true;
+ }
+ if(this.showTickMarks) {
+ this._ticks[1].showTickMarks = true;
+ }
+ }
+ // if user specified min and max are null, we set those to make best ticks.
+ else if (this.min == null && this.max == null) {
+
+ var opts = $.extend(true, {}, this.tickOptions, {name: this.name, value: null});
+
+ // want to find a nice interval
+ var nttarget,
+ titarget;
+
+ // if no tickInterval or numberTicks options specified, make a good guess.
+ if (!this.tickInterval && !this.numberTicks) {
+ var tdim = Math.max(dim, threshold+1);
+ // how many ticks to put on the axis?
+ // date labels tend to be long. If ticks not rotated,
+ // don't use too many and have a high spacing factor.
+ // If we are rotating ticks, use a lower factor.
+ var spacingFactor = 115;
+ if (this.tickRenderer === $.jqplot.CanvasAxisTickRenderer && this.tickOptions.angle) {
+ spacingFactor = 115 - 40 * Math.abs(Math.sin(this.tickOptions.angle/180*Math.PI));
+ }
+
+ nttarget = Math.ceil((tdim-threshold)/spacingFactor + 1);
+ titarget = (max - min) / (nttarget - 1);
+ }
+
+ // If tickInterval is specified, we'll try to honor it.
+ // Not guaranteed to get this interval, but we'll get as close as
+ // we can.
+ // tickInterval will be used before numberTicks, that is if
+ // both are specified, numberTicks will be ignored.
+ else if (this.tickInterval) {
+ titarget = new $.jsDate(0).add(daTickInterval[0], daTickInterval[1]).getTime();
+ }
+
+ // if numberTicks specified, try to honor it.
+ // Not guaranteed, but will try to get close.
+ else if (this.numberTicks) {
+ nttarget = this.numberTicks;
+ titarget = (max - min) / (nttarget - 1);
+ }
+
+ // If we can use an interval of 2 weeks or less, pick best one
+ if (titarget <= 19*day) {
+ var ret = bestDateInterval(min, max, titarget);
+ var tempti = ret[0];
+ this._autoFormatString = ret[1];
+
+ min = new $.jsDate(min);
+ min = Math.floor((min.getTime() - min.getUtcOffset())/tempti) * tempti + min.getUtcOffset();
+
+ nttarget = Math.ceil((max - min) / tempti) + 1;
+ this.min = min;
+ this.max = min + (nttarget - 1) * tempti;
+
+ // if max is less than max, add an interval
+ if (this.max < max) {
+ this.max += tempti;
+ nttarget += 1;
+ }
+ this.tickInterval = tempti;
+ this.numberTicks = nttarget;
+
+ for (var i=0; i<nttarget; i++) {
+ opts.value = this.min + i * tempti;
+ t = new this.tickRenderer(opts);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ this._ticks.push(t);
+ }
+
+ insetMult = this.tickInterval;
+ }
+
+ // should we use a monthly interval?
+ else if (titarget <= 9 * month) {
+
+ this._autoFormatString = '%v';
+
+ // how many months in an interval?
+ var intv = Math.round(titarget/month);
+ if (intv < 1) {
+ intv = 1;
+ }
+ else if (intv > 6) {
+ intv = 6;
+ }
+
+ // figure out the starting month and ending month.
+ var mstart = new $.jsDate(min).setDate(1).setHours(0,0,0,0);
+
+ // See if max ends exactly on a month
+ var tempmend = new $.jsDate(max);
+ var mend = new $.jsDate(max).setDate(1).setHours(0,0,0,0);
+
+ if (tempmend.getTime() !== mend.getTime()) {
+ mend = mend.add(1, 'month');
+ }
+
+ var nmonths = mend.diff(mstart, 'month');
+
+ nttarget = Math.ceil(nmonths/intv) + 1;
+
+ this.min = mstart.getTime();
+ this.max = mstart.clone().add((nttarget - 1) * intv, 'month').getTime();
+ this.numberTicks = nttarget;
+
+ for (var i=0; i<nttarget; i++) {
+ if (i === 0) {
+ opts.value = mstart.getTime();
+ }
+ else {
+ opts.value = mstart.add(intv, 'month').getTime();
+ }
+ t = new this.tickRenderer(opts);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ this._ticks.push(t);
+ }
+
+ insetMult = intv * month;
+ }
+
+ // use yearly intervals
+ else {
+
+ this._autoFormatString = '%v';
+
+ // how many years in an interval?
+ var intv = Math.round(titarget/year);
+ if (intv < 1) {
+ intv = 1;
+ }
+
+ // figure out the starting and ending years.
+ var mstart = new $.jsDate(min).setMonth(0, 1).setHours(0,0,0,0);
+ var mend = new $.jsDate(max).add(1, 'year').setMonth(0, 1).setHours(0,0,0,0);
+
+ var nyears = mend.diff(mstart, 'year');
+
+ nttarget = Math.ceil(nyears/intv) + 1;
+
+ this.min = mstart.getTime();
+ this.max = mstart.clone().add((nttarget - 1) * intv, 'year').getTime();
+ this.numberTicks = nttarget;
+
+ for (var i=0; i<nttarget; i++) {
+ if (i === 0) {
+ opts.value = mstart.getTime();
+ }
+ else {
+ opts.value = mstart.add(intv, 'year').getTime();
+ }
+ t = new this.tickRenderer(opts);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ this._ticks.push(t);
+ }
+
+ insetMult = intv * year;
+ }
+ }
+
+ ////////
+ // Some option(s) specified, work around that.
+ ////////
+
+ else {
+ if (name == 'xaxis' || name == 'x2axis') {
+ dim = this._plotDimensions.width;
+ }
+ else {
+ dim = this._plotDimensions.height;
+ }
+
+ // if min, max and number of ticks specified, user can't specify interval.
+ if (this.min != null && this.max != null && this.numberTicks != null) {
+ this.tickInterval = null;
+ }
+
+ if (this.tickInterval != null && daTickInterval != null) {
+ this.daTickInterval = daTickInterval;
+ }
+
+ // if min and max are same, space them out a bit
+ if (min == max) {
+ var adj = 24*60*60*500; // 1/2 day
+ min -= adj;
+ max += adj;
+ }
+
+ range = max - min;
+
+ var optNumTicks = 2 + parseInt(Math.max(0, dim-100)/100, 10);
+
+
+ var rmin, rmax;
+
+ rmin = (this.min != null) ? new $.jsDate(this.min).getTime() : min - range/2*(this.padMin - 1);
+ rmax = (this.max != null) ? new $.jsDate(this.max).getTime() : max + range/2*(this.padMax - 1);
+ this.min = rmin;
+ this.max = rmax;
+ range = this.max - this.min;
+
+ if (this.numberTicks == null){
+ // if tickInterval is specified by user, we will ignore computed maximum.
+ // max will be equal or greater to fit even # of ticks.
+ if (this.daTickInterval != null) {
+ var nc = new $.jsDate(this.max).diff(this.min, this.daTickInterval[1], true);
+ this.numberTicks = Math.ceil(nc/this.daTickInterval[0]) +1;
+ // this.max = new $.jsDate(this.min).add(this.numberTicks-1, this.daTickInterval[1]).getTime();
+ this.max = new $.jsDate(this.min).add((this.numberTicks-1) * this.daTickInterval[0], this.daTickInterval[1]).getTime();
+ }
+ else if (dim > 200) {
+ this.numberTicks = parseInt(3+(dim-200)/100, 10);
+ }
+ else {
+ this.numberTicks = 2;
+ }
+ }
+
+ insetMult = range / (this.numberTicks-1)/1000;
+
+ if (this.daTickInterval == null) {
+ this.daTickInterval = [insetMult, 'seconds'];
+ }
+
+
+ for (var i=0; i<this.numberTicks; i++){
+ var min = new $.jsDate(this.min);
+ tt = min.add(i*this.daTickInterval[0], this.daTickInterval[1]).getTime();
+ var t = new this.tickRenderer(this.tickOptions);
+ // var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+ }
+ }
+
+ if (this.tickInset) {
+ this.min = this.min - this.tickInset * insetMult;
+ this.max = this.max + this.tickInset * insetMult;
+ }
+
+ if (this._daTickInterval == null) {
+ this._daTickInterval = this.daTickInterval;
+ }
+
+ ticks = null;
+ };
+
+})(jQuery);
+
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.donutRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.donutRenderer.js
new file mode 100644
index 0000000..9684812
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.donutRenderer.js
@@ -0,0 +1,805 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.DonutRenderer
+ * Plugin renderer to draw a donut chart.
+ * x values, if present, will be used as slice labels.
+ * y values give slice size.
+ *
+ * To use this renderer, you need to include the
+ * donut renderer plugin, for example:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.donutRenderer.js"></script>
+ *
+ * Properties described here are passed into the $.jqplot function
+ * as options on the series renderer. For example:
+ *
+ * > plot2 = $.jqplot('chart2', [s1, s2], {
+ * > seriesDefaults: {
+ * > renderer:$.jqplot.DonutRenderer,
+ * > rendererOptions:{
+ * > sliceMargin: 2,
+ * > innerDiameter: 110,
+ * > startAngle: -90
+ * > }
+ * > }
+ * > });
+ *
+ * A donut plot will trigger events on the plot target
+ * according to user interaction. All events return the event object,
+ * the series index, the point (slice) index, and the point data for
+ * the appropriate slice.
+ *
+ * 'jqplotDataMouseOver' - triggered when user mouseing over a slice.
+ * 'jqplotDataHighlight' - triggered the first time user mouses over a slice,
+ * if highlighting is enabled.
+ * 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
+ * a highlighted slice.
+ * 'jqplotDataClick' - triggered when the user clicks on a slice.
+ * 'jqplotDataRightClick' - tiggered when the user right clicks on a slice if
+ * the "captureRightClick" option is set to true on the plot.
+ */
+ $.jqplot.DonutRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.DonutRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.DonutRenderer.prototype.constructor = $.jqplot.DonutRenderer;
+
+ // called with scope of a series
+ $.jqplot.DonutRenderer.prototype.init = function(options, plot) {
+ // Group: Properties
+ //
+ // prop: diameter
+ // Outer diameter of the donut, auto computed by default
+ this.diameter = null;
+ // prop: innerDiameter
+ // Inner diameter of the donut, auto calculated by default.
+ // If specified will override thickness value.
+ this.innerDiameter = null;
+ // prop: thickness
+ // thickness of the donut, auto computed by default
+ // Overridden by if innerDiameter is specified.
+ this.thickness = null;
+ // prop: padding
+ // padding between the donut and plot edges, legend, etc.
+ this.padding = 20;
+ // prop: sliceMargin
+ // angular spacing between donut slices in degrees.
+ this.sliceMargin = 0;
+ // prop: ringMargin
+ // pixel distance between rings, or multiple series in a donut plot.
+ // null will compute ringMargin based on sliceMargin.
+ this.ringMargin = null;
+ // prop: fill
+ // true or false, whether to fil the slices.
+ this.fill = true;
+ // prop: shadowOffset
+ // offset of the shadow from the slice and offset of
+ // each succesive stroke of the shadow from the last.
+ this.shadowOffset = 2;
+ // prop: shadowAlpha
+ // transparency of the shadow (0 = transparent, 1 = opaque)
+ this.shadowAlpha = 0.07;
+ // prop: shadowDepth
+ // number of strokes to apply to the shadow,
+ // each stroke offset shadowOffset from the last.
+ this.shadowDepth = 5;
+ // prop: highlightMouseOver
+ // True to highlight slice when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on a slice.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over a slice.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColors
+ // an array of colors to use when highlighting a slice.
+ this.highlightColors = [];
+ // prop: dataLabels
+ // Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
+ // Defaults to percentage of each pie slice.
+ this.dataLabels = 'percent';
+ // prop: showDataLabels
+ // true to show data labels on slices.
+ this.showDataLabels = false;
+ // prop: dataLabelFormatString
+ // Format string for data labels. If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
+ this.dataLabelFormatString = null;
+ // prop: dataLabelThreshold
+ // Threshhold in percentage (0 - 100) of pie area, below which no label will be displayed.
+ // This applies to all label types, not just to percentage labels.
+ this.dataLabelThreshold = 3;
+ // prop: dataLabelPositionFactor
+ // A Multiplier (0-1) of the pie radius which controls position of label on slice.
+ // Increasing will slide label toward edge of pie, decreasing will slide label toward center of pie.
+ this.dataLabelPositionFactor = 0.4;
+ // prop: dataLabelNudge
+ // Number of pixels to slide the label away from (+) or toward (-) the center of the pie.
+ this.dataLabelNudge = 0;
+ // prop: startAngle
+ // Angle to start drawing donut in degrees.
+ // According to orientation of canvas coordinate system:
+ // 0 = on the positive x axis
+ // -90 = on the positive y axis.
+ // 90 = on the negaive y axis.
+ // 180 or - 180 = on the negative x axis.
+ this.startAngle = 0;
+ this.tickRenderer = $.jqplot.DonutTickRenderer;
+ // Used as check for conditions where donut shouldn't be drawn.
+ this._drawData = true;
+ this._type = 'donut';
+
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (options.highlightMouseDown && options.highlightMouseOver == null) {
+ options.highlightMouseOver = false;
+ }
+
+ $.extend(true, this, options);
+ if (this.diameter != null) {
+ this.diameter = this.diameter - this.sliceMargin;
+ }
+ this._diameter = null;
+ this._innerDiameter = null;
+ this._radius = null;
+ this._innerRadius = null;
+ this._thickness = null;
+ // references to the previous series in the plot to properly calculate diameters
+ // and thicknesses of nested rings.
+ this._previousSeries = [];
+ this._numberSeries = 1;
+ // array of [start,end] angles arrays, one for each slice. In radians.
+ this._sliceAngles = [];
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+
+ // set highlight colors if none provided
+ if (this.highlightColors.length == 0) {
+ for (var i=0; i<this.seriesColors.length; i++){
+ var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
+ newrgb[j] = parseInt(newrgb[j], 10);
+ }
+ this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
+ }
+ }
+
+ plot.postParseOptionsHooks.addOnce(postParseOptions);
+ plot.postInitHooks.addOnce(postInit);
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+ plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
+ plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
+ plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
+ plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
+ plot.postDrawHooks.addOnce(postPlotDraw);
+
+
+ };
+
+ $.jqplot.DonutRenderer.prototype.setGridData = function(plot) {
+ // set gridData property. This will hold angle in radians of each data point.
+ var stack = [];
+ var td = [];
+ var sa = this.startAngle/180*Math.PI;
+ var tot = 0;
+ // don't know if we have any valid data yet, so set plot to not draw.
+ this._drawData = false;
+ for (var i=0; i<this.data.length; i++){
+ if (this.data[i][1] != 0) {
+ // we have data, O.K. to draw.
+ this._drawData = true;
+ }
+ stack.push(this.data[i][1]);
+ td.push([this.data[i][0]]);
+ if (i>0) {
+ stack[i] += stack[i-1];
+ }
+ tot += this.data[i][1];
+ }
+ var fact = Math.PI*2/stack[stack.length - 1];
+
+ for (var i=0; i<stack.length; i++) {
+ td[i][1] = stack[i] * fact;
+ td[i][2] = this.data[i][1]/tot;
+ }
+ this.gridData = td;
+ };
+
+ $.jqplot.DonutRenderer.prototype.makeGridData = function(data, plot) {
+ var stack = [];
+ var td = [];
+ var tot = 0;
+ var sa = this.startAngle/180*Math.PI;
+ // don't know if we have any valid data yet, so set plot to not draw.
+ this._drawData = false;
+ for (var i=0; i<data.length; i++){
+ if (this.data[i][1] != 0) {
+ // we have data, O.K. to draw.
+ this._drawData = true;
+ }
+ stack.push(data[i][1]);
+ td.push([data[i][0]]);
+ if (i>0) {
+ stack[i] += stack[i-1];
+ }
+ tot += data[i][1];
+ }
+ var fact = Math.PI*2/stack[stack.length - 1];
+
+ for (var i=0; i<stack.length; i++) {
+ td[i][1] = stack[i] * fact;
+ td[i][2] = data[i][1]/tot;
+ }
+ return td;
+ };
+
+ $.jqplot.DonutRenderer.prototype.drawSlice = function (ctx, ang1, ang2, color, isShadow) {
+ var r = this._diameter / 2;
+ var ri = r - this._thickness;
+ var fill = this.fill;
+ // var lineWidth = this.lineWidth;
+ ctx.save();
+ ctx.translate(this._center[0], this._center[1]);
+ // ctx.translate(this.sliceMargin*Math.cos((ang1+ang2)/2), this.sliceMargin*Math.sin((ang1+ang2)/2));
+
+ if (isShadow) {
+ for (var i=0; i<this.shadowDepth; i++) {
+ ctx.save();
+ ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
+ doDraw();
+ }
+ }
+
+ else {
+ doDraw();
+ }
+
+ function doDraw () {
+ // Fix for IE and Chrome that can't seem to draw circles correctly.
+ // ang2 should always be <= 2 pi since that is the way the data is converted.
+ if (ang2 > 6.282 + this.startAngle) {
+ ang2 = 6.282 + this.startAngle;
+ if (ang1 > ang2) {
+ ang1 = 6.281 + this.startAngle;
+ }
+ }
+ // Fix for IE, where it can't seem to handle 0 degree angles. Also avoids
+ // ugly line on unfilled donuts.
+ if (ang1 >= ang2) {
+ return;
+ }
+ ctx.beginPath();
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ // ctx.lineWidth = lineWidth;
+ ctx.arc(0, 0, r, ang1, ang2, false);
+ ctx.lineTo(ri*Math.cos(ang2), ri*Math.sin(ang2));
+ ctx.arc(0,0, ri, ang2, ang1, true);
+ ctx.closePath();
+ if (fill) {
+ ctx.fill();
+ }
+ else {
+ ctx.stroke();
+ }
+ }
+
+ if (isShadow) {
+ for (var i=0; i<this.shadowDepth; i++) {
+ ctx.restore();
+ }
+ }
+
+ ctx.restore();
+ };
+
+ // called with scope of series
+ $.jqplot.DonutRenderer.prototype.draw = function (ctx, gd, options, plot) {
+ var i;
+ var opts = (options != undefined) ? options : {};
+ // offset and direction of offset due to legend placement
+ var offx = 0;
+ var offy = 0;
+ var trans = 1;
+ // var colorGenerator = new this.colorGenerator(this.seriesColors);
+ if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
+ var li = options.legendInfo;
+ switch (li.location) {
+ case 'nw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'w':
+ offx = li.width + li.xoffset;
+ break;
+ case 'sw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'ne':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'e':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'se':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'n':
+ offy = li.height + li.yoffset;
+ break;
+ case 's':
+ offy = li.height + li.yoffset;
+ trans = -1;
+ break;
+ default:
+ break;
+ }
+ }
+
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var cw = ctx.canvas.width;
+ var ch = ctx.canvas.height;
+ var w = cw - offx - 2 * this.padding;
+ var h = ch - offy - 2 * this.padding;
+ var mindim = Math.min(w,h);
+ var d = mindim;
+ var ringmargin = (this.ringMargin == null) ? this.sliceMargin * 2.0 : this.ringMargin;
+
+ for (var i=0; i<this._previousSeries.length; i++) {
+ d -= 2.0 * this._previousSeries[i]._thickness + 2.0 * ringmargin;
+ }
+ this._diameter = this.diameter || d;
+ if (this.innerDiameter != null) {
+ var od = (this._numberSeries > 1 && this.index > 0) ? this._previousSeries[0]._diameter : this._diameter;
+ this._thickness = this.thickness || (od - this.innerDiameter - 2.0*ringmargin*this._numberSeries) / this._numberSeries/2.0;
+ }
+ else {
+ this._thickness = this.thickness || mindim / 2 / (this._numberSeries + 1) * 0.85;
+ }
+
+ var r = this._radius = this._diameter/2;
+ this._innerRadius = this._radius - this._thickness;
+ var sa = this.startAngle / 180 * Math.PI;
+ this._center = [(cw - trans * offx)/2 + trans * offx, (ch - trans*offy)/2 + trans * offy];
+
+ if (this.shadow) {
+ var shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
+ for (var i=0; i<gd.length; i++) {
+ var ang1 = (i == 0) ? sa : gd[i-1][1] + sa;
+ // Adjust ang1 and ang2 for sliceMargin
+ ang1 += this.sliceMargin/180*Math.PI;
+ this.renderer.drawSlice.call (this, ctx, ang1, gd[i][1]+sa, shadowColor, true);
+ }
+
+ }
+ for (var i=0; i<gd.length; i++) {
+ var ang1 = (i == 0) ? sa : gd[i-1][1] + sa;
+ // Adjust ang1 and ang2 for sliceMargin
+ ang1 += this.sliceMargin/180*Math.PI;
+ var ang2 = gd[i][1] + sa;
+ this._sliceAngles.push([ang1, ang2]);
+ this.renderer.drawSlice.call (this, ctx, ang1, ang2, this.seriesColors[i], false);
+
+ if (this.showDataLabels && gd[i][2]*100 >= this.dataLabelThreshold) {
+ var fstr, avgang = (ang1+ang2)/2, label;
+
+ if (this.dataLabels == 'label') {
+ fstr = this.dataLabelFormatString || '%s';
+ label = $.jqplot.sprintf(fstr, gd[i][0]);
+ }
+ else if (this.dataLabels == 'value') {
+ fstr = this.dataLabelFormatString || '%d';
+ label = $.jqplot.sprintf(fstr, this.data[i][1]);
+ }
+ else if (this.dataLabels == 'percent') {
+ fstr = this.dataLabelFormatString || '%d%%';
+ label = $.jqplot.sprintf(fstr, gd[i][2]*100);
+ }
+ else if (this.dataLabels.constructor == Array) {
+ fstr = this.dataLabelFormatString || '%s';
+ label = $.jqplot.sprintf(fstr, this.dataLabels[i]);
+ }
+
+ var fact = this._innerRadius + this._thickness * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
+
+ var x = this._center[0] + Math.cos(avgang) * fact + this.canvas._offsets.left;
+ var y = this._center[1] + Math.sin(avgang) * fact + this.canvas._offsets.top;
+
+ var labelelem = $('<span class="jqplot-donut-series jqplot-data-label" style="position:absolute;">' + label + '</span>').insertBefore(plot.eventCanvas._elem);
+ x -= labelelem.width()/2;
+ y -= labelelem.height()/2;
+ x = Math.round(x);
+ y = Math.round(y);
+ labelelem.css({left: x, top: y});
+ }
+ }
+
+ };
+
+ $.jqplot.DonutAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.DonutAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.DonutAxisRenderer.prototype.constructor = $.jqplot.DonutAxisRenderer;
+
+
+ // There are no traditional axes on a donut chart. We just need to provide
+ // dummy objects with properties so the plot will render.
+ // called with scope of axis object.
+ $.jqplot.DonutAxisRenderer.prototype.init = function(options){
+ //
+ this.tickRenderer = $.jqplot.DonutTickRenderer;
+ $.extend(true, this, options);
+ // I don't think I'm going to need _dataBounds here.
+ // have to go Axis scaling in a way to fit chart onto plot area
+ // and provide u2p and p2u functionality for mouse cursor, etc.
+ // for convienence set _dataBounds to 0 and 100 and
+ // set min/max to 0 and 100.
+ this._dataBounds = {min:0, max:100};
+ this.min = 0;
+ this.max = 100;
+ this.showTicks = false;
+ this.ticks = [];
+ this.showMark = false;
+ this.show = false;
+ };
+
+
+
+
+ $.jqplot.DonutLegendRenderer = function(){
+ $.jqplot.TableLegendRenderer.call(this);
+ };
+
+ $.jqplot.DonutLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
+ $.jqplot.DonutLegendRenderer.prototype.constructor = $.jqplot.DonutLegendRenderer;
+
+ /**
+ * Class: $.jqplot.DonutLegendRenderer
+ * Legend Renderer specific to donut plots. Set by default
+ * when user creates a donut plot.
+ */
+ $.jqplot.DonutLegendRenderer.prototype.init = function(options) {
+ // Group: Properties
+ //
+ // prop: numberRows
+ // Maximum number of rows in the legend. 0 or null for unlimited.
+ this.numberRows = null;
+ // prop: numberColumns
+ // Maximum number of columns in the legend. 0 or null for unlimited.
+ this.numberColumns = null;
+ $.extend(true, this, options);
+ };
+
+ // called with context of legend
+ $.jqplot.DonutLegendRenderer.prototype.draw = function() {
+ var legend = this;
+ if (this.show) {
+ var series = this._series;
+ var ss = 'position:absolute;';
+ ss += (this.background) ? 'background:'+this.background+';' : '';
+ ss += (this.border) ? 'border:'+this.border+';' : '';
+ ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
+ ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
+ ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
+ ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
+ ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
+ ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
+ ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
+ this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
+ // Donut charts legends don't go by number of series, but by number of data points
+ // in the series. Refactor things here for that.
+
+ var pad = false,
+ reverse = false,
+ nr, nc;
+ var s = series[0];
+ var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
+
+ if (s.show) {
+ var pd = s.data;
+ if (this.numberRows) {
+ nr = this.numberRows;
+ if (!this.numberColumns){
+ nc = Math.ceil(pd.length/nr);
+ }
+ else{
+ nc = this.numberColumns;
+ }
+ }
+ else if (this.numberColumns) {
+ nc = this.numberColumns;
+ nr = Math.ceil(pd.length/this.numberColumns);
+ }
+ else {
+ nr = pd.length;
+ nc = 1;
+ }
+
+ var i, j, tr, td1, td2, lt, rs, color;
+ var idx = 0;
+
+ for (i=0; i<nr; i++) {
+ if (reverse){
+ tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
+ }
+ else{
+ tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
+ }
+ for (j=0; j<nc; j++) {
+ if (idx < pd.length){
+ lt = this.labels[idx] || pd[idx][0].toString();
+ color = colorGenerator.next();
+ if (!reverse){
+ if (i>0){
+ pad = true;
+ }
+ else{
+ pad = false;
+ }
+ }
+ else{
+ if (i == nr -1){
+ pad = false;
+ }
+ else{
+ pad = true;
+ }
+ }
+ rs = (pad) ? this.rowSpacing : '0';
+
+ td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
+ '<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
+ '</div></td>');
+ td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
+ if (this.escapeHtml){
+ td2.text(lt);
+ }
+ else {
+ td2.html(lt);
+ }
+ if (reverse) {
+ td2.prependTo(tr);
+ td1.prependTo(tr);
+ }
+ else {
+ td1.appendTo(tr);
+ td2.appendTo(tr);
+ }
+ pad = true;
+ }
+ idx++;
+ }
+ }
+ }
+ }
+ return this._elem;
+ };
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.legend = options.legend || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ // only set these if there is a donut series
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.DonutRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.DonutRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.DonutAxisRenderer;
+ options.legend.renderer = $.jqplot.DonutLegendRenderer;
+ options.legend.preDraw = true;
+ options.seriesDefaults.pointLabels = {show: false};
+ }
+ }
+
+ // called with scope of plot.
+ function postInit(target, data, options) {
+ // if multiple series, add a reference to the previous one so that
+ // donut rings can nest.
+ for (var i=1; i<this.series.length; i++) {
+ if (!this.series[i]._previousSeries.length){
+ for (var j=0; j<i; j++) {
+ if (this.series[i].renderer.constructor == $.jqplot.DonutRenderer && this.series[j].renderer.constructor == $.jqplot.DonutRenderer) {
+ this.series[i]._previousSeries.push(this.series[j]);
+ }
+ }
+ }
+ }
+ for (i=0; i<this.series.length; i++) {
+ if (this.series[i].renderer.constructor == $.jqplot.DonutRenderer) {
+ this.series[i]._numberSeries = this.series.length;
+ // don't allow mouseover and mousedown at same time.
+ if (this.series[i].highlightMouseOver) {
+ this.series[i].highlightMouseDown = false;
+ }
+ }
+ }
+ }
+
+ var postParseOptionsRun = false;
+ // called with scope of plot
+ function postParseOptions(options) {
+ for (var i=0; i<this.series.length; i++) {
+ this.series[i].seriesColors = this.seriesColors;
+ this.series[i].colorGenerator = $.jqplot.colorGenerator;
+ }
+ }
+
+ function highlight (plot, sidx, pidx) {
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.donutRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.donutRenderer.highlightedSeriesIndex = sidx;
+ s.renderer.drawSlice.call(s, canvas._ctx, s._sliceAngles[pidx][0], s._sliceAngles[pidx][1], s.highlightColors[pidx], false);
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.donutRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.donutRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ }
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.donutRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.donutRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
+ var idx = plot.plugins.donutRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ }
+
+ function handleClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt = jQuery.Event('jqplotDataClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var idx = plot.plugins.donutRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ var evt = jQuery.Event('jqplotDataRightClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.donutRenderer && this.plugins.donutRenderer.highlightCanvas) {
+ this.plugins.donutRenderer.highlightCanvas.resetCanvas();
+ this.plugins.donutRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.donutRenderer = {highlightedSeriesIndex:null};
+ this.plugins.donutRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+ // do we have any data labels? if so, put highlight canvas before those
+ // Fix for broken jquery :first selector with canvas (VML) elements.
+ var labels = $(this.targetId+' .jqplot-data-label');
+ if (labels.length) {
+ $(labels[0]).before(this.plugins.donutRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-donutRenderer-highlight-canvas', this._plotDimensions, this));
+ }
+ // else put highlight canvas before event canvas.
+ else {
+ this.eventCanvas._elem.before(this.plugins.donutRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-donutRenderer-highlight-canvas', this._plotDimensions, this));
+ }
+ var hctx = this.plugins.donutRenderer.highlightCanvas.setContext();
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+
+ $.jqplot.DonutTickRenderer = function() {
+ $.jqplot.AxisTickRenderer.call(this);
+ };
+
+ $.jqplot.DonutTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
+ $.jqplot.DonutTickRenderer.prototype.constructor = $.jqplot.DonutTickRenderer;
+
+})(jQuery);
+
+ \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.dragable.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.dragable.js
new file mode 100644
index 0000000..a9639bf
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.dragable.js
@@ -0,0 +1,225 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+
+ /**
+ * Class: $.jqplot.Dragable
+ * Plugin to make plotted points dragable by the user.
+ */
+ $.jqplot.Dragable = function(options) {
+ // Group: Properties
+ this.markerRenderer = new $.jqplot.MarkerRenderer({shadow:false});
+ this.shapeRenderer = new $.jqplot.ShapeRenderer();
+ this.isDragging = false;
+ this.isOver = false;
+ this._ctx;
+ this._elem;
+ this._point;
+ this._gridData;
+ // prop: color
+ // CSS color spec for the dragged point (and adjacent line segment or bar).
+ this.color;
+ // prop: constrainTo
+ // Constrain dragging motion to an axis or to none.
+ // Allowable values are 'none', 'x', 'y'
+ this.constrainTo = 'none'; // 'x', 'y', or 'none';
+ $.extend(true, this, options);
+ };
+
+ function DragCanvas() {
+ $.jqplot.GenericCanvas.call(this);
+ this.isDragging = false;
+ this.isOver = false;
+ this._neighbor;
+ this._cursors = [];
+ }
+
+ DragCanvas.prototype = new $.jqplot.GenericCanvas();
+ DragCanvas.prototype.constructor = DragCanvas;
+
+
+ // called within scope of series
+ $.jqplot.Dragable.parseOptions = function (defaults, opts) {
+ var options = opts || {};
+ this.plugins.dragable = new $.jqplot.Dragable(options.dragable);
+ // since this function is called before series options are parsed,
+ // we can set this here and it will be overridden if needed.
+ this.isDragable = $.jqplot.config.enablePlugins;
+ };
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ // add a new DragCanvas object to the plot plugins to handle drawing on this new canvas.
+ $.jqplot.Dragable.postPlotDraw = function() {
+ // Memory Leaks patch
+ if (this.plugins.dragable && this.plugins.dragable.highlightCanvas) {
+ this.plugins.dragable.highlightCanvas.resetCanvas();
+ this.plugins.dragable.highlightCanvas = null;
+ }
+
+ this.plugins.dragable = {previousCursor:'auto', isOver:false};
+ this.plugins.dragable.dragCanvas = new DragCanvas();
+
+ this.eventCanvas._elem.before(this.plugins.dragable.dragCanvas.createElement(this._gridPadding, 'jqplot-dragable-canvas', this._plotDimensions, this));
+ var dctx = this.plugins.dragable.dragCanvas.setContext();
+ };
+
+ //$.jqplot.preInitHooks.push($.jqplot.Dragable.init);
+ $.jqplot.preParseSeriesOptionsHooks.push($.jqplot.Dragable.parseOptions);
+ $.jqplot.postDrawHooks.push($.jqplot.Dragable.postPlotDraw);
+ $.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);
+ $.jqplot.eventListenerHooks.push(['jqplotMouseDown', handleDown]);
+ $.jqplot.eventListenerHooks.push(['jqplotMouseUp', handleUp]);
+
+
+ function initDragPoint(plot, neighbor) {
+ var s = plot.series[neighbor.seriesIndex];
+ var drag = s.plugins.dragable;
+
+ // first, init the mark renderer for the dragged point
+ var smr = s.markerRenderer;
+ var mr = drag.markerRenderer;
+ mr.style = smr.style;
+ mr.lineWidth = smr.lineWidth + 2.5;
+ mr.size = smr.size + 5;
+ if (!drag.color) {
+ var rgba = $.jqplot.getColorComponents(smr.color);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var alpha = (rgba[3] >= 0.6) ? rgba[3]*0.6 : rgba[3]*(2-rgba[3]);
+ drag.color = 'rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+','+alpha+')';
+ }
+ mr.color = drag.color;
+ mr.init();
+
+ var start = (neighbor.pointIndex > 0) ? neighbor.pointIndex - 1 : 0;
+ var end = neighbor.pointIndex+2;
+ drag._gridData = s.gridData.slice(start, end);
+ }
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (plot.plugins.dragable.dragCanvas.isDragging) {
+ var dc = plot.plugins.dragable.dragCanvas;
+ var dp = dc._neighbor;
+ var s = plot.series[dp.seriesIndex];
+ var drag = s.plugins.dragable;
+ var gd = s.gridData;
+
+ // compute the new grid position with any constraints.
+ var x = (drag.constrainTo == 'y') ? dp.gridData[0] : gridpos.x;
+ var y = (drag.constrainTo == 'x') ? dp.gridData[1] : gridpos.y;
+
+ // compute data values for any listeners.
+ var xu = s._xaxis.series_p2u(x);
+ var yu = s._yaxis.series_p2u(y);
+
+ // clear the canvas then redraw effect at new position.
+ var ctx = dc._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+ // adjust our gridData for the new mouse position
+ if (dp.pointIndex > 0) {
+ drag._gridData[1] = [x, y];
+ }
+ else {
+ drag._gridData[0] = [x, y];
+ }
+ plot.series[dp.seriesIndex].draw(dc._ctx, {gridData:drag._gridData, shadow:false, preventJqPlotSeriesDrawTrigger:true, color:drag.color, markerOptions:{color:drag.color, shadow:false}, trendline:{show:false}});
+ plot.target.trigger('jqplotSeriesPointChange', [dp.seriesIndex, dp.pointIndex, [xu,yu], [x,y]]);
+ }
+ else if (neighbor != null) {
+ var series = plot.series[neighbor.seriesIndex];
+ if (series.isDragable) {
+ var dc = plot.plugins.dragable.dragCanvas;
+ if (!dc.isOver) {
+ dc._cursors.push(ev.target.style.cursor);
+ ev.target.style.cursor = "pointer";
+ }
+ dc.isOver = true;
+ }
+ }
+ else if (neighbor == null) {
+ var dc = plot.plugins.dragable.dragCanvas;
+ if (dc.isOver) {
+ ev.target.style.cursor = dc._cursors.pop();
+ dc.isOver = false;
+ }
+ }
+ }
+
+ function handleDown(ev, gridpos, datapos, neighbor, plot) {
+ var dc = plot.plugins.dragable.dragCanvas;
+ dc._cursors.push(ev.target.style.cursor);
+ if (neighbor != null) {
+ var s = plot.series[neighbor.seriesIndex];
+ var drag = s.plugins.dragable;
+ if (s.isDragable && !dc.isDragging) {
+ dc._neighbor = neighbor;
+ dc.isDragging = true;
+ initDragPoint(plot, neighbor);
+ drag.markerRenderer.draw(s.gridData[neighbor.pointIndex][0], s.gridData[neighbor.pointIndex][1], dc._ctx);
+ ev.target.style.cursor = "move";
+ plot.target.trigger('jqplotDragStart', [neighbor.seriesIndex, neighbor.pointIndex, gridpos, datapos]);
+ }
+ }
+ // Just in case of a hickup, we'll clear the drag canvas and reset.
+ else {
+ var ctx = dc._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ dc.isDragging = false;
+ }
+ }
+
+ function handleUp(ev, gridpos, datapos, neighbor, plot) {
+ if (plot.plugins.dragable.dragCanvas.isDragging) {
+ var dc = plot.plugins.dragable.dragCanvas;
+ // clear the canvas
+ var ctx = dc._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ dc.isDragging = false;
+ // redraw the series canvas at the new point.
+ var dp = dc._neighbor;
+ var s = plot.series[dp.seriesIndex];
+ var drag = s.plugins.dragable;
+ // compute the new grid position with any constraints.
+ var x = (drag.constrainTo == 'y') ? dp.data[0] : datapos[s.xaxis];
+ var y = (drag.constrainTo == 'x') ? dp.data[1] : datapos[s.yaxis];
+ // var x = datapos[s.xaxis];
+ // var y = datapos[s.yaxis];
+ s.data[dp.pointIndex][0] = x;
+ s.data[dp.pointIndex][1] = y;
+ plot.drawSeries({preventJqPlotSeriesDrawTrigger:true}, dp.seriesIndex);
+ dc._neighbor = null;
+ ev.target.style.cursor = dc._cursors.pop();
+ plot.target.trigger('jqplotDragStop', [gridpos, datapos]);
+ }
+ }
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js
new file mode 100644
index 0000000..5a2cbc6
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js
@@ -0,0 +1,305 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ // class $.jqplot.EnhancedLegendRenderer
+ // Legend renderer which can specify the number of rows and/or columns in the legend.
+ $.jqplot.EnhancedLegendRenderer = function(){
+ $.jqplot.TableLegendRenderer.call(this);
+ };
+
+ $.jqplot.EnhancedLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
+ $.jqplot.EnhancedLegendRenderer.prototype.constructor = $.jqplot.EnhancedLegendRenderer;
+
+ // called with scope of legend.
+ $.jqplot.EnhancedLegendRenderer.prototype.init = function(options) {
+ // prop: numberRows
+ // Maximum number of rows in the legend. 0 or null for unlimited.
+ this.numberRows = null;
+ // prop: numberColumns
+ // Maximum number of columns in the legend. 0 or null for unlimited.
+ this.numberColumns = null;
+ // prop: seriesToggle
+ // false to not enable series on/off toggling on the legend.
+ // true or a fadein/fadeout speed (number of milliseconds or 'fast', 'normal', 'slow')
+ // to enable show/hide of series on click of legend item.
+ this.seriesToggle = 'normal';
+ // prop: seriesToggleReplot
+ // True to replot the chart after toggling series on/off.
+ // This will set the series show property to false.
+ // This allows for rescaling or other maniplation of chart.
+ // Set to an options object (e.g. {resetAxes: true}) for replot options.
+ this.seriesToggleReplot = false;
+ // prop: disableIEFading
+ // true to toggle series with a show/hide method only and not allow fading in/out.
+ // This is to overcome poor performance of fade in some versions of IE.
+ this.disableIEFading = true;
+ $.extend(true, this, options);
+
+ if (this.seriesToggle) {
+ $.jqplot.postDrawHooks.push(postDraw);
+ }
+ };
+
+ // called with scope of legend
+ $.jqplot.EnhancedLegendRenderer.prototype.draw = function(offsets, plot) {
+ var legend = this;
+ if (this.show) {
+ var series = this._series;
+ var s;
+ var ss = 'position:absolute;';
+ ss += (this.background) ? 'background:'+this.background+';' : '';
+ ss += (this.border) ? 'border:'+this.border+';' : '';
+ ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
+ ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
+ ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
+ ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
+ ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
+ ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
+ ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
+ this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
+ if (this.seriesToggle) {
+ this._elem.css('z-index', '3');
+ }
+
+ var pad = false,
+ reverse = false,
+ nr, nc;
+ if (this.numberRows) {
+ nr = this.numberRows;
+ if (!this.numberColumns){
+ nc = Math.ceil(series.length/nr);
+ }
+ else{
+ nc = this.numberColumns;
+ }
+ }
+ else if (this.numberColumns) {
+ nc = this.numberColumns;
+ nr = Math.ceil(series.length/this.numberColumns);
+ }
+ else {
+ nr = series.length;
+ nc = 1;
+ }
+
+ var i, j, tr, td1, td2, lt, rs, div, div0, div1;
+ var idx = 0;
+ // check to see if we need to reverse
+ for (i=series.length-1; i>=0; i--) {
+ if (nc == 1 && series[i]._stack || series[i].renderer.constructor == $.jqplot.BezierCurveRenderer){
+ reverse = true;
+ }
+ }
+
+ for (i=0; i<nr; i++) {
+ tr = $(document.createElement('tr'));
+ tr.addClass('jqplot-table-legend');
+ if (reverse){
+ tr.prependTo(this._elem);
+ }
+ else{
+ tr.appendTo(this._elem);
+ }
+ for (j=0; j<nc; j++) {
+ if (idx < series.length && (series[idx].show || series[idx].showLabel)){
+ s = series[idx];
+ lt = this.labels[idx] || s.label.toString();
+ if (lt) {
+ var color = s.color;
+ if (!reverse){
+ if (i>0){
+ pad = true;
+ }
+ else{
+ pad = false;
+ }
+ }
+ else{
+ if (i == nr -1){
+ pad = false;
+ }
+ else{
+ pad = true;
+ }
+ }
+ rs = (pad) ? this.rowSpacing : '0';
+
+ td1 = $(document.createElement('td'));
+ td1.addClass('jqplot-table-legend jqplot-table-legend-swatch');
+ td1.css({textAlign: 'center', paddingTop: rs});
+
+ div0 = $(document.createElement('div'));
+ div0.addClass('jqplot-table-legend-swatch-outline');
+ div1 = $(document.createElement('div'));
+ div1.addClass('jqplot-table-legend-swatch');
+ div1.css({backgroundColor: color, borderColor: color});
+
+ td1.append(div0.append(div1));
+
+ td2 = $(document.createElement('td'));
+ td2.addClass('jqplot-table-legend jqplot-table-legend-label');
+ td2.css('paddingTop', rs);
+
+ // td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
+ // '<div><div class="jqplot-table-legend-swatch" style="background-color:'+color+';border-color:'+color+';"></div>'+
+ // '</div></td>');
+ // td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
+ if (this.escapeHtml){
+ td2.text(lt);
+ }
+ else {
+ td2.html(lt);
+ }
+ if (reverse) {
+ if (this.showLabels) {td2.prependTo(tr);}
+ if (this.showSwatches) {td1.prependTo(tr);}
+ }
+ else {
+ if (this.showSwatches) {td1.appendTo(tr);}
+ if (this.showLabels) {td2.appendTo(tr);}
+ }
+
+ if (this.seriesToggle) {
+
+ // add an overlay for clicking series on/off
+ // div0 = $(document.createElement('div'));
+ // div0.addClass('jqplot-table-legend-overlay');
+ // div0.css({position:'relative', left:0, top:0, height:'100%', width:'100%'});
+ // tr.append(div0);
+
+ var speed;
+ if (typeof(this.seriesToggle) === 'string' || typeof(this.seriesToggle) === 'number') {
+ if (!$.jqplot.use_excanvas || !this.disableIEFading) {
+ speed = this.seriesToggle;
+ }
+ }
+ if (this.showSwatches) {
+ td1.bind('click', {series:s, speed:speed, plot: plot, replot:this.seriesToggleReplot}, handleToggle);
+ td1.addClass('jqplot-seriesToggle');
+ }
+ if (this.showLabels) {
+ td2.bind('click', {series:s, speed:speed, plot: plot, replot:this.seriesToggleReplot}, handleToggle);
+ td2.addClass('jqplot-seriesToggle');
+ }
+
+ // for series that are already hidden, add the hidden class
+ if (!s.show && s.showLabel) {
+ td1.addClass('jqplot-series-hidden');
+ td2.addClass('jqplot-series-hidden');
+ }
+ }
+
+ pad = true;
+ }
+ }
+ idx++;
+ }
+
+ td1 = td2 = div0 = div1 = null;
+ }
+ }
+ return this._elem;
+ };
+
+ var handleToggle = function (ev) {
+ var d = ev.data,
+ s = d.series,
+ replot = d.replot,
+ plot = d.plot,
+ speed = d.speed,
+ sidx = s.index,
+ showing = false;
+
+ if (s.canvas._elem.is(':hidden') || !s.show) {
+ showing = true;
+ }
+
+ var doLegendToggle = function() {
+
+ if (replot) {
+ var opts = {};
+
+ if ($.isPlainObject(replot)) {
+ $.extend(true, opts, replot);
+ }
+
+ plot.replot(opts);
+ // if showing, there was no canvas element to fade in, so hide here
+ // and then do a fade in.
+ if (showing && speed) {
+ var s = plot.series[sidx];
+
+ if (s.shadowCanvas._elem) {
+ s.shadowCanvas._elem.hide().fadeIn(speed);
+ }
+ s.canvas._elem.hide().fadeIn(speed);
+ s.canvas._elem.nextAll('.jqplot-point-label.jqplot-series-'+s.index).hide().fadeIn(speed);
+ }
+
+ }
+
+ else {
+ var s = plot.series[sidx];
+
+ if (s.canvas._elem.is(':hidden') || !s.show) {
+ // Not sure if there is a better way to check for showSwatches and showLabels === true.
+ // Test for "undefined" since default values for both showSwatches and showLables is true.
+ if (typeof plot.options.legend.showSwatches === 'undefined' || plot.options.legend.showSwatches === true) {
+ plot.legend._elem.find('td').eq(sidx * 2).addClass('jqplot-series-hidden');
+ }
+ if (typeof plot.options.legend.showLabels === 'undefined' || plot.options.legend.showLabels === true) {
+ plot.legend._elem.find('td').eq((sidx * 2) + 1).addClass('jqplot-series-hidden');
+ }
+ }
+ else {
+ if (typeof plot.options.legend.showSwatches === 'undefined' || plot.options.legend.showSwatches === true) {
+ plot.legend._elem.find('td').eq(sidx * 2).removeClass('jqplot-series-hidden');
+ }
+ if (typeof plot.options.legend.showLabels === 'undefined' || plot.options.legend.showLabels === true) {
+ plot.legend._elem.find('td').eq((sidx * 2) + 1).removeClass('jqplot-series-hidden');
+ }
+ }
+
+ }
+
+ };
+
+ s.toggleDisplay(ev, doLegendToggle);
+ };
+
+ // called with scope of plot.
+ var postDraw = function () {
+ if (this.legend.renderer.constructor == $.jqplot.EnhancedLegendRenderer && this.legend.seriesToggle){
+ var e = this.legend._elem.detach();
+ this.eventCanvas._elem.after(e);
+ }
+ };
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.funnelRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.funnelRenderer.js
new file mode 100644
index 0000000..2cbc9c2
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.funnelRenderer.js
@@ -0,0 +1,943 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.FunnelRenderer
+ * Plugin renderer to draw a funnel chart.
+ * x values, if present, will be used as labels.
+ * y values give area size.
+ *
+ * Funnel charts will draw a single series
+ * only.
+ *
+ * To use this renderer, you need to include the
+ * funnel renderer plugin, for example:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.funnelRenderer.js"></script>
+ *
+ * Properties described here are passed into the $.jqplot function
+ * as options on the series renderer. For example:
+ *
+ * > plot2 = $.jqplot('chart2', [s1, s2], {
+ * > seriesDefaults: {
+ * > renderer:$.jqplot.FunnelRenderer,
+ * > rendererOptions:{
+ * > sectionMargin: 12,
+ * > widthRatio: 0.3
+ * > }
+ * > }
+ * > });
+ *
+ * IMPORTANT
+ *
+ * *The funnel renderer will reorder data in descending order* so the largest value in
+ * the data set is first and displayed on top of the funnel. Data will then
+ * be displayed in descending order down the funnel. The area of each funnel
+ * section will correspond to the value of each data point relative to the sum
+ * of all values. That is section area is proportional to section value divided by
+ * sum of all section values.
+ *
+ * If your data is not in descending order when passed into the plot, *it will be
+ * reordered* when stored in the series.data property. A copy of the unordered
+ * data is kept in the series._unorderedData property.
+ *
+ * A funnel plot will trigger events on the plot target
+ * according to user interaction. All events return the event object,
+ * the series index, the point (section) index, and the point data for
+ * the appropriate section. *Note* the point index will referr to the ordered
+ * data, not the original unordered data.
+ *
+ * 'jqplotDataMouseOver' - triggered when mousing over a section.
+ * 'jqplotDataHighlight' - triggered the first time user mouses over a section,
+ * if highlighting is enabled.
+ * 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
+ * a highlighted section.
+ * 'jqplotDataClick' - triggered when the user clicks on a section.
+ * 'jqplotDataRightClick' - tiggered when the user right clicks on a section if
+ * the "captureRightClick" option is set to true on the plot.
+ */
+ $.jqplot.FunnelRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.FunnelRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.FunnelRenderer.prototype.constructor = $.jqplot.FunnelRenderer;
+
+ // called with scope of a series
+ $.jqplot.FunnelRenderer.prototype.init = function(options, plot) {
+ // Group: Properties
+ //
+ // prop: padding
+ // padding between the funnel and plot edges, legend, etc.
+ this.padding = {top: 20, right: 20, bottom: 20, left: 20};
+ // prop: sectionMargin
+ // spacing between funnel sections in pixels.
+ this.sectionMargin = 6;
+ // prop: fill
+ // true or false, whether to fill the areas.
+ this.fill = true;
+ // prop: shadowOffset
+ // offset of the shadow from the area and offset of
+ // each succesive stroke of the shadow from the last.
+ this.shadowOffset = 2;
+ // prop: shadowAlpha
+ // transparency of the shadow (0 = transparent, 1 = opaque)
+ this.shadowAlpha = 0.07;
+ // prop: shadowDepth
+ // number of strokes to apply to the shadow,
+ // each stroke offset shadowOffset from the last.
+ this.shadowDepth = 5;
+ // prop: highlightMouseOver
+ // True to highlight area when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on a area.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over a area.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColors
+ // array of colors to use when highlighting an area.
+ this.highlightColors = [];
+ // prop: widthRatio
+ // The ratio of the width of the top of the funnel to the bottom.
+ // a ratio of 0 will make an upside down pyramid.
+ this.widthRatio = 0.2;
+ // prop: lineWidth
+ // width of line if areas are stroked and not filled.
+ this.lineWidth = 2;
+ // prop: dataLabels
+ // Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
+ // Defaults to percentage of each pie slice.
+ this.dataLabels = 'percent';
+ // prop: showDataLabels
+ // true to show data labels on slices.
+ this.showDataLabels = false;
+ // prop: dataLabelFormatString
+ // Format string for data labels. If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
+ this.dataLabelFormatString = null;
+ // prop: dataLabelThreshold
+ // Threshhold in percentage (0 - 100) of pie area, below which no label will be displayed.
+ // This applies to all label types, not just to percentage labels.
+ this.dataLabelThreshold = 3;
+ this._type = 'funnel';
+
+ this.tickRenderer = $.jqplot.FunnelTickRenderer;
+
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (options.highlightMouseDown && options.highlightMouseOver == null) {
+ options.highlightMouseOver = false;
+ }
+
+ $.extend(true, this, options);
+
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+
+ // lengths of bases, or horizontal sides of areas of trapezoid.
+ this._bases = [];
+ // total area
+ this._atot;
+ // areas of segments.
+ this._areas = [];
+ // vertical lengths of segments.
+ this._lengths = [];
+ // angle of the funnel to vertical.
+ this._angle;
+ this._dataIndices = [];
+
+ // sort data
+ this._unorderedData = $.extend(true, [], this.data);
+ var idxs = $.extend(true, [], this.data);
+ for (var i=0; i<idxs.length; i++) {
+ idxs[i].push(i);
+ }
+ this.data.sort( function (a, b) { return b[1] - a[1]; } );
+ idxs.sort( function (a, b) { return b[1] - a[1]; });
+ for (var i=0; i<idxs.length; i++) {
+ this._dataIndices.push(idxs[i][2]);
+ }
+
+ // set highlight colors if none provided
+ if (this.highlightColors.length == 0) {
+ for (var i=0; i<this.seriesColors.length; i++){
+ var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.4 * (255 - newrgb[j]);
+ newrgb[j] = parseInt(newrgb[j], 10);
+ }
+ this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
+ }
+ }
+
+ plot.postParseOptionsHooks.addOnce(postParseOptions);
+ plot.postInitHooks.addOnce(postInit);
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+ plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
+ plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
+ plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
+ plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
+ plot.postDrawHooks.addOnce(postPlotDraw);
+
+ };
+
+ // gridData will be of form [label, percentage of total]
+ $.jqplot.FunnelRenderer.prototype.setGridData = function(plot) {
+ // set gridData property. This will hold angle in radians of each data point.
+ var sum = 0;
+ var td = [];
+ for (var i=0; i<this.data.length; i++){
+ sum += this.data[i][1];
+ td.push([this.data[i][0], this.data[i][1]]);
+ }
+
+ // normalize y values, so areas are proportional.
+ for (var i=0; i<td.length; i++) {
+ td[i][1] = td[i][1]/sum;
+ }
+
+ this._bases = new Array(td.length + 1);
+ this._lengths = new Array(td.length);
+
+ this.gridData = td;
+ };
+
+ $.jqplot.FunnelRenderer.prototype.makeGridData = function(data, plot) {
+ // set gridData property. This will hold angle in radians of each data point.
+ var sum = 0;
+ var td = [];
+ for (var i=0; i<this.data.length; i++){
+ sum += this.data[i][1];
+ td.push([this.data[i][0], this.data[i][1]]);
+ }
+
+ // normalize y values, so areas are proportional.
+ for (var i=0; i<td.length; i++) {
+ td[i][1] = td[i][1]/sum;
+ }
+
+ this._bases = new Array(td.length + 1);
+ this._lengths = new Array(td.length);
+
+ return td;
+ };
+
+ $.jqplot.FunnelRenderer.prototype.drawSection = function (ctx, vertices, color, isShadow) {
+ var fill = this.fill;
+ var lineWidth = this.lineWidth;
+ ctx.save();
+
+ if (isShadow) {
+ for (var i=0; i<this.shadowDepth; i++) {
+ ctx.save();
+ ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
+ doDraw();
+ }
+ }
+
+ else {
+ doDraw();
+ }
+
+ function doDraw () {
+ ctx.beginPath();
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.lineWidth = lineWidth;
+ ctx.moveTo(vertices[0][0], vertices[0][1]);
+ for (var i=1; i<4; i++) {
+ ctx.lineTo(vertices[i][0], vertices[i][1]);
+ }
+ ctx.closePath();
+ if (fill) {
+ ctx.fill();
+ }
+ else {
+ ctx.stroke();
+ }
+ }
+
+ if (isShadow) {
+ for (var i=0; i<this.shadowDepth; i++) {
+ ctx.restore();
+ }
+ }
+
+ ctx.restore();
+ };
+
+ // called with scope of series
+ $.jqplot.FunnelRenderer.prototype.draw = function (ctx, gd, options, plot) {
+ var i;
+ var opts = (options != undefined) ? options : {};
+ // offset and direction of offset due to legend placement
+ var offx = 0;
+ var offy = 0;
+ var trans = 1;
+ this._areas = [];
+ // var colorGenerator = new this.colorGenerator(this.seriesColors);
+ if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
+ var li = options.legendInfo;
+ switch (li.location) {
+ case 'nw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'w':
+ offx = li.width + li.xoffset;
+ break;
+ case 'sw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'ne':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'e':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'se':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'n':
+ offy = li.height + li.yoffset;
+ break;
+ case 's':
+ offy = li.height + li.yoffset;
+ trans = -1;
+ break;
+ default:
+ break;
+ }
+ }
+
+ var loff = (trans==1) ? this.padding.left + offx : this.padding.left;
+ var toff = (trans==1) ? this.padding.top + offy : this.padding.top;
+ var roff = (trans==-1) ? this.padding.right + offx : this.padding.right;
+ var boff = (trans==-1) ? this.padding.bottom + offy : this.padding.bottom;
+
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var cw = ctx.canvas.width;
+ var ch = ctx.canvas.height;
+ this._bases[0] = cw - loff - roff;
+ var ltot = this._length = ch - toff - boff;
+
+ var hend = this._bases[0]*this.widthRatio;
+ this._atot = ltot/2 * (this._bases[0] + this._bases[0]*this.widthRatio);
+
+ this._angle = Math.atan((this._bases[0] - hend)/2/ltot);
+
+ for (i=0; i<gd.length; i++) {
+ this._areas.push(gd[i][1] * this._atot);
+ }
+
+
+ var guess, err, count, lsum=0;
+ var tolerance = 0.0001;
+
+ for (i=0; i<this._areas.length; i++) {
+ guess = this._areas[i]/this._bases[i];
+ err = 999999;
+ this._lengths[i] = guess;
+ count = 0;
+ while (err > this._lengths[i]*tolerance && count < 100) {
+ this._lengths[i] = this._areas[i]/(this._bases[i] - this._lengths[i] * Math.tan(this._angle));
+ err = Math.abs(this._lengths[i] - guess);
+ this._bases[i+1] = this._bases[i] - (2*this._lengths[i]*Math.tan(this._angle));
+ guess = this._lengths[i];
+ count++;
+ }
+ lsum += this._lengths[i];
+ }
+
+ // figure out vertices of each section
+ this._vertices = new Array(gd.length);
+
+ // these are 4 coners of entire trapezoid
+ var p0 = [loff, toff],
+ p1 = [loff+this._bases[0], toff],
+ p2 = [loff + (this._bases[0] - this._bases[this._bases.length-1])/2, toff + this._length],
+ p3 = [p2[0] + this._bases[this._bases.length-1], p2[1]];
+
+ // equations of right and left sides, returns x, y values given height of section (y value)
+ function findleft (l) {
+ var m = (p0[1] - p2[1])/(p0[0] - p2[0]);
+ var b = p0[1] - m*p0[0];
+ var y = l + p0[1];
+
+ return [(y - b)/m, y];
+ }
+
+ function findright (l) {
+ var m = (p1[1] - p3[1])/(p1[0] - p3[0]);
+ var b = p1[1] - m*p1[0];
+ var y = l + p1[1];
+
+ return [(y - b)/m, y];
+ }
+
+ var x = offx, y = offy;
+ var h=0, adj=0;
+
+ for (i=0; i<gd.length; i++) {
+ this._vertices[i] = new Array();
+ var v = this._vertices[i];
+ var sm = this.sectionMargin;
+ if (i == 0) {
+ adj = 0;
+ }
+ if (i == 1) {
+ adj = sm/3;
+ }
+ else if (i > 0 && i < gd.length-1) {
+ adj = sm/2;
+ }
+ else if (i == gd.length -1) {
+ adj = 2*sm/3;
+ }
+ v.push(findleft(h+adj));
+ v.push(findright(h+adj));
+ h += this._lengths[i];
+ if (i == 0) {
+ adj = -2*sm/3;
+ }
+ else if (i > 0 && i < gd.length-1) {
+ adj = -sm/2;
+ }
+ else if (i == gd.length - 1) {
+ adj = 0;
+ }
+ v.push(findright(h+adj));
+ v.push(findleft(h+adj));
+
+ }
+
+ if (this.shadow) {
+ var shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
+ for (var i=0; i<gd.length; i++) {
+ this.renderer.drawSection.call (this, ctx, this._vertices[i], shadowColor, true);
+ }
+
+ }
+ for (var i=0; i<gd.length; i++) {
+ var v = this._vertices[i];
+ this.renderer.drawSection.call (this, ctx, v, this.seriesColors[i]);
+
+ if (this.showDataLabels && gd[i][1]*100 >= this.dataLabelThreshold) {
+ var fstr, label;
+
+ if (this.dataLabels == 'label') {
+ fstr = this.dataLabelFormatString || '%s';
+ label = $.jqplot.sprintf(fstr, gd[i][0]);
+ }
+ else if (this.dataLabels == 'value') {
+ fstr = this.dataLabelFormatString || '%d';
+ label = $.jqplot.sprintf(fstr, this.data[i][1]);
+ }
+ else if (this.dataLabels == 'percent') {
+ fstr = this.dataLabelFormatString || '%d%%';
+ label = $.jqplot.sprintf(fstr, gd[i][1]*100);
+ }
+ else if (this.dataLabels.constructor == Array) {
+ fstr = this.dataLabelFormatString || '%s';
+ label = $.jqplot.sprintf(fstr, this.dataLabels[this._dataIndices[i]]);
+ }
+
+ var fact = (this._radius ) * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
+
+ var x = (v[0][0] + v[1][0])/2 + this.canvas._offsets.left;
+ var y = (v[1][1] + v[2][1])/2 + this.canvas._offsets.top;
+
+ var labelelem = $('<span class="jqplot-funnel-series jqplot-data-label" style="position:absolute;">' + label + '</span>').insertBefore(plot.eventCanvas._elem);
+ x -= labelelem.width()/2;
+ y -= labelelem.height()/2;
+ x = Math.round(x);
+ y = Math.round(y);
+ labelelem.css({left: x, top: y});
+ }
+
+ }
+
+ };
+
+ $.jqplot.FunnelAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.FunnelAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.FunnelAxisRenderer.prototype.constructor = $.jqplot.FunnelAxisRenderer;
+
+
+ // There are no traditional axes on a funnel chart. We just need to provide
+ // dummy objects with properties so the plot will render.
+ // called with scope of axis object.
+ $.jqplot.FunnelAxisRenderer.prototype.init = function(options){
+ //
+ this.tickRenderer = $.jqplot.FunnelTickRenderer;
+ $.extend(true, this, options);
+ // I don't think I'm going to need _dataBounds here.
+ // have to go Axis scaling in a way to fit chart onto plot area
+ // and provide u2p and p2u functionality for mouse cursor, etc.
+ // for convienence set _dataBounds to 0 and 100 and
+ // set min/max to 0 and 100.
+ this._dataBounds = {min:0, max:100};
+ this.min = 0;
+ this.max = 100;
+ this.showTicks = false;
+ this.ticks = [];
+ this.showMark = false;
+ this.show = false;
+ };
+
+
+
+ /**
+ * Class: $.jqplot.FunnelLegendRenderer
+ * Legend Renderer specific to funnel plots. Set by default
+ * when the user creates a funnel plot.
+ */
+ $.jqplot.FunnelLegendRenderer = function(){
+ $.jqplot.TableLegendRenderer.call(this);
+ };
+
+ $.jqplot.FunnelLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
+ $.jqplot.FunnelLegendRenderer.prototype.constructor = $.jqplot.FunnelLegendRenderer;
+
+ $.jqplot.FunnelLegendRenderer.prototype.init = function(options) {
+ // Group: Properties
+ //
+ // prop: numberRows
+ // Maximum number of rows in the legend. 0 or null for unlimited.
+ this.numberRows = null;
+ // prop: numberColumns
+ // Maximum number of columns in the legend. 0 or null for unlimited.
+ this.numberColumns = null;
+ $.extend(true, this, options);
+ };
+
+ // called with context of legend
+ $.jqplot.FunnelLegendRenderer.prototype.draw = function() {
+ var legend = this;
+ if (this.show) {
+ var series = this._series;
+ var ss = 'position:absolute;';
+ ss += (this.background) ? 'background:'+this.background+';' : '';
+ ss += (this.border) ? 'border:'+this.border+';' : '';
+ ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
+ ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
+ ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
+ ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
+ ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
+ ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
+ ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
+ this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
+ // Funnel charts legends don't go by number of series, but by number of data points
+ // in the series. Refactor things here for that.
+
+ var pad = false,
+ reverse = false,
+ nr, nc;
+ var s = series[0];
+ var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
+
+ if (s.show) {
+ var pd = s.data;
+ if (this.numberRows) {
+ nr = this.numberRows;
+ if (!this.numberColumns){
+ nc = Math.ceil(pd.length/nr);
+ }
+ else{
+ nc = this.numberColumns;
+ }
+ }
+ else if (this.numberColumns) {
+ nc = this.numberColumns;
+ nr = Math.ceil(pd.length/this.numberColumns);
+ }
+ else {
+ nr = pd.length;
+ nc = 1;
+ }
+
+ var i, j, tr, td1, td2, lt, rs, color;
+ var idx = 0;
+
+ for (i=0; i<nr; i++) {
+ if (reverse){
+ tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
+ }
+ else{
+ tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
+ }
+ for (j=0; j<nc; j++) {
+ if (idx < pd.length){
+ lt = this.labels[idx] || pd[idx][0].toString();
+ color = colorGenerator.next();
+ if (!reverse){
+ if (i>0){
+ pad = true;
+ }
+ else{
+ pad = false;
+ }
+ }
+ else{
+ if (i == nr -1){
+ pad = false;
+ }
+ else{
+ pad = true;
+ }
+ }
+ rs = (pad) ? this.rowSpacing : '0';
+
+ td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
+ '<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
+ '</div></td>');
+ td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
+ if (this.escapeHtml){
+ td2.text(lt);
+ }
+ else {
+ td2.html(lt);
+ }
+ if (reverse) {
+ td2.prependTo(tr);
+ td1.prependTo(tr);
+ }
+ else {
+ td1.appendTo(tr);
+ td2.appendTo(tr);
+ }
+ pad = true;
+ }
+ idx++;
+ }
+ }
+ }
+ }
+ return this._elem;
+ };
+
+ // $.jqplot.FunnelLegendRenderer.prototype.pack = function(offsets) {
+ // if (this.show) {
+ // // fake a grid for positioning
+ // var grid = {_top:offsets.top, _left:offsets.left, _right:offsets.right, _bottom:this._plotDimensions.height - offsets.bottom};
+ // if (this.placement == 'insideGrid') {
+ // switch (this.location) {
+ // case 'nw':
+ // var a = grid._left + this.xoffset;
+ // var b = grid._top + this.yoffset;
+ // this._elem.css('left', a);
+ // this._elem.css('top', b);
+ // break;
+ // case 'n':
+ // var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ // var b = grid._top + this.yoffset;
+ // this._elem.css('left', a);
+ // this._elem.css('top', b);
+ // break;
+ // case 'ne':
+ // var a = offsets.right + this.xoffset;
+ // var b = grid._top + this.yoffset;
+ // this._elem.css({right:a, top:b});
+ // break;
+ // case 'e':
+ // var a = offsets.right + this.xoffset;
+ // var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ // this._elem.css({right:a, top:b});
+ // break;
+ // case 'se':
+ // var a = offsets.right + this.xoffset;
+ // var b = offsets.bottom + this.yoffset;
+ // this._elem.css({right:a, bottom:b});
+ // break;
+ // case 's':
+ // var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ // var b = offsets.bottom + this.yoffset;
+ // this._elem.css({left:a, bottom:b});
+ // break;
+ // case 'sw':
+ // var a = grid._left + this.xoffset;
+ // var b = offsets.bottom + this.yoffset;
+ // this._elem.css({left:a, bottom:b});
+ // break;
+ // case 'w':
+ // var a = grid._left + this.xoffset;
+ // var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ // this._elem.css({left:a, top:b});
+ // break;
+ // default: // same as 'se'
+ // var a = grid._right - this.xoffset;
+ // var b = grid._bottom + this.yoffset;
+ // this._elem.css({right:a, bottom:b});
+ // break;
+ // }
+ //
+ // }
+ // else {
+ // switch (this.location) {
+ // case 'nw':
+ // var a = this._plotDimensions.width - grid._left + this.xoffset;
+ // var b = grid._top + this.yoffset;
+ // this._elem.css('right', a);
+ // this._elem.css('top', b);
+ // break;
+ // case 'n':
+ // var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ // var b = this._plotDimensions.height - grid._top + this.yoffset;
+ // this._elem.css('left', a);
+ // this._elem.css('bottom', b);
+ // break;
+ // case 'ne':
+ // var a = this._plotDimensions.width - offsets.right + this.xoffset;
+ // var b = grid._top + this.yoffset;
+ // this._elem.css({left:a, top:b});
+ // break;
+ // case 'e':
+ // var a = this._plotDimensions.width - offsets.right + this.xoffset;
+ // var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ // this._elem.css({left:a, top:b});
+ // break;
+ // case 'se':
+ // var a = this._plotDimensions.width - offsets.right + this.xoffset;
+ // var b = offsets.bottom + this.yoffset;
+ // this._elem.css({left:a, bottom:b});
+ // break;
+ // case 's':
+ // var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ // var b = this._plotDimensions.height - offsets.bottom + this.yoffset;
+ // this._elem.css({left:a, top:b});
+ // break;
+ // case 'sw':
+ // var a = this._plotDimensions.width - grid._left + this.xoffset;
+ // var b = offsets.bottom + this.yoffset;
+ // this._elem.css({right:a, bottom:b});
+ // break;
+ // case 'w':
+ // var a = this._plotDimensions.width - grid._left + this.xoffset;
+ // var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ // this._elem.css({right:a, top:b});
+ // break;
+ // default: // same as 'se'
+ // var a = grid._right - this.xoffset;
+ // var b = grid._bottom + this.yoffset;
+ // this._elem.css({right:a, bottom:b});
+ // break;
+ // }
+ // }
+ // }
+ // };
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.legend = options.legend || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ // only set these if there is a funnel series
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.FunnelRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.FunnelRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.FunnelAxisRenderer;
+ options.legend.renderer = $.jqplot.FunnelLegendRenderer;
+ options.legend.preDraw = true;
+ options.sortData = false;
+ options.seriesDefaults.pointLabels = {show: false};
+ }
+ }
+
+ function postInit(target, data, options) {
+ // if multiple series, add a reference to the previous one so that
+ // funnel rings can nest.
+ for (var i=0; i<this.series.length; i++) {
+ if (this.series[i].renderer.constructor == $.jqplot.FunnelRenderer) {
+ // don't allow mouseover and mousedown at same time.
+ if (this.series[i].highlightMouseOver) {
+ this.series[i].highlightMouseDown = false;
+ }
+ }
+ }
+ }
+
+ // called with scope of plot
+ function postParseOptions(options) {
+ for (var i=0; i<this.series.length; i++) {
+ this.series[i].seriesColors = this.seriesColors;
+ this.series[i].colorGenerator = $.jqplot.colorGenerator;
+ }
+ }
+
+ function highlight (plot, sidx, pidx) {
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.funnelRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.funnelRenderer.highlightedSeriesIndex = sidx;
+ s.renderer.drawSection.call(s, canvas._ctx, s._vertices[pidx], s.highlightColors[pidx], false);
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.funnelRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.funnelRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ }
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.funnelRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.funnelRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
+ var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ }
+
+ function handleClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt = jQuery.Event('jqplotDataClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ var evt = jQuery.Event('jqplotDataRightClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.funnelRenderer && this.plugins.funnelRenderer.highlightCanvas) {
+ this.plugins.funnelRenderer.highlightCanvas.resetCanvas();
+ this.plugins.funnelRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.funnelRenderer = {};
+ this.plugins.funnelRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+
+ // do we have any data labels? if so, put highlight canvas before those
+ var labels = $(this.targetId+' .jqplot-data-label');
+ if (labels.length) {
+ $(labels[0]).before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
+ }
+ // else put highlight canvas before event canvas.
+ else {
+ this.eventCanvas._elem.before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
+ }
+ var hctx = this.plugins.funnelRenderer.highlightCanvas.setContext();
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+
+ $.jqplot.FunnelTickRenderer = function() {
+ $.jqplot.AxisTickRenderer.call(this);
+ };
+
+ $.jqplot.FunnelTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
+ $.jqplot.FunnelTickRenderer.prototype.constructor = $.jqplot.FunnelTickRenderer;
+
+})(jQuery);
+
+ \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.highlighter.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.highlighter.js
new file mode 100644
index 0000000..3105c85
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.highlighter.js
@@ -0,0 +1,465 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ $.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);
+
+ /**
+ * Class: $.jqplot.Highlighter
+ * Plugin which will highlight data points when they are moused over.
+ *
+ * To use this plugin, include the js
+ * file in your source:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.highlighter.js"></script>
+ *
+ * A tooltip providing information about the data point is enabled by default.
+ * To disable the tooltip, set "showTooltip" to false.
+ *
+ * You can control what data is displayed in the tooltip with various
+ * options. The "tooltipAxes" option controls whether the x, y or both
+ * data values are displayed.
+ *
+ * Some chart types (e.g. hi-low-close) have more than one y value per
+ * data point. To display the additional values in the tooltip, set the
+ * "yvalues" option to the desired number of y values present (3 for a hlc chart).
+ *
+ * By default, data values will be formatted with the same formatting
+ * specifiers as used to format the axis ticks. A custom format code
+ * can be supplied with the tooltipFormatString option. This will apply
+ * to all values in the tooltip.
+ *
+ * For more complete control, the "formatString" option can be set. This
+ * Allows conplete control over tooltip formatting. Values are passed to
+ * the format string in an order determined by the "tooltipAxes" and "yvalues"
+ * options. So, if you have a hi-low-close chart and you just want to display
+ * the hi-low-close values in the tooltip, you could set a formatString like:
+ *
+ * > highlighter: {
+ * > tooltipAxes: 'y',
+ * > yvalues: 3,
+ * > formatString:'<table class="jqplot-highlighter">
+ * > <tr><td>hi:</td><td>%s</td></tr>
+ * > <tr><td>low:</td><td>%s</td></tr>
+ * > <tr><td>close:</td><td>%s</td></tr></table>'
+ * > }
+ *
+ */
+ $.jqplot.Highlighter = function(options) {
+ // Group: Properties
+ //
+ //prop: show
+ // true to show the highlight.
+ this.show = $.jqplot.config.enablePlugins;
+ // prop: markerRenderer
+ // Renderer used to draw the marker of the highlighted point.
+ // Renderer will assimilate attributes from the data point being highlighted,
+ // so no attributes need set on the renderer directly.
+ // Default is to turn off shadow drawing on the highlighted point.
+ this.markerRenderer = new $.jqplot.MarkerRenderer({shadow:false});
+ // prop: showMarker
+ // true to show the marker
+ this.showMarker = true;
+ // prop: lineWidthAdjust
+ // Pixels to add to the lineWidth of the highlight.
+ this.lineWidthAdjust = 2.5;
+ // prop: sizeAdjust
+ // Pixels to add to the overall size of the highlight.
+ this.sizeAdjust = 5;
+ // prop: showTooltip
+ // Show a tooltip with data point values.
+ this.showTooltip = true;
+ // prop: tooltipLocation
+ // Where to position tooltip, 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
+ this.tooltipLocation = 'nw';
+ // prop: fadeTooltip
+ // true = fade in/out tooltip, flase = show/hide tooltip
+ this.fadeTooltip = true;
+ // prop: tooltipFadeSpeed
+ // 'slow', 'def', 'fast', or number of milliseconds.
+ this.tooltipFadeSpeed = "fast";
+ // prop: tooltipOffset
+ // Pixel offset of tooltip from the highlight.
+ this.tooltipOffset = 2;
+ // prop: tooltipAxes
+ // Which axes to display in tooltip, 'x', 'y' or 'both', 'xy' or 'yx'
+ // 'both' and 'xy' are equivalent, 'yx' reverses order of labels.
+ this.tooltipAxes = 'both';
+ // prop; tooltipSeparator
+ // String to use to separate x and y axes in tooltip.
+ this.tooltipSeparator = ', ';
+ // prop; tooltipContentEditor
+ // Function used to edit/augment/replace the formatted tooltip contents.
+ // Called as str = tooltipContentEditor(str, seriesIndex, pointIndex)
+ // where str is the generated tooltip html and seriesIndex and pointIndex identify
+ // the data point being highlighted. Should return the html for the tooltip contents.
+ this.tooltipContentEditor = null;
+ // prop: useAxesFormatters
+ // Use the x and y axes formatters to format the text in the tooltip.
+ this.useAxesFormatters = true;
+ // prop: tooltipFormatString
+ // sprintf format string for the tooltip.
+ // Uses Ash Searle's javascript sprintf implementation
+ // found here: http://hexmen.com/blog/2007/03/printf-sprintf/
+ // See http://perldoc.perl.org/functions/sprintf.html for reference.
+ // Additional "p" and "P" format specifiers added by Chris Leonello.
+ this.tooltipFormatString = '%.5P';
+ // prop: formatString
+ // alternative to tooltipFormatString
+ // will format the whole tooltip text, populating with x, y values as
+ // indicated by tooltipAxes option. So, you could have a tooltip like:
+ // 'Date: %s, number of cats: %d' to format the whole tooltip at one go.
+ // If useAxesFormatters is true, values will be formatted according to
+ // Axes formatters and you can populate your tooltip string with
+ // %s placeholders.
+ this.formatString = null;
+ // prop: yvalues
+ // Number of y values to expect in the data point array.
+ // Typically this is 1. Certain plots, like OHLC, will
+ // have more y values in each data point array.
+ this.yvalues = 1;
+ // prop: bringSeriesToFront
+ // This option requires jQuery 1.4+
+ // True to bring the series of the highlighted point to the front
+ // of other series.
+ this.bringSeriesToFront = false;
+ this._tooltipElem;
+ this.isHighlighting = false;
+ this.currentNeighbor = null;
+
+ $.extend(true, this, options);
+ };
+
+ var locations = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
+ var locationIndicies = {'nw':0, 'n':1, 'ne':2, 'e':3, 'se':4, 's':5, 'sw':6, 'w':7};
+ var oppositeLocations = ['se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'];
+
+ // axis.renderer.tickrenderer.formatter
+
+ // called with scope of plot
+ $.jqplot.Highlighter.init = function (target, data, opts){
+ var options = opts || {};
+ // add a highlighter attribute to the plot
+ this.plugins.highlighter = new $.jqplot.Highlighter(options.highlighter);
+ };
+
+ // called within scope of series
+ $.jqplot.Highlighter.parseOptions = function (defaults, options) {
+ // Add a showHighlight option to the series
+ // and set it to true by default.
+ this.showHighlight = true;
+ };
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ $.jqplot.Highlighter.postPlotDraw = function() {
+ // Memory Leaks patch
+ if (this.plugins.highlighter && this.plugins.highlighter.highlightCanvas) {
+ this.plugins.highlighter.highlightCanvas.resetCanvas();
+ this.plugins.highlighter.highlightCanvas = null;
+ }
+
+ if (this.plugins.highlighter && this.plugins.highlighter._tooltipElem) {
+ this.plugins.highlighter._tooltipElem.emptyForce();
+ this.plugins.highlighter._tooltipElem = null;
+ }
+
+ this.plugins.highlighter.highlightCanvas = new $.jqplot.GenericCanvas();
+
+ this.eventCanvas._elem.before(this.plugins.highlighter.highlightCanvas.createElement(this._gridPadding, 'jqplot-highlight-canvas', this._plotDimensions, this));
+ this.plugins.highlighter.highlightCanvas.setContext();
+
+ var elem = document.createElement('div');
+ this.plugins.highlighter._tooltipElem = $(elem);
+ elem = null;
+ this.plugins.highlighter._tooltipElem.addClass('jqplot-highlighter-tooltip');
+ this.plugins.highlighter._tooltipElem.css({position:'absolute', display:'none'});
+
+ this.eventCanvas._elem.before(this.plugins.highlighter._tooltipElem);
+ };
+
+ $.jqplot.preInitHooks.push($.jqplot.Highlighter.init);
+ $.jqplot.preParseSeriesOptionsHooks.push($.jqplot.Highlighter.parseOptions);
+ $.jqplot.postDrawHooks.push($.jqplot.Highlighter.postPlotDraw);
+
+ function draw(plot, neighbor) {
+ var hl = plot.plugins.highlighter;
+ var s = plot.series[neighbor.seriesIndex];
+ var smr = s.markerRenderer;
+ var mr = hl.markerRenderer;
+ mr.style = smr.style;
+ mr.lineWidth = smr.lineWidth + hl.lineWidthAdjust;
+ mr.size = smr.size + hl.sizeAdjust;
+ var rgba = $.jqplot.getColorComponents(smr.color);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var alpha = (rgba[3] >= 0.6) ? rgba[3]*0.6 : rgba[3]*(2-rgba[3]);
+ mr.color = 'rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+','+alpha+')';
+ mr.init();
+ mr.draw(s.gridData[neighbor.pointIndex][0], s.gridData[neighbor.pointIndex][1], hl.highlightCanvas._ctx);
+ }
+
+ function showTooltip(plot, series, neighbor) {
+ // neighbor looks like: {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]}
+ // gridData should be x,y pixel coords on the grid.
+ // add the plot._gridPadding to that to get x,y in the target.
+ var hl = plot.plugins.highlighter;
+ var elem = hl._tooltipElem;
+ var serieshl = series.highlighter || {};
+
+ var opts = $.extend(true, {}, hl, serieshl);
+
+ if (opts.useAxesFormatters) {
+ var xf = series._xaxis._ticks[0].formatter;
+ var yf = series._yaxis._ticks[0].formatter;
+ var xfstr = series._xaxis._ticks[0].formatString;
+ var yfstr = series._yaxis._ticks[0].formatString;
+ var str;
+ var xstr = xf(xfstr, neighbor.data[0]);
+ var ystrs = [];
+ for (var i=1; i<opts.yvalues+1; i++) {
+ ystrs.push(yf(yfstr, neighbor.data[i]));
+ }
+ if (typeof opts.formatString === 'string') {
+ switch (opts.tooltipAxes) {
+ case 'both':
+ case 'xy':
+ ystrs.unshift(xstr);
+ ystrs.unshift(opts.formatString);
+ str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
+ break;
+ case 'yx':
+ ystrs.push(xstr);
+ ystrs.unshift(opts.formatString);
+ str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
+ break;
+ case 'x':
+ str = $.jqplot.sprintf.apply($.jqplot.sprintf, [opts.formatString, xstr]);
+ break;
+ case 'y':
+ ystrs.unshift(opts.formatString);
+ str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
+ break;
+ default: // same as xy
+ ystrs.unshift(xstr);
+ ystrs.unshift(opts.formatString);
+ str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
+ break;
+ }
+ }
+ else {
+ switch (opts.tooltipAxes) {
+ case 'both':
+ case 'xy':
+ str = xstr;
+ for (var i=0; i<ystrs.length; i++) {
+ str += opts.tooltipSeparator + ystrs[i];
+ }
+ break;
+ case 'yx':
+ str = '';
+ for (var i=0; i<ystrs.length; i++) {
+ str += ystrs[i] + opts.tooltipSeparator;
+ }
+ str += xstr;
+ break;
+ case 'x':
+ str = xstr;
+ break;
+ case 'y':
+ str = ystrs.join(opts.tooltipSeparator);
+ break;
+ default: // same as 'xy'
+ str = xstr;
+ for (var i=0; i<ystrs.length; i++) {
+ str += opts.tooltipSeparator + ystrs[i];
+ }
+ break;
+
+ }
+ }
+ }
+ else {
+ var str;
+ if (typeof opts.formatString === 'string') {
+ str = $.jqplot.sprintf.apply($.jqplot.sprintf, [opts.formatString].concat(neighbor.data));
+ }
+
+ else {
+ if (opts.tooltipAxes == 'both' || opts.tooltipAxes == 'xy') {
+ str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[0]) + opts.tooltipSeparator + $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[1]);
+ }
+ else if (opts.tooltipAxes == 'yx') {
+ str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[1]) + opts.tooltipSeparator + $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[0]);
+ }
+ else if (opts.tooltipAxes == 'x') {
+ str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[0]);
+ }
+ else if (opts.tooltipAxes == 'y') {
+ str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[1]);
+ }
+ }
+ }
+ if ($.isFunction(opts.tooltipContentEditor)) {
+ // args str, seriesIndex, pointIndex are essential so the hook can look up
+ // extra data for the point.
+ str = opts.tooltipContentEditor(str, neighbor.seriesIndex, neighbor.pointIndex, plot);
+ }
+ elem.html(str);
+ var gridpos = {x:neighbor.gridData[0], y:neighbor.gridData[1]};
+ var ms = 0;
+ var fact = 0.707;
+ if (series.markerRenderer.show == true) {
+ ms = (series.markerRenderer.size + opts.sizeAdjust)/2;
+ }
+
+ var loc = locations;
+ if (series.fillToZero && series.fill && neighbor.data[1] < 0) {
+ loc = oppositeLocations;
+ }
+
+ switch (loc[locationIndicies[opts.tooltipLocation]]) {
+ case 'nw':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - fact * ms;
+ var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - fact * ms;
+ break;
+ case 'n':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true)/2;
+ var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - ms;
+ break;
+ case 'ne':
+ var x = gridpos.x + plot._gridPadding.left + opts.tooltipOffset + fact * ms;
+ var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - fact * ms;
+ break;
+ case 'e':
+ var x = gridpos.x + plot._gridPadding.left + opts.tooltipOffset + ms;
+ var y = gridpos.y + plot._gridPadding.top - elem.outerHeight(true)/2;
+ break;
+ case 'se':
+ var x = gridpos.x + plot._gridPadding.left + opts.tooltipOffset + fact * ms;
+ var y = gridpos.y + plot._gridPadding.top + opts.tooltipOffset + fact * ms;
+ break;
+ case 's':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true)/2;
+ var y = gridpos.y + plot._gridPadding.top + opts.tooltipOffset + ms;
+ break;
+ case 'sw':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - fact * ms;
+ var y = gridpos.y + plot._gridPadding.top + opts.tooltipOffset + fact * ms;
+ break;
+ case 'w':
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - ms;
+ var y = gridpos.y + plot._gridPadding.top - elem.outerHeight(true)/2;
+ break;
+ default: // same as 'nw'
+ var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - fact * ms;
+ var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - fact * ms;
+ break;
+ }
+ elem.css('left', x);
+ elem.css('top', y);
+ if (opts.fadeTooltip) {
+ // Fix for stacked up animations. Thnanks Trevor!
+ elem.stop(true,true).fadeIn(opts.tooltipFadeSpeed);
+ }
+ else {
+ elem.show();
+ }
+ elem = null;
+
+ }
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ var hl = plot.plugins.highlighter;
+ var c = plot.plugins.cursor;
+ if (hl.show) {
+ if (neighbor == null && hl.isHighlighting) {
+ var evt = jQuery.Event('jqplotHighlighterUnhighlight');
+ plot.target.trigger(evt);
+
+ var ctx = hl.highlightCanvas._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ if (hl.fadeTooltip) {
+ hl._tooltipElem.fadeOut(hl.tooltipFadeSpeed);
+ }
+ else {
+ hl._tooltipElem.hide();
+ }
+ if (hl.bringSeriesToFront) {
+ plot.restorePreviousSeriesOrder();
+ }
+ hl.isHighlighting = false;
+ hl.currentNeighbor = null;
+ ctx = null;
+ }
+ else if (neighbor != null && plot.series[neighbor.seriesIndex].showHighlight && !hl.isHighlighting) {
+ var evt = jQuery.Event('jqplotHighlighterHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data, plot];
+ plot.target.trigger(evt, ins);
+
+ hl.isHighlighting = true;
+ hl.currentNeighbor = neighbor;
+ if (hl.showMarker) {
+ draw(plot, neighbor);
+ }
+ if (plot.series[neighbor.seriesIndex].show && hl.showTooltip && (!c || !c._zoom.started)) {
+ showTooltip(plot, plot.series[neighbor.seriesIndex], neighbor);
+ }
+ if (hl.bringSeriesToFront) {
+ plot.moveSeriesToFront(neighbor.seriesIndex);
+ }
+ }
+ // check to see if we're highlighting the wrong point.
+ else if (neighbor != null && hl.isHighlighting && hl.currentNeighbor != neighbor) {
+ // highlighting the wrong point.
+
+ // if new series allows highlighting, highlight new point.
+ if (plot.series[neighbor.seriesIndex].showHighlight) {
+ var ctx = hl.highlightCanvas._ctx;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ hl.isHighlighting = true;
+ hl.currentNeighbor = neighbor;
+ if (hl.showMarker) {
+ draw(plot, neighbor);
+ }
+ if (plot.series[neighbor.seriesIndex].show && hl.showTooltip && (!c || !c._zoom.started)) {
+ showTooltip(plot, plot.series[neighbor.seriesIndex], neighbor);
+ }
+ if (hl.bringSeriesToFront) {
+ plot.moveSeriesToFront(neighbor.seriesIndex);
+ }
+ }
+ }
+ }
+ }
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.json2.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.json2.js
new file mode 100644
index 0000000..46fb942
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.json2.js
@@ -0,0 +1,475 @@
+/*
+ 2010-11-01 Chris Leonello
+
+ Slightly modified version of the original json2.js to put JSON
+ functions under the $.jqplot namespace.
+
+ licensing and orignal comments follow:
+
+ http://www.JSON.org/json2.js
+ 2010-08-25
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ $.jqplot.JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array of strings.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or '&nbsp;'),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the value
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array of strings, then it will be
+ used to select the members to be serialized. It filters the results
+ such that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ $.jqplot.JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = $.jqplot.JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = $.jqplot.JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = $.jqplot.JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ $.jqplot.JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = $.jqplot.JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = $.jqplot.JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+*/
+
+(function($) {
+
+ $.jqplot.JSON = window.JSON;
+
+ if (!window.JSON) {
+ $.jqplot.JSON = {};
+ }
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return isFinite(this.valueOf()) ?
+ this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z' : null;
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapable.lastIndex = 0;
+ return escapable.test(string) ?
+ '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string' ? c :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' :
+ '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// Is the value an array?
+
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0 ? '[]' :
+ gap ? '[\n' + gap +
+ partial.join(',\n' + gap) + '\n' +
+ mind + ']' :
+ '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ k = rep[i];
+ if (typeof k === 'string') {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0 ? '{}' :
+ gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+ mind + '}' : '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof $.jqplot.JSON.stringify !== 'function') {
+ $.jqplot.JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('$.jqplot.JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof $.jqplot.JSON.parse !== 'function') {
+ $.jqplot.JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ text = String(text);
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function' ?
+ walk({'': j}, '') : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('$.jqplot.JSON.parse');
+ };
+ }
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.logAxisRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.logAxisRenderer.js
new file mode 100644
index 0000000..3bd3e2a
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.logAxisRenderer.js
@@ -0,0 +1,534 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * class: $.jqplot.LogAxisRenderer
+ * A plugin for a jqPlot to render a logarithmic axis.
+ *
+ * To use this renderer, include the plugin in your source
+ * > <script type="text/javascript" language="javascript" src="plugins/jqplot.logAxisRenderer.js"></script>
+ *
+ * and supply the appropriate options to your plot
+ *
+ * > {axes:{xaxis:{renderer:$.jqplot.LogAxisRenderer}}}
+ **/
+ $.jqplot.LogAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ // prop: axisDefaults
+ // Default properties which will be applied directly to the series.
+ //
+ // Group: Properties
+ //
+ // Properties
+ //
+ // base - the logarithmic base, commonly 2, 10 or Math.E
+ // tickDistribution - Deprecated. "power" distribution of ticks
+ // always used. Option has no effect.
+ this.axisDefaults = {
+ base : 10,
+ tickDistribution :'power'
+ };
+ };
+
+ $.jqplot.LogAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.LogAxisRenderer.prototype.constructor = $.jqplot.LogAxisRenderer;
+
+ $.jqplot.LogAxisRenderer.prototype.init = function(options) {
+ // prop: drawBaseline
+ // True to draw the axis baseline.
+ this.drawBaseline = true;
+ // prop: minorTicks
+ // Number of ticks to add between "major" ticks.
+ // Major ticks are ticks supplied by user or auto computed.
+ // Minor ticks cannot be created by user.
+ this.minorTicks = 'auto';
+ this._scalefact = 1.0;
+
+ $.extend(true, this, options);
+
+ this._autoFormatString = '%d';
+ this._overrideFormatString = false;
+
+ for (var d in this.renderer.axisDefaults) {
+ if (this[d] == null) {
+ this[d] = this.renderer.axisDefaults[d];
+ }
+ }
+
+ this.resetDataBounds();
+ };
+
+ $.jqplot.LogAxisRenderer.prototype.createTicks = function(plot) {
+ // we're are operating on an axis here
+ var ticks = this._ticks;
+ var userTicks = this.ticks;
+ var name = this.name;
+ var db = this._dataBounds;
+ var dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
+ var interval;
+ var min, max;
+ var pos1, pos2;
+ var tt, i;
+
+ var threshold = 30;
+ // For some reason scalefactor is screwing up ticks.
+ this._scalefact = (Math.max(dim, threshold+1) - threshold)/300;
+
+ // if we already have ticks, use them.
+ // ticks must be in order of increasing value.
+ if (userTicks.length) {
+ // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+ for (i=0; i<userTicks.length; i++){
+ var ut = userTicks[i];
+ var t = new this.tickRenderer(this.tickOptions);
+ if (ut.constructor == Array) {
+ t.value = ut[0];
+ t.label = ut[1];
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(ut[0], this.name);
+ this._ticks.push(t);
+ }
+
+ else if ($.isPlainObject(ut)) {
+ $.extend(true, t, ut);
+ t.axis = this.name;
+ this._ticks.push(t);
+ }
+
+ else {
+ t.value = ut;
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(ut, this.name);
+ this._ticks.push(t);
+ }
+ }
+ this.numberTicks = userTicks.length;
+ this.min = this._ticks[0].value;
+ this.max = this._ticks[this.numberTicks-1].value;
+ }
+
+ // we don't have any ticks yet, let's make some!
+ else if (this.min == null && this.max == null) {
+ min = db.min * (2 - this.padMin);
+ max = db.max * this.padMax;
+
+ // if min and max are same, space them out a bit
+ if (min == max) {
+ var adj = 0.05;
+ min = min*(1-adj);
+ max = max*(1+adj);
+ }
+
+ // perform some checks
+ if (this.min != null && this.min <= 0) {
+ throw new Error("Log axis minimum must be greater than 0");
+ }
+ if (this.max != null && this.max <= 0) {
+ throw new Error("Log axis maximum must be greater than 0");
+ }
+
+ function findCeil (val) {
+ var order = Math.pow(10, Math.floor(Math.log(val)/Math.LN10));
+ return Math.ceil(val/order) * order;
+ }
+
+ function findFloor(val) {
+ var order = Math.pow(10, Math.floor(Math.log(val)/Math.LN10));
+ return Math.floor(val/order) * order;
+ }
+
+ // var range = max - min;
+ var rmin, rmax;
+
+ // for power distribution, open up range to get a nice power of axis.renderer.base.
+ // power distribution won't respect the user's min/max settings.
+ rmin = Math.pow(this.base, Math.floor(Math.log(min)/Math.log(this.base)));
+ rmax = Math.pow(this.base, Math.ceil(Math.log(max)/Math.log(this.base)));
+
+ // // if min and max are same, space them out a bit
+ // if (rmin === rmax) {
+ // var adj = 0.05;
+ // rmin = rmin*(1-adj);
+ // rmax = rmax*(1+adj);
+ // }
+
+ // Handle case where a data value was zero
+ if (rmin === 0) {
+ rmin = 1;
+ }
+
+ var order = Math.round(Math.log(rmin)/Math.LN10);
+
+ if (this.tickOptions == null || !this.tickOptions.formatString) {
+ this._overrideFormatString = true;
+ }
+
+ this.min = rmin;
+ this.max = rmax;
+ var range = this.max - this.min;
+
+ var minorTicks = (this.minorTicks === 'auto') ? 0 : this.minorTicks;
+ var numberTicks;
+ if (this.numberTicks == null){
+ if (dim > 140) {
+ numberTicks = Math.round(Math.log(this.max/this.min)/Math.log(this.base) + 1);
+ if (numberTicks < 2) {
+ numberTicks = 2;
+ }
+ if (minorTicks === 0) {
+ var temp = dim/(numberTicks - 1);
+ if (temp < 100) {
+ minorTicks = 0;
+ }
+ else if (temp < 190) {
+ minorTicks = 1;
+ }
+ else if (temp < 250) {
+ minorTicks = 3;
+ }
+ else if (temp < 600) {
+ minorTicks = 4;
+ }
+ else {
+ minorTicks = 9;
+ }
+ }
+ }
+ else {
+ numberTicks = 2;
+ if (minorTicks === 0) {
+ minorTicks = 1;
+ }
+ minorTicks = 0;
+ }
+ }
+ else {
+ numberTicks = this.numberTicks;
+ }
+
+ if (order >= 0 && minorTicks !== 3) {
+ this._autoFormatString = '%d';
+ }
+ // Adjust format string for case with 3 ticks where we'll have like 1, 2.5, 5, 7.5, 10
+ else if (order <= 0 && minorTicks === 3) {
+ var temp = -(order - 1);
+ this._autoFormatString = '%.'+ Math.abs(order-1) + 'f';
+ }
+
+ // Adjust format string for values less than 1.
+ else if (order < 0) {
+ var temp = -order;
+ this._autoFormatString = '%.'+ Math.abs(order) + 'f';
+ }
+
+ else {
+ this._autoFormatString = '%d';
+ }
+
+ var to, t, val, tt1, spread, interval;
+ for (var i=0; i<numberTicks; i++){
+ tt = Math.pow(this.base, i - numberTicks + 1) * this.max;
+
+ t = new this.tickRenderer(this.tickOptions);
+
+ if (this._overrideFormatString) {
+ t.formatString = this._autoFormatString;
+ }
+
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+
+ if (minorTicks && i<numberTicks-1) {
+ tt1 = Math.pow(this.base, i - numberTicks + 2) * this.max;
+ spread = tt1 - tt;
+ interval = tt1 / (minorTicks+1);
+ for (var j=minorTicks-1; j>=0; j--) {
+ val = tt1-interval*(j+1);
+ t = new this.tickRenderer(this.tickOptions);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(val, this.name);
+ this._ticks.push(t);
+ }
+ }
+ }
+ }
+
+ // min and max are set as would be the case with zooming
+ else if (this.min != null && this.max != null) {
+ var opts = $.extend(true, {}, this.tickOptions, {name: this.name, value: null});
+ var nt, ti;
+ // don't have an interval yet, pick one that gives the most
+ // "round" ticks we can get.
+ if (this.numberTicks == null && this.tickInterval == null) {
+ // var threshold = 30;
+ var tdim = Math.max(dim, threshold+1);
+ var nttarget = Math.ceil((tdim-threshold)/35 + 1);
+
+ var ret = $.jqplot.LinearTickGenerator.bestConstrainedInterval(this.min, this.max, nttarget);
+
+ this._autoFormatString = ret[3];
+ nt = ret[2];
+ ti = ret[4];
+
+ for (var i=0; i<nt; i++) {
+ opts.value = this.min + i * ti;
+ t = new this.tickRenderer(opts);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ this._ticks.push(t);
+ }
+ }
+
+ // for loose zoom, number ticks and interval are also set.
+ else if (this.numberTicks != null && this.tickInterval != null) {
+ nt = this.numberTicks;
+ for (var i=0; i<nt; i++) {
+ opts.value = this.min + i * this.tickInterval;
+ t = new this.tickRenderer(opts);
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ t.formatString = this._autoFormatString;
+ }
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ this._ticks.push(t);
+ }
+ }
+ }
+ };
+
+ $.jqplot.LogAxisRenderer.prototype.pack = function(pos, offsets) {
+ var lb = parseInt(this.base, 10);
+ var ticks = this._ticks;
+ var trans = function (v) { return Math.log(v)/Math.log(lb); };
+ var invtrans = function (v) { return Math.pow(Math.E, (Math.log(lb)*v)); };
+ var max = trans(this.max);
+ var min = trans(this.min);
+ var offmax = offsets.max;
+ var offmin = offsets.min;
+ var lshow = (this._label == null) ? false : this._label.show;
+
+ for (var p in pos) {
+ this._elem.css(p, pos[p]);
+ }
+
+ this._offsets = offsets;
+ // pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
+ var pixellength = offmax - offmin;
+ var unitlength = max - min;
+
+ // point to unit and unit to point conversions references to Plot DOM element top left corner.
+ this.p2u = function(p){
+ return invtrans((p - offmin) * unitlength / pixellength + min);
+ };
+
+ this.u2p = function(u){
+ return (trans(u) - min) * pixellength / unitlength + offmin;
+ };
+
+ if (this.name == 'xaxis' || this.name == 'x2axis'){
+ this.series_u2p = function(u){
+ return (trans(u) - min) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return invtrans(p * unitlength / pixellength + min);
+ };
+ }
+ // yaxis is max at top of canvas.
+ else {
+ this.series_u2p = function(u){
+ return (trans(u) - max) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return invtrans(p * unitlength / pixellength + max);
+ };
+ }
+
+ if (this.show) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ if (t.angle < 0) {
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ }
+ // position at start
+ else {
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'end':
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ case 'start':
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ break;
+ case 'middle':
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ default:
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getWidth()/2;
+ }
+ // var shim = t.getWidth()/2;
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('left', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var w = this._label._elem.outerWidth(true);
+ this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
+ if (this.name == 'xaxis') {
+ this._label._elem.css('bottom', '0px');
+ }
+ else {
+ this._label._elem.css('top', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ else {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ case 'end':
+ if (t.angle < 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'start':
+ if (t.angle > 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'middle':
+ // if (t.angle > 0) {
+ // shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ // }
+ // else {
+ // shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ // }
+ shim = -t.getHeight()/2;
+ break;
+ default:
+ shim = -t.getHeight()/2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getHeight()/2;
+ }
+
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('top', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var h = this._label._elem.outerHeight(true);
+ this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
+ if (this.name == 'yaxis') {
+ this._label._elem.css('left', '0px');
+ }
+ else {
+ this._label._elem.css('right', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ }
+ };
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js
new file mode 100644
index 0000000..b90c5bc
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js
@@ -0,0 +1,611 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ // class: $.jqplot.MekkoAxisRenderer
+ // An axis renderer for a Mekko chart.
+ // Should be used with a Mekko chart where the mekkoRenderer is used on the series.
+ // Displays the Y axis as a range from 0 to 1 (0 to 100%) and the x axis with a tick
+ // for each series scaled to the sum of all the y values.
+ $.jqplot.MekkoAxisRenderer = function() {
+ };
+
+ // called with scope of axis object.
+ $.jqplot.MekkoAxisRenderer.prototype.init = function(options){
+ // prop: tickMode
+ // How to space the ticks on the axis.
+ // 'bar' will place a tick at the width of each bar.
+ // This is the default for the x axis.
+ // 'even' will place ticks at even intervals. This is
+ // the default for x2 axis and y axis. y axis cannot be changed.
+ this.tickMode;
+ // prop: barLabelRenderer
+ // renderer to use to draw labels under each bar.
+ this.barLabelRenderer = $.jqplot.AxisLabelRenderer;
+ // prop: barLabels
+ // array of labels to put under each bar.
+ this.barLabels = this.barLabels || [];
+ // prop: barLabelOptions
+ // options object to pass to the bar label renderer.
+ this.barLabelOptions = {};
+ this.tickOptions = $.extend(true, {showGridline:false}, this.tickOptions);
+ this._barLabels = [];
+ $.extend(true, this, options);
+ if (this.name == 'yaxis') {
+ this.tickOptions.formatString = this.tickOptions.formatString || "%d\%";
+ }
+ var db = this._dataBounds;
+ db.min = 0;
+ // for y axes, scale always go from 0 to 1 (0 to 100%)
+ if (this.name == 'yaxis' || this.name == 'y2axis') {
+ db.max = 100;
+ this.tickMode = 'even';
+ }
+ // For x axes, scale goes from 0 to sum of all y values.
+ else if (this.name == 'xaxis'){
+ this.tickMode = (this.tickMode == null) ? 'bar' : this.tickMode;
+ for (var i=0; i<this._series.length; i++) {
+ db.max += this._series[i]._sumy;
+ }
+ }
+ else if (this.name == 'x2axis'){
+ this.tickMode = (this.tickMode == null) ? 'even' : this.tickMode;
+ for (var i=0; i<this._series.length; i++) {
+ db.max += this._series[i]._sumy;
+ }
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.MekkoAxisRenderer.prototype.draw = function(ctx, plot) {
+ if (this.show) {
+ // populate the axis label and value properties.
+ // createTicks is a method on the renderer, but
+ // call it within the scope of the axis.
+ this.renderer.createTicks.call(this);
+ // fill a div with axes labels in the right direction.
+ // Need to pregenerate each axis to get its bounds and
+ // position it and the labels correctly on the plot.
+ var dim=0;
+ var temp;
+
+ var elem = document.createElement('div');
+ this._elem = $(elem);
+ this._elem.addClass('jqplot-axis jqplot-'+this.name);
+ this._elem.css('position', 'absolute');
+ elem = null;
+
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ this._elem.width(this._plotDimensions.width);
+ }
+ else {
+ this._elem.height(this._plotDimensions.height);
+ }
+
+ // draw the axis label
+ // create a _label object.
+ this.labelOptions.axis = this.name;
+ this._label = new this.labelRenderer(this.labelOptions);
+ if (this._label.show) {
+ this._elem.append(this._label.draw(ctx));
+ }
+
+ var t, tick, elem;
+ if (this.showTicks) {
+ t = this._ticks;
+ for (var i=0; i<t.length; i++) {
+ tick = t[i];
+ if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
+ this._elem.append(tick.draw(ctx));
+ }
+ }
+ }
+
+ // draw the series labels
+ for (i=0; i<this.barLabels.length; i++) {
+ this.barLabelOptions.axis = this.name;
+ this.barLabelOptions.label = this.barLabels[i];
+ this._barLabels.push(new this.barLabelRenderer(this.barLabelOptions));
+ if (this.tickMode != 'bar') {
+ this._barLabels[i].show = false;
+ }
+ if (this._barLabels[i].show) {
+ var elem = this._barLabels[i].draw(ctx, plot);
+ elem.removeClass('jqplot-'+this.name+'-label');
+ elem.addClass('jqplot-'+this.name+'-tick');
+ elem.addClass('jqplot-mekko-barLabel');
+ elem.appendTo(this._elem);
+ elem = null;
+ }
+ }
+
+ }
+ return this._elem;
+ };
+
+ // called with scope of an axis
+ $.jqplot.MekkoAxisRenderer.prototype.reset = function() {
+ this.min = this._min;
+ this.max = this._max;
+ this.tickInterval = this._tickInterval;
+ this.numberTicks = this._numberTicks;
+ // this._ticks = this.__ticks;
+ };
+
+ // called with scope of axis
+ $.jqplot.MekkoAxisRenderer.prototype.set = function() {
+ var dim = 0;
+ var temp;
+ var w = 0;
+ var h = 0;
+ var lshow = (this._label == null) ? false : this._label.show;
+ if (this.show && this.showTicks) {
+ var t = this._ticks;
+ for (var i=0; i<t.length; i++) {
+ var tick = t[i];
+ if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ temp = tick._elem.outerHeight(true);
+ }
+ else {
+ temp = tick._elem.outerWidth(true);
+ }
+ if (temp > dim) {
+ dim = temp;
+ }
+ }
+ }
+
+ if (lshow) {
+ w = this._label._elem.outerWidth(true);
+ h = this._label._elem.outerHeight(true);
+ }
+ if (this.name == 'xaxis') {
+ dim = dim + h;
+ this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
+ }
+ else if (this.name == 'x2axis') {
+ dim = dim + h;
+ this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
+ }
+ else if (this.name == 'yaxis') {
+ dim = dim + w;
+ this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ else {
+ dim = dim + w;
+ this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.MekkoAxisRenderer.prototype.createTicks = function() {
+ // we're are operating on an axis here
+ var ticks = this._ticks;
+ var userTicks = this.ticks;
+ var name = this.name;
+ // databounds were set on axis initialization.
+ var db = this._dataBounds;
+ var dim, interval;
+ var min, max;
+ var pos1, pos2;
+ var t, tt, i, j;
+
+ // if we already have ticks, use them.
+ // ticks must be in order of increasing value.
+
+ if (userTicks.length) {
+ // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+ for (i=0; i<userTicks.length; i++){
+ var ut = userTicks[i];
+ var t = new this.tickRenderer(this.tickOptions);
+ if (ut.constructor == Array) {
+ t.value = ut[0];
+ t.label = ut[1];
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(ut[0], this.name);
+ this._ticks.push(t);
+ }
+
+ else {
+ t.value = ut;
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(ut, this.name);
+ this._ticks.push(t);
+ }
+ }
+ this.numberTicks = userTicks.length;
+ this.min = this._ticks[0].value;
+ this.max = this._ticks[this.numberTicks-1].value;
+ this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
+ }
+
+ // we don't have any ticks yet, let's make some!
+ else {
+ if (name == 'xaxis' || name == 'x2axis') {
+ dim = this._plotDimensions.width;
+ }
+ else {
+ dim = this._plotDimensions.height;
+ }
+
+ // if min, max and number of ticks specified, user can't specify interval.
+ if (this.min != null && this.max != null && this.numberTicks != null) {
+ this.tickInterval = null;
+ }
+
+ min = (this.min != null) ? this.min : db.min;
+ max = (this.max != null) ? this.max : db.max;
+
+ // if min and max are same, space them out a bit.+
+ if (min == max) {
+ var adj = 0.05;
+ if (min > 0) {
+ adj = Math.max(Math.log(min)/Math.LN10, 0.05);
+ }
+ min -= adj;
+ max += adj;
+ }
+
+ var range = max - min;
+ var rmin, rmax;
+ var temp, prev, curr;
+ var ynumticks = [3,5,6,11,21];
+
+ // yaxis divide ticks in nice intervals from 0 to 1.
+ if (this.name == 'yaxis' || this.name == 'y2axis') {
+ this.min = 0;
+ this.max = 100;
+ // user didn't specify number of ticks.
+ if (!this.numberTicks){
+ if (this.tickInterval) {
+ this.numberTicks = 3 + Math.ceil(range / this.tickInterval);
+ }
+ else {
+ temp = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
+ for (i=0; i<ynumticks.length; i++) {
+ curr = temp/ynumticks[i];
+ if (curr == 1) {
+ this.numberTicks = ynumticks[i];
+ break;
+ }
+ else if (curr > 1) {
+ prev = curr;
+ continue;
+ }
+ else if (curr < 1) {
+ // was prev or is curr closer to one?
+ if (Math.abs(prev - 1) < Math.abs(curr - 1)) {
+ this.numberTicks = ynumticks[i-1];
+ break;
+ }
+ else {
+ this.numberTicks = ynumticks[i];
+ break;
+ }
+ }
+ else if (i == ynumticks.length -1) {
+ this.numberTicks = ynumticks[i];
+ }
+ }
+ this.tickInterval = range / (this.numberTicks - 1);
+ }
+ }
+
+ // user did specify number of ticks.
+ else {
+ this.tickInterval = range / (this.numberTicks - 1);
+ }
+
+ for (var i=0; i<this.numberTicks; i++){
+ tt = this.min + i * this.tickInterval;
+ t = new this.tickRenderer(this.tickOptions);
+ // var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+ }
+ }
+
+ // for x axes, have number ot ticks equal to number of series and ticks placed
+ // at sum of y values for each series.
+ else if (this.tickMode == 'bar') {
+ this.min = 0;
+ this.numberTicks = this._series.length + 1;
+ t = new this.tickRenderer(this.tickOptions);
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(0, this.name);
+ this._ticks.push(t);
+
+ temp = 0;
+
+ for (i=1; i<this.numberTicks; i++){
+ temp += this._series[i-1]._sumy;
+ t = new this.tickRenderer(this.tickOptions);
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(temp, this.name);
+ this._ticks.push(t);
+ }
+ this.max = this.max || temp;
+
+ // if user specified a max and it is greater than sum, add a tick
+ if (this.max > temp) {
+ t = new this.tickRenderer(this.tickOptions);
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(this.max, this.name);
+ this._ticks.push(t);
+
+ }
+ }
+
+ else if (this.tickMode == 'even') {
+ this.min = 0;
+ this.max = this.max || db.max;
+ // get a desired number of ticks
+ var nt = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
+ range = this.max - this.min;
+ this.numberTicks = nt;
+ this.tickInterval = range / (this.numberTicks - 1);
+
+ for (i=0; i<this.numberTicks; i++){
+ tt = this.min + i * this.tickInterval;
+ t = new this.tickRenderer(this.tickOptions);
+ // var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
+ if (!this.showTicks) {
+ t.showLabel = false;
+ t.showMark = false;
+ }
+ else if (!this.showTickMarks) {
+ t.showMark = false;
+ }
+ t.setTick(tt, this.name);
+ this._ticks.push(t);
+ }
+
+ }
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.MekkoAxisRenderer.prototype.pack = function(pos, offsets) {
+ var ticks = this._ticks;
+ var max = this.max;
+ var min = this.min;
+ var offmax = offsets.max;
+ var offmin = offsets.min;
+ var lshow = (this._label == null) ? false : this._label.show;
+
+ for (var p in pos) {
+ this._elem.css(p, pos[p]);
+ }
+
+ this._offsets = offsets;
+ // pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
+ var pixellength = offmax - offmin;
+ var unitlength = max - min;
+
+ // point to unit and unit to point conversions references to Plot DOM element top left corner.
+ this.p2u = function(p){
+ return (p - offmin) * unitlength / pixellength + min;
+ };
+
+ this.u2p = function(u){
+ return (u - min) * pixellength / unitlength + offmin;
+ };
+
+ if (this.name == 'xaxis' || this.name == 'x2axis'){
+ this.series_u2p = function(u){
+ return (u - min) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+
+ else {
+ this.series_u2p = function(u){
+ return (u - max) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + max;
+ };
+ }
+
+ if (this.show) {
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'xaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ if (temp * t.angle < 0) {
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ }
+ // position at start
+ else {
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'end':
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ case 'start':
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ break;
+ case 'middle':
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ default:
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getWidth()/2;
+ }
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('left', val);
+ t.pack();
+ }
+ }
+ var w;
+ if (lshow) {
+ w = this._label._elem.outerWidth(true);
+ this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
+ if (this.name == 'xaxis') {
+ this._label._elem.css('bottom', '0px');
+ }
+ else {
+ this._label._elem.css('top', '0px');
+ }
+ this._label.pack();
+ }
+ // now show the labels under the bars.
+ var b, l, r;
+ for (var i=0; i<this.barLabels.length; i++) {
+ b = this._barLabels[i];
+ if (b.show) {
+ w = b.getWidth();
+ l = this._ticks[i].getLeft() + this._ticks[i].getWidth();
+ r = this._ticks[i+1].getLeft();
+ b._elem.css('left', (r+l-w)/2+'px');
+ b._elem.css('top', this._ticks[i]._elem.css('top'));
+ b.pack();
+ }
+ }
+ }
+ else {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'yaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ case 'end':
+ if (temp * t.angle < 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'start':
+ if (t.angle > 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'middle':
+ shim = -t.getHeight()/2;
+ break;
+ default:
+ shim = -t.getHeight()/2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getHeight()/2;
+ }
+
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('top', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var h = this._label._elem.outerHeight(true);
+ this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
+ if (this.name == 'yaxis') {
+ this._label._elem.css('left', '0px');
+ }
+ else {
+ this._label._elem.css('right', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ }
+ };
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoRenderer.js
new file mode 100644
index 0000000..05f495f
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.mekkoRenderer.js
@@ -0,0 +1,437 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.MekkoRenderer
+ * Draws a Mekko style chart which shows 3 dimensional data on a 2 dimensional graph.
+ * the <$.jqplot.MekkoAxisRenderer> should be used with mekko charts. The mekko renderer
+ * overrides the default legend renderer with its own $.jqplot.MekkoLegendRenderer
+ * which allows more flexibility to specify number of rows and columns in the legend.
+ *
+ * Data is specified per bar in the chart. You can specify data as an array of y values, or as
+ * an array of [label, value] pairs. Note that labels are used only on the first series.
+ * Labels on subsequent series are ignored:
+ *
+ * > bar1 = [['shirts', 8],['hats', 14],['shoes', 6],['gloves', 16],['dolls', 12]];
+ * > bar2 = [15,6,9,13,6];
+ * > bar3 = [['grumpy',4],['sneezy',2],['happy',7],['sleepy',9],['doc',7]];
+ *
+ * If you want to place labels for each bar under the axis, you use the barLabels option on
+ * the axes. The bar labels can be styled with the ".jqplot-mekko-barLabel" css class.
+ *
+ * > barLabels = ['Mickey Mouse', 'Donald Duck', 'Goofy'];
+ * > axes:{xaxis:{barLabels:barLabels}}
+ *
+ */
+
+
+ $.jqplot.MekkoRenderer = function(){
+ this.shapeRenderer = new $.jqplot.ShapeRenderer();
+ // prop: borderColor
+ // color of the borders between areas on the chart
+ this.borderColor = null;
+ // prop: showBorders
+ // True to draw borders lines between areas on the chart.
+ // False will draw borders lines with the same color as the area.
+ this.showBorders = true;
+ };
+
+ // called with scope of series.
+ $.jqplot.MekkoRenderer.prototype.init = function(options, plot) {
+ this.fill = false;
+ this.fillRect = true;
+ this.strokeRect = true;
+ this.shadow = false;
+ // width of bar on x axis.
+ this._xwidth = 0;
+ this._xstart = 0;
+ $.extend(true, this.renderer, options);
+ // set the shape renderer options
+ var opts = {lineJoin:'miter', lineCap:'butt', isarc:false, fillRect:this.fillRect, strokeRect:this.strokeRect};
+ this.renderer.shapeRenderer.init(opts);
+ plot.axes.x2axis._series.push(this);
+ this._type = 'mekko';
+ };
+
+ // Method: setGridData
+ // converts the user data values to grid coordinates and stores them
+ // in the gridData array. Will convert user data into appropriate
+ // rectangles.
+ // Called with scope of a series.
+ $.jqplot.MekkoRenderer.prototype.setGridData = function(plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var data = this._plotData;
+ this.gridData = [];
+ // figure out width on x axis.
+ // this._xwidth = this._sumy / plot._sumy * this.canvas.getWidth();
+ this._xwidth = xp(this._sumy) - xp(0);
+ if (this.index>0) {
+ this._xstart = plot.series[this.index-1]._xstart + plot.series[this.index-1]._xwidth;
+ }
+ var totheight = this.canvas.getHeight();
+ var sumy = 0;
+ var cury;
+ var curheight;
+ for (var i=0; i<data.length; i++) {
+ if (data[i] != null) {
+ sumy += data[i][1];
+ cury = totheight - (sumy / this._sumy * totheight);
+ curheight = data[i][1] / this._sumy * totheight;
+ this.gridData.push([this._xstart, cury, this._xwidth, curheight]);
+ }
+ }
+ };
+
+ // Method: makeGridData
+ // converts any arbitrary data values to grid coordinates and
+ // returns them. This method exists so that plugins can use a series'
+ // linerenderer to generate grid data points without overwriting the
+ // grid data associated with that series.
+ // Called with scope of a series.
+ $.jqplot.MekkoRenderer.prototype.makeGridData = function(data, plot) {
+ // recalculate the grid data
+ // figure out width on x axis.
+ var xp = this._xaxis.series_u2p;
+ var totheight = this.canvas.getHeight();
+ var sumy = 0;
+ var cury;
+ var curheight;
+ var gd = [];
+ for (var i=0; i<data.length; i++) {
+ if (data[i] != null) {
+ sumy += data[i][1];
+ cury = totheight - (sumy / this._sumy * totheight);
+ curheight = data[i][1] / this._sumy * totheight;
+ gd.push([this._xstart, cury, this._xwidth, curheight]);
+ }
+ }
+ return gd;
+ };
+
+
+ // called within scope of series.
+ $.jqplot.MekkoRenderer.prototype.draw = function(ctx, gd, options) {
+ var i;
+ var opts = (options != undefined) ? options : {};
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
+ ctx.save();
+ if (gd.length) {
+ if (showLine) {
+ for (i=0; i<gd.length; i++){
+ opts.fillStyle = colorGenerator.next();
+ if (this.renderer.showBorders) {
+ opts.strokeStyle = this.renderer.borderColor;
+ }
+ else {
+ opts.strokeStyle = opts.fillStyle;
+ }
+ this.renderer.shapeRenderer.draw(ctx, gd[i], opts);
+ }
+ }
+ }
+
+ ctx.restore();
+ };
+
+ $.jqplot.MekkoRenderer.prototype.drawShadow = function(ctx, gd, options) {
+ // This is a no-op, no shadows on mekko charts.
+ };
+
+ /**
+ * Class: $.jqplot.MekkoLegendRenderer
+ * Legend renderer used by mekko charts with options for
+ * controlling number or rows and columns as well as placement
+ * outside of plot area.
+ *
+ */
+ $.jqplot.MekkoLegendRenderer = function(){
+ //
+ };
+
+ $.jqplot.MekkoLegendRenderer.prototype.init = function(options) {
+ // prop: numberRows
+ // Maximum number of rows in the legend. 0 or null for unlimited.
+ this.numberRows = null;
+ // prop: numberColumns
+ // Maximum number of columns in the legend. 0 or null for unlimited.
+ this.numberColumns = null;
+ // this will override the placement option on the Legend object
+ this.placement = "outside";
+ $.extend(true, this, options);
+ };
+
+ // called with scope of legend
+ $.jqplot.MekkoLegendRenderer.prototype.draw = function() {
+ var legend = this;
+ if (this.show) {
+ var series = this._series;
+ var ss = 'position:absolute;';
+ ss += (this.background) ? 'background:'+this.background+';' : '';
+ ss += (this.border) ? 'border:'+this.border+';' : '';
+ ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
+ ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
+ ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
+ this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
+ // Mekko charts legends don't go by number of series, but by number of data points
+ // in the series. Refactor things here for that.
+
+ var pad = false,
+ reverse = true, // mekko charts are always stacked, so reverse
+ nr, nc;
+ var s = series[0];
+ var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
+
+ if (s.show) {
+ var pd = s.data;
+ if (this.numberRows) {
+ nr = this.numberRows;
+ if (!this.numberColumns){
+ nc = Math.ceil(pd.length/nr);
+ }
+ else{
+ nc = this.numberColumns;
+ }
+ }
+ else if (this.numberColumns) {
+ nc = this.numberColumns;
+ nr = Math.ceil(pd.length/this.numberColumns);
+ }
+ else {
+ nr = pd.length;
+ nc = 1;
+ }
+
+ var i, j, tr, td1, td2, lt, rs, color;
+ var idx = 0;
+
+ for (i=0; i<nr; i++) {
+ if (reverse){
+ tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
+ }
+ else{
+ tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
+ }
+ for (j=0; j<nc; j++) {
+ if (idx < pd.length) {
+ lt = this.labels[idx] || pd[idx][0].toString();
+ color = colorGenerator.next();
+ if (!reverse){
+ if (i>0){
+ pad = true;
+ }
+ else{
+ pad = false;
+ }
+ }
+ else{
+ if (i == nr -1){
+ pad = false;
+ }
+ else{
+ pad = true;
+ }
+ }
+ rs = (pad) ? this.rowSpacing : '0';
+
+ td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
+ '<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
+ '</div></td>');
+ td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
+ if (this.escapeHtml){
+ td2.text(lt);
+ }
+ else {
+ td2.html(lt);
+ }
+ if (reverse) {
+ td2.prependTo(tr);
+ td1.prependTo(tr);
+ }
+ else {
+ td1.appendTo(tr);
+ td2.appendTo(tr);
+ }
+ pad = true;
+ }
+ idx++;
+ }
+ }
+
+ tr = null;
+ td1 = null;
+ td2 = null;
+ }
+ }
+ return this._elem;
+ };
+
+ $.jqplot.MekkoLegendRenderer.prototype.pack = function(offsets) {
+ if (this.show) {
+ // fake a grid for positioning
+ var grid = {_top:offsets.top, _left:offsets.left, _right:offsets.right, _bottom:this._plotDimensions.height - offsets.bottom};
+ if (this.placement == 'insideGrid') {
+ switch (this.location) {
+ case 'nw':
+ var a = grid._left + this.xoffset;
+ var b = grid._top + this.yoffset;
+ this._elem.css('left', a);
+ this._elem.css('top', b);
+ break;
+ case 'n':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = grid._top + this.yoffset;
+ this._elem.css('left', a);
+ this._elem.css('top', b);
+ break;
+ case 'ne':
+ var a = offsets.right + this.xoffset;
+ var b = grid._top + this.yoffset;
+ this._elem.css({right:a, top:b});
+ break;
+ case 'e':
+ var a = offsets.right + this.xoffset;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({right:a, top:b});
+ break;
+ case 'se':
+ var a = offsets.right + this.xoffset;
+ var b = offsets.bottom + this.yoffset;
+ this._elem.css({right:a, bottom:b});
+ break;
+ case 's':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = offsets.bottom + this.yoffset;
+ this._elem.css({left:a, bottom:b});
+ break;
+ case 'sw':
+ var a = grid._left + this.xoffset;
+ var b = offsets.bottom + this.yoffset;
+ this._elem.css({left:a, bottom:b});
+ break;
+ case 'w':
+ var a = grid._left + this.xoffset;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({left:a, top:b});
+ break;
+ default: // same as 'se'
+ var a = grid._right - this.xoffset;
+ var b = grid._bottom + this.yoffset;
+ this._elem.css({right:a, bottom:b});
+ break;
+ }
+
+ }
+ else {
+ switch (this.location) {
+ case 'nw':
+ var a = this._plotDimensions.width - grid._left + this.xoffset;
+ var b = grid._top + this.yoffset;
+ this._elem.css('right', a);
+ this._elem.css('top', b);
+ break;
+ case 'n':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = this._plotDimensions.height - grid._top + this.yoffset;
+ this._elem.css('left', a);
+ this._elem.css('bottom', b);
+ break;
+ case 'ne':
+ var a = this._plotDimensions.width - offsets.right + this.xoffset;
+ var b = grid._top + this.yoffset;
+ this._elem.css({left:a, top:b});
+ break;
+ case 'e':
+ var a = this._plotDimensions.width - offsets.right + this.xoffset;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({left:a, top:b});
+ break;
+ case 'se':
+ var a = this._plotDimensions.width - offsets.right + this.xoffset;
+ var b = offsets.bottom + this.yoffset;
+ this._elem.css({left:a, bottom:b});
+ break;
+ case 's':
+ var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
+ var b = this._plotDimensions.height - offsets.bottom + this.yoffset;
+ this._elem.css({left:a, top:b});
+ break;
+ case 'sw':
+ var a = this._plotDimensions.width - grid._left + this.xoffset;
+ var b = offsets.bottom + this.yoffset;
+ this._elem.css({right:a, bottom:b});
+ break;
+ case 'w':
+ var a = this._plotDimensions.width - grid._left + this.xoffset;
+ var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
+ this._elem.css({right:a, top:b});
+ break;
+ default: // same as 'se'
+ var a = grid._right - this.xoffset;
+ var b = grid._bottom + this.yoffset;
+ this._elem.css({right:a, bottom:b});
+ break;
+ }
+ }
+ }
+ };
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.legend = options.legend || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.MekkoRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.MekkoRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.MekkoAxisRenderer;
+ options.legend.renderer = $.jqplot.MekkoLegendRenderer;
+ options.legend.preDraw = true;
+ }
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js
new file mode 100644
index 0000000..8bd899d
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js
@@ -0,0 +1,1029 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.MeterGaugeRenderer
+ * Plugin renderer to draw a meter gauge chart.
+ *
+ * Data consists of a single series with 1 data point to position the gauge needle.
+ *
+ * To use this renderer, you need to include the
+ * meter gauge renderer plugin, for example:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.meterGaugeRenderer.js"></script>
+ *
+ * Properties described here are passed into the $.jqplot function
+ * as options on the series renderer. For example:
+ *
+ * > plot0 = $.jqplot('chart0',[[18]],{
+ * > title: 'Network Speed',
+ * > seriesDefaults: {
+ * > renderer: $.jqplot.MeterGaugeRenderer,
+ * > rendererOptions: {
+ * > label: 'MB/s'
+ * > }
+ * > }
+ * > });
+ *
+ * A meterGauge plot does not support events.
+ */
+ $.jqplot.MeterGaugeRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.MeterGaugeRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.MeterGaugeRenderer.prototype.constructor = $.jqplot.MeterGaugeRenderer;
+
+ // called with scope of a series
+ $.jqplot.MeterGaugeRenderer.prototype.init = function(options) {
+ // Group: Properties
+ //
+ // prop: diameter
+ // Outer diameter of the meterGauge, auto computed by default
+ this.diameter = null;
+ // prop: padding
+ // padding between the meterGauge and plot edges, auto
+ // calculated by default.
+ this.padding = null;
+ // prop: shadowOffset
+ // offset of the shadow from the gauge ring and offset of
+ // each succesive stroke of the shadow from the last.
+ this.shadowOffset = 2;
+ // prop: shadowAlpha
+ // transparency of the shadow (0 = transparent, 1 = opaque)
+ this.shadowAlpha = 0.07;
+ // prop: shadowDepth
+ // number of strokes to apply to the shadow,
+ // each stroke offset shadowOffset from the last.
+ this.shadowDepth = 4;
+ // prop: background
+ // background color of the inside of the gauge.
+ this.background = "#efefef";
+ // prop: ringColor
+ // color of the outer ring, hub, and needle of the gauge.
+ this.ringColor = "#BBC6D0";
+ // needle color not implemented yet.
+ this.needleColor = "#C3D3E5";
+ // prop: tickColor
+ // color of the tick marks around the gauge.
+ this.tickColor = "#989898";
+ // prop: ringWidth
+ // width of the ring around the gauge. Auto computed by default.
+ this.ringWidth = null;
+ // prop: min
+ // Minimum value on the gauge. Auto computed by default
+ this.min;
+ // prop: max
+ // Maximum value on the gauge. Auto computed by default
+ this.max;
+ // prop: ticks
+ // Array of tick values. Auto computed by default.
+ this.ticks = [];
+ // prop: showTicks
+ // true to show ticks around gauge.
+ this.showTicks = true;
+ // prop: showTickLabels
+ // true to show tick labels next to ticks.
+ this.showTickLabels = true;
+ // prop: label
+ // A gauge label like 'kph' or 'Volts'
+ this.label = null;
+ // prop: labelHeightAdjust
+ // Number of Pixels to offset the label up (-) or down (+) from its default position.
+ this.labelHeightAdjust = 0;
+ // prop: labelPosition
+ // Where to position the label, either 'inside' or 'bottom'.
+ this.labelPosition = 'inside';
+ // prop: intervals
+ // Array of ranges to be drawn around the gauge.
+ // Array of form:
+ // > [value1, value2, ...]
+ // indicating the values for the first, second, ... intervals.
+ this.intervals = [];
+ // prop: intervalColors
+ // Array of colors to use for the intervals.
+ this.intervalColors = [ "#4bb2c5", "#EAA228", "#c5b47f", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc", "#c747a3", "#cddf54", "#FBD178", "#26B4E3", "#bd70c7"];
+ // prop: intervalInnerRadius
+ // Radius of the inner circle of the interval ring.
+ this.intervalInnerRadius = null;
+ // prop: intervalOuterRadius
+ // Radius of the outer circle of the interval ring.
+ this.intervalOuterRadius = null;
+ this.tickRenderer = $.jqplot.MeterGaugeTickRenderer;
+ // ticks spaced every 1, 2, 2.5, 5, 10, 20, .1, .2, .25, .5, etc.
+ this.tickPositions = [1, 2, 2.5, 5, 10];
+ // prop: tickSpacing
+ // Degrees between ticks. This is a target number, if
+ // incompatible span and ticks are supplied, a suitable
+ // spacing close to this value will be computed.
+ this.tickSpacing = 30;
+ this.numberMinorTicks = null;
+ // prop: hubRadius
+ // Radius of the hub at the bottom center of gauge which the needle attaches to.
+ // Auto computed by default
+ this.hubRadius = null;
+ // prop: tickPadding
+ // padding of the tick marks to the outer ring and the tick labels to marks.
+ // Auto computed by default.
+ this.tickPadding = null;
+ // prop: needleThickness
+ // Maximum thickness the needle. Auto computed by default.
+ this.needleThickness = null;
+ // prop: needlePad
+ // Padding between needle and inner edge of the ring when the needle is at the min or max gauge value.
+ this.needlePad = 6;
+ // prop: pegNeedle
+ // True will stop needle just below/above the min/max values if data is below/above min/max,
+ // as if the meter is "pegged".
+ this.pegNeedle = true;
+ this._type = 'meterGauge';
+
+ $.extend(true, this, options);
+ this.type = null;
+ this.numberTicks = null;
+ this.tickInterval = null;
+ // span, the sweep (in degrees) from min to max. This gauge is
+ // a semi-circle.
+ this.span = 180;
+ // get rid of this nonsense
+ // this.innerSpan = this.span;
+ if (this.type == 'circular') {
+ this.semiCircular = false;
+ }
+ else if (this.type != 'circular') {
+ this.semiCircular = true;
+ }
+ else {
+ this.semiCircular = (this.span <= 180) ? true : false;
+ }
+ this._tickPoints = [];
+ // reference to label element.
+ this._labelElem = null;
+
+ // start the gauge at the beginning of the span
+ this.startAngle = (90 + (360 - this.span)/2) * Math.PI/180;
+ this.endAngle = (90 - (360 - this.span)/2) * Math.PI/180;
+
+ this.setmin = !!(this.min == null);
+ this.setmax = !!(this.max == null);
+
+ // if given intervals and is an array of values, create labels and colors.
+ if (this.intervals.length) {
+ if (this.intervals[0].length == null || this.intervals.length == 1) {
+ for (var i=0; i<this.intervals.length; i++) {
+ this.intervals[i] = [this.intervals[i], this.intervals[i], this.intervalColors[i]];
+ }
+ }
+ else if (this.intervals[0].length == 2) {
+ for (i=0; i<this.intervals.length; i++) {
+ this.intervals[i] = [this.intervals[i][0], this.intervals[i][1], this.intervalColors[i]];
+ }
+ }
+ }
+
+ // compute min, max and ticks if not supplied:
+ if (this.ticks.length) {
+ if (this.ticks[0].length == null || this.ticks[0].length == 1) {
+ for (var i=0; i<this.ticks.length; i++) {
+ this.ticks[i] = [this.ticks[i], this.ticks[i]];
+ }
+ }
+ this.min = (this.min == null) ? this.ticks[0][0] : this.min;
+ this.max = (this.max == null) ? this.ticks[this.ticks.length-1][0] : this.max;
+ this.setmin = false;
+ this.setmax = false;
+ this.numberTicks = this.ticks.length;
+ this.tickInterval = this.ticks[1][0] - this.ticks[0][0];
+ this.tickFactor = Math.floor(parseFloat((Math.log(this.tickInterval)/Math.log(10)).toFixed(11)));
+ // use the first interal to calculate minor ticks;
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor);
+ if (!this.numberMinorTicks) {
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor-1);
+ }
+ if (!this.numberMinorTicks) {
+ this.numberMinorTicks = 1;
+ }
+ }
+
+ else if (this.intervals.length) {
+ this.min = (this.min == null) ? 0 : this.min;
+ this.setmin = false;
+ if (this.max == null) {
+ if (this.intervals[this.intervals.length-1][0] >= this.data[0][1]) {
+ this.max = this.intervals[this.intervals.length-1][0];
+ this.setmax = false;
+ }
+ }
+ else {
+ this.setmax = false;
+ }
+ }
+
+ else {
+ // no ticks and no intervals supplied, put needle in middle
+ this.min = (this.min == null) ? 0 : this.min;
+ this.setmin = false;
+ if (this.max == null) {
+ this.max = this.data[0][1] * 1.25;
+ this.setmax = true;
+ }
+ else {
+ this.setmax = false;
+ }
+ }
+ };
+
+ $.jqplot.MeterGaugeRenderer.prototype.setGridData = function(plot) {
+ // set gridData property. This will hold angle in radians of each data point.
+ var stack = [];
+ var td = [];
+ var sa = this.startAngle;
+ for (var i=0; i<this.data.length; i++){
+ stack.push(this.data[i][1]);
+ td.push([this.data[i][0]]);
+ if (i>0) {
+ stack[i] += stack[i-1];
+ }
+ }
+ var fact = Math.PI*2/stack[stack.length - 1];
+
+ for (var i=0; i<stack.length; i++) {
+ td[i][1] = stack[i] * fact;
+ }
+ this.gridData = td;
+ };
+
+ $.jqplot.MeterGaugeRenderer.prototype.makeGridData = function(data, plot) {
+ var stack = [];
+ var td = [];
+ var sa = this.startAngle;
+ for (var i=0; i<data.length; i++){
+ stack.push(data[i][1]);
+ td.push([data[i][0]]);
+ if (i>0) {
+ stack[i] += stack[i-1];
+ }
+ }
+ var fact = Math.PI*2/stack[stack.length - 1];
+
+ for (var i=0; i<stack.length; i++) {
+ td[i][1] = stack[i] * fact;
+ }
+ return td;
+ };
+
+
+ function getnmt(pos, interval, fact) {
+ var temp;
+ for (var i=pos.length-1; i>=0; i--) {
+ temp = interval/(pos[i] * Math.pow(10, fact));
+ if (temp == 4 || temp == 5) {
+ return temp - 1;
+ }
+ }
+ return null;
+ }
+
+ // called with scope of series
+ $.jqplot.MeterGaugeRenderer.prototype.draw = function (ctx, gd, options) {
+ var i;
+ var opts = (options != undefined) ? options : {};
+ // offset and direction of offset due to legend placement
+ var offx = 0;
+ var offy = 0;
+ var trans = 1;
+ if (options.legendInfo && options.legendInfo.placement == 'inside') {
+ var li = options.legendInfo;
+ switch (li.location) {
+ case 'nw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'w':
+ offx = li.width + li.xoffset;
+ break;
+ case 'sw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'ne':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'e':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'se':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'n':
+ offy = li.height + li.yoffset;
+ break;
+ case 's':
+ offy = li.height + li.yoffset;
+ trans = -1;
+ break;
+ default:
+ break;
+ }
+ }
+
+
+
+ // pre-draw so can get its dimensions.
+ if (this.label) {
+ this._labelElem = $('<div class="jqplot-meterGauge-label" style="position:absolute;">'+this.label+'</div>');
+ this.canvas._elem.after(this._labelElem);
+ }
+
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var cw = ctx.canvas.width;
+ var ch = ctx.canvas.height;
+ if (this.padding == null) {
+ this.padding = Math.round(Math.min(cw, ch)/30);
+ }
+ var w = cw - offx - 2 * this.padding;
+ var h = ch - offy - 2 * this.padding;
+ if (this.labelPosition == 'bottom' && this.label) {
+ h -= this._labelElem.outerHeight(true);
+ }
+ var mindim = Math.min(w,h);
+ var d = mindim;
+
+ if (!this.diameter) {
+ if (this.semiCircular) {
+ if ( w >= 2*h) {
+ if (!this.ringWidth) {
+ this.ringWidth = 2*h/35;
+ }
+ this.needleThickness = this.needleThickness || 2+Math.pow(this.ringWidth, 0.8);
+ this.innerPad = this.ringWidth/2 + this.needleThickness/2 + this.needlePad;
+ this.diameter = 2 * (h - 2*this.innerPad);
+ }
+ else {
+ if (!this.ringWidth) {
+ this.ringWidth = w/35;
+ }
+ this.needleThickness = this.needleThickness || 2+Math.pow(this.ringWidth, 0.8);
+ this.innerPad = this.ringWidth/2 + this.needleThickness/2 + this.needlePad;
+ this.diameter = w - 2*this.innerPad - this.ringWidth - this.padding;
+ }
+ // center taking into account legend and over draw for gauge bottom below hub.
+ // this will be center of hub.
+ this._center = [(cw - trans * offx)/2 + trans * offx, (ch + trans*offy - this.padding - this.ringWidth - this.innerPad)];
+ }
+ else {
+ if (!this.ringWidth) {
+ this.ringWidth = d/35;
+ }
+ this.needleThickness = this.needleThickness || 2+Math.pow(this.ringWidth, 0.8);
+ this.innerPad = 0;
+ this.diameter = d - this.ringWidth;
+ // center in middle of canvas taking into account legend.
+ // will be center of hub.
+ this._center = [(cw-trans*offx)/2 + trans * offx, (ch-trans*offy)/2 + trans * offy];
+ }
+ if (this._labelElem && this.labelPosition == 'bottom') {
+ this._center[1] -= this._labelElem.outerHeight(true);
+ }
+
+ }
+
+ this._radius = this.diameter/2;
+
+ this.tickSpacing = 6000/this.diameter;
+
+ if (!this.hubRadius) {
+ this.hubRadius = this.diameter/18;
+ }
+
+ this.shadowOffset = 0.5 + this.ringWidth/9;
+ this.shadowWidth = this.ringWidth*1;
+
+ this.tickPadding = 3 + Math.pow(this.diameter/20, 0.7);
+ this.tickOuterRadius = this._radius - this.ringWidth/2 - this.tickPadding;
+ this.tickLength = (this.showTicks) ? this._radius/13 : 0;
+
+ if (this.ticks.length == 0) {
+ // no ticks, lets make some.
+ var max = this.max,
+ min = this.min,
+ setmax = this.setmax,
+ setmin = this.setmin,
+ ti = (max - min) * this.tickSpacing / this.span;
+ var tf = Math.floor(parseFloat((Math.log(ti)/Math.log(10)).toFixed(11)));
+ var tp = (ti/Math.pow(10, tf));
+ (tp > 2 && tp <= 2.5) ? tp = 2.5 : tp = Math.ceil(tp);
+ var t = this.tickPositions;
+ var tpindex, nt;
+
+ for (i=0; i<t.length; i++) {
+ if (tp == t[i] || i && t[i-1] < tp && tp < t[i]) {
+ ti = t[i]*Math.pow(10, tf);
+ tpindex = i;
+ }
+ }
+
+ for (i=0; i<t.length; i++) {
+ if (tp == t[i] || i && t[i-1] < tp && tp < t[i]) {
+ ti = t[i]*Math.pow(10, tf);
+ nt = Math.ceil((max - min) / ti);
+ }
+ }
+
+ // both max and min are free
+ if (setmax && setmin) {
+ var tmin = (min > 0) ? min - min % ti : min - min % ti - ti;
+ if (!this.forceZero) {
+ var diff = Math.min(min - tmin, 0.8*ti);
+ var ntp = Math.floor(diff/t[tpindex]);
+ if (ntp > 1) {
+ tmin = tmin + t[tpindex] * (ntp-1);
+ if (parseInt(tmin, 10) != tmin && parseInt(tmin-t[tpindex], 10) == tmin-t[tpindex]) {
+ tmin = tmin - t[tpindex];
+ }
+ }
+ }
+ if (min == tmin) {
+ min -= ti;
+ }
+ else {
+ // tmin should always be lower than dataMin
+ if (min - tmin > 0.23*ti) {
+ min = tmin;
+ }
+ else {
+ min = tmin -ti;
+ nt += 1;
+ }
+ }
+ nt += 1;
+ var tmax = min + (nt - 1) * ti;
+ if (max >= tmax) {
+ tmax += ti;
+ nt += 1;
+ }
+ // now tmax should always be mroe than dataMax
+ if (tmax - max < 0.23*ti) {
+ tmax += ti;
+ nt += 1;
+ }
+ this.max = max = tmax;
+ this.min = min;
+
+ this.tickInterval = ti;
+ this.numberTicks = nt;
+ var it;
+ for (i=0; i<nt; i++) {
+ it = parseFloat((min+i*ti).toFixed(11));
+ this.ticks.push([it, it]);
+ }
+ this.max = this.ticks[nt-1][1];
+
+ this.tickFactor = tf;
+ // determine number of minor ticks
+
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor);
+
+ if (!this.numberMinorTicks) {
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor-1);
+ }
+ }
+ // max is free, min is fixed
+ else if (setmax) {
+ var tmax = min + (nt - 1) * ti;
+ if (max >= tmax) {
+ max = tmax + ti;
+ nt += 1;
+ }
+ else {
+ max = tmax;
+ }
+
+ this.tickInterval = this.tickInterval || ti;
+ this.numberTicks = this.numberTicks || nt;
+ var it;
+ for (i=0; i<this.numberTicks; i++) {
+ it = parseFloat((min+i*this.tickInterval).toFixed(11));
+ this.ticks.push([it, it]);
+ }
+ this.max = this.ticks[this.numberTicks-1][1];
+
+ this.tickFactor = tf;
+ // determine number of minor ticks
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor);
+
+ if (!this.numberMinorTicks) {
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor-1);
+ }
+ }
+
+ // not setting max or min
+ if (!setmax && !setmin) {
+ var range = this.max - this.min;
+ tf = Math.floor(parseFloat((Math.log(range)/Math.log(10)).toFixed(11))) - 1;
+ var nticks = [5,6,4,7,3,8,9,10,2], res, numticks, nonSigDigits=0, sigRange;
+ // check to see how many zeros are at the end of the range
+ if (range > 1) {
+ var rstr = String(range);
+ if (rstr.search(/\./) == -1) {
+ var pos = rstr.search(/0+$/);
+ nonSigDigits = (pos > 0) ? rstr.length - pos - 1 : 0;
+ }
+ }
+ sigRange = range/Math.pow(10, nonSigDigits);
+ for (i=0; i<nticks.length; i++) {
+ res = sigRange/(nticks[i]-1);
+ if (res == parseInt(res, 10)) {
+ this.numberTicks = nticks[i];
+ this.tickInterval = range/(this.numberTicks-1);
+ this.tickFactor = tf+1;
+ break;
+ }
+ }
+ var it;
+ for (i=0; i<this.numberTicks; i++) {
+ it = parseFloat((this.min+i*this.tickInterval).toFixed(11));
+ this.ticks.push([it, it]);
+ }
+ // determine number of minor ticks
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor);
+
+ if (!this.numberMinorTicks) {
+ this.numberMinorTicks = getnmt(this.tickPositions, this.tickInterval, this.tickFactor-1);
+ }
+
+ if (!this.numberMinorTicks) {
+ this.numberMinorTicks = 1;
+ var nums = [4, 5, 3, 6, 2];
+ for (i=0; i<5; i++) {
+ var temp = this.tickInterval/nums[i];
+ if (temp == parseInt(temp, 10)) {
+ this.numberMinorTicks = nums[i]-1;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+
+ var r = this._radius,
+ sa = this.startAngle,
+ ea = this.endAngle,
+ pi = Math.PI,
+ hpi = Math.PI/2;
+
+ if (this.semiCircular) {
+ var overAngle = Math.atan(this.innerPad/r),
+ outersa = this.outerStartAngle = sa - overAngle,
+ outerea = this.outerEndAngle = ea + overAngle,
+ hubsa = this.hubStartAngle = sa - Math.atan(this.innerPad/this.hubRadius*2),
+ hubea = this.hubEndAngle = ea + Math.atan(this.innerPad/this.hubRadius*2);
+
+ ctx.save();
+
+ ctx.translate(this._center[0], this._center[1]);
+ ctx.lineJoin = "round";
+ ctx.lineCap = "round";
+
+ // draw the innerbackground
+ ctx.save();
+ ctx.beginPath();
+ ctx.fillStyle = this.background;
+ ctx.arc(0, 0, r, outersa, outerea, false);
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+
+ // draw the shadow
+ // the outer ring.
+ var shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
+ ctx.save();
+ for (var i=0; i<this.shadowDepth; i++) {
+ ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
+ ctx.beginPath();
+ ctx.strokeStyle = shadowColor;
+ ctx.lineWidth = this.shadowWidth;
+ ctx.arc(0 ,0, r, outersa, outerea, false);
+ ctx.closePath();
+ ctx.stroke();
+ }
+ ctx.restore();
+
+ // the inner hub.
+ ctx.save();
+ var tempd = parseInt((this.shadowDepth+1)/2, 10);
+ for (var i=0; i<tempd; i++) {
+ ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
+ ctx.beginPath();
+ ctx.fillStyle = shadowColor;
+ ctx.arc(0 ,0, this.hubRadius, hubsa, hubea, false);
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.restore();
+
+ // draw the outer ring.
+ ctx.save();
+ ctx.beginPath();
+ ctx.strokeStyle = this.ringColor;
+ ctx.lineWidth = this.ringWidth;
+ ctx.arc(0 ,0, r, outersa, outerea, false);
+ ctx.closePath();
+ ctx.stroke();
+ ctx.restore();
+
+ // draw the hub
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.fillStyle = this.ringColor;
+ ctx.arc(0 ,0, this.hubRadius,hubsa, hubea, false);
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+
+ // draw the ticks
+ if (this.showTicks) {
+ ctx.save();
+ var orad = this.tickOuterRadius,
+ tl = this.tickLength,
+ mtl = tl/2,
+ nmt = this.numberMinorTicks,
+ ts = this.span * Math.PI / 180 / (this.ticks.length-1),
+ mts = ts/(nmt + 1);
+
+ for (i = 0; i<this.ticks.length; i++) {
+ ctx.beginPath();
+ ctx.lineWidth = 1.5 + this.diameter/360;
+ ctx.strokeStyle = this.ringColor;
+ var wps = ts*i+sa;
+ ctx.moveTo(-orad * Math.cos(ts*i+sa), orad * Math.sin(ts*i+sa));
+ ctx.lineTo(-(orad-tl) * Math.cos(ts*i+sa), (orad - tl) * Math.sin(ts*i+sa));
+ this._tickPoints.push([(orad-tl) * Math.cos(ts*i+sa) + this._center[0] + this.canvas._offsets.left, (orad - tl) * Math.sin(ts*i+sa) + this._center[1] + this.canvas._offsets.top, ts*i+sa]);
+ ctx.stroke();
+ ctx.lineWidth = 1.0 + this.diameter/440;
+ if (i<this.ticks.length-1) {
+ for (var j=1; j<=nmt; j++) {
+ ctx.beginPath();
+ ctx.moveTo(-orad * Math.cos(ts*i+mts*j+sa), orad * Math.sin(ts*i+mts*j+sa));
+ ctx.lineTo(-(orad-mtl) * Math.cos(ts*i+mts*j+sa), (orad-mtl) * Math.sin(ts*i+mts*j+sa));
+ ctx.stroke();
+ }
+ }
+ }
+ ctx.restore();
+ }
+
+ // draw the tick labels
+ if (this.showTickLabels) {
+ var elem, l, t, ew, eh, dim, maxdim=0;
+ var tp = this.tickPadding * (1 - 1/(this.diameter/80+1));
+ for (i=0; i<this.ticks.length; i++) {
+ elem = $('<div class="jqplot-meterGauge-tick" style="position:absolute;">'+this.ticks[i][1]+'</div>');
+ this.canvas._elem.after(elem);
+ ew = elem.outerWidth(true);
+ eh = elem.outerHeight(true);
+ l = this._tickPoints[i][0] - ew * (this._tickPoints[i][2]-Math.PI)/Math.PI - tp * Math.cos(this._tickPoints[i][2]);
+ t = this._tickPoints[i][1] - eh/2 + eh/2 * Math.pow(Math.abs((Math.sin(this._tickPoints[i][2]))), 0.5) + tp/3 * Math.pow(Math.abs((Math.sin(this._tickPoints[i][2]))), 0.5) ;
+ // t = this._tickPoints[i][1] - eh/2 - eh/2 * Math.sin(this._tickPoints[i][2]) - tp/2 * Math.sin(this._tickPoints[i][2]);
+ elem.css({left:l, top:t, color: this.tickColor});
+ dim = ew*Math.cos(this._tickPoints[i][2]) + eh*Math.sin(Math.PI/2+this._tickPoints[i][2]/2);
+ maxdim = (dim > maxdim) ? dim : maxdim;
+ }
+ }
+
+ // draw the gauge label
+ if (this.label && this.labelPosition == 'inside') {
+ var l = this._center[0] + this.canvas._offsets.left;
+ var tp = this.tickPadding * (1 - 1/(this.diameter/80+1));
+ var t = 0.5*(this._center[1] + this.canvas._offsets.top - this.hubRadius) + 0.5*(this._center[1] + this.canvas._offsets.top - this.tickOuterRadius + this.tickLength + tp) + this.labelHeightAdjust;
+ // this._labelElem = $('<div class="jqplot-meterGauge-label" style="position:absolute;">'+this.label+'</div>');
+ // this.canvas._elem.after(this._labelElem);
+ l -= this._labelElem.outerWidth(true)/2;
+ t -= this._labelElem.outerHeight(true)/2;
+ this._labelElem.css({left:l, top:t});
+ }
+
+ else if (this.label && this.labelPosition == 'bottom') {
+ var l = this._center[0] + this.canvas._offsets.left - this._labelElem.outerWidth(true)/2;
+ var t = this._center[1] + this.canvas._offsets.top + this.innerPad + this.ringWidth + this.padding + this.labelHeightAdjust;
+ this._labelElem.css({left:l, top:t});
+
+ }
+
+ // draw the intervals
+
+ ctx.save();
+ var inner = this.intervalInnerRadius || this.hubRadius * 1.5;
+ if (this.intervalOuterRadius == null) {
+ if (this.showTickLabels) {
+ var outer = (this.tickOuterRadius - this.tickLength - this.tickPadding - this.diameter/8);
+ }
+ else {
+ var outer = (this.tickOuterRadius - this.tickLength - this.diameter/16);
+ }
+ }
+ else {
+ var outer = this.intervalOuterRadius;
+ }
+ var range = this.max - this.min;
+ var intrange = this.intervals[this.intervals.length-1] - this.min;
+ var start, end, span = this.span*Math.PI/180;
+ for (i=0; i<this.intervals.length; i++) {
+ start = (i == 0) ? sa : sa + (this.intervals[i-1][0] - this.min)*span/range;
+ if (start < 0) {
+ start = 0;
+ }
+ end = sa + (this.intervals[i][0] - this.min)*span/range;
+ if (end < 0) {
+ end = 0;
+ }
+ ctx.beginPath();
+ ctx.fillStyle = this.intervals[i][2];
+ ctx.arc(0, 0, inner, start, end, false);
+ ctx.lineTo(outer*Math.cos(end), outer*Math.sin(end));
+ ctx.arc(0, 0, outer, end, start, true);
+ ctx.lineTo(inner*Math.cos(start), inner*Math.sin(start));
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.restore();
+
+ // draw the needle
+ var datapoint = this.data[0][1];
+ var dataspan = this.max - this.min;
+ if (this.pegNeedle) {
+ if (this.data[0][1] > this.max + dataspan*3/this.span) {
+ datapoint = this.max + dataspan*3/this.span;
+ }
+ if (this.data[0][1] < this.min - dataspan*3/this.span) {
+ datapoint = this.min - dataspan*3/this.span;
+ }
+ }
+ var dataang = (datapoint - this.min)/dataspan * this.span * Math.PI/180 + this.startAngle;
+
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.fillStyle = this.ringColor;
+ ctx.strokeStyle = this.ringColor;
+ this.needleLength = (this.tickOuterRadius - this.tickLength) * 0.85;
+ this.needleThickness = (this.needleThickness < 2) ? 2 : this.needleThickness;
+ var endwidth = this.needleThickness * 0.4;
+
+
+ var dl = this.needleLength/10;
+ var dt = (this.needleThickness - endwidth)/10;
+ var templ;
+ for (var i=0; i<10; i++) {
+ templ = this.needleThickness - i*dt;
+ ctx.moveTo(dl*i*Math.cos(dataang), dl*i*Math.sin(dataang));
+ ctx.lineWidth = templ;
+ ctx.lineTo(dl*(i+1)*Math.cos(dataang), dl*(i+1)*Math.sin(dataang));
+ ctx.stroke();
+ }
+
+ ctx.restore();
+ }
+ else {
+ this._center = [(cw - trans * offx)/2 + trans * offx, (ch - trans*offy)/2 + trans * offy];
+ }
+ };
+
+ $.jqplot.MeterGaugeAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.MeterGaugeAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.MeterGaugeAxisRenderer.prototype.constructor = $.jqplot.MeterGaugeAxisRenderer;
+
+
+ // There are no traditional axes on a gauge chart. We just need to provide
+ // dummy objects with properties so the plot will render.
+ // called with scope of axis object.
+ $.jqplot.MeterGaugeAxisRenderer.prototype.init = function(options){
+ //
+ this.tickRenderer = $.jqplot.MeterGaugeTickRenderer;
+ $.extend(true, this, options);
+ // I don't think I'm going to need _dataBounds here.
+ // have to go Axis scaling in a way to fit chart onto plot area
+ // and provide u2p and p2u functionality for mouse cursor, etc.
+ // for convienence set _dataBounds to 0 and 100 and
+ // set min/max to 0 and 100.
+ this._dataBounds = {min:0, max:100};
+ this.min = 0;
+ this.max = 100;
+ this.showTicks = false;
+ this.ticks = [];
+ this.showMark = false;
+ this.show = false;
+ };
+
+ $.jqplot.MeterGaugeLegendRenderer = function(){
+ $.jqplot.TableLegendRenderer.call(this);
+ };
+
+ $.jqplot.MeterGaugeLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
+ $.jqplot.MeterGaugeLegendRenderer.prototype.constructor = $.jqplot.MeterGaugeLegendRenderer;
+
+ /**
+ * Class: $.jqplot.MeterGaugeLegendRenderer
+ *Meter gauges don't typically have a legend, this overrides the default legend renderer.
+ */
+ $.jqplot.MeterGaugeLegendRenderer.prototype.init = function(options) {
+ // Maximum number of rows in the legend. 0 or null for unlimited.
+ this.numberRows = null;
+ // Maximum number of columns in the legend. 0 or null for unlimited.
+ this.numberColumns = null;
+ $.extend(true, this, options);
+ };
+
+ // called with context of legend
+ $.jqplot.MeterGaugeLegendRenderer.prototype.draw = function() {
+ if (this.show) {
+ var series = this._series;
+ var ss = 'position:absolute;';
+ ss += (this.background) ? 'background:'+this.background+';' : '';
+ ss += (this.border) ? 'border:'+this.border+';' : '';
+ ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
+ ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
+ ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
+ ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
+ ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
+ ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
+ ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
+ this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
+ // MeterGauge charts legends don't go by number of series, but by number of data points
+ // in the series. Refactor things here for that.
+
+ var pad = false,
+ reverse = false,
+ nr, nc;
+ var s = series[0];
+
+ if (s.show) {
+ var pd = s.data;
+ if (this.numberRows) {
+ nr = this.numberRows;
+ if (!this.numberColumns){
+ nc = Math.ceil(pd.length/nr);
+ }
+ else{
+ nc = this.numberColumns;
+ }
+ }
+ else if (this.numberColumns) {
+ nc = this.numberColumns;
+ nr = Math.ceil(pd.length/this.numberColumns);
+ }
+ else {
+ nr = pd.length;
+ nc = 1;
+ }
+
+ var i, j, tr, td1, td2, lt, rs, color;
+ var idx = 0;
+
+ for (i=0; i<nr; i++) {
+ if (reverse){
+ tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
+ }
+ else{
+ tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
+ }
+ for (j=0; j<nc; j++) {
+ if (idx < pd.length){
+ // debugger
+ lt = this.labels[idx] || pd[idx][0].toString();
+ color = s.color;
+ if (!reverse){
+ if (i>0){
+ pad = true;
+ }
+ else{
+ pad = false;
+ }
+ }
+ else{
+ if (i == nr -1){
+ pad = false;
+ }
+ else{
+ pad = true;
+ }
+ }
+ rs = (pad) ? this.rowSpacing : '0';
+
+ td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
+ '<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
+ '</div></td>');
+ td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
+ if (this.escapeHtml){
+ td2.text(lt);
+ }
+ else {
+ td2.html(lt);
+ }
+ if (reverse) {
+ td2.prependTo(tr);
+ td1.prependTo(tr);
+ }
+ else {
+ td1.appendTo(tr);
+ td2.appendTo(tr);
+ }
+ pad = true;
+ }
+ idx++;
+ }
+ }
+ }
+ }
+ return this._elem;
+ };
+
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ // debugger
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.legend = options.legend || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ options.grid = options.grid || {};
+
+ // only set these if there is a gauge series
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.MeterGaugeRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.MeterGaugeRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.MeterGaugeAxisRenderer;
+ options.legend.renderer = $.jqplot.MeterGaugeLegendRenderer;
+ options.legend.preDraw = true;
+ options.grid.background = options.grid.background || 'white';
+ options.grid.drawGridlines = false;
+ options.grid.borderWidth = (options.grid.borderWidth != null) ? options.grid.borderWidth : 0;
+ options.grid.shadow = (options.grid.shadow != null) ? options.grid.shadow : false;
+ }
+ }
+
+ // called with scope of plot
+ function postParseOptions(options) {
+ //
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+ $.jqplot.postParseOptionsHooks.push(postParseOptions);
+
+ $.jqplot.MeterGaugeTickRenderer = function() {
+ $.jqplot.AxisTickRenderer.call(this);
+ };
+
+ $.jqplot.MeterGaugeTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
+ $.jqplot.MeterGaugeTickRenderer.prototype.constructor = $.jqplot.MeterGaugeTickRenderer;
+
+})(jQuery);
+
+
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.mobile.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.mobile.js
new file mode 100644
index 0000000..7fd60f1
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.mobile.js
@@ -0,0 +1,45 @@
+/**
+ * jqplot.jquerymobile plugin
+ * jQuery Mobile virtual event support.
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2011 Takashi Okamoto
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ */
+(function($) {
+ function postInit(target, data, options){
+ this.bindCustomEvents = function() {
+ this.eventCanvas._elem.bind('vclick', {plot:this}, this.onClick);
+ this.eventCanvas._elem.bind('dblclick', {plot:this}, this.onDblClick);
+ this.eventCanvas._elem.bind('taphold', {plot:this}, this.onDblClick);
+ this.eventCanvas._elem.bind('vmousedown', {plot:this}, this.onMouseDown);
+ this.eventCanvas._elem.bind('vmousemove', {plot:this}, this.onMouseMove);
+ this.eventCanvas._elem.bind('mouseenter', {plot:this}, this.onMouseEnter);
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, this.onMouseLeave);
+ if (this.captureRightClick) {
+ this.eventCanvas._elem.bind('vmouseup', {plot:this}, this.onRightClick);
+ this.eventCanvas._elem.get(0).oncontextmenu = function() {
+ return false;
+ };
+ }
+ else {
+ this.eventCanvas._elem.bind('vmouseup', {plot:this}, this.onMouseUp);
+ }
+ };
+ this.plugins.mobile = true;
+ }
+ $.jqplot.postInitHooks.push(postInit);
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.ohlcRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.ohlcRenderer.js
new file mode 100644
index 0000000..3cffbfb
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.ohlcRenderer.js
@@ -0,0 +1,373 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.OHLCRenderer
+ * jqPlot Plugin to draw Open Hi Low Close, Candlestick and Hi Low Close charts.
+ *
+ * To use this plugin, include the renderer js file in
+ * your source:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.ohlcRenderer.js"></script>
+ *
+ * You will most likely want to use a date axis renderer
+ * for the x axis also, so include the date axis render js file also:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.dateAxisRenderer.js"></script>
+ *
+ * Then you set the renderer in the series options on your plot:
+ *
+ * > series: [{renderer:$.jqplot.OHLCRenderer}]
+ *
+ * For OHLC and candlestick charts, data should be specified
+ * like so:
+ *
+ * > dat = [['07/06/2009',138.7,139.68,135.18,135.4], ['06/29/2009',143.46,144.66,139.79,140.02], ...]
+ *
+ * If the data array has only 4 values per point instead of 5,
+ * the renderer will create a Hi Low Close chart instead. In that case,
+ * data should be supplied like:
+ *
+ * > dat = [['07/06/2009',139.68,135.18,135.4], ['06/29/2009',144.66,139.79,140.02], ...]
+ *
+ * To generate a candlestick chart instead of an OHLC chart,
+ * set the "candlestick" option to true:
+ *
+ * > series: [{renderer:$.jqplot.OHLCRenderer, rendererOptions:{candleStick:true}}],
+ *
+ */
+ $.jqplot.OHLCRenderer = function(){
+ // subclass line renderer to make use of some of its methods.
+ $.jqplot.LineRenderer.call(this);
+ // prop: candleStick
+ // true to render chart as candleStick.
+ // Must have an open price, cannot be a hlc chart.
+ this.candleStick = false;
+ // prop: tickLength
+ // length of the line in pixels indicating open and close price.
+ // Default will auto calculate based on plot width and
+ // number of points displayed.
+ this.tickLength = 'auto';
+ // prop: bodyWidth
+ // width of the candlestick body in pixels. Default will auto calculate
+ // based on plot width and number of candlesticks displayed.
+ this.bodyWidth = 'auto';
+ // prop: openColor
+ // color of the open price tick mark. Default is series color.
+ this.openColor = null;
+ // prop: closeColor
+ // color of the close price tick mark. Default is series color.
+ this.closeColor = null;
+ // prop: wickColor
+ // color of the hi-lo line thorugh the candlestick body.
+ // Default is the series color.
+ this.wickColor = null;
+ // prop: fillUpBody
+ // true to render an "up" day (close price greater than open price)
+ // with a filled candlestick body.
+ this.fillUpBody = false;
+ // prop: fillDownBody
+ // true to render a "down" day (close price lower than open price)
+ // with a filled candlestick body.
+ this.fillDownBody = true;
+ // prop: upBodyColor
+ // Color of candlestick body of an "up" day. Default is series color.
+ this.upBodyColor = null;
+ // prop: downBodyColor
+ // Color of candlestick body on a "down" day. Default is series color.
+ this.downBodyColor = null;
+ // prop: hlc
+ // true if is a hi-low-close chart (no open price).
+ // This is determined automatically from the series data.
+ this.hlc = false;
+ // prop: lineWidth
+ // Width of the hi-low line and open/close ticks.
+ // Must be set in the rendererOptions for the series.
+ this.lineWidth = 1.5;
+ this._tickLength;
+ this._bodyWidth;
+ };
+
+ $.jqplot.OHLCRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.OHLCRenderer.prototype.constructor = $.jqplot.OHLCRenderer;
+
+ // called with scope of series.
+ $.jqplot.OHLCRenderer.prototype.init = function(options) {
+ options = options || {};
+ // lineWidth has to be set on the series, changes in renderer
+ // constructor have no effect. set the default here
+ // if no renderer option for lineWidth is specified.
+ this.lineWidth = options.lineWidth || 1.5;
+ $.jqplot.LineRenderer.prototype.init.call(this, options);
+ this._type = 'ohlc';
+ // set the yaxis data bounds here to account for hi and low values
+ var db = this._yaxis._dataBounds;
+ var d = this._plotData;
+ // if data points have less than 5 values, force a hlc chart.
+ if (d[0].length < 5) {
+ this.renderer.hlc = true;
+
+ for (var j=0; j<d.length; j++) {
+ if (d[j][2] < db.min || db.min == null) {
+ db.min = d[j][2];
+ }
+ if (d[j][1] > db.max || db.max == null) {
+ db.max = d[j][1];
+ }
+ }
+ }
+ else {
+ for (var j=0; j<d.length; j++) {
+ if (d[j][3] < db.min || db.min == null) {
+ db.min = d[j][3];
+ }
+ if (d[j][2] > db.max || db.max == null) {
+ db.max = d[j][2];
+ }
+ }
+ }
+
+ };
+
+ // called within scope of series.
+ $.jqplot.OHLCRenderer.prototype.draw = function(ctx, gd, options) {
+ var d = this.data;
+ var xmin = this._xaxis.min;
+ var xmax = this._xaxis.max;
+ // index of last value below range of plot.
+ var xminidx = 0;
+ // index of first value above range of plot.
+ var xmaxidx = d.length;
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var i, prevColor, ops, b, h, w, a, points;
+ var o;
+ var r = this.renderer;
+ var opts = (options != undefined) ? options : {};
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var fillAndStroke = (opts.fillAndStroke != undefined) ? opts.fillAndStroke : this.fillAndStroke;
+ r.bodyWidth = (opts.bodyWidth != undefined) ? opts.bodyWidth : r.bodyWidth;
+ r.tickLength = (opts.tickLength != undefined) ? opts.tickLength : r.tickLength;
+ ctx.save();
+ if (this.show) {
+ var x, open, hi, low, close;
+ // need to get widths based on number of points shown,
+ // not on total number of points. Use the results
+ // to speed up drawing in next step.
+ for (var i=0; i<d.length; i++) {
+ if (d[i][0] < xmin) {
+ xminidx = i;
+ }
+ else if (d[i][0] < xmax) {
+ xmaxidx = i+1;
+ }
+ }
+
+ var dwidth = this.gridData[xmaxidx-1][0] - this.gridData[xminidx][0];
+ var nvisiblePoints = xmaxidx - xminidx;
+ try {
+ var dinterval = Math.abs(this._xaxis.series_u2p(parseInt(this._xaxis._intervalStats[0].sortedIntervals[0].interval, 10)) - this._xaxis.series_u2p(0));
+ }
+
+ catch (e) {
+ var dinterval = dwidth / nvisiblePoints;
+ }
+
+ if (r.candleStick) {
+ if (typeof(r.bodyWidth) == 'number') {
+ r._bodyWidth = r.bodyWidth;
+ }
+ else {
+ r._bodyWidth = Math.min(20, dinterval/1.65);
+ }
+ }
+ else {
+ if (typeof(r.tickLength) == 'number') {
+ r._tickLength = r.tickLength;
+ }
+ else {
+ r._tickLength = Math.min(10, dinterval/3.5);
+ }
+ }
+
+ for (var i=xminidx; i<xmaxidx; i++) {
+ x = xp(d[i][0]);
+ if (r.hlc) {
+ open = null;
+ hi = yp(d[i][1]);
+ low = yp(d[i][2]);
+ close = yp(d[i][3]);
+ }
+ else {
+ open = yp(d[i][1]);
+ hi = yp(d[i][2]);
+ low = yp(d[i][3]);
+ close = yp(d[i][4]);
+ }
+ o = {};
+ if (r.candleStick && !r.hlc) {
+ w = r._bodyWidth;
+ a = x - w/2;
+ // draw candle
+ // determine if candle up or down
+ // up, remember grid coordinates increase downward
+ if (close < open) {
+ // draw wick
+ if (r.wickColor) {
+ o.color = r.wickColor;
+ }
+ else if (r.downBodyColor) {
+ o.color = r.upBodyColor;
+ }
+ ops = $.extend(true, {}, opts, o);
+ r.shapeRenderer.draw(ctx, [[x, hi], [x, close]], ops);
+ r.shapeRenderer.draw(ctx, [[x, open], [x, low]], ops);
+ o = {};
+ b = close;
+ h = open - close;
+ // if color specified, use it
+ if (r.fillUpBody) {
+ o.fillRect = true;
+ }
+ else {
+ o.strokeRect = true;
+ w = w - this.lineWidth;
+ a = x - w/2;
+ }
+ if (r.upBodyColor) {
+ o.color = r.upBodyColor;
+ o.fillStyle = r.upBodyColor;
+ }
+ points = [a, b, w, h];
+ }
+ // down
+ else if (close > open) {
+ // draw wick
+ if (r.wickColor) {
+ o.color = r.wickColor;
+ }
+ else if (r.downBodyColor) {
+ o.color = r.downBodyColor;
+ }
+ ops = $.extend(true, {}, opts, o);
+ r.shapeRenderer.draw(ctx, [[x, hi], [x, open]], ops);
+ r.shapeRenderer.draw(ctx, [[x, close], [x, low]], ops);
+
+ o = {};
+
+ b = open;
+ h = close - open;
+ // if color specified, use it
+ if (r.fillDownBody) {
+ o.fillRect = true;
+ }
+ else {
+ o.strokeRect = true;
+ w = w - this.lineWidth;
+ a = x - w/2;
+ }
+ if (r.downBodyColor) {
+ o.color = r.downBodyColor;
+ o.fillStyle = r.downBodyColor;
+ }
+ points = [a, b, w, h];
+ }
+ // even, open = close
+ else {
+ // draw wick
+ if (r.wickColor) {
+ o.color = r.wickColor;
+ }
+ ops = $.extend(true, {}, opts, o);
+ r.shapeRenderer.draw(ctx, [[x, hi], [x, low]], ops);
+ o = {};
+ o.fillRect = false;
+ o.strokeRect = false;
+ a = [x - w/2, open];
+ b = [x + w/2, close];
+ w = null;
+ h = null;
+ points = [a, b];
+ }
+ ops = $.extend(true, {}, opts, o);
+ r.shapeRenderer.draw(ctx, points, ops);
+ }
+ else {
+ prevColor = opts.color;
+ if (r.openColor) {
+ opts.color = r.openColor;
+ }
+ // draw open tick
+ if (!r.hlc) {
+ r.shapeRenderer.draw(ctx, [[x-r._tickLength, open], [x, open]], opts);
+ }
+ opts.color = prevColor;
+ // draw wick
+ if (r.wickColor) {
+ opts.color = r.wickColor;
+ }
+ r.shapeRenderer.draw(ctx, [[x, hi], [x, low]], opts);
+ opts.color = prevColor;
+ // draw close tick
+ if (r.closeColor) {
+ opts.color = r.closeColor;
+ }
+ r.shapeRenderer.draw(ctx, [[x, close], [x+r._tickLength, close]], opts);
+ opts.color = prevColor;
+ }
+ }
+ }
+
+ ctx.restore();
+ };
+
+ $.jqplot.OHLCRenderer.prototype.drawShadow = function(ctx, gd, options) {
+ // This is a no-op, shadows drawn with lines.
+ };
+
+ // called with scope of plot.
+ $.jqplot.OHLCRenderer.checkOptions = function(target, data, options) {
+ // provide some sensible highlighter options by default
+ // These aren't good for hlc, only for ohlc or candlestick
+ if (!options.highlighter) {
+ options.highlighter = {
+ showMarker:false,
+ tooltipAxes: 'y',
+ yvalues: 4,
+ formatString:'<table class="jqplot-highlighter"><tr><td>date:</td><td>%s</td></tr><tr><td>open:</td><td>%s</td></tr><tr><td>hi:</td><td>%s</td></tr><tr><td>low:</td><td>%s</td></tr><tr><td>close:</td><td>%s</td></tr></table>'
+ };
+ }
+ };
+
+ //$.jqplot.preInitHooks.push($.jqplot.OHLCRenderer.checkOptions);
+
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.pieRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.pieRenderer.js
new file mode 100644
index 0000000..1d508bd
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.pieRenderer.js
@@ -0,0 +1,904 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ /**
+ * Class: $.jqplot.PieRenderer
+ * Plugin renderer to draw a pie chart.
+ * x values, if present, will be used as slice labels.
+ * y values give slice size.
+ *
+ * To use this renderer, you need to include the
+ * pie renderer plugin, for example:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.pieRenderer.js"></script>
+ *
+ * Properties described here are passed into the $.jqplot function
+ * as options on the series renderer. For example:
+ *
+ * > plot2 = $.jqplot('chart2', [s1, s2], {
+ * > seriesDefaults: {
+ * > renderer:$.jqplot.PieRenderer,
+ * > rendererOptions:{
+ * > sliceMargin: 2,
+ * > startAngle: -90
+ * > }
+ * > }
+ * > });
+ *
+ * A pie plot will trigger events on the plot target
+ * according to user interaction. All events return the event object,
+ * the series index, the point (slice) index, and the point data for
+ * the appropriate slice.
+ *
+ * 'jqplotDataMouseOver' - triggered when user mouseing over a slice.
+ * 'jqplotDataHighlight' - triggered the first time user mouses over a slice,
+ * if highlighting is enabled.
+ * 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
+ * a highlighted slice.
+ * 'jqplotDataClick' - triggered when the user clicks on a slice.
+ * 'jqplotDataRightClick' - tiggered when the user right clicks on a slice if
+ * the "captureRightClick" option is set to true on the plot.
+ */
+ $.jqplot.PieRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.PieRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.PieRenderer.prototype.constructor = $.jqplot.PieRenderer;
+
+ // called with scope of a series
+ $.jqplot.PieRenderer.prototype.init = function(options, plot) {
+ // Group: Properties
+ //
+ // prop: diameter
+ // Outer diameter of the pie, auto computed by default
+ this.diameter = null;
+ // prop: padding
+ // padding between the pie and plot edges, legend, etc.
+ this.padding = 20;
+ // prop: sliceMargin
+ // angular spacing between pie slices in degrees.
+ this.sliceMargin = 0;
+ // prop: fill
+ // true or false, whether to fil the slices.
+ this.fill = true;
+ // prop: shadowOffset
+ // offset of the shadow from the slice and offset of
+ // each succesive stroke of the shadow from the last.
+ this.shadowOffset = 2;
+ // prop: shadowAlpha
+ // transparency of the shadow (0 = transparent, 1 = opaque)
+ this.shadowAlpha = 0.07;
+ // prop: shadowDepth
+ // number of strokes to apply to the shadow,
+ // each stroke offset shadowOffset from the last.
+ this.shadowDepth = 5;
+ // prop: highlightMouseOver
+ // True to highlight slice when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on a slice.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over a slice.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColors
+ // an array of colors to use when highlighting a slice.
+ this.highlightColors = [];
+ // prop: dataLabels
+ // Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
+ // Defaults to percentage of each pie slice.
+ this.dataLabels = 'percent';
+ // prop: showDataLabels
+ // true to show data labels on slices.
+ this.showDataLabels = false;
+ // prop: dataLabelFormatString
+ // Format string for data labels. If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
+ this.dataLabelFormatString = null;
+ // prop: dataLabelThreshold
+ // Threshhold in percentage (0-100) of pie area, below which no label will be displayed.
+ // This applies to all label types, not just to percentage labels.
+ this.dataLabelThreshold = 3;
+ // prop: dataLabelPositionFactor
+ // A Multiplier (0-1) of the pie radius which controls position of label on slice.
+ // Increasing will slide label toward edge of pie, decreasing will slide label toward center of pie.
+ this.dataLabelPositionFactor = 0.52;
+ // prop: dataLabelNudge
+ // Number of pixels to slide the label away from (+) or toward (-) the center of the pie.
+ this.dataLabelNudge = 2;
+ // prop: dataLabelCenterOn
+ // True to center the data label at its position.
+ // False to set the inside facing edge of the label at its position.
+ this.dataLabelCenterOn = true;
+ // prop: startAngle
+ // Angle to start drawing pie in degrees.
+ // According to orientation of canvas coordinate system:
+ // 0 = on the positive x axis
+ // -90 = on the positive y axis.
+ // 90 = on the negaive y axis.
+ // 180 or - 180 = on the negative x axis.
+ this.startAngle = 0;
+ this.tickRenderer = $.jqplot.PieTickRenderer;
+ // Used as check for conditions where pie shouldn't be drawn.
+ this._drawData = true;
+ this._type = 'pie';
+
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (options.highlightMouseDown && options.highlightMouseOver == null) {
+ options.highlightMouseOver = false;
+ }
+
+ $.extend(true, this, options);
+
+ if (this.sliceMargin < 0) {
+ this.sliceMargin = 0;
+ }
+
+ this._diameter = null;
+ this._radius = null;
+ // array of [start,end] angles arrays, one for each slice. In radians.
+ this._sliceAngles = [];
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+
+ // set highlight colors if none provided
+ if (this.highlightColors.length == 0) {
+ for (var i=0; i<this.seriesColors.length; i++){
+ var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
+ var newrgb = [rgba[0], rgba[1], rgba[2]];
+ var sum = newrgb[0] + newrgb[1] + newrgb[2];
+ for (var j=0; j<3; j++) {
+ // when darkening, lowest color component can be is 60.
+ newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
+ newrgb[j] = parseInt(newrgb[j], 10);
+ }
+ this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
+ }
+ }
+
+ this.highlightColorGenerator = new $.jqplot.ColorGenerator(this.highlightColors);
+
+ plot.postParseOptionsHooks.addOnce(postParseOptions);
+ plot.postInitHooks.addOnce(postInit);
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+ plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
+ plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
+ plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
+ plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
+ plot.postDrawHooks.addOnce(postPlotDraw);
+ };
+
+ $.jqplot.PieRenderer.prototype.setGridData = function(plot) {
+ // set gridData property. This will hold angle in radians of each data point.
+ var stack = [];
+ var td = [];
+ var sa = this.startAngle/180*Math.PI;
+ var tot = 0;
+ // don't know if we have any valid data yet, so set plot to not draw.
+ this._drawData = false;
+ for (var i=0; i<this.data.length; i++){
+ if (this.data[i][1] != 0) {
+ // we have data, O.K. to draw.
+ this._drawData = true;
+ }
+ stack.push(this.data[i][1]);
+ td.push([this.data[i][0]]);
+ if (i>0) {
+ stack[i] += stack[i-1];
+ }
+ tot += this.data[i][1];
+ }
+ var fact = Math.PI*2/stack[stack.length - 1];
+
+ for (var i=0; i<stack.length; i++) {
+ td[i][1] = stack[i] * fact;
+ td[i][2] = this.data[i][1]/tot;
+ }
+ this.gridData = td;
+ };
+
+ $.jqplot.PieRenderer.prototype.makeGridData = function(data, plot) {
+ var stack = [];
+ var td = [];
+ var tot = 0;
+ var sa = this.startAngle/180*Math.PI;
+ // don't know if we have any valid data yet, so set plot to not draw.
+ this._drawData = false;
+ for (var i=0; i<data.length; i++){
+ if (this.data[i][1] != 0) {
+ // we have data, O.K. to draw.
+ this._drawData = true;
+ }
+ stack.push(data[i][1]);
+ td.push([data[i][0]]);
+ if (i>0) {
+ stack[i] += stack[i-1];
+ }
+ tot += data[i][1];
+ }
+ var fact = Math.PI*2/stack[stack.length - 1];
+
+ for (var i=0; i<stack.length; i++) {
+ td[i][1] = stack[i] * fact;
+ td[i][2] = data[i][1]/tot;
+ }
+ return td;
+ };
+
+ function calcRadiusAdjustment(ang) {
+ return Math.sin((ang - (ang-Math.PI) / 8 / Math.PI )/2.0);
+ }
+
+ function calcRPrime(ang1, ang2, sliceMargin, fill, lineWidth) {
+ var rprime = 0;
+ var ang = ang2 - ang1;
+ var absang = Math.abs(ang);
+ var sm = sliceMargin;
+ if (fill == false) {
+ sm += lineWidth;
+ }
+
+ if (sm > 0 && absang > 0.01 && absang < 6.282) {
+ rprime = parseFloat(sm) / 2.0 / calcRadiusAdjustment(ang);
+ }
+
+ return rprime;
+ }
+
+ $.jqplot.PieRenderer.prototype.drawSlice = function (ctx, ang1, ang2, color, isShadow) {
+ if (this._drawData) {
+ var r = this._radius;
+ var fill = this.fill;
+ var lineWidth = this.lineWidth;
+ var sm = this.sliceMargin;
+ if (this.fill == false) {
+ sm += this.lineWidth;
+ }
+ ctx.save();
+ ctx.translate(this._center[0], this._center[1]);
+
+ var rprime = calcRPrime(ang1, ang2, this.sliceMargin, this.fill, this.lineWidth);
+
+ var transx = rprime * Math.cos((ang1 + ang2) / 2.0);
+ var transy = rprime * Math.sin((ang1 + ang2) / 2.0);
+
+ if ((ang2 - ang1) <= Math.PI) {
+ r -= rprime;
+ }
+ else {
+ r += rprime;
+ }
+
+ ctx.translate(transx, transy);
+
+ if (isShadow) {
+ for (var i=0, l=this.shadowDepth; i<l; i++) {
+ ctx.save();
+ ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
+ doDraw(r);
+ }
+ for (var i=0, l=this.shadowDepth; i<l; i++) {
+ ctx.restore();
+ }
+ }
+
+ else {
+ doDraw(r);
+ }
+ ctx.restore();
+ }
+
+ function doDraw (rad) {
+ // Fix for IE and Chrome that can't seem to draw circles correctly.
+ // ang2 should always be <= 2 pi since that is the way the data is converted.
+ // 2Pi = 6.2831853, Pi = 3.1415927
+ if (ang2 > 6.282 + this.startAngle) {
+ ang2 = 6.282 + this.startAngle;
+ if (ang1 > ang2) {
+ ang1 = 6.281 + this.startAngle;
+ }
+ }
+ // Fix for IE, where it can't seem to handle 0 degree angles. Also avoids
+ // ugly line on unfilled pies.
+ if (ang1 >= ang2) {
+ return;
+ }
+
+ ctx.beginPath();
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.lineWidth = lineWidth;
+ ctx.arc(0, 0, rad, ang1, ang2, false);
+ ctx.lineTo(0,0);
+ ctx.closePath();
+
+ if (fill) {
+ ctx.fill();
+ }
+ else {
+ ctx.stroke();
+ }
+ }
+ };
+
+ // called with scope of series
+ $.jqplot.PieRenderer.prototype.draw = function (ctx, gd, options, plot) {
+ var i;
+ var opts = (options != undefined) ? options : {};
+ // offset and direction of offset due to legend placement
+ var offx = 0;
+ var offy = 0;
+ var trans = 1;
+ var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
+ if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
+ var li = options.legendInfo;
+ switch (li.location) {
+ case 'nw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'w':
+ offx = li.width + li.xoffset;
+ break;
+ case 'sw':
+ offx = li.width + li.xoffset;
+ break;
+ case 'ne':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'e':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'se':
+ offx = li.width + li.xoffset;
+ trans = -1;
+ break;
+ case 'n':
+ offy = li.height + li.yoffset;
+ break;
+ case 's':
+ offy = li.height + li.yoffset;
+ trans = -1;
+ break;
+ default:
+ break;
+ }
+ }
+
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var cw = ctx.canvas.width;
+ var ch = ctx.canvas.height;
+ var w = cw - offx - 2 * this.padding;
+ var h = ch - offy - 2 * this.padding;
+ var mindim = Math.min(w,h);
+ var d = mindim;
+
+ // Fixes issue #272. Thanks hugwijst!
+ // reset slice angles array.
+ this._sliceAngles = [];
+
+ var sm = this.sliceMargin;
+ if (this.fill == false) {
+ sm += this.lineWidth;
+ }
+
+ var rprime;
+ var maxrprime = 0;
+
+ var ang, ang1, ang2, shadowColor;
+ var sa = this.startAngle / 180 * Math.PI;
+
+ // have to pre-draw shadows, so loop throgh here and calculate some values also.
+ for (var i=0, l=gd.length; i<l; i++) {
+ ang1 = (i == 0) ? sa : gd[i-1][1] + sa;
+ ang2 = gd[i][1] + sa;
+
+ this._sliceAngles.push([ang1, ang2]);
+
+ rprime = calcRPrime(ang1, ang2, this.sliceMargin, this.fill, this.lineWidth);
+
+ if (Math.abs(ang2-ang1) > Math.PI) {
+ maxrprime = Math.max(rprime, maxrprime);
+ }
+ }
+
+ if (this.diameter != null && this.diameter > 0) {
+ this._diameter = this.diameter - 2*maxrprime;
+ }
+ else {
+ this._diameter = d - 2*maxrprime;
+ }
+
+ // Need to check for undersized pie. This can happen if
+ // plot area too small and legend is too big.
+ if (this._diameter < 6) {
+ $.jqplot.log('Diameter of pie too small, not rendering.');
+ return;
+ }
+
+ var r = this._radius = this._diameter/2;
+
+ this._center = [(cw - trans * offx)/2 + trans * offx + maxrprime * Math.cos(sa), (ch - trans*offy)/2 + trans * offy + maxrprime * Math.sin(sa)];
+
+ if (this.shadow) {
+ for (var i=0, l=gd.length; i<l; i++) {
+ shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
+ this.renderer.drawSlice.call (this, ctx, this._sliceAngles[i][0], this._sliceAngles[i][1], shadowColor, true);
+ }
+ }
+
+ for (var i=0; i<gd.length; i++) {
+
+ this.renderer.drawSlice.call (this, ctx, this._sliceAngles[i][0], this._sliceAngles[i][1], colorGenerator.next(), false);
+
+ if (this.showDataLabels && gd[i][2]*100 >= this.dataLabelThreshold) {
+ var fstr, avgang = (this._sliceAngles[i][0] + this._sliceAngles[i][1])/2, label;
+
+ if (this.dataLabels == 'label') {
+ fstr = this.dataLabelFormatString || '%s';
+ label = $.jqplot.sprintf(fstr, gd[i][0]);
+ }
+ else if (this.dataLabels == 'value') {
+ fstr = this.dataLabelFormatString || '%d';
+ label = $.jqplot.sprintf(fstr, this.data[i][1]);
+ }
+ else if (this.dataLabels == 'percent') {
+ fstr = this.dataLabelFormatString || '%d%%';
+ label = $.jqplot.sprintf(fstr, gd[i][2]*100);
+ }
+ else if (this.dataLabels.constructor == Array) {
+ fstr = this.dataLabelFormatString || '%s';
+ label = $.jqplot.sprintf(fstr, this.dataLabels[i]);
+ }
+
+ var fact = (this._radius ) * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
+
+ var x = this._center[0] + Math.cos(avgang) * fact + this.canvas._offsets.left;
+ var y = this._center[1] + Math.sin(avgang) * fact + this.canvas._offsets.top;
+
+ var labelelem = $('<div class="jqplot-pie-series jqplot-data-label" style="position:absolute;">' + label + '</div>').insertBefore(plot.eventCanvas._elem);
+ if (this.dataLabelCenterOn) {
+ x -= labelelem.width()/2;
+ y -= labelelem.height()/2;
+ }
+ else {
+ x -= labelelem.width() * Math.sin(avgang/2);
+ y -= labelelem.height()/2;
+ }
+ x = Math.round(x);
+ y = Math.round(y);
+ labelelem.css({left: x, top: y});
+ }
+ }
+ };
+
+ $.jqplot.PieAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.PieAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.PieAxisRenderer.prototype.constructor = $.jqplot.PieAxisRenderer;
+
+
+ // There are no traditional axes on a pie chart. We just need to provide
+ // dummy objects with properties so the plot will render.
+ // called with scope of axis object.
+ $.jqplot.PieAxisRenderer.prototype.init = function(options){
+ //
+ this.tickRenderer = $.jqplot.PieTickRenderer;
+ $.extend(true, this, options);
+ // I don't think I'm going to need _dataBounds here.
+ // have to go Axis scaling in a way to fit chart onto plot area
+ // and provide u2p and p2u functionality for mouse cursor, etc.
+ // for convienence set _dataBounds to 0 and 100 and
+ // set min/max to 0 and 100.
+ this._dataBounds = {min:0, max:100};
+ this.min = 0;
+ this.max = 100;
+ this.showTicks = false;
+ this.ticks = [];
+ this.showMark = false;
+ this.show = false;
+ };
+
+
+
+
+ $.jqplot.PieLegendRenderer = function(){
+ $.jqplot.TableLegendRenderer.call(this);
+ };
+
+ $.jqplot.PieLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
+ $.jqplot.PieLegendRenderer.prototype.constructor = $.jqplot.PieLegendRenderer;
+
+ /**
+ * Class: $.jqplot.PieLegendRenderer
+ * Legend Renderer specific to pie plots. Set by default
+ * when user creates a pie plot.
+ */
+ $.jqplot.PieLegendRenderer.prototype.init = function(options) {
+ // Group: Properties
+ //
+ // prop: numberRows
+ // Maximum number of rows in the legend. 0 or null for unlimited.
+ this.numberRows = null;
+ // prop: numberColumns
+ // Maximum number of columns in the legend. 0 or null for unlimited.
+ this.numberColumns = null;
+ $.extend(true, this, options);
+ };
+
+ // called with context of legend
+ $.jqplot.PieLegendRenderer.prototype.draw = function() {
+ var legend = this;
+ if (this.show) {
+ var series = this._series;
+
+
+ this._elem = $(document.createElement('table'));
+ this._elem.addClass('jqplot-table-legend');
+
+ var ss = {position:'absolute'};
+ if (this.background) {
+ ss['background'] = this.background;
+ }
+ if (this.border) {
+ ss['border'] = this.border;
+ }
+ if (this.fontSize) {
+ ss['fontSize'] = this.fontSize;
+ }
+ if (this.fontFamily) {
+ ss['fontFamily'] = this.fontFamily;
+ }
+ if (this.textColor) {
+ ss['textColor'] = this.textColor;
+ }
+ if (this.marginTop != null) {
+ ss['marginTop'] = this.marginTop;
+ }
+ if (this.marginBottom != null) {
+ ss['marginBottom'] = this.marginBottom;
+ }
+ if (this.marginLeft != null) {
+ ss['marginLeft'] = this.marginLeft;
+ }
+ if (this.marginRight != null) {
+ ss['marginRight'] = this.marginRight;
+ }
+
+ this._elem.css(ss);
+
+ // Pie charts legends don't go by number of series, but by number of data points
+ // in the series. Refactor things here for that.
+
+ var pad = false,
+ reverse = false,
+ nr,
+ nc;
+ var s = series[0];
+ var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
+
+ if (s.show) {
+ var pd = s.data;
+ if (this.numberRows) {
+ nr = this.numberRows;
+ if (!this.numberColumns){
+ nc = Math.ceil(pd.length/nr);
+ }
+ else{
+ nc = this.numberColumns;
+ }
+ }
+ else if (this.numberColumns) {
+ nc = this.numberColumns;
+ nr = Math.ceil(pd.length/this.numberColumns);
+ }
+ else {
+ nr = pd.length;
+ nc = 1;
+ }
+
+ var i, j;
+ var tr, td1, td2;
+ var lt, rs, color;
+ var idx = 0;
+ var div0, div1;
+
+ for (i=0; i<nr; i++) {
+ tr = $(document.createElement('tr'));
+ tr.addClass('jqplot-table-legend');
+
+ if (reverse){
+ tr.prependTo(this._elem);
+ }
+
+ else{
+ tr.appendTo(this._elem);
+ }
+
+ for (j=0; j<nc; j++) {
+ if (idx < pd.length){
+ lt = this.labels[idx] || pd[idx][0].toString();
+ color = colorGenerator.next();
+ if (!reverse){
+ if (i>0){
+ pad = true;
+ }
+ else{
+ pad = false;
+ }
+ }
+ else{
+ if (i == nr -1){
+ pad = false;
+ }
+ else{
+ pad = true;
+ }
+ }
+ rs = (pad) ? this.rowSpacing : '0';
+
+
+
+ td1 = $(document.createElement('td'));
+ td1.addClass('jqplot-table-legend jqplot-table-legend-swatch');
+ td1.css({textAlign: 'center', paddingTop: rs});
+
+ div0 = $(document.createElement('div'));
+ div0.addClass('jqplot-table-legend-swatch-outline');
+ div1 = $(document.createElement('div'));
+ div1.addClass('jqplot-table-legend-swatch');
+ div1.css({backgroundColor: color, borderColor: color});
+ td1.append(div0.append(div1));
+
+ td2 = $(document.createElement('td'));
+ td2.addClass('jqplot-table-legend jqplot-table-legend-label');
+ td2.css('paddingTop', rs);
+
+ if (this.escapeHtml){
+ td2.text(lt);
+ }
+ else {
+ td2.html(lt);
+ }
+ if (reverse) {
+ td2.prependTo(tr);
+ td1.prependTo(tr);
+ }
+ else {
+ td1.appendTo(tr);
+ td2.appendTo(tr);
+ }
+ pad = true;
+ }
+ idx++;
+ }
+ }
+ }
+ }
+ return this._elem;
+ };
+
+ $.jqplot.PieRenderer.prototype.handleMove = function(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ plot.target.trigger('jqplotDataMouseOver', ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.pieRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ plot.target.trigger('jqplotDataHighlight', ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ };
+
+
+ // this.eventCanvas._elem.bind($.jqplot.eventListenerHooks[i][0], {plot:this}, $.jqplot.eventListenerHooks[i][1]);
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.legend = options.legend || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ // only set these if there is a pie series
+ var setopts = false;
+ if (options.seriesDefaults.renderer == $.jqplot.PieRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer == $.jqplot.PieRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.PieAxisRenderer;
+ options.legend.renderer = $.jqplot.PieLegendRenderer;
+ options.legend.preDraw = true;
+ options.seriesDefaults.pointLabels = {show: false};
+ }
+ }
+
+ function postInit(target, data, options) {
+ for (var i=0; i<this.series.length; i++) {
+ if (this.series[i].renderer.constructor == $.jqplot.PieRenderer) {
+ // don't allow mouseover and mousedown at same time.
+ if (this.series[i].highlightMouseOver) {
+ this.series[i].highlightMouseDown = false;
+ }
+ }
+ }
+ }
+
+ // called with scope of plot
+ function postParseOptions(options) {
+ for (var i=0; i<this.series.length; i++) {
+ this.series[i].seriesColors = this.seriesColors;
+ this.series[i].colorGenerator = $.jqplot.colorGenerator;
+ }
+ }
+
+ function highlight (plot, sidx, pidx) {
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.pieRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.pieRenderer.highlightedSeriesIndex = sidx;
+ s.renderer.drawSlice.call(s, canvas._ctx, s._sliceAngles[pidx][0], s._sliceAngles[pidx][1], s.highlightColorGenerator.get(pidx), false);
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.pieRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.pieRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ }
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.pieRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.pieRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, ins[0], ins[1]);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
+ var idx = plot.plugins.pieRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ }
+
+ function handleClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt = jQuery.Event('jqplotDataClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var idx = plot.plugins.pieRenderer.highlightedSeriesIndex;
+ if (idx != null && plot.series[idx].highlightMouseDown) {
+ unhighlight(plot);
+ }
+ var evt = jQuery.Event('jqplotDataRightClick');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.pieRenderer && this.plugins.pieRenderer.highlightCanvas) {
+ this.plugins.pieRenderer.highlightCanvas.resetCanvas();
+ this.plugins.pieRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.pieRenderer = {highlightedSeriesIndex:null};
+ this.plugins.pieRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+
+ // do we have any data labels? if so, put highlight canvas before those
+ var labels = $(this.targetId+' .jqplot-data-label');
+ if (labels.length) {
+ $(labels[0]).before(this.plugins.pieRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-pieRenderer-highlight-canvas', this._plotDimensions, this));
+ }
+ // else put highlight canvas before event canvas.
+ else {
+ this.eventCanvas._elem.before(this.plugins.pieRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-pieRenderer-highlight-canvas', this._plotDimensions, this));
+ }
+
+ var hctx = this.plugins.pieRenderer.highlightCanvas.setContext();
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
+ }
+
+ $.jqplot.preInitHooks.push(preInit);
+
+ $.jqplot.PieTickRenderer = function() {
+ $.jqplot.AxisTickRenderer.call(this);
+ };
+
+ $.jqplot.PieTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
+ $.jqplot.PieTickRenderer.prototype.constructor = $.jqplot.PieTickRenderer;
+
+})(jQuery);
+
+ \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.pointLabels.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.pointLabels.js
new file mode 100644
index 0000000..4edc69e
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.pointLabels.js
@@ -0,0 +1,377 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+
+ /**
+ * Class: $.jqplot.PointLabels
+ * Plugin for putting labels at the data points.
+ *
+ * To use this plugin, include the js
+ * file in your source:
+ *
+ * > <script type="text/javascript" src="plugins/jqplot.pointLabels.js"></script>
+ *
+ * By default, the last value in the data ponit array in the data series is used
+ * for the label. For most series renderers, extra data can be added to the
+ * data point arrays and the last value will be used as the label.
+ *
+ * For instance,
+ * this series:
+ *
+ * > [[1,4], [3,5], [7,2]]
+ *
+ * Would, by default, use the y values in the labels.
+ * Extra data can be added to the series like so:
+ *
+ * > [[1,4,'mid'], [3 5,'hi'], [7,2,'low']]
+ *
+ * And now the point labels would be 'mid', 'low', and 'hi'.
+ *
+ * Options to the point labels and a custom labels array can be passed into the
+ * "pointLabels" option on the series option like so:
+ *
+ * > series:[{pointLabels:{
+ * > labels:['mid', 'hi', 'low'],
+ * > location:'se',
+ * > ypadding: 12
+ * > }
+ * > }]
+ *
+ * A custom labels array in the options takes precendence over any labels
+ * in the series data. If you have a custom labels array in the options,
+ * but still want to use values from the series array as labels, set the
+ * "labelsFromSeries" option to true.
+ *
+ * By default, html entities (<, >, etc.) are escaped in point labels.
+ * If you want to include actual html markup in the labels,
+ * set the "escapeHTML" option to false.
+ *
+ */
+ $.jqplot.PointLabels = function(options) {
+ // Group: Properties
+ //
+ // prop: show
+ // show the labels or not.
+ this.show = $.jqplot.config.enablePlugins;
+ // prop: location
+ // compass location where to position the label around the point.
+ // 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
+ this.location = 'n';
+ // prop: labelsFromSeries
+ // true to use labels within data point arrays.
+ this.labelsFromSeries = false;
+ // prop: seriesLabelIndex
+ // array index for location of labels within data point arrays.
+ // if null, will use the last element of the data point array.
+ this.seriesLabelIndex = null;
+ // prop: labels
+ // array of arrays of labels, one array for each series.
+ this.labels = [];
+ // actual labels that will get displayed.
+ // needed to preserve user specified labels in labels array.
+ this._labels = [];
+ // prop: stackedValue
+ // true to display value as stacked in a stacked plot.
+ // no effect if labels is specified.
+ this.stackedValue = false;
+ // prop: ypadding
+ // vertical padding in pixels between point and label
+ this.ypadding = 6;
+ // prop: xpadding
+ // horizontal padding in pixels between point and label
+ this.xpadding = 6;
+ // prop: escapeHTML
+ // true to escape html entities in the labels.
+ // If you want to include markup in the labels, set to false.
+ this.escapeHTML = true;
+ // prop: edgeTolerance
+ // Number of pixels that the label must be away from an axis
+ // boundary in order to be drawn. Negative values will allow overlap
+ // with the grid boundaries.
+ this.edgeTolerance = -5;
+ // prop: formatter
+ // A class of a formatter for the tick text. sprintf by default.
+ this.formatter = $.jqplot.DefaultTickFormatter;
+ // prop: formatString
+ // string passed to the formatter.
+ this.formatString = '';
+ // prop: hideZeros
+ // true to not show a label for a value which is 0.
+ this.hideZeros = false;
+ this._elems = [];
+
+ $.extend(true, this, options);
+ };
+
+ var locations = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
+ var locationIndicies = {'nw':0, 'n':1, 'ne':2, 'e':3, 'se':4, 's':5, 'sw':6, 'w':7};
+ var oppositeLocations = ['se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'];
+
+ // called with scope of a series
+ $.jqplot.PointLabels.init = function (target, data, seriesDefaults, opts, plot){
+ var options = $.extend(true, {}, seriesDefaults, opts);
+ options.pointLabels = options.pointLabels || {};
+ if (this.renderer.constructor === $.jqplot.BarRenderer && this.barDirection === 'horizontal' && !options.pointLabels.location) {
+ options.pointLabels.location = 'e';
+ }
+ // add a pointLabels attribute to the series plugins
+ this.plugins.pointLabels = new $.jqplot.PointLabels(options.pointLabels);
+ this.plugins.pointLabels.setLabels.call(this);
+ };
+
+ // called with scope of series
+ $.jqplot.PointLabels.prototype.setLabels = function() {
+ var p = this.plugins.pointLabels;
+ var labelIdx;
+ if (p.seriesLabelIndex != null) {
+ labelIdx = p.seriesLabelIndex;
+ }
+ else if (this.renderer.constructor === $.jqplot.BarRenderer && this.barDirection === 'horizontal') {
+ labelIdx = (this._plotData[0].length < 3) ? 0 : this._plotData[0].length -1;
+ }
+ else {
+ labelIdx = (this._plotData.length === 0) ? 0 : this._plotData[0].length -1;
+ }
+ p._labels = [];
+ if (p.labels.length === 0 || p.labelsFromSeries) {
+ if (p.stackedValue) {
+ if (this._plotData.length && this._plotData[0].length){
+ // var idx = p.seriesLabelIndex || this._plotData[0].length -1;
+ for (var i=0; i<this._plotData.length; i++) {
+ p._labels.push(this._plotData[i][labelIdx]);
+ }
+ }
+ }
+ else {
+ // var d = this._plotData;
+ var d = this.data;
+ if (this.renderer.constructor === $.jqplot.BarRenderer && this.waterfall) {
+ d = this._data;
+ }
+ if (d.length && d[0].length) {
+ // var idx = p.seriesLabelIndex || d[0].length -1;
+ for (var i=0; i<d.length; i++) {
+ p._labels.push(d[i][labelIdx]);
+ }
+ }
+ d = null;
+ }
+ }
+ else if (p.labels.length){
+ p._labels = p.labels;
+ }
+ };
+
+ $.jqplot.PointLabels.prototype.xOffset = function(elem, location, padding) {
+ location = location || this.location;
+ padding = padding || this.xpadding;
+ var offset;
+
+ switch (location) {
+ case 'nw':
+ offset = -elem.outerWidth(true) - this.xpadding;
+ break;
+ case 'n':
+ offset = -elem.outerWidth(true)/2;
+ break;
+ case 'ne':
+ offset = this.xpadding;
+ break;
+ case 'e':
+ offset = this.xpadding;
+ break;
+ case 'se':
+ offset = this.xpadding;
+ break;
+ case 's':
+ offset = -elem.outerWidth(true)/2;
+ break;
+ case 'sw':
+ offset = -elem.outerWidth(true) - this.xpadding;
+ break;
+ case 'w':
+ offset = -elem.outerWidth(true) - this.xpadding;
+ break;
+ default: // same as 'nw'
+ offset = -elem.outerWidth(true) - this.xpadding;
+ break;
+ }
+ return offset;
+ };
+
+ $.jqplot.PointLabels.prototype.yOffset = function(elem, location, padding) {
+ location = location || this.location;
+ padding = padding || this.xpadding;
+ var offset;
+
+ switch (location) {
+ case 'nw':
+ offset = -elem.outerHeight(true) - this.ypadding;
+ break;
+ case 'n':
+ offset = -elem.outerHeight(true) - this.ypadding;
+ break;
+ case 'ne':
+ offset = -elem.outerHeight(true) - this.ypadding;
+ break;
+ case 'e':
+ offset = -elem.outerHeight(true)/2;
+ break;
+ case 'se':
+ offset = this.ypadding;
+ break;
+ case 's':
+ offset = this.ypadding;
+ break;
+ case 'sw':
+ offset = this.ypadding;
+ break;
+ case 'w':
+ offset = -elem.outerHeight(true)/2;
+ break;
+ default: // same as 'nw'
+ offset = -elem.outerHeight(true) - this.ypadding;
+ break;
+ }
+ return offset;
+ };
+
+ // called with scope of series
+ $.jqplot.PointLabels.draw = function (sctx, options, plot) {
+ var p = this.plugins.pointLabels;
+ // set labels again in case they have changed.
+ p.setLabels.call(this);
+ // remove any previous labels
+ for (var i=0; i<p._elems.length; i++) {
+ // Memory Leaks patch
+ // p._elems[i].remove();
+ p._elems[i].emptyForce();
+ }
+ p._elems.splice(0, p._elems.length);
+
+ if (p.show) {
+ var ax = '_'+this._stackAxis+'axis';
+
+ if (!p.formatString) {
+ p.formatString = this[ax]._ticks[0].formatString;
+ p.formatter = this[ax]._ticks[0].formatter;
+ }
+
+ var pd = this._plotData;
+ var ppd = this._prevPlotData;
+ var xax = this._xaxis;
+ var yax = this._yaxis;
+ var elem, helem;
+
+ for (var i=0, l=p._labels.length; i < l; i++) {
+ var label = p._labels[i];
+
+ if (label == null || (p.hideZeros && parseInt(label, 10) == 0)) {
+ continue;
+ }
+
+ label = p.formatter(p.formatString, label);
+
+ helem = document.createElement('div');
+ p._elems[i] = $(helem);
+
+ elem = p._elems[i];
+
+
+ elem.addClass('jqplot-point-label jqplot-series-'+this.index+' jqplot-point-'+i);
+ elem.css('position', 'absolute');
+ elem.insertAfter(sctx.canvas);
+
+ if (p.escapeHTML) {
+ elem.text(label);
+ }
+ else {
+ elem.html(label);
+ }
+ var location = p.location;
+ if ((this.fillToZero && pd[i][1] < 0) || (this.fillToZero && this._type === 'bar' && this.barDirection === 'horizontal' && pd[i][0] < 0) || (this.waterfall && parseInt(label, 10)) < 0) {
+ location = oppositeLocations[locationIndicies[location]];
+ }
+
+
+ var ell = xax.u2p(pd[i][0]) + p.xOffset(elem, location);
+ var elt = yax.u2p(pd[i][1]) + p.yOffset(elem, location);
+
+ // we have stacked chart but are not showing stacked values,
+ // place labels in center.
+ if (this._stack && !p.stackedValue) {
+ if (this.barDirection === "vertical") {
+ elt = (this._barPoints[i][0][1] + this._barPoints[i][1][1]) / 2 + plot._gridPadding.top - 0.5 * elem.outerHeight(true);
+ }
+ else {
+ ell = (this._barPoints[i][2][0] + this._barPoints[i][0][0]) / 2 + plot._gridPadding.left - 0.5 * elem.outerWidth(true);
+ }
+ }
+
+ if (this.renderer.constructor == $.jqplot.BarRenderer) {
+ if (this.barDirection == "vertical") {
+ ell += this._barNudge;
+ }
+ else {
+ elt -= this._barNudge;
+ }
+ }
+ elem.css('left', ell);
+ elem.css('top', elt);
+ var elr = ell + elem.width();
+ var elb = elt + elem.height();
+ var et = p.edgeTolerance;
+ var scl = $(sctx.canvas).position().left;
+ var sct = $(sctx.canvas).position().top;
+ var scr = sctx.canvas.width + scl;
+ var scb = sctx.canvas.height + sct;
+ // if label is outside of allowed area, remove it
+ if (ell - et < scl || elt - et < sct || elr + et > scr || elb + et > scb) {
+ elem.remove();
+ }
+
+ elem = null;
+ helem = null;
+ }
+
+ // finally, animate them if the series is animated
+ // if (this.renderer.animation && this.renderer.animation._supported && this.renderer.animation.show && plot._drawCount < 2) {
+ // var sel = '.jqplot-point-label.jqplot-series-'+this.index;
+ // $(sel).hide();
+ // $(sel).fadeIn(1000);
+ // }
+
+ }
+ };
+
+ $.jqplot.postSeriesInitHooks.push($.jqplot.PointLabels.init);
+ $.jqplot.postDrawSeriesHooks.push($.jqplot.PointLabels.draw);
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js
new file mode 100644
index 0000000..7650e30
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js
@@ -0,0 +1,728 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ $.jqplot.PyramidAxisRenderer = function() {
+ $.jqplot.LinearAxisRenderer.call(this);
+ };
+
+ $.jqplot.PyramidAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+ $.jqplot.PyramidAxisRenderer.prototype.constructor = $.jqplot.PyramidAxisRenderer;
+
+ // called with scope of axis
+ $.jqplot.PyramidAxisRenderer.prototype.init = function(options){
+ // Group: Properties
+ //
+ // prop: position
+ // Position of axis. Values are: top, bottom , left, center, right.
+ // By default, x and x2 axes are bottom, y axis is center.
+ this.position = null;
+ // prop: drawBaseline
+ // True to draw the axis baseline.
+ this.drawBaseline = true;
+ // prop: baselineWidth
+ // width of the baseline in pixels.
+ this.baselineWidth = null;
+ // prop: baselineColor
+ // CSS color spec for the baseline.
+ this.baselineColor = null;
+ this.tickSpacingFactor = 25;
+ this._type = 'pyramid';
+ this._splitAxis = false;
+ this._splitLength = null;
+ this.category = false;
+ this._autoFormatString = '';
+ this._overrideFormatString = false;
+
+ $.extend(true, this, options);
+ this.renderer.options = options;
+
+ this.resetDataBounds = this.renderer.resetDataBounds;
+ this.resetDataBounds();
+
+ };
+
+ $.jqplot.PyramidAxisRenderer.prototype.resetDataBounds = function() {
+ // Go through all the series attached to this axis and find
+ // the min/max bounds for this axis.
+ var db = this._dataBounds;
+ db.min = null;
+ db.max = null;
+ var temp;
+ for (var i=0; i<this._series.length; i++) {
+ var s = this._series[i];
+ var d = s._plotData;
+
+ for (var j=0, l=d.length; j<l; j++) {
+ if (this.name.charAt(0) === 'x') {
+ temp = d[j][1];
+ if ((temp !== null && temp < db.min) || db.min === null) {
+ db.min = temp;
+ }
+ if ((temp !== null && temp > db.max) || db.max === null) {
+ db.max = temp;
+ }
+ }
+ else {
+ temp = d[j][0];
+ if ((temp !== null && temp < db.min) || db.min === null) {
+ db.min = temp;
+ }
+ if ((temp !== null && temp > db.max) || db.max === null) {
+ db.max = temp;
+ }
+ }
+ }
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.PyramidAxisRenderer.prototype.draw = function(ctx, plot) {
+ if (this.show) {
+ // populate the axis label and value properties.
+ // createTicks is a method on the renderer, but
+ // call it within the scope of the axis.
+ this.renderer.createTicks.call(this, plot);
+ // fill a div with axes labels in the right direction.
+ // Need to pregenerate each axis to get its bounds and
+ // position it and the labels correctly on the plot.
+ var dim=0;
+ var temp;
+ // Added for theming.
+ if (this._elem) {
+ // Memory Leaks patch
+ //this._elem.empty();
+ this._elem.emptyForce();
+ this._elem = null;
+ }
+
+ this._elem = $(document.createElement('div'));
+ this._elem.addClass('jqplot-axis jqplot-'+this.name);
+ this._elem.css('position', 'absolute');
+
+
+ if (this.name == 'xaxis' || this.name == 'x2axis') {
+ this._elem.width(this._plotDimensions.width);
+ }
+ else {
+ this._elem.height(this._plotDimensions.height);
+ }
+
+ // create a _label object.
+ this.labelOptions.axis = this.name;
+ this._label = new this.labelRenderer(this.labelOptions);
+ if (this._label.show) {
+ var elem = this._label.draw(ctx, plot);
+ elem.appendTo(this._elem);
+ elem = null;
+ }
+
+ var t = this._ticks;
+ var tick;
+ for (var i=0; i<t.length; i++) {
+ tick = t[i];
+ if (tick.show && tick.showLabel && (!tick.isMinorTick)) {
+ this._elem.append(tick.draw(ctx, plot));
+ }
+ }
+ tick = null;
+ t = null;
+ }
+ return this._elem;
+ };
+
+ // Note, primes can be found on http://primes.utm.edu/
+ var _primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997];
+
+
+ var _primesHash = {};
+
+ for (var i =0, l = _primes.length; i < l; i++) {
+ _primesHash[_primes[i]] = _primes[i];
+ }
+
+ // called with scope of axis
+ $.jqplot.PyramidAxisRenderer.prototype.createTicks = function(plot) {
+ // we're are operating on an axis here
+ var userTicks = this.ticks;
+ // databounds were set on axis initialization.
+ var db = this._dataBounds;
+ var dim;
+ var interval;
+ var min;
+ var max;
+ var range;
+ var pos1;
+ var pos2;
+ var tt;
+ var i;
+ var l;
+ var s;
+ // get a copy of user's settings for min/max.
+ var userMin = this.min;
+ var userMax = this.max;
+ var ut;
+ var t;
+ var threshold;
+ var tdim;
+ var scalefact;
+ var ret;
+ var tumin;
+ var tumax;
+ var maxVisibleTicks;
+ var val;
+ var skip = null;
+ var temp;
+
+ // if we already have ticks, use them.
+ // ticks must be in order of increasing value.
+
+ if (userTicks.length) {
+ // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+ for (i=0, l=userTicks.length; i<l; i++){
+ ut = userTicks[i];
+ t = new this.tickRenderer(this.tickOptions);
+ if ($.isArray(ut)) {
+ t.value = ut[0];
+ t.label = ut[1];
+ t.setTick(ut[0], this.name);
+ this._ticks.push(t);
+ }
+
+ else if ($.isPlainObject(ut)) {
+ $.extend(true, t, ut);
+ t.axis = this.name;
+ this._ticks.push(t);
+ }
+
+ else {
+ if (typeof ut === 'string') {
+ val = i + plot.defaultAxisStart;
+ }
+ else {
+ val = ut;
+ }
+ t.value = val;
+ t.label = ut;
+ t.axis = this.name;
+ this._ticks.push(t);
+ }
+ }
+ this.numberTicks = userTicks.length;
+ this.min = this._ticks[0].value;
+ this.max = this._ticks[this.numberTicks-1].value;
+ this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
+
+ // use user specified tickInterval if there is one
+ if (this._options.tickInterval) {
+ // hide every tick except for ticks on interval
+ var ti = this._options.tickInterval;
+ for (i=0; i<this.numberTicks; i++) {
+ if (i%ti !== 0) {
+ // this._ticks[i].show = false;
+ this._ticks[i].isMinorTick = true;
+ }
+ }
+ }
+
+ else {
+ // check if we have too many ticks
+ dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
+ maxVisibleTicks = Math.round(2.0 + dim/this.tickSpacingFactor);
+
+ if (this.numberTicks > maxVisibleTicks) {
+ // check for number of ticks we can skip
+ temp = this.numberTicks - 1;
+ for (i=2; i<temp; i++) {
+ if (temp % i === 0 && temp/i < maxVisibleTicks) {
+ skip = i-1;
+ break;
+ }
+ }
+
+ if (skip !== null) {
+ var count = 1;
+ for (i=1, l=this._ticks.length; i<l; i++) {
+ if (count <= skip) {
+ this._ticks[i].show = false;
+ count += 1;
+ }
+ else {
+ count = 1;
+ }
+ }
+ }
+ }
+ }
+
+ // if category style, add minor ticks in between
+ temp = [];
+ if (this.category) {
+ // turn off gridline and mark on first tick
+ this._ticks[0].showGridline = false;
+ this._ticks[0].showMark = false;
+
+ for (i=this._ticks.length-1; i>0; i--) {
+ t = new this.tickRenderer(this.tickOptions);
+ t.value = this._ticks[i-1].value + this.tickInterval/2.0;
+ t.label = '';
+ t.showLabel = false;
+ t.axis = this.name;
+ this._ticks[i].showGridline = false;
+ this._ticks[i].showMark = false;
+ this._ticks.splice(i, 0, t);
+ // temp.push(t);
+ }
+
+ // merge in the new ticks
+ // for (i=1, l=temp.length; i<l; i++) {
+ // this._ticks.splice(i, 0, temp[i]);
+ // }
+
+ // now add a tick at beginning and end
+ t = new this.tickRenderer(this.tickOptions);
+ t.value = this._ticks[0].value - this.tickInterval/2.0;
+ t.label = '';
+ t.showLabel = false;
+ t.axis = this.name;
+ this._ticks.unshift(t);
+
+ t = new this.tickRenderer(this.tickOptions);
+ t.value = this._ticks[this._ticks.length-1].value + this.tickInterval/2.0;
+ t.label = '';
+ t.showLabel = false;
+ t.axis = this.name;
+ this._ticks.push(t);
+
+ this.tickInterval = this.tickInterval / 2.0;
+ this.numberTicks = this._ticks.length;
+ this.min = this._ticks[0].value;
+ this.max = this._ticks[this._ticks.length-1].value;
+ }
+ }
+
+ // we don't have any ticks yet, let's make some!
+ else {
+ if (this.name.charAt(0) === 'x') {
+ dim = this._plotDimensions.width;
+ // make sure x axis is symetric about 0.
+ var tempmax = Math.max(db.max, Math.abs(db.min));
+ var tempmin = Math.min(db.min, -tempmax);
+ // min = ((this.min != null) ? this.min : tempmin);
+ // max = ((this.max != null) ? this.max : tempmax);
+ min = tempmin;
+ max = tempmax;
+ range = max - min;
+
+ if (this.tickOptions == null || !this.tickOptions.formatString) {
+ this._overrideFormatString = true;
+ }
+
+ threshold = 30;
+ tdim = Math.max(dim, threshold+1);
+ scalefact = (tdim-threshold)/300.0;
+ ret = $.jqplot.LinearTickGenerator(min, max, scalefact);
+ // calculate a padded max and min, points should be less than these
+ // so that they aren't too close to the edges of the plot.
+ // User can adjust how much padding is allowed with pad, padMin and PadMax options.
+ tumin = min + range*(this.padMin - 1);
+ tumax = max - range*(this.padMax - 1);
+
+ if (min < tumin || max > tumax) {
+ tumin = min - range*(this.padMin - 1);
+ tumax = max + range*(this.padMax - 1);
+ ret = $.jqplot.LinearTickGenerator(tumin, tumax, scalefact);
+ }
+
+ this.min = ret[0];
+ this.max = ret[1];
+ this.numberTicks = ret[2];
+ this._autoFormatString = ret[3];
+ this.tickInterval = ret[4];
+ }
+ else {
+ dim = this._plotDimensions.height;
+
+ // ticks will be on whole integers like 1, 2, 3, ... or 1, 4, 7, ...
+ min = db.min;
+ max = db.max;
+ s = this._series[0];
+ this._ticks = [];
+
+ range = max - min;
+
+ // if range is a prime, will get only 2 ticks, expand range in that case.
+ if (_primesHash[range]) {
+ range += 1;
+ max += 1;
+ }
+
+ this.max = max;
+ this.min = min;
+
+ maxVisibleTicks = Math.round(2.0 + dim/this.tickSpacingFactor);
+
+ if (range + 1 <= maxVisibleTicks) {
+ this.numberTicks = range + 1;
+ this.tickInterval = 1.0;
+ }
+
+ else {
+ // figure out a round number of ticks to skip in every interval
+ // range / ti + 1 = nt
+ // ti = range / (nt - 1)
+ for (var i=maxVisibleTicks; i>1; i--) {
+ if (range/(i - 1) === Math.round(range/(i - 1))) {
+ this.numberTicks = i;
+ this.tickInterval = range/(i - 1);
+ break;
+ }
+
+ }
+ }
+ }
+
+ if (this._overrideFormatString && this._autoFormatString != '') {
+ this.tickOptions = this.tickOptions || {};
+ this.tickOptions.formatString = this._autoFormatString;
+ }
+
+ var labelval;
+ for (i=0; i<this.numberTicks; i++) {
+ this.tickOptions.axis = this.name;
+ labelval = this.min + this.tickInterval * i;
+ if (this.name.charAt(0) === 'x') {
+ labelval = Math.abs(labelval);
+ }
+ // this.tickOptions.label = String (labelval);
+ this.tickOptions.value = this.min + this.tickInterval * i;
+ t = new this.tickRenderer(this.tickOptions);
+
+ t.label = t.prefix + t.formatter(t.formatString, labelval);
+
+ this._ticks.push(t);
+ // for x axis, if y axis is in middle, add a symetrical 0 tick
+ if (this.name.charAt(0) === 'x' && plot.axes.yMidAxis.show && this.tickOptions.value === 0) {
+ this._splitAxis = true;
+ this._splitLength = plot.axes.yMidAxis.getWidth();
+ // t.value = -this.max/2000.0;
+ t = new this.tickRenderer(this.tickOptions);
+ this._ticks.push(t);
+ t.value = this.max/2000.0;
+ }
+ }
+ t = null;
+ }
+ };
+
+ // called with scope of axis
+ $.jqplot.PyramidAxisRenderer.prototype.set = function() {
+ var dim = 0;
+ var temp;
+ var w = 0;
+ var h = 0;
+ var i;
+ var t;
+ var tick;
+ var lshow = (this._label == null) ? false : this._label.show;
+ if (this.show) {
+ t = this._ticks;
+ l = t.length;
+ for (i=0; i<l; i++) {
+ tick = t[i];
+ if (!tick._breakTick && tick.show && tick.showLabel && !tick.isMinorTick) {
+ if (this.name.charAt(0) === 'x') {
+ temp = tick._elem.outerHeight(true);
+ }
+ else {
+ temp = tick._elem.outerWidth(true);
+ }
+ if (temp > dim) {
+ dim = temp;
+ }
+ }
+ }
+
+ if (this.name === 'yMidAxis') {
+ for (i=0; i<l; i++) {
+ tick = t[i];
+ if (tick._elem) {
+ temp = (dim - tick._elem.outerWidth(true))/2.0;
+ tick._elem.css('left', temp);
+ }
+ }
+ }
+ tick = null;
+ t = null;
+
+ if (lshow) {
+ w = this._label._elem.outerWidth(true);
+ h = this._label._elem.outerHeight(true);
+ }
+ if (this.name === 'xaxis') {
+ dim = dim + h;
+ this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
+ }
+ else if (this.name === 'x2axis') {
+ dim = dim + h;
+ this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
+ }
+ else if (this.name === 'yaxis') {
+ dim = dim + w;
+ this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ else if (this.name === 'yMidAxis') {
+ // don't include width of label at all in width of axis?
+ // dim = (dim > w) ? dim : w;
+ var temp = dim/2.0 - w/2.0;
+ this._elem.css({'width':dim+'px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css({width: w, left: temp, top: 0});
+ }
+ }
+ else {
+ dim = dim + w;
+ this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
+ if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
+ this._label._elem.css('width', w+'px');
+ }
+ }
+ }
+ };
+
+ $.jqplot.PyramidAxisRenderer.prototype.pack = function(pos, offsets) {
+ // Add defaults for repacking from resetTickValues function.
+ pos = pos || {};
+ offsets = offsets || this._offsets;
+
+ var ticks = this._ticks;
+ var max = this.max;
+ var min = this.min;
+ var offmax = offsets.max;
+ var offmin = offsets.min;
+ var lshow = (this._label == null) ? false : this._label.show;
+
+ for (var p in pos) {
+ this._elem.css(p, pos[p]);
+ }
+
+ this._offsets = offsets;
+ // pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
+ var pixellength = offmax - offmin;
+ var unitlength = max - min;
+ var sl = this._splitLength;
+
+ // point to unit and unit to point conversions references to Plot DOM element top left corner.
+ if (this._splitAxis) {
+ pixellength -= this._splitLength;
+
+ // don't know that this one is correct.
+ this.p2u = function(p){
+ return (p - offmin) * unitlength / pixellength + min;
+ };
+
+ this.u2p = function(u){
+ if (u <= 0) {
+ return (u - min) * pixellength / unitlength + offmin;
+ }
+ else {
+ return (u - min) * pixellength / unitlength + offmin + sl;
+ }
+ };
+
+ this.series_u2p = function(u){
+ if (u <= 0) {
+ return (u - min) * pixellength / unitlength;
+ }
+ else {
+ return (u - min) * pixellength / unitlength + sl;
+ }
+ };
+
+ // don't know that this one is correct.
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+ else {
+ this.p2u = function(p){
+ return (p - offmin) * unitlength / pixellength + min;
+ };
+
+ this.u2p = function(u){
+ return (u - min) * pixellength / unitlength + offmin;
+ };
+
+ if (this.name.charAt(0) === 'x'){
+ this.series_u2p = function(u){
+ return (u - min) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + min;
+ };
+ }
+
+ else {
+ this.series_u2p = function(u){
+ return (u - max) * pixellength / unitlength;
+ };
+ this.series_p2u = function(p){
+ return p * unitlength / pixellength + max;
+ };
+ }
+ }
+
+ if (this.show) {
+ if (this.name.charAt(0) === 'x') {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel) {
+ var shim;
+
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'xaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ if (temp * t.angle < 0) {
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ }
+ // position at start
+ else {
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'end':
+ shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ case 'start':
+ shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ break;
+ case 'middle':
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ default:
+ shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getWidth()/2;
+ }
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('left', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var w = this._label._elem.outerWidth(true);
+ this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
+ if (this.name == 'xaxis') {
+ this._label._elem.css('bottom', '0px');
+ }
+ else {
+ this._label._elem.css('top', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ else {
+ for (var i=0; i<ticks.length; i++) {
+ var t = ticks[i];
+ if (t.show && t.showLabel && !t.isMinorTick) {
+ var shim;
+ if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
+ // will need to adjust auto positioning based on which axis this is.
+ var temp = (this.name == 'yaxis') ? 1 : -1;
+ switch (t.labelPosition) {
+ case 'auto':
+ // position at end
+ case 'end':
+ if (temp * t.angle < 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'start':
+ if (t.angle > 0) {
+ shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
+ }
+ else {
+ shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
+ }
+ break;
+ case 'middle':
+ // if (t.angle > 0) {
+ // shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
+ // }
+ // else {
+ // shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
+ // }
+ shim = -t.getHeight()/2;
+ break;
+ default:
+ shim = -t.getHeight()/2;
+ break;
+ }
+ }
+ else {
+ shim = -t.getHeight()/2;
+ }
+
+ var val = this.u2p(t.value) + shim + 'px';
+ t._elem.css('top', val);
+ t.pack();
+ }
+ }
+ if (lshow) {
+ var h = this._label._elem.outerHeight(true);
+ if (this.name !== 'yMidAxis') {
+ this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
+ }
+ if (this.name == 'yaxis') {
+ this._label._elem.css('left', '0px');
+ }
+ else if (this.name !== 'yMidAxis') {
+ this._label._elem.css('right', '0px');
+ }
+ this._label.pack();
+ }
+ }
+ }
+
+ ticks = null;
+ };
+})(jQuery);
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js
new file mode 100644
index 0000000..aeea423
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js
@@ -0,0 +1,429 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+ // Class: $.jqplot.CanvasGridRenderer
+ // The default jqPlot grid renderer, creating a grid on a canvas element.
+ // The renderer has no additional options beyond the <Grid> class.
+ $.jqplot.PyramidGridRenderer = function(){
+ $.jqplot.CanvasGridRenderer.call(this);
+ };
+
+ $.jqplot.PyramidGridRenderer.prototype = new $.jqplot.CanvasGridRenderer();
+ $.jqplot.PyramidGridRenderer.prototype.constructor = $.jqplot.PyramidGridRenderer;
+
+ // called with context of Grid object
+ $.jqplot.CanvasGridRenderer.prototype.init = function(options) {
+ this._ctx;
+ this.plotBands = {
+ show: false,
+ color: 'rgb(230, 219, 179)',
+ axis: 'y',
+ start: null,
+ interval: 10
+ };
+ $.extend(true, this, options);
+ // set the shadow renderer options
+ var sopts = {lineJoin:'miter', lineCap:'round', fill:false, isarc:false, angle:this.shadowAngle, offset:this.shadowOffset, alpha:this.shadowAlpha, depth:this.shadowDepth, lineWidth:this.shadowWidth, closePath:false, strokeStyle:this.shadowColor};
+ this.renderer.shadowRenderer.init(sopts);
+ };
+
+ $.jqplot.PyramidGridRenderer.prototype.draw = function() {
+ this._ctx = this._elem.get(0).getContext("2d");
+ var ctx = this._ctx;
+ var axes = this._axes;
+ var xp = axes.xaxis.u2p;
+ var yp = axes.yMidAxis.u2p;
+ var xnudge = axes.xaxis.max/1000.0;
+ var xp0 = xp(0);
+ var xpn = xp(xnudge);
+ var ax = ['xaxis', 'yaxis', 'x2axis', 'y2axis','yMidAxis'];
+ // Add the grid onto the grid canvas. This is the bottom most layer.
+ ctx.save();
+ ctx.clearRect(0, 0, this._plotDimensions.width, this._plotDimensions.height);
+ ctx.fillStyle = this.backgroundColor || this.background;
+
+ ctx.fillRect(this._left, this._top, this._width, this._height);
+
+ if (this.plotBands.show) {
+ ctx.save();
+ var pb = this.plotBands;
+ ctx.fillStyle = pb.color;
+ var axis;
+ var x, y, w, h;
+ // find axis to work with
+ if (pb.axis.charAt(0) === 'x') {
+ if (axes.xaxis.show) {
+ axis = axes.xaxis;
+ }
+ }
+ else if (pb.axis.charAt(0) === 'y') {
+ if (axes.yaxis.show) {
+ axis = axes.yaxis;
+ }
+ else if (axes.y2axis.show) {
+ axis = axes.y2axis;
+ }
+ else if (axes.yMidAxis.show) {
+ axis = axes.yMidAxis;
+ }
+ }
+
+ if (axis !== undefined) {
+ // draw some rectangles
+ var start = pb.start;
+ if (start === null) {
+ start = axis.min;
+ }
+ for (var i = start; i < axis.max; i += 2 * pb.interval) {
+ if (axis.name.charAt(0) === 'y') {
+ x = this._left;
+ if ((i + pb.interval) < axis.max) {
+ y = axis.series_u2p(i + pb.interval) + this._top;
+ }
+ else {
+ y = axis.series_u2p(axis.max) + this._top;
+ }
+ w = this._right - this._left;
+ h = axis.series_u2p(start) - axis.series_u2p(start + pb.interval);
+ ctx.fillRect(x, y, w, h);
+ }
+ // else {
+ // y = 0;
+ // x = axis.series_u2p(i);
+ // h = this._height;
+ // w = axis.series_u2p(start + pb.interval) - axis.series_u2p(start);
+ // }
+
+ }
+ }
+ ctx.restore();
+ }
+
+ ctx.save();
+ ctx.lineJoin = 'miter';
+ ctx.lineCap = 'butt';
+ ctx.lineWidth = this.gridLineWidth;
+ ctx.strokeStyle = this.gridLineColor;
+ var b, e, s, m;
+ for (var i=5; i>0; i--) {
+ var name = ax[i-1];
+ var axis = axes[name];
+ var ticks = axis._ticks;
+ var numticks = ticks.length;
+ if (axis.show) {
+ if (axis.drawBaseline) {
+ var bopts = {};
+ if (axis.baselineWidth !== null) {
+ bopts.lineWidth = axis.baselineWidth;
+ }
+ if (axis.baselineColor !== null) {
+ bopts.strokeStyle = axis.baselineColor;
+ }
+ switch (name) {
+ case 'xaxis':
+ if (axes.yMidAxis.show) {
+ drawLine (this._left, this._bottom, xp0, this._bottom, bopts);
+ drawLine (xpn, this._bottom, this._right, this._bottom, bopts);
+ }
+ else {
+ drawLine (this._left, this._bottom, this._right, this._bottom, bopts);
+ }
+ break;
+ case 'yaxis':
+ drawLine (this._left, this._bottom, this._left, this._top, bopts);
+ break;
+ case 'yMidAxis':
+ drawLine(xp0, this._bottom, xp0, this._top, bopts);
+ drawLine(xpn, this._bottom, xpn, this._top, bopts);
+ break;
+ case 'x2axis':
+ if (axes.yMidAxis.show) {
+ drawLine (this._left, this._top, xp0, this._top, bopts);
+ drawLine (xpn, this._top, this._right, this._top, bopts);
+ }
+ else {
+ drawLine (this._left, this._bottom, this._right, this._bottom, bopts);
+ }
+ break;
+ case 'y2axis':
+ drawLine (this._right, this._bottom, this._right, this._top, bopts);
+ break;
+
+ }
+ }
+ for (var j=numticks; j>0; j--) {
+ var t = ticks[j-1];
+ if (t.show) {
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (name) {
+ case 'xaxis':
+ // draw the grid line if we should
+ if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
+ drawLine(pos, this._top, pos, this._bottom);
+ }
+
+ // draw the mark
+ if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._bottom;
+ e = this._bottom+s;
+ break;
+ case 'inside':
+ b = this._bottom-s;
+ e = this._bottom;
+ break;
+ case 'cross':
+ b = this._bottom-s;
+ e = this._bottom+s;
+ break;
+ default:
+ b = this._bottom;
+ e = this._bottom+s;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[pos,b],[pos,e]], {lineCap:'butt', lineWidth:this.gridLineWidth, offset:this.gridLineWidth*0.75, depth:2, fill:false, closePath:false});
+ }
+ // draw the line
+ drawLine(pos, b, pos, e);
+ }
+ break;
+ case 'yaxis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
+ drawLine(this._right, pos, this._left, pos);
+ }
+
+ // draw the mark
+ if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._left-s;
+ e = this._left;
+ break;
+ case 'inside':
+ b = this._left;
+ e = this._left+s;
+ break;
+ case 'cross':
+ b = this._left-s;
+ e = this._left+s;
+ break;
+ default:
+ b = this._left-s;
+ e = this._left;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+ }
+ break;
+ case 'yMidAxis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
+ drawLine(this._left, pos, xp0, pos);
+ drawLine(xpn, pos, this._right, pos);
+ }
+ // draw the mark
+ if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+
+ b = xp0;
+ e = xp0 + s;
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+
+ b = xpn - s;
+ e = xpn;
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+ }
+ break;
+ case 'x2axis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
+ drawLine(pos, this._bottom, pos, this._top);
+ }
+
+ // draw the mark
+ if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._top-s;
+ e = this._top;
+ break;
+ case 'inside':
+ b = this._top;
+ e = this._top+s;
+ break;
+ case 'cross':
+ b = this._top-s;
+ e = this._top+s;
+ break;
+ default:
+ b = this._top-s;
+ e = this._top;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[pos,b],[pos,e]], {lineCap:'butt', lineWidth:this.gridLineWidth, offset:this.gridLineWidth*0.75, depth:2, fill:false, closePath:false});
+ }
+ drawLine(pos, b, pos, e);
+ }
+ break;
+ case 'y2axis':
+ // draw the grid line
+ if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
+ drawLine(this._left, pos, this._right, pos);
+ }
+
+ // draw the mark
+ if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
+ s = t.markSize;
+ m = t.mark;
+ var pos = Math.round(axis.u2p(t.value)) + 0.5;
+ switch (m) {
+ case 'outside':
+ b = this._right;
+ e = this._right+s;
+ break;
+ case 'inside':
+ b = this._right-s;
+ e = this._right;
+ break;
+ case 'cross':
+ b = this._right-s;
+ e = this._right+s;
+ break;
+ default:
+ b = this._right;
+ e = this._right+s;
+ break;
+ }
+ // draw the shadow
+ if (this.shadow) {
+ this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
+ }
+ drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ t = null;
+ }
+ axis = null;
+ ticks = null;
+ }
+
+ ctx.restore();
+
+ function drawLine(bx, by, ex, ey, opts) {
+ ctx.save();
+ opts = opts || {};
+ if (opts.lineWidth == null || opts.lineWidth != 0){
+ $.extend(true, ctx, opts);
+ ctx.beginPath();
+ ctx.moveTo(bx, by);
+ ctx.lineTo(ex, ey);
+ ctx.stroke();
+ }
+ ctx.restore();
+ }
+
+ if (this.shadow) {
+ if (axes.yMidAxis.show) {
+ var points = [[this._left, this._bottom], [xp0, this._bottom]];
+ this.renderer.shadowRenderer.draw(ctx, points);
+ var points = [[xpn, this._bottom], [this._right, this._bottom], [this._right, this._top]];
+ this.renderer.shadowRenderer.draw(ctx, points);
+ var points = [[xp0, this._bottom], [xp0, this._top]];
+ this.renderer.shadowRenderer.draw(ctx, points);
+ }
+ else {
+ var points = [[this._left, this._bottom], [this._right, this._bottom], [this._right, this._top]];
+ this.renderer.shadowRenderer.draw(ctx, points);
+ }
+ }
+ // Now draw border around grid. Use axis border definitions. start at
+ // upper left and go clockwise.
+ if (this.borderWidth != 0 && this.drawBorder) {
+ if (axes.yMidAxis.show) {
+ drawLine (this._left, this._top, xp0, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
+ drawLine (xpn, this._top, this._right, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
+ drawLine (this._right, this._top, this._right, this._bottom, {lineCap:'round', strokeStyle:axes.y2axis.borderColor, lineWidth:axes.y2axis.borderWidth});
+ drawLine (this._right, this._bottom, xpn, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
+ drawLine (xp0, this._bottom, this._left, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
+ drawLine (this._left, this._bottom, this._left, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
+ drawLine (xp0, this._bottom, xp0, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
+ drawLine (xpn, this._bottom, xpn, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
+ }
+ else {
+ drawLine (this._left, this._top, this._right, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
+ drawLine (this._right, this._top, this._right, this._bottom, {lineCap:'round', strokeStyle:axes.y2axis.borderColor, lineWidth:axes.y2axis.borderWidth});
+ drawLine (this._right, this._bottom, this._left, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
+ drawLine (this._left, this._bottom, this._left, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
+ }
+ }
+ // ctx.lineWidth = this.borderWidth;
+ // ctx.strokeStyle = this.borderColor;
+ // ctx.strokeRect(this._left, this._top, this._width, this._height);
+
+ ctx.restore();
+ ctx = null;
+ axes = null;
+ };
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidRenderer.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidRenderer.js
new file mode 100644
index 0000000..f230d9f
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.pyramidRenderer.js
@@ -0,0 +1,514 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+
+ // Need to ensure pyramid axis and grid renderers are loaded.
+ // You should load these with script tags in the html head, that is more efficient
+ // as the browser will cache the request.
+ // Note, have to block with synchronous request in order to execute bar renderer code.
+ if ($.jqplot.PyramidAxisRenderer === undefined) {
+ $.ajax({
+ url: $.jqplot.pluginLocation + 'jqplot.pyramidAxisRenderer.js',
+ dataType: "script",
+ async: false
+ });
+ }
+
+ if ($.jqplot.PyramidGridRenderer === undefined) {
+ $.ajax({
+ url: $.jqplot.pluginLocation + 'jqplot.pyramidGridRenderer.js',
+ dataType: "script",
+ async: false
+ });
+ }
+
+ $.jqplot.PyramidRenderer = function(){
+ $.jqplot.LineRenderer.call(this);
+ };
+
+ $.jqplot.PyramidRenderer.prototype = new $.jqplot.LineRenderer();
+ $.jqplot.PyramidRenderer.prototype.constructor = $.jqplot.PyramidRenderer;
+
+ // called with scope of a series
+ $.jqplot.PyramidRenderer.prototype.init = function(options, plot) {
+ options = options || {};
+ this._type = 'pyramid';
+ // Group: Properties
+ //
+ // prop: barPadding
+ this.barPadding = 10;
+ this.barWidth = null;
+ // prop: fill
+ // True to fill the bars.
+ this.fill = true;
+ // prop: highlightMouseOver
+ // True to highlight slice when moused over.
+ // This must be false to enable highlightMouseDown to highlight when clicking on a slice.
+ this.highlightMouseOver = true;
+ // prop: highlightMouseDown
+ // True to highlight when a mouse button is pressed over a slice.
+ // This will be disabled if highlightMouseOver is true.
+ this.highlightMouseDown = false;
+ // prop: highlightColors
+ // an array of colors to use when highlighting a slice.
+ this.highlightColors = [];
+ // prop highlightThreshold
+ // Expand the highlightable region in the x direction.
+ // E.g. a value of 3 will highlight a bar when the mouse is
+ // within 3 pixels of the bar in the x direction.
+ this.highlightThreshold = 2;
+ // prop: synchronizeHighlight
+ // Index of another series to highlight when this series is highlighted.
+ // null or false to not synchronize.
+ this.synchronizeHighlight = false;
+ // prop: offsetBars
+ // False will center bars on their y value.
+ // True will push bars up by 1/2 bar width to fill between their y values.
+ // If true, there needs to be 1 more tick than there are bars.
+ this.offsetBars = false;
+
+ // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
+ if (options.highlightMouseDown && options.highlightMouseOver == null) {
+ options.highlightMouseOver = false;
+ }
+
+ this.side = 'right';
+
+ $.extend(true, this, options);
+
+ // if (this.fill === false) {
+ // this.shadow = false;
+ // }
+
+ if (this.side === 'left') {
+ this._highlightThreshold = [[-this.highlightThreshold, 0], [-this.highlightThreshold, 0], [0,0], [0,0]];
+ }
+
+ else {
+ this._highlightThreshold = [[0,0], [0,0], [this.highlightThreshold, 0], [this.highlightThreshold, 0]];
+ }
+
+ this.renderer.options = options;
+ // index of the currenty highlighted point, if any
+ this._highlightedPoint = null;
+ // Array of actual data colors used for each data point.
+ this._dataColors = [];
+ this._barPoints = [];
+ this.fillAxis = 'y';
+ this._primaryAxis = '_yaxis';
+ this._xnudge = 0;
+
+ // set the shape renderer options
+ var opts = {lineJoin:'miter', lineCap:'butt', fill:this.fill, fillRect:this.fill, isarc:false, strokeStyle:this.color, fillStyle:this.color, closePath:this.fill, lineWidth: this.lineWidth};
+ this.renderer.shapeRenderer.init(opts);
+ // set the shadow renderer options
+ var shadow_offset = options.shadowOffset;
+ // set the shadow renderer options
+ if (shadow_offset == null) {
+ // scale the shadowOffset to the width of the line.
+ if (this.lineWidth > 2.5) {
+ shadow_offset = 1.25 * (1 + (Math.atan((this.lineWidth/2.5))/0.785398163 - 1)*0.6);
+ // var shadow_offset = this.shadowOffset;
+ }
+ // for skinny lines, don't make such a big shadow.
+ else {
+ shadow_offset = 1.25 * Math.atan((this.lineWidth/2.5))/0.785398163;
+ }
+ }
+ var sopts = {lineJoin:'miter', lineCap:'butt', fill:this.fill, fillRect:this.fill, isarc:false, angle:this.shadowAngle, offset:shadow_offset, alpha:this.shadowAlpha, depth:this.shadowDepth, closePath:this.fill, lineWidth: this.lineWidth};
+ this.renderer.shadowRenderer.init(sopts);
+
+ plot.postDrawHooks.addOnce(postPlotDraw);
+ plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
+
+ // if this is the left side of pyramid, set y values to negative.
+ if (this.side === 'left') {
+ for (var i=0, l=this.data.length; i<l; i++) {
+ this.data[i][1] = -Math.abs(this.data[i][1]);
+ }
+ }
+ };
+
+ // setGridData
+ // converts the user data values to grid coordinates and stores them
+ // in the gridData array.
+ // Called with scope of a series.
+ $.jqplot.PyramidRenderer.prototype.setGridData = function(plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var data = this._plotData;
+ var pdata = this._prevPlotData;
+ this.gridData = [];
+ this._prevGridData = [];
+ var l = data.length;
+ var adjust = false;
+ var i;
+
+ // if any data values are < 0, consider this a negative series
+ for (i = 0; i < l; i++) {
+ if (data[i][1] < 0) {
+ this.side = 'left';
+ }
+ }
+
+ if (this._yaxis.name === 'yMidAxis' && this.side === 'right') {
+ this._xnudge = this._xaxis.max/2000.0;
+ adjust = true;
+ }
+
+ for (i = 0; i < l; i++) {
+ // if not a line series or if no nulls in data, push the converted point onto the array.
+ if (data[i][0] != null && data[i][1] != null) {
+ this.gridData.push([xp(data[i][1]), yp(data[i][0])]);
+ }
+ // else if there is a null, preserve it.
+ else if (data[i][0] == null) {
+ this.gridData.push([xp(data[i][1]), null]);
+ }
+ else if (data[i][1] == null) {
+ this.gridData.push(null, [yp(data[i][0])]);
+ }
+ // finally, adjust x grid data if have to
+ if (data[i][1] === 0 && adjust) {
+ this.gridData[i][0] = xp(this._xnudge);
+ }
+ }
+ };
+
+ // makeGridData
+ // converts any arbitrary data values to grid coordinates and
+ // returns them. This method exists so that plugins can use a series'
+ // linerenderer to generate grid data points without overwriting the
+ // grid data associated with that series.
+ // Called with scope of a series.
+ $.jqplot.PyramidRenderer.prototype.makeGridData = function(data, plot) {
+ // recalculate the grid data
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var gd = [];
+ var l = data.length;
+ var adjust = false;
+ var i;
+
+ // if any data values are < 0, consider this a negative series
+ for (i = 0; i < l; i++) {
+ if (data[i][1] < 0) {
+ this.side = 'left';
+ }
+ }
+
+ if (this._yaxis.name === 'yMidAxis' && this.side === 'right') {
+ this._xnudge = this._xaxis.max/2000.0;
+ adjust = true;
+ }
+
+ for (i = 0; i < l; i++) {
+ // if not a line series or if no nulls in data, push the converted point onto the array.
+ if (data[i][0] != null && data[i][1] != null) {
+ gd.push([xp(data[i][1]), yp(data[i][0])]);
+ }
+ // else if there is a null, preserve it.
+ else if (data[i][0] == null) {
+ gd.push([xp(data[i][1]), null]);
+ }
+ else if (data[i][1] == null) {
+ gd.push([null, yp(data[i][0])]);
+ }
+ // finally, adjust x grid data if have to
+ if (data[i][1] === 0 && adjust) {
+ gd[i][0] = xp(this._xnudge);
+ }
+ }
+
+ return gd;
+ };
+
+ $.jqplot.PyramidRenderer.prototype.setBarWidth = function() {
+ // need to know how many data values we have on the approprate axis and figure it out.
+ var i;
+ var nvals = 0;
+ var nseries = 0;
+ var paxis = this[this._primaryAxis];
+ var s, series, pos;
+ nvals = paxis.max - paxis.min;
+ var nticks = paxis.numberTicks;
+ var nbins = (nticks-1)/2;
+ // so, now we have total number of axis values.
+ var temp = (this.barPadding === 0) ? 1.0 : 0;
+ if (paxis.name == 'xaxis' || paxis.name == 'x2axis') {
+ this.barWidth = (paxis._offsets.max - paxis._offsets.min) / nvals - this.barPadding + temp;
+ }
+ else {
+ if (this.fill) {
+ this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals - this.barPadding + temp;
+ }
+ else {
+ this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals;
+ }
+ }
+ };
+
+ $.jqplot.PyramidRenderer.prototype.draw = function(ctx, gridData, options) {
+ var i;
+ // Ughhh, have to make a copy of options b/c it may be modified later.
+ var opts = $.extend({}, options);
+ var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
+ var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
+ var fill = (opts.fill != undefined) ? opts.fill : this.fill;
+ var xp = this._xaxis.series_u2p;
+ var yp = this._yaxis.series_u2p;
+ var pointx, pointy;
+ // clear out data colors.
+ this._dataColors = [];
+ this._barPoints = [];
+
+ if (this.renderer.options.barWidth == null) {
+ this.renderer.setBarWidth.call(this);
+ }
+
+ // var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
+ // var nvals = temp[0];
+ // var nseries = temp[1];
+ // var pos = temp[2];
+ var points = [],
+ w,
+ h;
+
+ // this._barNudge = 0;
+
+ if (showLine) {
+ var negativeColors = new $.jqplot.ColorGenerator(this.negativeSeriesColors);
+ var positiveColors = new $.jqplot.ColorGenerator(this.seriesColors);
+ var negativeColor = negativeColors.get(this.index);
+ if (! this.useNegativeColors) {
+ negativeColor = opts.fillStyle;
+ }
+ var positiveColor = opts.fillStyle;
+ var base;
+ var xstart = this._xaxis.series_u2p(this._xnudge);
+ var ystart = this._yaxis.series_u2p(this._yaxis.min);
+ var yend = this._yaxis.series_u2p(this._yaxis.max);
+ var bw = this.barWidth;
+ var bw2 = bw/2.0;
+ var points = [];
+ var yadj = this.offsetBars ? bw2 : 0;
+
+ for (var i=0, l=gridData.length; i<l; i++) {
+ if (this.data[i][0] == null) {
+ continue;
+ }
+ base = gridData[i][1];
+ // not stacked and first series in stack
+
+ if (this._plotData[i][1] < 0) {
+ if (this.varyBarColor && !this._stack) {
+ if (this.useNegativeColors) {
+ opts.fillStyle = negativeColors.next();
+ }
+ else {
+ opts.fillStyle = positiveColors.next();
+ }
+ }
+ }
+ else {
+ if (this.varyBarColor && !this._stack) {
+ opts.fillStyle = positiveColors.next();
+ }
+ else {
+ opts.fillStyle = positiveColor;
+ }
+ }
+
+ if (this.fill) {
+
+ if (this._plotData[i][1] >= 0) {
+ // xstart = this._xaxis.series_u2p(this._xnudge);
+ w = gridData[i][0] - xstart;
+ h = this.barWidth;
+ points = [xstart, base - bw2 - yadj, w, h];
+ }
+ else {
+ // xstart = this._xaxis.series_u2p(0);
+ w = xstart - gridData[i][0];
+ h = this.barWidth;
+ points = [gridData[i][0], base - bw2 - yadj, w, h];
+ }
+
+ this._barPoints.push([[points[0], points[1] + h], [points[0], points[1]], [points[0] + w, points[1]], [points[0] + w, points[1] + h]]);
+
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, points);
+ }
+ var clr = opts.fillStyle || this.color;
+ this._dataColors.push(clr);
+ this.renderer.shapeRenderer.draw(ctx, points, opts);
+ }
+
+ else {
+ if (i === 0) {
+ points =[[xstart, ystart], [gridData[i][0], ystart], [gridData[i][0], gridData[i][1] - bw2 - yadj]];
+ }
+
+ else if (i < l-1) {
+ points = points.concat([[gridData[i-1][0], gridData[i-1][1] - bw2 - yadj], [gridData[i][0], gridData[i][1] + bw2 - yadj], [gridData[i][0], gridData[i][1] - bw2 - yadj]]);
+ }
+
+ // finally, draw the line
+ else {
+ points = points.concat([[gridData[i-1][0], gridData[i-1][1] - bw2 - yadj], [gridData[i][0], gridData[i][1] + bw2 - yadj], [gridData[i][0], yend], [xstart, yend]]);
+
+ if (shadow) {
+ this.renderer.shadowRenderer.draw(ctx, points);
+ }
+ var clr = opts.fillStyle || this.color;
+ this._dataColors.push(clr);
+ this.renderer.shapeRenderer.draw(ctx, points, opts);
+ }
+ }
+ }
+ }
+
+ if (this.highlightColors.length == 0) {
+ this.highlightColors = $.jqplot.computeHighlightColors(this._dataColors);
+ }
+
+ else if (typeof(this.highlightColors) == 'string') {
+ this.highlightColors = [];
+ for (var i=0; i<this._dataColors.length; i++) {
+ this.highlightColors.push(this.highlightColors);
+ }
+ }
+
+ };
+
+
+ // setup default renderers for axes and legend so user doesn't have to
+ // called with scope of plot
+ function preInit(target, data, options) {
+ options = options || {};
+ options.axesDefaults = options.axesDefaults || {};
+ options.grid = options.grid || {};
+ options.legend = options.legend || {};
+ options.seriesDefaults = options.seriesDefaults || {};
+ // only set these if there is a pie series
+ var setopts = false;
+ if (options.seriesDefaults.renderer === $.jqplot.PyramidRenderer) {
+ setopts = true;
+ }
+ else if (options.series) {
+ for (var i=0; i < options.series.length; i++) {
+ if (options.series[i].renderer === $.jqplot.PyramidRenderer) {
+ setopts = true;
+ }
+ }
+ }
+
+ if (setopts) {
+ options.axesDefaults.renderer = $.jqplot.PyramidAxisRenderer;
+ options.grid.renderer = $.jqplot.PyramidGridRenderer;
+ options.seriesDefaults.pointLabels = {show: false};
+ }
+ }
+
+ // called within context of plot
+ // create a canvas which we can draw on.
+ // insert it before the eventCanvas, so eventCanvas will still capture events.
+ function postPlotDraw() {
+ // Memory Leaks patch
+ if (this.plugins.pyramidRenderer && this.plugins.pyramidRenderer.highlightCanvas) {
+
+ this.plugins.pyramidRenderer.highlightCanvas.resetCanvas();
+ this.plugins.pyramidRenderer.highlightCanvas = null;
+ }
+
+ this.plugins.pyramidRenderer = {highlightedSeriesIndex:null};
+ this.plugins.pyramidRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
+
+ this.eventCanvas._elem.before(this.plugins.pyramidRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-pyramidRenderer-highlight-canvas', this._plotDimensions, this));
+ this.plugins.pyramidRenderer.highlightCanvas.setContext();
+ this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
+ }
+
+ function highlight (plot, sidx, pidx, points) {
+ var s = plot.series[sidx];
+ var canvas = plot.plugins.pyramidRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ s._highlightedPoint = pidx;
+ plot.plugins.pyramidRenderer.highlightedSeriesIndex = sidx;
+ var opts = {fillStyle: s.highlightColors[pidx], fillRect: false};
+ s.renderer.shapeRenderer.draw(canvas._ctx, points, opts);
+ if (s.synchronizeHighlight !== false && plot.series.length >= s.synchronizeHighlight && s.synchronizeHighlight !== sidx) {
+ s = plot.series[s.synchronizeHighlight];
+ opts = {fillStyle: s.highlightColors[pidx], fillRect: false};
+ s.renderer.shapeRenderer.draw(canvas._ctx, s._barPoints[pidx], opts);
+ }
+ canvas = null;
+ }
+
+ function unhighlight (plot) {
+ var canvas = plot.plugins.pyramidRenderer.highlightCanvas;
+ canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
+ for (var i=0; i<plot.series.length; i++) {
+ plot.series[i]._highlightedPoint = null;
+ }
+ plot.plugins.pyramidRenderer.highlightedSeriesIndex = null;
+ plot.target.trigger('jqplotDataUnhighlight');
+ canvas = null;
+ }
+
+
+ function handleMove(ev, gridpos, datapos, neighbor, plot) {
+ if (neighbor) {
+ var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
+ var evt1 = jQuery.Event('jqplotDataMouseOver');
+ evt1.pageX = ev.pageX;
+ evt1.pageY = ev.pageY;
+ plot.target.trigger(evt1, ins);
+ if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.pyramidRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
+ var evt = jQuery.Event('jqplotDataHighlight');
+ evt.which = ev.which;
+ evt.pageX = ev.pageX;
+ evt.pageY = ev.pageY;
+ plot.target.trigger(evt, ins);
+ highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
+ }
+ }
+ else if (neighbor == null) {
+ unhighlight (plot);
+ }
+ }
+
+ // Have to add hook here, becuase it needs called before series is inited.
+ $.jqplot.preInitHooks.push(preInit);
+
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jqplot/plugins/jqplot.trendline.js b/tipboard/static/js/lib/jqplot/plugins/jqplot.trendline.js
new file mode 100644
index 0000000..666498a
--- /dev/null
+++ b/tipboard/static/js/lib/jqplot/plugins/jqplot.trendline.js
@@ -0,0 +1,223 @@
+/**
+ * jqPlot
+ * Pure JavaScript plotting plugin using jQuery
+ *
+ * Version: 1.0.8
+ * Revision: 1250
+ *
+ * Copyright (c) 2009-2013 Chris Leonello
+ * jqPlot is currently available for use in all personal or commercial projects
+ * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
+ * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
+ * choose the license that best suits your project and use it accordingly.
+ *
+ * Although not required, the author would appreciate an email letting him
+ * know of any substantial use of jqPlot. You can reach the author at:
+ * chris at jqplot dot com or see http://www.jqplot.com/info.php .
+ *
+ * If you are feeling kind and generous, consider supporting the project by
+ * making a donation at: http://www.jqplot.com/donate.php .
+ *
+ * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
+ *
+ * version 2007.04.27
+ * author Ash Searle
+ * http://hexmen.com/blog/2007/03/printf-sprintf/
+ * http://hexmen.com/js/sprintf.js
+ * The author (Ash Searle) has placed this code in the public domain:
+ * "This code is unrestricted: you are free to use it however you like."
+ *
+ */
+(function($) {
+
+ /**
+ * Class: $.jqplot.Trendline
+ * Plugin which will automatically compute and draw trendlines for plotted data.
+ */
+ $.jqplot.Trendline = function() {
+ // Group: Properties
+
+ // prop: show
+ // Wether or not to show the trend line.
+ this.show = $.jqplot.config.enablePlugins;
+ // prop: color
+ // CSS color spec for the trend line.
+ // By default this wil be the same color as the primary line.
+ this.color = '#666666';
+ // prop: renderer
+ // Renderer to use to draw the trend line.
+ // The data series that is plotted may not be rendered as a line.
+ // Therefore, we use our own line renderer here to draw a trend line.
+ this.renderer = new $.jqplot.LineRenderer();
+ // prop: rendererOptions
+ // Options to pass to the line renderer.
+ // By default, markers are not shown on trend lines.
+ this.rendererOptions = {marker:{show:false}};
+ // prop: label
+ // Label for the trend line to use in the legend.
+ this.label = '';
+ // prop: type
+ // Either 'exponential', 'exp', or 'linear'.
+ this.type = 'linear';
+ // prop: shadow
+ // true or false, whether or not to show the shadow.
+ this.shadow = true;
+ // prop: markerRenderer
+ // Renderer to use to draw markers on the line.
+ // I think this is wrong.
+ this.markerRenderer = {show:false};
+ // prop: lineWidth
+ // Width of the trend line.
+ this.lineWidth = 1.5;
+ // prop: shadowAngle
+ // Angle of the shadow on the trend line.
+ this.shadowAngle = 45;
+ // prop: shadowOffset
+ // pixel offset for each stroke of the shadow.
+ this.shadowOffset = 1.0;
+ // prop: shadowAlpha
+ // Alpha transparency of the shadow.
+ this.shadowAlpha = 0.07;
+ // prop: shadowDepth
+ // number of strokes to make of the shadow.
+ this.shadowDepth = 3;
+ this.isTrendline = true;
+
+ };
+
+ $.jqplot.postSeriesInitHooks.push(parseTrendLineOptions);
+ $.jqplot.postDrawSeriesHooks.push(drawTrendline);
+ $.jqplot.addLegendRowHooks.push(addTrendlineLegend);
+
+ // called witin scope of the legend object
+ // current series passed in
+ // must return null or an object {label:label, color:color}
+ function addTrendlineLegend(series) {
+ var ret = null;
+ if (series.trendline && series.trendline.show) {
+ var lt = series.trendline.label.toString();
+ if (lt) {
+ ret = {label:lt, color:series.trendline.color};
+ }
+ }
+ return ret;
+ }
+
+ // called within scope of a series
+ function parseTrendLineOptions (target, data, seriesDefaults, options, plot) {
+ if (this._type && (this._type === 'line' || this._type == 'bar')) {
+ this.trendline = new $.jqplot.Trendline();
+ options = options || {};
+ $.extend(true, this.trendline, {color:this.color}, seriesDefaults.trendline, options.trendline);
+ this.trendline.renderer.init.call(this.trendline, null);
+ }
+ }
+
+ // called within scope of series object
+ function drawTrendline(sctx, options) {
+ // if we have options, merge trendline options in with precedence
+ options = $.extend(true, {}, this.trendline, options);
+
+ if (this.trendline && options.show) {
+ var fit;
+ // this.renderer.setGridData.call(this);
+ var data = options.data || this.data;
+ fit = fitData(data, this.trendline.type);
+ var gridData = options.gridData || this.renderer.makeGridData.call(this, fit.data);
+ this.trendline.renderer.draw.call(this.trendline, sctx, gridData, {showLine:true, shadow:this.trendline.shadow});
+ }
+ }
+
+ function regression(x, y, typ) {
+ var type = (typ == null) ? 'linear' : typ;
+ var N = x.length;
+ var slope;
+ var intercept;
+ var SX = 0;
+ var SY = 0;
+ var SXX = 0;
+ var SXY = 0;
+ var SYY = 0;
+ var Y = [];
+ var X = [];
+
+ if (type == 'linear') {
+ X = x;
+ Y = y;
+ }
+ else if (type == 'exp' || type == 'exponential') {
+ for ( var i=0; i<y.length; i++) {
+ // ignore points <= 0, log undefined.
+ if (y[i] <= 0) {
+ N--;
+ }
+ else {
+ X.push(x[i]);
+ Y.push(Math.log(y[i]));
+ }
+ }
+ }
+
+ for ( var i = 0; i < N; i++) {
+ SX = SX + X[i];
+ SY = SY + Y[i];
+ SXY = SXY + X[i]* Y[i];
+ SXX = SXX + X[i]* X[i];
+ SYY = SYY + Y[i]* Y[i];
+ }
+
+ slope = (N*SXY - SX*SY)/(N*SXX - SX*SX);
+ intercept = (SY - slope*SX)/N;
+
+ return [slope, intercept];
+ }
+
+ function linearRegression(X,Y) {
+ var ret;
+ ret = regression(X,Y,'linear');
+ return [ret[0],ret[1]];
+ }
+
+ function expRegression(X,Y) {
+ var ret;
+ var x = X;
+ var y = Y;
+ ret = regression(x, y,'exp');
+ var base = Math.exp(ret[0]);
+ var coeff = Math.exp(ret[1]);
+ return [base, coeff];
+ }
+
+ function fitData(data, typ) {
+ var type = (typ == null) ? 'linear' : typ;
+ var ret;
+ var res;
+ var x = [];
+ var y = [];
+ var ypred = [];
+
+ for (i=0; i<data.length; i++){
+ if (data[i] != null && data[i][0] != null && data[i][1] != null) {
+ x.push(data[i][0]);
+ y.push(data[i][1]);
+ }
+ }
+
+ if (type == 'linear') {
+ ret = linearRegression(x,y);
+ for ( var i=0; i<x.length; i++){
+ res = ret[0]*x[i] + ret[1];
+ ypred.push([x[i], res]);
+ }
+ }
+ else if (type == 'exp' || type == 'exponential') {
+ ret = expRegression(x,y);
+ for ( var i=0; i<x.length; i++){
+ res = ret[1]*Math.pow(ret[0],x[i]);
+ ypred.push([x[i], res]);
+ }
+ }
+ return {data: ypred, slope: ret[0], intercept: ret[1]};
+ }
+
+})(jQuery); \ No newline at end of file
diff --git a/tipboard/static/js/lib/jquery.fullscreen.js b/tipboard/static/js/lib/jquery.fullscreen.js
new file mode 100644
index 0000000..0527664
--- /dev/null
+++ b/tipboard/static/js/lib/jquery.fullscreen.js
@@ -0,0 +1,123 @@
+(function($, window, documentElement, height, width) {
+
+ // browser detection code courtesy of quirksmode, http://www.quirksmode.org/js/detect.html
+ // slightly simplified, as well as minor changes for readability purposes
+
+ var BrowserDetect = {
+ init: function () {
+ this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
+ this.version = this.searchVersion(navigator.userAgent)
+ || this.searchVersion(navigator.appVersion)
+ || "an unknown version";
+ this.OS = this.searchString(this.dataOS) || "an unknown OS";
+ },
+
+ searchString: function (data) {
+ for (var i=0;i<data.length;i++) {
+ var dataString = data[i].string;
+ var dataProp = data[i].prop;
+ this.versionSearchString = data[i].versionSearch || data[i].identity;
+ if (dataString) {
+ if (dataString.indexOf(data[i].subString) != -1)
+ return data[i].identity;
+ }
+ else if (dataProp)
+ return data[i].identity;
+ }
+ },
+
+ searchVersion: function (dataString) {
+ var index = dataString.indexOf(this.versionSearchString);
+ if (index == -1) return;
+ return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
+ },
+
+ dataBrowser: [
+ { string: navigator.userAgent, subString: "Chrome", identity: "Chrome" },
+ { string: navigator.vendor, subString: "Apple", identity: "Safari", versionSearch: "Version" },
+ { prop: window.opera, identity: "Opera", versionSearch: "Version" },
+ { string: navigator.userAgent, subString: "Firefox", identity: "Firefox" },
+ { string: navigator.userAgent, subString: "MSIE", identity: "Explorer", versionSearch: "MSIE" }
+ ],
+
+ dataOS : [
+ { string: navigator.platform, subString: "Win", identity: "Windows" },
+ { string: navigator.platform, subString: "Mac", identity: "Mac" },
+ { string: navigator.platform, subString: "Linux", identity: "Linux" }
+ ]
+
+ };
+
+ BrowserDetect.init();
+ // Browser name: BrowserDetect.browser
+ // Browser version: BrowserDetect.version
+ // OS name: BrowserDetect.OS
+
+ // here are major browsers' keyboard mapping for triggering fullscreen on/off
+ var keys = {
+ "MSIE": {
+ "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" }
+ },
+ "Firefox": {
+ "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" },
+ "Linux": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" },
+ "Mac": { ctrlKey: false, altKey: false, metaKey: true, shiftKey: true, which: 70, string: "&#x21E7;&#x2318;F", alt: "Shift+Command+F" }
+ },
+ "Chrome": {
+ "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" },
+ "Linux": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" },
+ "Mac": { ctrlKey: false, altKey: false, metaKey: true, shiftKey: true, which: 70, string: "&#x21E7;&#x2318;F", alt: "Shift+Command+F" }
+ },
+ "Safari": { // still missing Safari on Windows... help!
+ "Mac": { ctrlKey: true, altKey: false, metaKey: false, shiftKey: true, which: 70, string: "^&#x2318;F", alt: "Control+Command+F" }
+ },
+ "Opera": {
+ "Windows": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" },
+ "Linux": { ctrlKey: false, altKey: false, metaKey: false, shiftKey: false, which: 122, string: "F11", alt: "F11" },
+ "Mac": { ctrlKey: false, altKey: false, metaKey: true, shiftKey: true, which: 70, string: "&#x21E7;&#x2318;F", alt: "Shift+Command+F" }
+ },
+
+ };
+
+ var
+ isFullScreen = function() {
+ return (documentElement.clientHeight == height && documentElement.clientWidth == width) ||
+ window.fullScreen ||
+ (window.outerHeight == height && window.outerWidth == width) ||
+ (BrowserDetect.browser == "Safari" && window.outerHeight == (height - 40) && window.outerWidth == width)
+ ;
+ }
+ ,$window = $(window)
+ ;
+
+ var thisKeys = keys[BrowserDetect.browser][BrowserDetect.OS];
+ var shortcut = { shortcut: thisKeys.string, longform: thisKeys.alt };
+
+ $window
+ .data('fullscreen-state', isFullScreen())
+ .data('fullscreen-key', shortcut)
+ .resize(function() {
+ var fullscreenState = isFullScreen();
+
+ if ($window.data('fullscreen-state') && !fullscreenState) {
+ $window
+ .data('fullscreen-state', fullscreenState)
+ .trigger('fullscreen-toggle', [false])
+ .trigger('fullscreen-off')
+ ;
+ }
+ else if (!$window.data('fullscreen-state') && fullscreenState) {
+ $window
+ .data('fullscreen-state', fullscreenState)
+ .trigger('fullscreen-toggle', [true])
+ .trigger('fullscreen-on')
+ ;
+ }
+ })
+ .keydown(function(e) {
+ if (thisKeys && e.ctrlKey == thisKeys.ctrlKey && e.altKey == thisKeys.altKey && e.metaKey == thisKeys.metaKey && e.shiftKey == thisKeys.shiftKey && e.which == thisKeys.which)
+ $window.trigger('fullscreen-key', [thisKeys.string, thisKeys.alt]);
+ })
+ ;
+
+})(jQuery, this, document.documentElement, screen.height, screen.width);
diff --git a/tipboard/static/js/lib/jquery.js b/tipboard/static/js/lib/jquery.js
new file mode 100644
index 0000000..e2c203f
--- /dev/null
+++ b/tipboard/static/js/lib/jquery.js
@@ -0,0 +1,9597 @@
+/*!
+ * jQuery JavaScript Library v1.9.1
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2013-2-4
+ */
+(function( window, undefined ) {
+
+// Can't do this because several apps including ASP.NET trace
+// the stack via arguments.caller.callee and Firefox dies if
+// you try to trace through "use strict" call chains. (#13335)
+// Support: Firefox 18+
+//"use strict";
+var
+ // The deferred used on DOM ready
+ readyList,
+
+ // A central reference to the root jQuery(document)
+ rootjQuery,
+
+ // Support: IE<9
+ // For `typeof node.method` instead of `node.method !== undefined`
+ core_strundefined = typeof undefined,
+
+ // Use the correct document accordingly with window argument (sandbox)
+ document = window.document,
+ location = window.location,
+
+ // Map over jQuery in case of overwrite
+ _jQuery = window.jQuery,
+
+ // Map over the $ in case of overwrite
+ _$ = window.$,
+
+ // [[Class]] -> type pairs
+ class2type = {},
+
+ // List of deleted data cache ids, so we can reuse them
+ core_deletedIds = [],
+
+ core_version = "1.9.1",
+
+ // Save a reference to some core methods
+ core_concat = core_deletedIds.concat,
+ core_push = core_deletedIds.push,
+ core_slice = core_deletedIds.slice,
+ core_indexOf = core_deletedIds.indexOf,
+ core_toString = class2type.toString,
+ core_hasOwn = class2type.hasOwnProperty,
+ core_trim = core_version.trim,
+
+ // Define a local copy of jQuery
+ jQuery = function( selector, context ) {
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context, rootjQuery );
+ },
+
+ // Used for matching numbers
+ core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,
+
+ // Used for splitting on whitespace
+ core_rnotwhite = /\S+/g,
+
+ // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
+ rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+ // A simple way to check for HTML strings
+ // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+ // Strict HTML recognition (#11290: must start with <)
+ rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,
+
+ // Match a standalone tag
+ rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+
+ // JSON RegExp
+ rvalidchars = /^[\],:{}\s]*$/,
+ rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+ rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
+ rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,
+
+ // Matches dashed string for camelizing
+ rmsPrefix = /^-ms-/,
+ rdashAlpha = /-([\da-z])/gi,
+
+ // Used by jQuery.camelCase as callback to replace()
+ fcamelCase = function( all, letter ) {
+ return letter.toUpperCase();
+ },
+
+ // The ready event handler
+ completed = function( event ) {
+
+ // readyState === "complete" is good enough for us to call the dom ready in oldIE
+ if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) {
+ detach();
+ jQuery.ready();
+ }
+ },
+ // Clean-up method for dom ready events
+ detach = function() {
+ if ( document.addEventListener ) {
+ document.removeEventListener( "DOMContentLoaded", completed, false );
+ window.removeEventListener( "load", completed, false );
+
+ } else {
+ document.detachEvent( "onreadystatechange", completed );
+ window.detachEvent( "onload", completed );
+ }
+ };
+
+jQuery.fn = jQuery.prototype = {
+ // The current version of jQuery being used
+ jquery: core_version,
+
+ constructor: jQuery,
+ init: function( selector, context, rootjQuery ) {
+ var match, elem;
+
+ // HANDLE: $(""), $(null), $(undefined), $(false)
+ if ( !selector ) {
+ return this;
+ }
+
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+ // Assume that strings that start and end with <> are HTML and skip the regex check
+ match = [ null, selector, null ];
+
+ } else {
+ match = rquickExpr.exec( selector );
+ }
+
+ // Match html or make sure no context is specified for #id
+ if ( match && (match[1] || !context) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[1] ) {
+ context = context instanceof jQuery ? context[0] : context;
+
+ // scripts is true for back-compat
+ jQuery.merge( this, jQuery.parseHTML(
+ match[1],
+ context && context.nodeType ? context.ownerDocument || context : document,
+ true
+ ) );
+
+ // HANDLE: $(html, props)
+ if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
+ for ( match in context ) {
+ // Properties of context are called as methods if possible
+ if ( jQuery.isFunction( this[ match ] ) ) {
+ this[ match ]( context[ match ] );
+
+ // ...and otherwise set as attributes
+ } else {
+ this.attr( match, context[ match ] );
+ }
+ }
+ }
+
+ return this;
+
+ // HANDLE: $(#id)
+ } else {
+ elem = document.getElementById( match[2] );
+
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id !== match[2] ) {
+ return rootjQuery.find( selector );
+ }
+
+ // Otherwise, we inject the element directly into the jQuery object
+ this.length = 1;
+ this[0] = elem;
+ }
+
+ this.context = document;
+ this.selector = selector;
+ return this;
+ }
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return ( context || rootjQuery ).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
+ return this.constructor( context ).find( selector );
+ }
+
+ // HANDLE: $(DOMElement)
+ } else if ( selector.nodeType ) {
+ this.context = this[0] = selector;
+ this.length = 1;
+ return this;
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) ) {
+ return rootjQuery.ready( selector );
+ }
+
+ if ( selector.selector !== undefined ) {
+ this.selector = selector.selector;
+ this.context = selector.context;
+ }
+
+ return jQuery.makeArray( selector, this );
+ },
+
+ // Start with an empty selector
+ selector: "",
+
+ // The default length of a jQuery object is 0
+ length: 0,
+
+ // The number of elements contained in the matched element set
+ size: function() {
+ return this.length;
+ },
+
+ toArray: function() {
+ return core_slice.call( this );
+ },
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+ return num == null ?
+
+ // Return a 'clean' array
+ this.toArray() :
+
+ // Return just the object
+ ( num < 0 ? this[ this.length + num ] : this[ num ] );
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems ) {
+
+ // Build a new jQuery matched element set
+ var ret = jQuery.merge( this.constructor(), elems );
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+ ret.context = this.context;
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Execute a callback for every element in the matched set.
+ // (You can seed the arguments with an array of args, but this is
+ // only used internally.)
+ each: function( callback, args ) {
+ return jQuery.each( this, callback, args );
+ },
+
+ ready: function( fn ) {
+ // Add the callback
+ jQuery.ready.promise().done( fn );
+
+ return this;
+ },
+
+ slice: function() {
+ return this.pushStack( core_slice.apply( this, arguments ) );
+ },
+
+ first: function() {
+ return this.eq( 0 );
+ },
+
+ last: function() {
+ return this.eq( -1 );
+ },
+
+ eq: function( i ) {
+ var len = this.length,
+ j = +i + ( i < 0 ? len : 0 );
+ return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map(this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ }));
+ },
+
+ end: function() {
+ return this.prevObject || this.constructor(null);
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: core_push,
+ sort: [].sort,
+ splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+ var src, copyIsArray, copy, name, options, clone,
+ target = arguments[0] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+ target = {};
+ }
+
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && jQuery.isArray(src) ? src : [];
+
+ } else {
+ clone = src && jQuery.isPlainObject(src) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+
+jQuery.extend({
+ noConflict: function( deep ) {
+ if ( window.$ === jQuery ) {
+ window.$ = _$;
+ }
+
+ if ( deep && window.jQuery === jQuery ) {
+ window.jQuery = _jQuery;
+ }
+
+ return jQuery;
+ },
+
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+
+ // Hold (or release) the ready event
+ holdReady: function( hold ) {
+ if ( hold ) {
+ jQuery.readyWait++;
+ } else {
+ jQuery.ready( true );
+ }
+ },
+
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+
+ // Abort if there are pending holds or we're already ready
+ if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+ return;
+ }
+
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( jQuery.ready );
+ }
+
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --jQuery.readyWait > 0 ) {
+ return;
+ }
+
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ jQuery ] );
+
+ // Trigger any bound ready events
+ if ( jQuery.fn.trigger ) {
+ jQuery( document ).trigger("ready").off("ready");
+ }
+ },
+
+ // See test/unit/core.js for details concerning isFunction.
+ // Since version 1.3, DOM methods and functions like alert
+ // aren't supported. They return false on IE (#2968).
+ isFunction: function( obj ) {
+ return jQuery.type(obj) === "function";
+ },
+
+ isArray: Array.isArray || function( obj ) {
+ return jQuery.type(obj) === "array";
+ },
+
+ isWindow: function( obj ) {
+ return obj != null && obj == obj.window;
+ },
+
+ isNumeric: function( obj ) {
+ return !isNaN( parseFloat(obj) ) && isFinite( obj );
+ },
+
+ type: function( obj ) {
+ if ( obj == null ) {
+ return String( obj );
+ }
+ return typeof obj === "object" || typeof obj === "function" ?
+ class2type[ core_toString.call(obj) ] || "object" :
+ typeof obj;
+ },
+
+ isPlainObject: function( obj ) {
+ // Must be an Object.
+ // Because of IE, we also have to check the presence of the constructor property.
+ // Make sure that DOM nodes and window objects don't pass through, as well
+ if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ try {
+ // Not own constructor property must be Object
+ if ( obj.constructor &&
+ !core_hasOwn.call(obj, "constructor") &&
+ !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+ return false;
+ }
+ } catch ( e ) {
+ // IE8,9 Will throw exceptions on certain host objects #9897
+ return false;
+ }
+
+ // Own properties are enumerated firstly, so to speed up,
+ // if last one is own, then all properties are own.
+
+ var key;
+ for ( key in obj ) {}
+
+ return key === undefined || core_hasOwn.call( obj, key );
+ },
+
+ isEmptyObject: function( obj ) {
+ var name;
+ for ( name in obj ) {
+ return false;
+ }
+ return true;
+ },
+
+ error: function( msg ) {
+ throw new Error( msg );
+ },
+
+ // data: string of html
+ // context (optional): If specified, the fragment will be created in this context, defaults to document
+ // keepScripts (optional): If true, will include scripts passed in the html string
+ parseHTML: function( data, context, keepScripts ) {
+ if ( !data || typeof data !== "string" ) {
+ return null;
+ }
+ if ( typeof context === "boolean" ) {
+ keepScripts = context;
+ context = false;
+ }
+ context = context || document;
+
+ var parsed = rsingleTag.exec( data ),
+ scripts = !keepScripts && [];
+
+ // Single tag
+ if ( parsed ) {
+ return [ context.createElement( parsed[1] ) ];
+ }
+
+ parsed = jQuery.buildFragment( [ data ], context, scripts );
+ if ( scripts ) {
+ jQuery( scripts ).remove();
+ }
+ return jQuery.merge( [], parsed.childNodes );
+ },
+
+ parseJSON: function( data ) {
+ // Attempt to parse using the native JSON parser first
+ if ( window.JSON && window.JSON.parse ) {
+ return window.JSON.parse( data );
+ }
+
+ if ( data === null ) {
+ return data;
+ }
+
+ if ( typeof data === "string" ) {
+
+ // Make sure leading/trailing whitespace is removed (IE can't handle it)
+ data = jQuery.trim( data );
+
+ if ( data ) {
+ // Make sure the incoming data is actual JSON
+ // Logic borrowed from http://json.org/json2.js
+ if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+ .replace( rvalidtokens, "]" )
+ .replace( rvalidbraces, "")) ) {
+
+ return ( new Function( "return " + data ) )();
+ }
+ }
+ }
+
+ jQuery.error( "Invalid JSON: " + data );
+ },
+
+ // Cross-browser xml parsing
+ parseXML: function( data ) {
+ var xml, tmp;
+ if ( !data || typeof data !== "string" ) {
+ return null;
+ }
+ try {
+ if ( window.DOMParser ) { // Standard
+ tmp = new DOMParser();
+ xml = tmp.parseFromString( data , "text/xml" );
+ } else { // IE
+ xml = new ActiveXObject( "Microsoft.XMLDOM" );
+ xml.async = "false";
+ xml.loadXML( data );
+ }
+ } catch( e ) {
+ xml = undefined;
+ }
+ if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+ jQuery.error( "Invalid XML: " + data );
+ }
+ return xml;
+ },
+
+ noop: function() {},
+
+ // Evaluates a script in a global context
+ // Workarounds based on findings by Jim Driscoll
+ // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+ globalEval: function( data ) {
+ if ( data && jQuery.trim( data ) ) {
+ // We use execScript on Internet Explorer
+ // We use an anonymous function so that context is window
+ // rather than jQuery in Firefox
+ ( window.execScript || function( data ) {
+ window[ "eval" ].call( window, data );
+ } )( data );
+ }
+ },
+
+ // Convert dashed to camelCase; used by the css and data modules
+ // Microsoft forgot to hump their vendor prefix (#9572)
+ camelCase: function( string ) {
+ return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+ },
+
+ nodeName: function( elem, name ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+ },
+
+ // args is for internal usage only
+ each: function( obj, callback, args ) {
+ var value,
+ i = 0,
+ length = obj.length,
+ isArray = isArraylike( obj );
+
+ if ( args ) {
+ if ( isArray ) {
+ for ( ; i < length; i++ ) {
+ value = callback.apply( obj[ i ], args );
+
+ if ( value === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( i in obj ) {
+ value = callback.apply( obj[ i ], args );
+
+ if ( value === false ) {
+ break;
+ }
+ }
+ }
+
+ // A special, fast, case for the most common use of each
+ } else {
+ if ( isArray ) {
+ for ( ; i < length; i++ ) {
+ value = callback.call( obj[ i ], i, obj[ i ] );
+
+ if ( value === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( i in obj ) {
+ value = callback.call( obj[ i ], i, obj[ i ] );
+
+ if ( value === false ) {
+ break;
+ }
+ }
+ }
+ }
+
+ return obj;
+ },
+
+ // Use native String.trim function wherever possible
+ trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
+ function( text ) {
+ return text == null ?
+ "" :
+ core_trim.call( text );
+ } :
+
+ // Otherwise use our own trimming functionality
+ function( text ) {
+ return text == null ?
+ "" :
+ ( text + "" ).replace( rtrim, "" );
+ },
+
+ // results is for internal usage only
+ makeArray: function( arr, results ) {
+ var ret = results || [];
+
+ if ( arr != null ) {
+ if ( isArraylike( Object(arr) ) ) {
+ jQuery.merge( ret,
+ typeof arr === "string" ?
+ [ arr ] : arr
+ );
+ } else {
+ core_push.call( ret, arr );
+ }
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, arr, i ) {
+ var len;
+
+ if ( arr ) {
+ if ( core_indexOf ) {
+ return core_indexOf.call( arr, elem, i );
+ }
+
+ len = arr.length;
+ i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+ for ( ; i < len; i++ ) {
+ // Skip accessing in sparse arrays
+ if ( i in arr && arr[ i ] === elem ) {
+ return i;
+ }
+ }
+ }
+
+ return -1;
+ },
+
+ merge: function( first, second ) {
+ var l = second.length,
+ i = first.length,
+ j = 0;
+
+ if ( typeof l === "number" ) {
+ for ( ; j < l; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+ } else {
+ while ( second[j] !== undefined ) {
+ first[ i++ ] = second[ j++ ];
+ }
+ }
+
+ first.length = i;
+
+ return first;
+ },
+
+ grep: function( elems, callback, inv ) {
+ var retVal,
+ ret = [],
+ i = 0,
+ length = elems.length;
+ inv = !!inv;
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( ; i < length; i++ ) {
+ retVal = !!callback( elems[ i ], i );
+ if ( inv !== retVal ) {
+ ret.push( elems[ i ] );
+ }
+ }
+
+ return ret;
+ },
+
+ // arg is for internal usage only
+ map: function( elems, callback, arg ) {
+ var value,
+ i = 0,
+ length = elems.length,
+ isArray = isArraylike( elems ),
+ ret = [];
+
+ // Go through the array, translating each of the items to their
+ if ( isArray ) {
+ for ( ; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret[ ret.length ] = value;
+ }
+ }
+
+ // Go through every key on the object,
+ } else {
+ for ( i in elems ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret[ ret.length ] = value;
+ }
+ }
+ }
+
+ // Flatten any nested arrays
+ return core_concat.apply( [], ret );
+ },
+
+ // A global GUID counter for objects
+ guid: 1,
+
+ // Bind a function to a context, optionally partially applying any
+ // arguments.
+ proxy: function( fn, context ) {
+ var args, proxy, tmp;
+
+ if ( typeof context === "string" ) {
+ tmp = fn[ context ];
+ context = fn;
+ fn = tmp;
+ }
+
+ // Quick check to determine if target is callable, in the spec
+ // this throws a TypeError, but we will just return undefined.
+ if ( !jQuery.isFunction( fn ) ) {
+ return undefined;
+ }
+
+ // Simulated bind
+ args = core_slice.call( arguments, 2 );
+ proxy = function() {
+ return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
+ };
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+ return proxy;
+ },
+
+ // Multifunctional method to get and set values of a collection
+ // The value/s can optionally be executed if it's a function
+ access: function( elems, fn, key, value, chainable, emptyGet, raw ) {
+ var i = 0,
+ length = elems.length,
+ bulk = key == null;
+
+ // Sets many values
+ if ( jQuery.type( key ) === "object" ) {
+ chainable = true;
+ for ( i in key ) {
+ jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
+ }
+
+ // Sets one value
+ } else if ( value !== undefined ) {
+ chainable = true;
+
+ if ( !jQuery.isFunction( value ) ) {
+ raw = true;
+ }
+
+ if ( bulk ) {
+ // Bulk operations run against the entire set
+ if ( raw ) {
+ fn.call( elems, value );
+ fn = null;
+
+ // ...except when executing function values
+ } else {
+ bulk = fn;
+ fn = function( elem, key, value ) {
+ return bulk.call( jQuery( elem ), value );
+ };
+ }
+ }
+
+ if ( fn ) {
+ for ( ; i < length; i++ ) {
+ fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
+ }
+ }
+ }
+
+ return chainable ?
+ elems :
+
+ // Gets
+ bulk ?
+ fn.call( elems ) :
+ length ? fn( elems[0], key ) : emptyGet;
+ },
+
+ now: function() {
+ return ( new Date() ).getTime();
+ }
+});
+
+jQuery.ready.promise = function( obj ) {
+ if ( !readyList ) {
+
+ readyList = jQuery.Deferred();
+
+ // Catch cases where $(document).ready() is called after the browser event has already occurred.
+ // we once tried to use readyState "interactive" here, but it caused issues like the one
+ // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+ if ( document.readyState === "complete" ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ setTimeout( jQuery.ready );
+
+ // Standards-based browsers support DOMContentLoaded
+ } else if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", completed, false );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", completed, false );
+
+ // If IE event model is used
+ } else {
+ // Ensure firing before onload, maybe late but safe also for iframes
+ document.attachEvent( "onreadystatechange", completed );
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", completed );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var top = false;
+
+ try {
+ top = window.frameElement == null && document.documentElement;
+ } catch(e) {}
+
+ if ( top && top.doScroll ) {
+ (function doScrollCheck() {
+ if ( !jQuery.isReady ) {
+
+ try {
+ // Use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ top.doScroll("left");
+ } catch(e) {
+ return setTimeout( doScrollCheck, 50 );
+ }
+
+ // detach all dom ready events
+ detach();
+
+ // and execute any waiting functions
+ jQuery.ready();
+ }
+ })();
+ }
+ }
+ }
+ return readyList.promise( obj );
+};
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
+ class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+function isArraylike( obj ) {
+ var length = obj.length,
+ type = jQuery.type( obj );
+
+ if ( jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ if ( obj.nodeType === 1 && length ) {
+ return true;
+ }
+
+ return type === "array" || type !== "function" &&
+ ( length === 0 ||
+ typeof length === "number" && length > 0 && ( length - 1 ) in obj );
+}
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+// String to Object options format cache
+var optionsCache = {};
+
+// Convert String-formatted options into Object-formatted ones and store in cache
+function createOptions( options ) {
+ var object = optionsCache[ options ] = {};
+ jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) {
+ object[ flag ] = true;
+ });
+ return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ * options: an optional list of space-separated options that will change how
+ * the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ * once: will ensure the callback list can only be fired once (like a Deferred)
+ *
+ * memory: will keep track of previous values and will call any callback added
+ * after the list has been fired right away with the latest "memorized"
+ * values (like a Deferred)
+ *
+ * unique: will ensure a callback can only be added once (no duplicate in the list)
+ *
+ * stopOnFalse: interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+ // Convert options from String-formatted to Object-formatted if needed
+ // (we check in cache first)
+ options = typeof options === "string" ?
+ ( optionsCache[ options ] || createOptions( options ) ) :
+ jQuery.extend( {}, options );
+
+ var // Flag to know if list is currently firing
+ firing,
+ // Last fire value (for non-forgettable lists)
+ memory,
+ // Flag to know if list was already fired
+ fired,
+ // End of the loop when firing
+ firingLength,
+ // Index of currently firing callback (modified by remove if needed)
+ firingIndex,
+ // First callback to fire (used internally by add and fireWith)
+ firingStart,
+ // Actual callback list
+ list = [],
+ // Stack of fire calls for repeatable lists
+ stack = !options.once && [],
+ // Fire callbacks
+ fire = function( data ) {
+ memory = options.memory && data;
+ fired = true;
+ firingIndex = firingStart || 0;
+ firingStart = 0;
+ firingLength = list.length;
+ firing = true;
+ for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+ if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
+ memory = false; // To prevent further calls using add
+ break;
+ }
+ }
+ firing = false;
+ if ( list ) {
+ if ( stack ) {
+ if ( stack.length ) {
+ fire( stack.shift() );
+ }
+ } else if ( memory ) {
+ list = [];
+ } else {
+ self.disable();
+ }
+ }
+ },
+ // Actual Callbacks object
+ self = {
+ // Add a callback or a collection of callbacks to the list
+ add: function() {
+ if ( list ) {
+ // First, we save the current length
+ var start = list.length;
+ (function add( args ) {
+ jQuery.each( args, function( _, arg ) {
+ var type = jQuery.type( arg );
+ if ( type === "function" ) {
+ if ( !options.unique || !self.has( arg ) ) {
+ list.push( arg );
+ }
+ } else if ( arg && arg.length && type !== "string" ) {
+ // Inspect recursively
+ add( arg );
+ }
+ });
+ })( arguments );
+ // Do we need to add the callbacks to the
+ // current firing batch?
+ if ( firing ) {
+ firingLength = list.length;
+ // With memory, if we're not firing then
+ // we should call right away
+ } else if ( memory ) {
+ firingStart = start;
+ fire( memory );
+ }
+ }
+ return this;
+ },
+ // Remove a callback from the list
+ remove: function() {
+ if ( list ) {
+ jQuery.each( arguments, function( _, arg ) {
+ var index;
+ while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+ list.splice( index, 1 );
+ // Handle firing indexes
+ if ( firing ) {
+ if ( index <= firingLength ) {
+ firingLength--;
+ }
+ if ( index <= firingIndex ) {
+ firingIndex--;
+ }
+ }
+ }
+ });
+ }
+ return this;
+ },
+ // Check if a given callback is in the list.
+ // If no argument is given, return whether or not list has callbacks attached.
+ has: function( fn ) {
+ return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
+ },
+ // Remove all callbacks from the list
+ empty: function() {
+ list = [];
+ return this;
+ },
+ // Have the list do nothing anymore
+ disable: function() {
+ list = stack = memory = undefined;
+ return this;
+ },
+ // Is it disabled?
+ disabled: function() {
+ return !list;
+ },
+ // Lock the list in its current state
+ lock: function() {
+ stack = undefined;
+ if ( !memory ) {
+ self.disable();
+ }
+ return this;
+ },
+ // Is it locked?
+ locked: function() {
+ return !stack;
+ },
+ // Call all callbacks with the given context and arguments
+ fireWith: function( context, args ) {
+ args = args || [];
+ args = [ context, args.slice ? args.slice() : args ];
+ if ( list && ( !fired || stack ) ) {
+ if ( firing ) {
+ stack.push( args );
+ } else {
+ fire( args );
+ }
+ }
+ return this;
+ },
+ // Call all the callbacks with the given arguments
+ fire: function() {
+ self.fireWith( this, arguments );
+ return this;
+ },
+ // To know if the callbacks have already been called at least once
+ fired: function() {
+ return !!fired;
+ }
+ };
+
+ return self;
+};
+jQuery.extend({
+
+ Deferred: function( func ) {
+ var tuples = [
+ // action, add listener, listener list, final state
+ [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
+ [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
+ [ "notify", "progress", jQuery.Callbacks("memory") ]
+ ],
+ state = "pending",
+ promise = {
+ state: function() {
+ return state;
+ },
+ always: function() {
+ deferred.done( arguments ).fail( arguments );
+ return this;
+ },
+ then: function( /* fnDone, fnFail, fnProgress */ ) {
+ var fns = arguments;
+ return jQuery.Deferred(function( newDefer ) {
+ jQuery.each( tuples, function( i, tuple ) {
+ var action = tuple[ 0 ],
+ fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
+ // deferred[ done | fail | progress ] for forwarding actions to newDefer
+ deferred[ tuple[1] ](function() {
+ var returned = fn && fn.apply( this, arguments );
+ if ( returned && jQuery.isFunction( returned.promise ) ) {
+ returned.promise()
+ .done( newDefer.resolve )
+ .fail( newDefer.reject )
+ .progress( newDefer.notify );
+ } else {
+ newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
+ }
+ });
+ });
+ fns = null;
+ }).promise();
+ },
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ return obj != null ? jQuery.extend( obj, promise ) : promise;
+ }
+ },
+ deferred = {};
+
+ // Keep pipe for back-compat
+ promise.pipe = promise.then;
+
+ // Add list-specific methods
+ jQuery.each( tuples, function( i, tuple ) {
+ var list = tuple[ 2 ],
+ stateString = tuple[ 3 ];
+
+ // promise[ done | fail | progress ] = list.add
+ promise[ tuple[1] ] = list.add;
+
+ // Handle state
+ if ( stateString ) {
+ list.add(function() {
+ // state = [ resolved | rejected ]
+ state = stateString;
+
+ // [ reject_list | resolve_list ].disable; progress_list.lock
+ }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+ }
+
+ // deferred[ resolve | reject | notify ]
+ deferred[ tuple[0] ] = function() {
+ deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
+ return this;
+ };
+ deferred[ tuple[0] + "With" ] = list.fireWith;
+ });
+
+ // Make the deferred a promise
+ promise.promise( deferred );
+
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+
+ // All done!
+ return deferred;
+ },
+
+ // Deferred helper
+ when: function( subordinate /* , ..., subordinateN */ ) {
+ var i = 0,
+ resolveValues = core_slice.call( arguments ),
+ length = resolveValues.length,
+
+ // the count of uncompleted subordinates
+ remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+ // the master Deferred. If resolveValues consist of only a single Deferred, just use that.
+ deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+ // Update function for both resolve and progress values
+ updateFunc = function( i, contexts, values ) {
+ return function( value ) {
+ contexts[ i ] = this;
+ values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
+ if( values === progressValues ) {
+ deferred.notifyWith( contexts, values );
+ } else if ( !( --remaining ) ) {
+ deferred.resolveWith( contexts, values );
+ }
+ };
+ },
+
+ progressValues, progressContexts, resolveContexts;
+
+ // add listeners to Deferred subordinates; treat others as resolved
+ if ( length > 1 ) {
+ progressValues = new Array( length );
+ progressContexts = new Array( length );
+ resolveContexts = new Array( length );
+ for ( ; i < length; i++ ) {
+ if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+ resolveValues[ i ].promise()
+ .done( updateFunc( i, resolveContexts, resolveValues ) )
+ .fail( deferred.reject )
+ .progress( updateFunc( i, progressContexts, progressValues ) );
+ } else {
+ --remaining;
+ }
+ }
+ }
+
+ // if we're not waiting on anything, resolve the master
+ if ( !remaining ) {
+ deferred.resolveWith( resolveContexts, resolveValues );
+ }
+
+ return deferred.promise();
+ }
+});
+jQuery.support = (function() {
+
+ var support, all, a,
+ input, select, fragment,
+ opt, eventName, isSupported, i,
+ div = document.createElement("div");
+
+ // Setup
+ div.setAttribute( "className", "t" );
+ div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+
+ // Support tests won't run in some limited or non-browser environments
+ all = div.getElementsByTagName("*");
+ a = div.getElementsByTagName("a")[ 0 ];
+ if ( !all || !a || !all.length ) {
+ return {};
+ }
+
+ // First batch of tests
+ select = document.createElement("select");
+ opt = select.appendChild( document.createElement("option") );
+ input = div.getElementsByTagName("input")[ 0 ];
+
+ a.style.cssText = "top:1px;float:left;opacity:.5";
+ support = {
+ // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+ getSetAttribute: div.className !== "t",
+
+ // IE strips leading whitespace when .innerHTML is used
+ leadingWhitespace: div.firstChild.nodeType === 3,
+
+ // Make sure that tbody elements aren't automatically inserted
+ // IE will insert them into empty tables
+ tbody: !div.getElementsByTagName("tbody").length,
+
+ // Make sure that link elements get serialized correctly by innerHTML
+ // This requires a wrapper element in IE
+ htmlSerialize: !!div.getElementsByTagName("link").length,
+
+ // Get the style information from getAttribute
+ // (IE uses .cssText instead)
+ style: /top/.test( a.getAttribute("style") ),
+
+ // Make sure that URLs aren't manipulated
+ // (IE normalizes it by default)
+ hrefNormalized: a.getAttribute("href") === "/a",
+
+ // Make sure that element opacity exists
+ // (IE uses filter instead)
+ // Use a regex to work around a WebKit issue. See #5145
+ opacity: /^0.5/.test( a.style.opacity ),
+
+ // Verify style float existence
+ // (IE uses styleFloat instead of cssFloat)
+ cssFloat: !!a.style.cssFloat,
+
+ // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
+ checkOn: !!input.value,
+
+ // Make sure that a selected-by-default option has a working selected property.
+ // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+ optSelected: opt.selected,
+
+ // Tests for enctype support on a form (#6743)
+ enctype: !!document.createElement("form").enctype,
+
+ // Makes sure cloning an html5 element does not cause problems
+ // Where outerHTML is undefined, this still works
+ html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",
+
+ // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode
+ boxModel: document.compatMode === "CSS1Compat",
+
+ // Will be defined later
+ deleteExpando: true,
+ noCloneEvent: true,
+ inlineBlockNeedsLayout: false,
+ shrinkWrapBlocks: false,
+ reliableMarginRight: true,
+ boxSizingReliable: true,
+ pixelPosition: false
+ };
+
+ // Make sure checked status is properly cloned
+ input.checked = true;
+ support.noCloneChecked = input.cloneNode( true ).checked;
+
+ // Make sure that the options inside disabled selects aren't marked as disabled
+ // (WebKit marks them as disabled)
+ select.disabled = true;
+ support.optDisabled = !opt.disabled;
+
+ // Support: IE<9
+ try {
+ delete div.test;
+ } catch( e ) {
+ support.deleteExpando = false;
+ }
+
+ // Check if we can trust getAttribute("value")
+ input = document.createElement("input");
+ input.setAttribute( "value", "" );
+ support.input = input.getAttribute( "value" ) === "";
+
+ // Check if an input maintains its value after becoming a radio
+ input.value = "t";
+ input.setAttribute( "type", "radio" );
+ support.radioValue = input.value === "t";
+
+ // #11217 - WebKit loses check when the name is after the checked attribute
+ input.setAttribute( "checked", "t" );
+ input.setAttribute( "name", "t" );
+
+ fragment = document.createDocumentFragment();
+ fragment.appendChild( input );
+
+ // Check if a disconnected checkbox will retain its checked
+ // value of true after appended to the DOM (IE6/7)
+ support.appendChecked = input.checked;
+
+ // WebKit doesn't clone checked state correctly in fragments
+ support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+ // Support: IE<9
+ // Opera does not clone events (and typeof div.attachEvent === undefined).
+ // IE9-10 clones events bound via attachEvent, but they don't trigger with .click()
+ if ( div.attachEvent ) {
+ div.attachEvent( "onclick", function() {
+ support.noCloneEvent = false;
+ });
+
+ div.cloneNode( true ).click();
+ }
+
+ // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event)
+ // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php
+ for ( i in { submit: true, change: true, focusin: true }) {
+ div.setAttribute( eventName = "on" + i, "t" );
+
+ support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false;
+ }
+
+ div.style.backgroundClip = "content-box";
+ div.cloneNode( true ).style.backgroundClip = "";
+ support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+ // Run tests that need a body at doc ready
+ jQuery(function() {
+ var container, marginDiv, tds,
+ divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",
+ body = document.getElementsByTagName("body")[0];
+
+ if ( !body ) {
+ // Return for frameset docs that don't have a body
+ return;
+ }
+
+ container = document.createElement("div");
+ container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";
+
+ body.appendChild( container ).appendChild( div );
+
+ // Support: IE8
+ // Check if table cells still have offsetWidth/Height when they are set
+ // to display:none and there are still other visible table cells in a
+ // table row; if so, offsetWidth/Height are not reliable for use when
+ // determining if an element has been hidden directly using
+ // display:none (it is still safe to use offsets if a parent element is
+ // hidden; don safety goggles and see bug #4512 for more information).
+ div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
+ tds = div.getElementsByTagName("td");
+ tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
+ isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+ tds[ 0 ].style.display = "";
+ tds[ 1 ].style.display = "none";
+
+ // Support: IE8
+ // Check if empty table cells still have offsetWidth/Height
+ support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+ // Check box-sizing and margin behavior
+ div.innerHTML = "";
+ div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
+ support.boxSizing = ( div.offsetWidth === 4 );
+ support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 );
+
+ // Use window.getComputedStyle because jsdom on node.js will break without it.
+ if ( window.getComputedStyle ) {
+ support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
+ support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
+
+ // Check if div with explicit width and no margin-right incorrectly
+ // gets computed margin-right based on width of container. (#3333)
+ // Fails in WebKit before Feb 2011 nightlies
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ marginDiv = div.appendChild( document.createElement("div") );
+ marginDiv.style.cssText = div.style.cssText = divReset;
+ marginDiv.style.marginRight = marginDiv.style.width = "0";
+ div.style.width = "1px";
+
+ support.reliableMarginRight =
+ !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
+ }
+
+ if ( typeof div.style.zoom !== core_strundefined ) {
+ // Support: IE<8
+ // Check if natively block-level elements act like inline-block
+ // elements when setting their display to 'inline' and giving
+ // them layout
+ div.innerHTML = "";
+ div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
+ support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
+
+ // Support: IE6
+ // Check if elements with layout shrink-wrap their children
+ div.style.display = "block";
+ div.innerHTML = "<div></div>";
+ div.firstChild.style.width = "5px";
+ support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
+
+ if ( support.inlineBlockNeedsLayout ) {
+ // Prevent IE 6 from affecting layout for positioned elements #11048
+ // Prevent IE from shrinking the body in IE 7 mode #12869
+ // Support: IE<8
+ body.style.zoom = 1;
+ }
+ }
+
+ body.removeChild( container );
+
+ // Null elements to avoid leaks in IE
+ container = div = tds = marginDiv = null;
+ });
+
+ // Null elements to avoid leaks in IE
+ all = select = fragment = opt = a = input = null;
+
+ return support;
+})();
+
+var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+ rmultiDash = /([A-Z])/g;
+
+function internalData( elem, name, data, pvt /* Internal Use Only */ ){
+ if ( !jQuery.acceptData( elem ) ) {
+ return;
+ }
+
+ var thisCache, ret,
+ internalKey = jQuery.expando,
+ getByName = typeof name === "string",
+
+ // We have to handle DOM nodes and JS objects differently because IE6-7
+ // can't GC object references properly across the DOM-JS boundary
+ isNode = elem.nodeType,
+
+ // Only DOM nodes need the global jQuery cache; JS object data is
+ // attached directly to the object so GC can occur automatically
+ cache = isNode ? jQuery.cache : elem,
+
+ // Only defining an ID for JS objects if its cache already exists allows
+ // the code to shortcut on the same path as a DOM node with no cache
+ id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
+
+ // Avoid doing any more work than we need to when trying to get data on an
+ // object that has no data at all
+ if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {
+ return;
+ }
+
+ if ( !id ) {
+ // Only DOM nodes need a new unique ID for each element since their data
+ // ends up in the global cache
+ if ( isNode ) {
+ elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++;
+ } else {
+ id = internalKey;
+ }
+ }
+
+ if ( !cache[ id ] ) {
+ cache[ id ] = {};
+
+ // Avoids exposing jQuery metadata on plain JS objects when the object
+ // is serialized using JSON.stringify
+ if ( !isNode ) {
+ cache[ id ].toJSON = jQuery.noop;
+ }
+ }
+
+ // An object can be passed to jQuery.data instead of a key/value pair; this gets
+ // shallow copied over onto the existing cache
+ if ( typeof name === "object" || typeof name === "function" ) {
+ if ( pvt ) {
+ cache[ id ] = jQuery.extend( cache[ id ], name );
+ } else {
+ cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+ }
+ }
+
+ thisCache = cache[ id ];
+
+ // jQuery data() is stored in a separate object inside the object's internal data
+ // cache in order to avoid key collisions between internal data and user-defined
+ // data.
+ if ( !pvt ) {
+ if ( !thisCache.data ) {
+ thisCache.data = {};
+ }
+
+ thisCache = thisCache.data;
+ }
+
+ if ( data !== undefined ) {
+ thisCache[ jQuery.camelCase( name ) ] = data;
+ }
+
+ // Check for both converted-to-camel and non-converted data property names
+ // If a data property was specified
+ if ( getByName ) {
+
+ // First Try to find as-is property data
+ ret = thisCache[ name ];
+
+ // Test for null|undefined property data
+ if ( ret == null ) {
+
+ // Try to find the camelCased property
+ ret = thisCache[ jQuery.camelCase( name ) ];
+ }
+ } else {
+ ret = thisCache;
+ }
+
+ return ret;
+}
+
+function internalRemoveData( elem, name, pvt ) {
+ if ( !jQuery.acceptData( elem ) ) {
+ return;
+ }
+
+ var i, l, thisCache,
+ isNode = elem.nodeType,
+
+ // See jQuery.data for more information
+ cache = isNode ? jQuery.cache : elem,
+ id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+ // If there is already no cache entry for this object, there is no
+ // purpose in continuing
+ if ( !cache[ id ] ) {
+ return;
+ }
+
+ if ( name ) {
+
+ thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+ if ( thisCache ) {
+
+ // Support array or space separated string names for data keys
+ if ( !jQuery.isArray( name ) ) {
+
+ // try the string as a key before any manipulation
+ if ( name in thisCache ) {
+ name = [ name ];
+ } else {
+
+ // split the camel cased version by spaces unless a key with the spaces exists
+ name = jQuery.camelCase( name );
+ if ( name in thisCache ) {
+ name = [ name ];
+ } else {
+ name = name.split(" ");
+ }
+ }
+ } else {
+ // If "name" is an array of keys...
+ // When data is initially created, via ("key", "val") signature,
+ // keys will be converted to camelCase.
+ // Since there is no way to tell _how_ a key was added, remove
+ // both plain key and camelCase key. #12786
+ // This will only penalize the array argument path.
+ name = name.concat( jQuery.map( name, jQuery.camelCase ) );
+ }
+
+ for ( i = 0, l = name.length; i < l; i++ ) {
+ delete thisCache[ name[i] ];
+ }
+
+ // If there is no data left in the cache, we want to continue
+ // and let the cache object itself get destroyed
+ if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
+ return;
+ }
+ }
+ }
+
+ // See jQuery.data for more information
+ if ( !pvt ) {
+ delete cache[ id ].data;
+
+ // Don't destroy the parent cache unless the internal data object
+ // had been the only thing left in it
+ if ( !isEmptyDataObject( cache[ id ] ) ) {
+ return;
+ }
+ }
+
+ // Destroy the cache
+ if ( isNode ) {
+ jQuery.cleanData( [ elem ], true );
+
+ // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
+ } else if ( jQuery.support.deleteExpando || cache != cache.window ) {
+ delete cache[ id ];
+
+ // When all else fails, null
+ } else {
+ cache[ id ] = null;
+ }
+}
+
+jQuery.extend({
+ cache: {},
+
+ // Unique for each copy of jQuery on the page
+ // Non-digits removed to match rinlinejQuery
+ expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
+
+ // The following elements throw uncatchable exceptions if you
+ // attempt to add expando properties to them.
+ noData: {
+ "embed": true,
+ // Ban all objects except for Flash (which handle expandos)
+ "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
+ "applet": true
+ },
+
+ hasData: function( elem ) {
+ elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+ return !!elem && !isEmptyDataObject( elem );
+ },
+
+ data: function( elem, name, data ) {
+ return internalData( elem, name, data );
+ },
+
+ removeData: function( elem, name ) {
+ return internalRemoveData( elem, name );
+ },
+
+ // For internal use only.
+ _data: function( elem, name, data ) {
+ return internalData( elem, name, data, true );
+ },
+
+ _removeData: function( elem, name ) {
+ return internalRemoveData( elem, name, true );
+ },
+
+ // A method for determining if a DOM node can handle the data expando
+ acceptData: function( elem ) {
+ // Do not set data on non-element because it will not be cleared (#8335).
+ if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) {
+ return false;
+ }
+
+ var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+ // nodes accept data unless otherwise specified; rejection can be conditional
+ return !noData || noData !== true && elem.getAttribute("classid") === noData;
+ }
+});
+
+jQuery.fn.extend({
+ data: function( key, value ) {
+ var attrs, name,
+ elem = this[0],
+ i = 0,
+ data = null;
+
+ // Gets all values
+ if ( key === undefined ) {
+ if ( this.length ) {
+ data = jQuery.data( elem );
+
+ if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+ attrs = elem.attributes;
+ for ( ; i < attrs.length; i++ ) {
+ name = attrs[i].name;
+
+ if ( !name.indexOf( "data-" ) ) {
+ name = jQuery.camelCase( name.slice(5) );
+
+ dataAttr( elem, name, data[ name ] );
+ }
+ }
+ jQuery._data( elem, "parsedAttrs", true );
+ }
+ }
+
+ return data;
+ }
+
+ // Sets multiple values
+ if ( typeof key === "object" ) {
+ return this.each(function() {
+ jQuery.data( this, key );
+ });
+ }
+
+ return jQuery.access( this, function( value ) {
+
+ if ( value === undefined ) {
+ // Try to fetch any internally stored data first
+ return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
+ }
+
+ this.each(function() {
+ jQuery.data( this, key, value );
+ });
+ }, null, value, arguments.length > 1, null, true );
+ },
+
+ removeData: function( key ) {
+ return this.each(function() {
+ jQuery.removeData( this, key );
+ });
+ }
+});
+
+function dataAttr( elem, key, data ) {
+ // If nothing was found internally, try to fetch any
+ // data from the HTML5 data-* attribute
+ if ( data === undefined && elem.nodeType === 1 ) {
+
+ var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+ data = elem.getAttribute( name );
+
+ if ( typeof data === "string" ) {
+ try {
+ data = data === "true" ? true :
+ data === "false" ? false :
+ data === "null" ? null :
+ // Only convert to a number if it doesn't change the string
+ +data + "" === data ? +data :
+ rbrace.test( data ) ? jQuery.parseJSON( data ) :
+ data;
+ } catch( e ) {}
+
+ // Make sure we set the data so it isn't changed later
+ jQuery.data( elem, key, data );
+
+ } else {
+ data = undefined;
+ }
+ }
+
+ return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+ var name;
+ for ( name in obj ) {
+
+ // if the public data object is empty, the private is still empty
+ if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+ continue;
+ }
+ if ( name !== "toJSON" ) {
+ return false;
+ }
+ }
+
+ return true;
+}
+jQuery.extend({
+ queue: function( elem, type, data ) {
+ var queue;
+
+ if ( elem ) {
+ type = ( type || "fx" ) + "queue";
+ queue = jQuery._data( elem, type );
+
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( data ) {
+ if ( !queue || jQuery.isArray(data) ) {
+ queue = jQuery._data( elem, type, jQuery.makeArray(data) );
+ } else {
+ queue.push( data );
+ }
+ }
+ return queue || [];
+ }
+ },
+
+ dequeue: function( elem, type ) {
+ type = type || "fx";
+
+ var queue = jQuery.queue( elem, type ),
+ startLength = queue.length,
+ fn = queue.shift(),
+ hooks = jQuery._queueHooks( elem, type ),
+ next = function() {
+ jQuery.dequeue( elem, type );
+ };
+
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ startLength--;
+ }
+
+ hooks.cur = fn;
+ if ( fn ) {
+
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift( "inprogress" );
+ }
+
+ // clear up the last queue stop function
+ delete hooks.stop;
+ fn.call( elem, next, hooks );
+ }
+
+ if ( !startLength && hooks ) {
+ hooks.empty.fire();
+ }
+ },
+
+ // not intended for public consumption - generates a queueHooks object, or returns the current one
+ _queueHooks: function( elem, type ) {
+ var key = type + "queueHooks";
+ return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+ empty: jQuery.Callbacks("once memory").add(function() {
+ jQuery._removeData( elem, type + "queue" );
+ jQuery._removeData( elem, key );
+ })
+ });
+ }
+});
+
+jQuery.fn.extend({
+ queue: function( type, data ) {
+ var setter = 2;
+
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ setter--;
+ }
+
+ if ( arguments.length < setter ) {
+ return jQuery.queue( this[0], type );
+ }
+
+ return data === undefined ?
+ this :
+ this.each(function() {
+ var queue = jQuery.queue( this, type, data );
+
+ // ensure a hooks for this queue
+ jQuery._queueHooks( this, type );
+
+ if ( type === "fx" && queue[0] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ });
+ },
+ dequeue: function( type ) {
+ return this.each(function() {
+ jQuery.dequeue( this, type );
+ });
+ },
+ // Based off of the plugin by Clint Helfers, with permission.
+ // http://blindsignals.com/index.php/2009/07/jquery-delay/
+ delay: function( time, type ) {
+ time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+ type = type || "fx";
+
+ return this.queue( type, function( next, hooks ) {
+ var timeout = setTimeout( next, time );
+ hooks.stop = function() {
+ clearTimeout( timeout );
+ };
+ });
+ },
+ clearQueue: function( type ) {
+ return this.queue( type || "fx", [] );
+ },
+ // Get a promise resolved when queues of a certain type
+ // are emptied (fx is the type by default)
+ promise: function( type, obj ) {
+ var tmp,
+ count = 1,
+ defer = jQuery.Deferred(),
+ elements = this,
+ i = this.length,
+ resolve = function() {
+ if ( !( --count ) ) {
+ defer.resolveWith( elements, [ elements ] );
+ }
+ };
+
+ if ( typeof type !== "string" ) {
+ obj = type;
+ type = undefined;
+ }
+ type = type || "fx";
+
+ while( i-- ) {
+ tmp = jQuery._data( elements[ i ], type + "queueHooks" );
+ if ( tmp && tmp.empty ) {
+ count++;
+ tmp.empty.add( resolve );
+ }
+ }
+ resolve();
+ return defer.promise( obj );
+ }
+});
+var nodeHook, boolHook,
+ rclass = /[\t\r\n]/g,
+ rreturn = /\r/g,
+ rfocusable = /^(?:input|select|textarea|button|object)$/i,
+ rclickable = /^(?:a|area)$/i,
+ rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i,
+ ruseDefault = /^(?:checked|selected)$/i,
+ getSetAttribute = jQuery.support.getSetAttribute,
+ getSetInput = jQuery.support.input;
+
+jQuery.fn.extend({
+ attr: function( name, value ) {
+ return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
+ },
+
+ removeAttr: function( name ) {
+ return this.each(function() {
+ jQuery.removeAttr( this, name );
+ });
+ },
+
+ prop: function( name, value ) {
+ return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
+ },
+
+ removeProp: function( name ) {
+ name = jQuery.propFix[ name ] || name;
+ return this.each(function() {
+ // try/catch handles cases where IE balks (such as removing a property on window)
+ try {
+ this[ name ] = undefined;
+ delete this[ name ];
+ } catch( e ) {}
+ });
+ },
+
+ addClass: function( value ) {
+ var classes, elem, cur, clazz, j,
+ i = 0,
+ len = this.length,
+ proceed = typeof value === "string" && value;
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function( j ) {
+ jQuery( this ).addClass( value.call( this, j, this.className ) );
+ });
+ }
+
+ if ( proceed ) {
+ // The disjunction here is for better compressibility (see removeClass)
+ classes = ( value || "" ).match( core_rnotwhite ) || [];
+
+ for ( ; i < len; i++ ) {
+ elem = this[ i ];
+ cur = elem.nodeType === 1 && ( elem.className ?
+ ( " " + elem.className + " " ).replace( rclass, " " ) :
+ " "
+ );
+
+ if ( cur ) {
+ j = 0;
+ while ( (clazz = classes[j++]) ) {
+ if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+ cur += clazz + " ";
+ }
+ }
+ elem.className = jQuery.trim( cur );
+
+ }
+ }
+ }
+
+ return this;
+ },
+
+ removeClass: function( value ) {
+ var classes, elem, cur, clazz, j,
+ i = 0,
+ len = this.length,
+ proceed = arguments.length === 0 || typeof value === "string" && value;
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function( j ) {
+ jQuery( this ).removeClass( value.call( this, j, this.className ) );
+ });
+ }
+ if ( proceed ) {
+ classes = ( value || "" ).match( core_rnotwhite ) || [];
+
+ for ( ; i < len; i++ ) {
+ elem = this[ i ];
+ // This expression is here for better compressibility (see addClass)
+ cur = elem.nodeType === 1 && ( elem.className ?
+ ( " " + elem.className + " " ).replace( rclass, " " ) :
+ ""
+ );
+
+ if ( cur ) {
+ j = 0;
+ while ( (clazz = classes[j++]) ) {
+ // Remove *all* instances
+ while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {
+ cur = cur.replace( " " + clazz + " ", " " );
+ }
+ }
+ elem.className = value ? jQuery.trim( cur ) : "";
+ }
+ }
+ }
+
+ return this;
+ },
+
+ toggleClass: function( value, stateVal ) {
+ var type = typeof value,
+ isBool = typeof stateVal === "boolean";
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function( i ) {
+ jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+ });
+ }
+
+ return this.each(function() {
+ if ( type === "string" ) {
+ // toggle individual class names
+ var className,
+ i = 0,
+ self = jQuery( this ),
+ state = stateVal,
+ classNames = value.match( core_rnotwhite ) || [];
+
+ while ( (className = classNames[ i++ ]) ) {
+ // check each className given, space separated list
+ state = isBool ? state : !self.hasClass( className );
+ self[ state ? "addClass" : "removeClass" ]( className );
+ }
+
+ // Toggle whole class name
+ } else if ( type === core_strundefined || type === "boolean" ) {
+ if ( this.className ) {
+ // store className if set
+ jQuery._data( this, "__className__", this.className );
+ }
+
+ // If the element has a class name or if we're passed "false",
+ // then remove the whole classname (if there was one, the above saved it).
+ // Otherwise bring back whatever was previously saved (if anything),
+ // falling back to the empty string if nothing was stored.
+ this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+ }
+ });
+ },
+
+ hasClass: function( selector ) {
+ var className = " " + selector + " ",
+ i = 0,
+ l = this.length;
+ for ( ; i < l; i++ ) {
+ if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ val: function( value ) {
+ var ret, hooks, isFunction,
+ elem = this[0];
+
+ if ( !arguments.length ) {
+ if ( elem ) {
+ hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+ return ret;
+ }
+
+ ret = elem.value;
+
+ return typeof ret === "string" ?
+ // handle most common string cases
+ ret.replace(rreturn, "") :
+ // handle cases where value is null/undef or number
+ ret == null ? "" : ret;
+ }
+
+ return;
+ }
+
+ isFunction = jQuery.isFunction( value );
+
+ return this.each(function( i ) {
+ var val,
+ self = jQuery(this);
+
+ if ( this.nodeType !== 1 ) {
+ return;
+ }
+
+ if ( isFunction ) {
+ val = value.call( this, i, self.val() );
+ } else {
+ val = value;
+ }
+
+ // Treat null/undefined as ""; convert numbers to string
+ if ( val == null ) {
+ val = "";
+ } else if ( typeof val === "number" ) {
+ val += "";
+ } else if ( jQuery.isArray( val ) ) {
+ val = jQuery.map(val, function ( value ) {
+ return value == null ? "" : value + "";
+ });
+ }
+
+ hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+ // If set returns undefined, fall back to normal setting
+ if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+ this.value = val;
+ }
+ });
+ }
+});
+
+jQuery.extend({
+ valHooks: {
+ option: {
+ get: function( elem ) {
+ // attributes.value is undefined in Blackberry 4.7 but
+ // uses .value. See #6932
+ var val = elem.attributes.value;
+ return !val || val.specified ? elem.value : elem.text;
+ }
+ },
+ select: {
+ get: function( elem ) {
+ var value, option,
+ options = elem.options,
+ index = elem.selectedIndex,
+ one = elem.type === "select-one" || index < 0,
+ values = one ? null : [],
+ max = one ? index + 1 : options.length,
+ i = index < 0 ?
+ max :
+ one ? index : 0;
+
+ // Loop through all the selected options
+ for ( ; i < max; i++ ) {
+ option = options[ i ];
+
+ // oldIE doesn't update selected after form reset (#2551)
+ if ( ( option.selected || i === index ) &&
+ // Don't return options that are disabled or in a disabled optgroup
+ ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
+ ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+
+ // Get the specific value for the option
+ value = jQuery( option ).val();
+
+ // We don't need an array for one selects
+ if ( one ) {
+ return value;
+ }
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ return values;
+ },
+
+ set: function( elem, value ) {
+ var values = jQuery.makeArray( value );
+
+ jQuery(elem).find("option").each(function() {
+ this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+ });
+
+ if ( !values.length ) {
+ elem.selectedIndex = -1;
+ }
+ return values;
+ }
+ }
+ },
+
+ attr: function( elem, name, value ) {
+ var hooks, notxml, ret,
+ nType = elem.nodeType;
+
+ // don't get/set attributes on text, comment and attribute nodes
+ if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+ return;
+ }
+
+ // Fallback to prop when attributes are not supported
+ if ( typeof elem.getAttribute === core_strundefined ) {
+ return jQuery.prop( elem, name, value );
+ }
+
+ notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+ // All attributes are lowercase
+ // Grab necessary hook if one is defined
+ if ( notxml ) {
+ name = name.toLowerCase();
+ hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
+ }
+
+ if ( value !== undefined ) {
+
+ if ( value === null ) {
+ jQuery.removeAttr( elem, name );
+
+ } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+ return ret;
+
+ } else {
+ elem.setAttribute( name, value + "" );
+ return value;
+ }
+
+ } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+ return ret;
+
+ } else {
+
+ // In IE9+, Flash objects don't have .getAttribute (#12945)
+ // Support: IE9+
+ if ( typeof elem.getAttribute !== core_strundefined ) {
+ ret = elem.getAttribute( name );
+ }
+
+ // Non-existent attributes return null, we normalize to undefined
+ return ret == null ?
+ undefined :
+ ret;
+ }
+ },
+
+ removeAttr: function( elem, value ) {
+ var name, propName,
+ i = 0,
+ attrNames = value && value.match( core_rnotwhite );
+
+ if ( attrNames && elem.nodeType === 1 ) {
+ while ( (name = attrNames[i++]) ) {
+ propName = jQuery.propFix[ name ] || name;
+
+ // Boolean attributes get special treatment (#10870)
+ if ( rboolean.test( name ) ) {
+ // Set corresponding property to false for boolean attributes
+ // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8
+ if ( !getSetAttribute && ruseDefault.test( name ) ) {
+ elem[ jQuery.camelCase( "default-" + name ) ] =
+ elem[ propName ] = false;
+ } else {
+ elem[ propName ] = false;
+ }
+
+ // See #9699 for explanation of this approach (setting first, then removal)
+ } else {
+ jQuery.attr( elem, name, "" );
+ }
+
+ elem.removeAttribute( getSetAttribute ? name : propName );
+ }
+ }
+ },
+
+ attrHooks: {
+ type: {
+ set: function( elem, value ) {
+ if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+ // Setting the type on a radio button after the value resets the value in IE6-9
+ // Reset value to default in case type is set after value during creation
+ var val = elem.value;
+ elem.setAttribute( "type", value );
+ if ( val ) {
+ elem.value = val;
+ }
+ return value;
+ }
+ }
+ }
+ },
+
+ propFix: {
+ tabindex: "tabIndex",
+ readonly: "readOnly",
+ "for": "htmlFor",
+ "class": "className",
+ maxlength: "maxLength",
+ cellspacing: "cellSpacing",
+ cellpadding: "cellPadding",
+ rowspan: "rowSpan",
+ colspan: "colSpan",
+ usemap: "useMap",
+ frameborder: "frameBorder",
+ contenteditable: "contentEditable"
+ },
+
+ prop: function( elem, name, value ) {
+ var ret, hooks, notxml,
+ nType = elem.nodeType;
+
+ // don't get/set properties on text, comment and attribute nodes
+ if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+ return;
+ }
+
+ notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+ if ( notxml ) {
+ // Fix name and attach hooks
+ name = jQuery.propFix[ name ] || name;
+ hooks = jQuery.propHooks[ name ];
+ }
+
+ if ( value !== undefined ) {
+ if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+ return ret;
+
+ } else {
+ return ( elem[ name ] = value );
+ }
+
+ } else {
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+ return ret;
+
+ } else {
+ return elem[ name ];
+ }
+ }
+ },
+
+ propHooks: {
+ tabIndex: {
+ get: function( elem ) {
+ // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+ // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+ var attributeNode = elem.getAttributeNode("tabindex");
+
+ return attributeNode && attributeNode.specified ?
+ parseInt( attributeNode.value, 10 ) :
+ rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+ 0 :
+ undefined;
+ }
+ }
+ }
+});
+
+// Hook for boolean attributes
+boolHook = {
+ get: function( elem, name ) {
+ var
+ // Use .prop to determine if this attribute is understood as boolean
+ prop = jQuery.prop( elem, name ),
+
+ // Fetch it accordingly
+ attr = typeof prop === "boolean" && elem.getAttribute( name ),
+ detail = typeof prop === "boolean" ?
+
+ getSetInput && getSetAttribute ?
+ attr != null :
+ // oldIE fabricates an empty string for missing boolean attributes
+ // and conflates checked/selected into attroperties
+ ruseDefault.test( name ) ?
+ elem[ jQuery.camelCase( "default-" + name ) ] :
+ !!attr :
+
+ // fetch an attribute node for properties not recognized as boolean
+ elem.getAttributeNode( name );
+
+ return detail && detail.value !== false ?
+ name.toLowerCase() :
+ undefined;
+ },
+ set: function( elem, value, name ) {
+ if ( value === false ) {
+ // Remove boolean attributes when set to false
+ jQuery.removeAttr( elem, name );
+ } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
+ // IE<8 needs the *property* name
+ elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );
+
+ // Use defaultChecked and defaultSelected for oldIE
+ } else {
+ elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true;
+ }
+
+ return name;
+ }
+};
+
+// fix oldIE value attroperty
+if ( !getSetInput || !getSetAttribute ) {
+ jQuery.attrHooks.value = {
+ get: function( elem, name ) {
+ var ret = elem.getAttributeNode( name );
+ return jQuery.nodeName( elem, "input" ) ?
+
+ // Ignore the value *property* by using defaultValue
+ elem.defaultValue :
+
+ ret && ret.specified ? ret.value : undefined;
+ },
+ set: function( elem, value, name ) {
+ if ( jQuery.nodeName( elem, "input" ) ) {
+ // Does not return so that setAttribute is also used
+ elem.defaultValue = value;
+ } else {
+ // Use nodeHook if defined (#1954); otherwise setAttribute is fine
+ return nodeHook && nodeHook.set( elem, value, name );
+ }
+ }
+ };
+}
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+ // Use this for any attribute in IE6/7
+ // This fixes almost every IE6/7 issue
+ nodeHook = jQuery.valHooks.button = {
+ get: function( elem, name ) {
+ var ret = elem.getAttributeNode( name );
+ return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ?
+ ret.value :
+ undefined;
+ },
+ set: function( elem, value, name ) {
+ // Set the existing or create a new attribute node
+ var ret = elem.getAttributeNode( name );
+ if ( !ret ) {
+ elem.setAttributeNode(
+ (ret = elem.ownerDocument.createAttribute( name ))
+ );
+ }
+
+ ret.value = value += "";
+
+ // Break association with cloned elements by also using setAttribute (#9646)
+ return name === "value" || value === elem.getAttribute( name ) ?
+ value :
+ undefined;
+ }
+ };
+
+ // Set contenteditable to false on removals(#10429)
+ // Setting to empty string throws an error as an invalid value
+ jQuery.attrHooks.contenteditable = {
+ get: nodeHook.get,
+ set: function( elem, value, name ) {
+ nodeHook.set( elem, value === "" ? false : value, name );
+ }
+ };
+
+ // Set width and height to auto instead of 0 on empty string( Bug #8150 )
+ // This is for removals
+ jQuery.each([ "width", "height" ], function( i, name ) {
+ jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+ set: function( elem, value ) {
+ if ( value === "" ) {
+ elem.setAttribute( name, "auto" );
+ return value;
+ }
+ }
+ });
+ });
+}
+
+
+// Some attributes require a special call on IE
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !jQuery.support.hrefNormalized ) {
+ jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
+ jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+ get: function( elem ) {
+ var ret = elem.getAttribute( name, 2 );
+ return ret == null ? undefined : ret;
+ }
+ });
+ });
+
+ // href/src property should get the full normalized URL (#10299/#12915)
+ jQuery.each([ "href", "src" ], function( i, name ) {
+ jQuery.propHooks[ name ] = {
+ get: function( elem ) {
+ return elem.getAttribute( name, 4 );
+ }
+ };
+ });
+}
+
+if ( !jQuery.support.style ) {
+ jQuery.attrHooks.style = {
+ get: function( elem ) {
+ // Return undefined in the case of empty string
+ // Note: IE uppercases css property names, but if we were to .toLowerCase()
+ // .cssText, that would destroy case senstitivity in URL's, like in "background"
+ return elem.style.cssText || undefined;
+ },
+ set: function( elem, value ) {
+ return ( elem.style.cssText = value + "" );
+ }
+ };
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+ jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
+ get: function( elem ) {
+ var parent = elem.parentNode;
+
+ if ( parent ) {
+ parent.selectedIndex;
+
+ // Make sure that it also works with optgroups, see #5701
+ if ( parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ }
+ return null;
+ }
+ });
+}
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+ jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+if ( !jQuery.support.checkOn ) {
+ jQuery.each([ "radio", "checkbox" ], function() {
+ jQuery.valHooks[ this ] = {
+ get: function( elem ) {
+ // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+ return elem.getAttribute("value") === null ? "on" : elem.value;
+ }
+ };
+ });
+}
+jQuery.each([ "radio", "checkbox" ], function() {
+ jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
+ set: function( elem, value ) {
+ if ( jQuery.isArray( value ) ) {
+ return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+ }
+ }
+ });
+});
+var rformElems = /^(?:input|select|textarea)$/i,
+ rkeyEvent = /^key/,
+ rmouseEvent = /^(?:mouse|contextmenu)|click/,
+ rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+ rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
+
+function returnTrue() {
+ return true;
+}
+
+function returnFalse() {
+ return false;
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+ global: {},
+
+ add: function( elem, types, handler, data, selector ) {
+ var tmp, events, t, handleObjIn,
+ special, eventHandle, handleObj,
+ handlers, type, namespaces, origType,
+ elemData = jQuery._data( elem );
+
+ // Don't attach events to noData or text/comment nodes (but allow plain objects)
+ if ( !elemData ) {
+ return;
+ }
+
+ // Caller can pass in an object of custom data in lieu of the handler
+ if ( handler.handler ) {
+ handleObjIn = handler;
+ handler = handleObjIn.handler;
+ selector = handleObjIn.selector;
+ }
+
+ // Make sure that the handler has a unique ID, used to find/remove it later
+ if ( !handler.guid ) {
+ handler.guid = jQuery.guid++;
+ }
+
+ // Init the element's event structure and main handler, if this is the first
+ if ( !(events = elemData.events) ) {
+ events = elemData.events = {};
+ }
+ if ( !(eventHandle = elemData.handle) ) {
+ eventHandle = elemData.handle = function( e ) {
+ // Discard the second event of a jQuery.event.trigger() and
+ // when an event is called after a page has unloaded
+ return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
+ jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+ undefined;
+ };
+ // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+ eventHandle.elem = elem;
+ }
+
+ // Handle multiple events separated by a space
+ // jQuery(...).bind("mouseover mouseout", fn);
+ types = ( types || "" ).match( core_rnotwhite ) || [""];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[t] ) || [];
+ type = origType = tmp[1];
+ namespaces = ( tmp[2] || "" ).split( "." ).sort();
+
+ // If event changes its type, use the special event handlers for the changed type
+ special = jQuery.event.special[ type ] || {};
+
+ // If selector defined, determine special event api type, otherwise given type
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+
+ // Update special based on newly reset type
+ special = jQuery.event.special[ type ] || {};
+
+ // handleObj is passed to all event handlers
+ handleObj = jQuery.extend({
+ type: type,
+ origType: origType,
+ data: data,
+ handler: handler,
+ guid: handler.guid,
+ selector: selector,
+ needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+ namespace: namespaces.join(".")
+ }, handleObjIn );
+
+ // Init the event handler queue if we're the first
+ if ( !(handlers = events[ type ]) ) {
+ handlers = events[ type ] = [];
+ handlers.delegateCount = 0;
+
+ // Only use addEventListener/attachEvent if the special events handler returns false
+ if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+ // Bind the global event handler to the element
+ if ( elem.addEventListener ) {
+ elem.addEventListener( type, eventHandle, false );
+
+ } else if ( elem.attachEvent ) {
+ elem.attachEvent( "on" + type, eventHandle );
+ }
+ }
+ }
+
+ if ( special.add ) {
+ special.add.call( elem, handleObj );
+
+ if ( !handleObj.handler.guid ) {
+ handleObj.handler.guid = handler.guid;
+ }
+ }
+
+ // Add to the element's handler list, delegates in front
+ if ( selector ) {
+ handlers.splice( handlers.delegateCount++, 0, handleObj );
+ } else {
+ handlers.push( handleObj );
+ }
+
+ // Keep track of which events have ever been used, for event optimization
+ jQuery.event.global[ type ] = true;
+ }
+
+ // Nullify elem to prevent memory leaks in IE
+ elem = null;
+ },
+
+ // Detach an event or set of events from an element
+ remove: function( elem, types, handler, selector, mappedTypes ) {
+ var j, handleObj, tmp,
+ origCount, t, events,
+ special, handlers, type,
+ namespaces, origType,
+ elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+
+ if ( !elemData || !(events = elemData.events) ) {
+ return;
+ }
+
+ // Once for each type.namespace in types; type may be omitted
+ types = ( types || "" ).match( core_rnotwhite ) || [""];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[t] ) || [];
+ type = origType = tmp[1];
+ namespaces = ( tmp[2] || "" ).split( "." ).sort();
+
+ // Unbind all events (on this namespace, if provided) for the element
+ if ( !type ) {
+ for ( type in events ) {
+ jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+ }
+ continue;
+ }
+
+ special = jQuery.event.special[ type ] || {};
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+ handlers = events[ type ] || [];
+ tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" );
+
+ // Remove matching events
+ origCount = j = handlers.length;
+ while ( j-- ) {
+ handleObj = handlers[ j ];
+
+ if ( ( mappedTypes || origType === handleObj.origType ) &&
+ ( !handler || handler.guid === handleObj.guid ) &&
+ ( !tmp || tmp.test( handleObj.namespace ) ) &&
+ ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+ handlers.splice( j, 1 );
+
+ if ( handleObj.selector ) {
+ handlers.delegateCount--;
+ }
+ if ( special.remove ) {
+ special.remove.call( elem, handleObj );
+ }
+ }
+ }
+
+ // Remove generic event handler if we removed something and no more handlers exist
+ // (avoids potential for endless recursion during removal of special event handlers)
+ if ( origCount && !handlers.length ) {
+ if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+ jQuery.removeEvent( elem, type, elemData.handle );
+ }
+
+ delete events[ type ];
+ }
+ }
+
+ // Remove the expando if it's no longer used
+ if ( jQuery.isEmptyObject( events ) ) {
+ delete elemData.handle;
+
+ // removeData also checks for emptiness and clears the expando if empty
+ // so use it instead of delete
+ jQuery._removeData( elem, "events" );
+ }
+ },
+
+ trigger: function( event, data, elem, onlyHandlers ) {
+ var handle, ontype, cur,
+ bubbleType, special, tmp, i,
+ eventPath = [ elem || document ],
+ type = core_hasOwn.call( event, "type" ) ? event.type : event,
+ namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : [];
+
+ cur = tmp = elem = elem || document;
+
+ // Don't do events on text and comment nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ // focus/blur morphs to focusin/out; ensure we're not firing them right now
+ if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+ return;
+ }
+
+ if ( type.indexOf(".") >= 0 ) {
+ // Namespaced trigger; create a regexp to match event type in handle()
+ namespaces = type.split(".");
+ type = namespaces.shift();
+ namespaces.sort();
+ }
+ ontype = type.indexOf(":") < 0 && "on" + type;
+
+ // Caller can pass in a jQuery.Event object, Object, or just an event type string
+ event = event[ jQuery.expando ] ?
+ event :
+ new jQuery.Event( type, typeof event === "object" && event );
+
+ event.isTrigger = true;
+ event.namespace = namespaces.join(".");
+ event.namespace_re = event.namespace ?
+ new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
+ null;
+
+ // Clean up the event in case it is being reused
+ event.result = undefined;
+ if ( !event.target ) {
+ event.target = elem;
+ }
+
+ // Clone any incoming data and prepend the event, creating the handler arg list
+ data = data == null ?
+ [ event ] :
+ jQuery.makeArray( data, [ event ] );
+
+ // Allow special events to draw outside the lines
+ special = jQuery.event.special[ type ] || {};
+ if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+ return;
+ }
+
+ // Determine event propagation path in advance, per W3C events spec (#9951)
+ // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+ if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+ bubbleType = special.delegateType || type;
+ if ( !rfocusMorph.test( bubbleType + type ) ) {
+ cur = cur.parentNode;
+ }
+ for ( ; cur; cur = cur.parentNode ) {
+ eventPath.push( cur );
+ tmp = cur;
+ }
+
+ // Only add window if we got to document (e.g., not plain obj or detached DOM)
+ if ( tmp === (elem.ownerDocument || document) ) {
+ eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+ }
+ }
+
+ // Fire handlers on the event path
+ i = 0;
+ while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {
+
+ event.type = i > 1 ?
+ bubbleType :
+ special.bindType || type;
+
+ // jQuery handler
+ handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+ if ( handle ) {
+ handle.apply( cur, data );
+ }
+
+ // Native handler
+ handle = ontype && cur[ ontype ];
+ if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
+ event.preventDefault();
+ }
+ }
+ event.type = type;
+
+ // If nobody prevented the default action, do it now
+ if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+ if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&
+ !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
+
+ // Call a native DOM method on the target with the same name name as the event.
+ // Can't use an .isFunction() check here because IE6/7 fails that test.
+ // Don't do default actions on window, that's where global variables be (#6170)
+ if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) {
+
+ // Don't re-trigger an onFOO event when we call its FOO() method
+ tmp = elem[ ontype ];
+
+ if ( tmp ) {
+ elem[ ontype ] = null;
+ }
+
+ // Prevent re-triggering of the same event, since we already bubbled it above
+ jQuery.event.triggered = type;
+ try {
+ elem[ type ]();
+ } catch ( e ) {
+ // IE<9 dies on focus/blur to hidden element (#1486,#12518)
+ // only reproducible on winXP IE8 native, not IE9 in IE8 mode
+ }
+ jQuery.event.triggered = undefined;
+
+ if ( tmp ) {
+ elem[ ontype ] = tmp;
+ }
+ }
+ }
+ }
+
+ return event.result;
+ },
+
+ dispatch: function( event ) {
+
+ // Make a writable jQuery.Event from the native event object
+ event = jQuery.event.fix( event );
+
+ var i, ret, handleObj, matched, j,
+ handlerQueue = [],
+ args = core_slice.call( arguments ),
+ handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
+ special = jQuery.event.special[ event.type ] || {};
+
+ // Use the fix-ed jQuery.Event rather than the (read-only) native event
+ args[0] = event;
+ event.delegateTarget = this;
+
+ // Call the preDispatch hook for the mapped type, and let it bail if desired
+ if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+ return;
+ }
+
+ // Determine handlers
+ handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+ // Run delegates first; they may want to stop propagation beneath us
+ i = 0;
+ while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
+ event.currentTarget = matched.elem;
+
+ j = 0;
+ while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
+
+ // Triggered event must either 1) have no namespace, or
+ // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+ if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
+
+ event.handleObj = handleObj;
+ event.data = handleObj.data;
+
+ ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+ .apply( matched.elem, args );
+
+ if ( ret !== undefined ) {
+ if ( (event.result = ret) === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ }
+ }
+ }
+
+ // Call the postDispatch hook for the mapped type
+ if ( special.postDispatch ) {
+ special.postDispatch.call( this, event );
+ }
+
+ return event.result;
+ },
+
+ handlers: function( event, handlers ) {
+ var sel, handleObj, matches, i,
+ handlerQueue = [],
+ delegateCount = handlers.delegateCount,
+ cur = event.target;
+
+ // Find delegate handlers
+ // Black-hole SVG <use> instance trees (#13180)
+ // Avoid non-left-click bubbling in Firefox (#3861)
+ if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
+
+ for ( ; cur != this; cur = cur.parentNode || this ) {
+
+ // Don't check non-elements (#13208)
+ // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+ if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) {
+ matches = [];
+ for ( i = 0; i < delegateCount; i++ ) {
+ handleObj = handlers[ i ];
+
+ // Don't conflict with Object.prototype properties (#13203)
+ sel = handleObj.selector + " ";
+
+ if ( matches[ sel ] === undefined ) {
+ matches[ sel ] = handleObj.needsContext ?
+ jQuery( sel, this ).index( cur ) >= 0 :
+ jQuery.find( sel, this, null, [ cur ] ).length;
+ }
+ if ( matches[ sel ] ) {
+ matches.push( handleObj );
+ }
+ }
+ if ( matches.length ) {
+ handlerQueue.push({ elem: cur, handlers: matches });
+ }
+ }
+ }
+ }
+
+ // Add the remaining (directly-bound) handlers
+ if ( delegateCount < handlers.length ) {
+ handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
+ }
+
+ return handlerQueue;
+ },
+
+ fix: function( event ) {
+ if ( event[ jQuery.expando ] ) {
+ return event;
+ }
+
+ // Create a writable copy of the event object and normalize some properties
+ var i, prop, copy,
+ type = event.type,
+ originalEvent = event,
+ fixHook = this.fixHooks[ type ];
+
+ if ( !fixHook ) {
+ this.fixHooks[ type ] = fixHook =
+ rmouseEvent.test( type ) ? this.mouseHooks :
+ rkeyEvent.test( type ) ? this.keyHooks :
+ {};
+ }
+ copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+ event = new jQuery.Event( originalEvent );
+
+ i = copy.length;
+ while ( i-- ) {
+ prop = copy[ i ];
+ event[ prop ] = originalEvent[ prop ];
+ }
+
+ // Support: IE<9
+ // Fix target property (#1925)
+ if ( !event.target ) {
+ event.target = originalEvent.srcElement || document;
+ }
+
+ // Support: Chrome 23+, Safari?
+ // Target should not be a text node (#504, #13143)
+ if ( event.target.nodeType === 3 ) {
+ event.target = event.target.parentNode;
+ }
+
+ // Support: IE<9
+ // For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
+ event.metaKey = !!event.metaKey;
+
+ return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
+ },
+
+ // Includes some event props shared by KeyEvent and MouseEvent
+ props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+ fixHooks: {},
+
+ keyHooks: {
+ props: "char charCode key keyCode".split(" "),
+ filter: function( event, original ) {
+
+ // Add which for key events
+ if ( event.which == null ) {
+ event.which = original.charCode != null ? original.charCode : original.keyCode;
+ }
+
+ return event;
+ }
+ },
+
+ mouseHooks: {
+ props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+ filter: function( event, original ) {
+ var body, eventDoc, doc,
+ button = original.button,
+ fromElement = original.fromElement;
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == null && original.clientX != null ) {
+ eventDoc = event.target.ownerDocument || document;
+ doc = eventDoc.documentElement;
+ body = eventDoc.body;
+
+ event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+ event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 );
+ }
+
+ // Add relatedTarget, if necessary
+ if ( !event.relatedTarget && fromElement ) {
+ event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+ }
+
+ // Add which for click: 1 === left; 2 === middle; 3 === right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && button !== undefined ) {
+ event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+ }
+
+ return event;
+ }
+ },
+
+ special: {
+ load: {
+ // Prevent triggered image.load events from bubbling to window.load
+ noBubble: true
+ },
+ click: {
+ // For checkbox, fire native event so checked state will be right
+ trigger: function() {
+ if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) {
+ this.click();
+ return false;
+ }
+ }
+ },
+ focus: {
+ // Fire native event if possible so blur/focus sequence is correct
+ trigger: function() {
+ if ( this !== document.activeElement && this.focus ) {
+ try {
+ this.focus();
+ return false;
+ } catch ( e ) {
+ // Support: IE<9
+ // If we error on focus to hidden element (#1486, #12518),
+ // let .trigger() run the handlers
+ }
+ }
+ },
+ delegateType: "focusin"
+ },
+ blur: {
+ trigger: function() {
+ if ( this === document.activeElement && this.blur ) {
+ this.blur();
+ return false;
+ }
+ },
+ delegateType: "focusout"
+ },
+
+ beforeunload: {
+ postDispatch: function( event ) {
+
+ // Even when returnValue equals to undefined Firefox will still show alert
+ if ( event.result !== undefined ) {
+ event.originalEvent.returnValue = event.result;
+ }
+ }
+ }
+ },
+
+ simulate: function( type, elem, event, bubble ) {
+ // Piggyback on a donor event to simulate a different one.
+ // Fake originalEvent to avoid donor's stopPropagation, but if the
+ // simulated event prevents default then we do the same on the donor.
+ var e = jQuery.extend(
+ new jQuery.Event(),
+ event,
+ { type: type,
+ isSimulated: true,
+ originalEvent: {}
+ }
+ );
+ if ( bubble ) {
+ jQuery.event.trigger( e, null, elem );
+ } else {
+ jQuery.event.dispatch.call( elem, e );
+ }
+ if ( e.isDefaultPrevented() ) {
+ event.preventDefault();
+ }
+ }
+};
+
+jQuery.removeEvent = document.removeEventListener ?
+ function( elem, type, handle ) {
+ if ( elem.removeEventListener ) {
+ elem.removeEventListener( type, handle, false );
+ }
+ } :
+ function( elem, type, handle ) {
+ var name = "on" + type;
+
+ if ( elem.detachEvent ) {
+
+ // #8545, #7054, preventing memory leaks for custom events in IE6-8
+ // detachEvent needed property on element, by name of that event, to properly expose it to GC
+ if ( typeof elem[ name ] === core_strundefined ) {
+ elem[ name ] = null;
+ }
+
+ elem.detachEvent( name, handle );
+ }
+ };
+
+jQuery.Event = function( src, props ) {
+ // Allow instantiation without the 'new' keyword
+ if ( !(this instanceof jQuery.Event) ) {
+ return new jQuery.Event( src, props );
+ }
+
+ // Event object
+ if ( src && src.type ) {
+ this.originalEvent = src;
+ this.type = src.type;
+
+ // Events bubbling up the document may have been marked as prevented
+ // by a handler lower down the tree; reflect the correct value.
+ this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+ src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+ // Event type
+ } else {
+ this.type = src;
+ }
+
+ // Put explicitly provided properties onto the event object
+ if ( props ) {
+ jQuery.extend( this, props );
+ }
+
+ // Create a timestamp if incoming event doesn't have one
+ this.timeStamp = src && src.timeStamp || jQuery.now();
+
+ // Mark it as fixed
+ this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+ isDefaultPrevented: returnFalse,
+ isPropagationStopped: returnFalse,
+ isImmediatePropagationStopped: returnFalse,
+
+ preventDefault: function() {
+ var e = this.originalEvent;
+
+ this.isDefaultPrevented = returnTrue;
+ if ( !e ) {
+ return;
+ }
+
+ // If preventDefault exists, run it on the original event
+ if ( e.preventDefault ) {
+ e.preventDefault();
+
+ // Support: IE
+ // Otherwise set the returnValue property of the original event to false
+ } else {
+ e.returnValue = false;
+ }
+ },
+ stopPropagation: function() {
+ var e = this.originalEvent;
+
+ this.isPropagationStopped = returnTrue;
+ if ( !e ) {
+ return;
+ }
+ // If stopPropagation exists, run it on the original event
+ if ( e.stopPropagation ) {
+ e.stopPropagation();
+ }
+
+ // Support: IE
+ // Set the cancelBubble property of the original event to true
+ e.cancelBubble = true;
+ },
+ stopImmediatePropagation: function() {
+ this.isImmediatePropagationStopped = returnTrue;
+ this.stopPropagation();
+ }
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+ mouseenter: "mouseover",
+ mouseleave: "mouseout"
+}, function( orig, fix ) {
+ jQuery.event.special[ orig ] = {
+ delegateType: fix,
+ bindType: fix,
+
+ handle: function( event ) {
+ var ret,
+ target = this,
+ related = event.relatedTarget,
+ handleObj = event.handleObj;
+
+ // For mousenter/leave call the handler if related is outside the target.
+ // NB: No relatedTarget if the mouse left/entered the browser window
+ if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+ event.type = handleObj.origType;
+ ret = handleObj.handler.apply( this, arguments );
+ event.type = fix;
+ }
+ return ret;
+ }
+ };
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+ jQuery.event.special.submit = {
+ setup: function() {
+ // Only need this for delegated form submit events
+ if ( jQuery.nodeName( this, "form" ) ) {
+ return false;
+ }
+
+ // Lazy-add a submit handler when a descendant form may potentially be submitted
+ jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+ // Node name check avoids a VML-related crash in IE (#9807)
+ var elem = e.target,
+ form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+ if ( form && !jQuery._data( form, "submitBubbles" ) ) {
+ jQuery.event.add( form, "submit._submit", function( event ) {
+ event._submit_bubble = true;
+ });
+ jQuery._data( form, "submitBubbles", true );
+ }
+ });
+ // return undefined since we don't need an event listener
+ },
+
+ postDispatch: function( event ) {
+ // If form was submitted by the user, bubble the event up the tree
+ if ( event._submit_bubble ) {
+ delete event._submit_bubble;
+ if ( this.parentNode && !event.isTrigger ) {
+ jQuery.event.simulate( "submit", this.parentNode, event, true );
+ }
+ }
+ },
+
+ teardown: function() {
+ // Only need this for delegated form submit events
+ if ( jQuery.nodeName( this, "form" ) ) {
+ return false;
+ }
+
+ // Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+ jQuery.event.remove( this, "._submit" );
+ }
+ };
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+ jQuery.event.special.change = {
+
+ setup: function() {
+
+ if ( rformElems.test( this.nodeName ) ) {
+ // IE doesn't fire change on a check/radio until blur; trigger it on click
+ // after a propertychange. Eat the blur-change in special.change.handle.
+ // This still fires onchange a second time for check/radio after blur.
+ if ( this.type === "checkbox" || this.type === "radio" ) {
+ jQuery.event.add( this, "propertychange._change", function( event ) {
+ if ( event.originalEvent.propertyName === "checked" ) {
+ this._just_changed = true;
+ }
+ });
+ jQuery.event.add( this, "click._change", function( event ) {
+ if ( this._just_changed && !event.isTrigger ) {
+ this._just_changed = false;
+ }
+ // Allow triggered, simulated change events (#11500)
+ jQuery.event.simulate( "change", this, event, true );
+ });
+ }
+ return false;
+ }
+ // Delegated event; lazy-add a change handler on descendant inputs
+ jQuery.event.add( this, "beforeactivate._change", function( e ) {
+ var elem = e.target;
+
+ if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) {
+ jQuery.event.add( elem, "change._change", function( event ) {
+ if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+ jQuery.event.simulate( "change", this.parentNode, event, true );
+ }
+ });
+ jQuery._data( elem, "changeBubbles", true );
+ }
+ });
+ },
+
+ handle: function( event ) {
+ var elem = event.target;
+
+ // Swallow native change events from checkbox/radio, we already triggered them above
+ if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+ return event.handleObj.handler.apply( this, arguments );
+ }
+ },
+
+ teardown: function() {
+ jQuery.event.remove( this, "._change" );
+
+ return !rformElems.test( this.nodeName );
+ }
+ };
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+ jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+ // Attach a single capturing handler while someone wants focusin/focusout
+ var attaches = 0,
+ handler = function( event ) {
+ jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+ };
+
+ jQuery.event.special[ fix ] = {
+ setup: function() {
+ if ( attaches++ === 0 ) {
+ document.addEventListener( orig, handler, true );
+ }
+ },
+ teardown: function() {
+ if ( --attaches === 0 ) {
+ document.removeEventListener( orig, handler, true );
+ }
+ }
+ };
+ });
+}
+
+jQuery.fn.extend({
+
+ on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+ var type, origFn;
+
+ // Types can be a map of types/handlers
+ if ( typeof types === "object" ) {
+ // ( types-Object, selector, data )
+ if ( typeof selector !== "string" ) {
+ // ( types-Object, data )
+ data = data || selector;
+ selector = undefined;
+ }
+ for ( type in types ) {
+ this.on( type, selector, data, types[ type ], one );
+ }
+ return this;
+ }
+
+ if ( data == null && fn == null ) {
+ // ( types, fn )
+ fn = selector;
+ data = selector = undefined;
+ } else if ( fn == null ) {
+ if ( typeof selector === "string" ) {
+ // ( types, selector, fn )
+ fn = data;
+ data = undefined;
+ } else {
+ // ( types, data, fn )
+ fn = data;
+ data = selector;
+ selector = undefined;
+ }
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ } else if ( !fn ) {
+ return this;
+ }
+
+ if ( one === 1 ) {
+ origFn = fn;
+ fn = function( event ) {
+ // Can use an empty set, since event contains the info
+ jQuery().off( event );
+ return origFn.apply( this, arguments );
+ };
+ // Use same guid so caller can remove using origFn
+ fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+ }
+ return this.each( function() {
+ jQuery.event.add( this, types, fn, data, selector );
+ });
+ },
+ one: function( types, selector, data, fn ) {
+ return this.on( types, selector, data, fn, 1 );
+ },
+ off: function( types, selector, fn ) {
+ var handleObj, type;
+ if ( types && types.preventDefault && types.handleObj ) {
+ // ( event ) dispatched jQuery.Event
+ handleObj = types.handleObj;
+ jQuery( types.delegateTarget ).off(
+ handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
+ handleObj.selector,
+ handleObj.handler
+ );
+ return this;
+ }
+ if ( typeof types === "object" ) {
+ // ( types-object [, selector] )
+ for ( type in types ) {
+ this.off( type, selector, types[ type ] );
+ }
+ return this;
+ }
+ if ( selector === false || typeof selector === "function" ) {
+ // ( types [, fn] )
+ fn = selector;
+ selector = undefined;
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ }
+ return this.each(function() {
+ jQuery.event.remove( this, types, fn, selector );
+ });
+ },
+
+ bind: function( types, data, fn ) {
+ return this.on( types, null, data, fn );
+ },
+ unbind: function( types, fn ) {
+ return this.off( types, null, fn );
+ },
+
+ delegate: function( selector, types, data, fn ) {
+ return this.on( types, selector, data, fn );
+ },
+ undelegate: function( selector, types, fn ) {
+ // ( namespace ) or ( selector, types [, fn] )
+ return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
+ },
+
+ trigger: function( type, data ) {
+ return this.each(function() {
+ jQuery.event.trigger( type, data, this );
+ });
+ },
+ triggerHandler: function( type, data ) {
+ var elem = this[0];
+ if ( elem ) {
+ return jQuery.event.trigger( type, data, elem, true );
+ }
+ }
+});
+/*!
+ * Sizzle CSS Selector Engine
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://sizzlejs.com/
+ */
+(function( window, undefined ) {
+
+var i,
+ cachedruns,
+ Expr,
+ getText,
+ isXML,
+ compile,
+ hasDuplicate,
+ outermostContext,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsXML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+ sortOrder,
+
+ // Instance-specific data
+ expando = "sizzle" + -(new Date()),
+ preferredDoc = window.document,
+ support = {},
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+
+ // General-purpose constants
+ strundefined = typeof undefined,
+ MAX_NEGATIVE = 1 << 31,
+
+ // Array methods
+ arr = [],
+ pop = arr.pop,
+ push = arr.push,
+ slice = arr.slice,
+ // Use a stripped-down indexOf if we can't use a native one
+ indexOf = arr.indexOf || function( elem ) {
+ var i = 0,
+ len = this.length;
+ for ( ; i < len; i++ ) {
+ if ( this[i] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+
+ // Regular expressions
+
+ // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+ // http://www.w3.org/TR/css3-syntax/#characters
+ characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+ // Loosely modeled on CSS identifier characters
+ // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
+ // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ identifier = characterEncoding.replace( "w", "w#" ),
+
+ // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
+ operators = "([*^$|!~]?=)",
+ attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
+ "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
+
+ // Prefer arguments quoted,
+ // then not containing pseudos/brackets,
+ // then attribute selectors/non-parenthetical expressions,
+ // then anything else
+ // These preferences are here to reduce the number of selectors
+ // needing tokenize in the PSEUDO preFilter
+ pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ),
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + characterEncoding + ")" ),
+ "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
+ "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ),
+ "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+ "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+ "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+ whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rsibling = /[\x20\t\r\n\f]*[+~]/,
+
+ rnative = /^[^{]+\{\s*\[native code/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rescape = /'|\\/g,
+ rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,
+
+ // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,
+ funescape = function( _, escaped ) {
+ var high = "0x" + escaped - 0x10000;
+ // NaN means non-codepoint
+ return high !== high ?
+ escaped :
+ // BMP codepoint
+ high < 0 ?
+ String.fromCharCode( high + 0x10000 ) :
+ // Supplemental Plane codepoint (surrogate pair)
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ };
+
+// Use a stripped-down slice if we can't use a native one
+try {
+ slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType;
+} catch ( e ) {
+ slice = function( i ) {
+ var elem,
+ results = [];
+ while ( (elem = this[i++]) ) {
+ results.push( elem );
+ }
+ return results;
+ };
+}
+
+/**
+ * For feature detection
+ * @param {Function} fn The function to test for native support
+ */
+function isNative( fn ) {
+ return rnative.test( fn + "" );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var cache,
+ keys = [];
+
+ return (cache = function( key, value ) {
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key += " " ) > Expr.cacheLength ) {
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return (cache[ key ] = value);
+ });
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+ var div = document.createElement("div");
+
+ try {
+ return fn( div );
+ } catch (e) {
+ return false;
+ } finally {
+ // release memory in IE
+ div = null;
+ }
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var match, elem, m, nodeType,
+ // QSA vars
+ i, groups, old, nid, newContext, newSelector;
+
+ if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+ setDocument( context );
+ }
+
+ context = context || document;
+ results = results || [];
+
+ if ( !selector || typeof selector !== "string" ) {
+ return results;
+ }
+
+ if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
+ return [];
+ }
+
+ if ( !documentIsXML && !seed ) {
+
+ // Shortcuts
+ if ( (match = rquickExpr.exec( selector )) ) {
+ // Speed-up: Sizzle("#ID")
+ if ( (m = match[1]) ) {
+ if ( nodeType === 9 ) {
+ elem = context.getElementById( m );
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE, Opera, and Webkit return items
+ // by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+ } else {
+ // Context is not a document
+ if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+ contains( context, elem ) && elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Speed-up: Sizzle("TAG")
+ } else if ( match[2] ) {
+ push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) );
+ return results;
+
+ // Speed-up: Sizzle(".CLASS")
+ } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) {
+ push.apply( results, slice.call(context.getElementsByClassName( m ), 0) );
+ return results;
+ }
+ }
+
+ // QSA path
+ if ( support.qsa && !rbuggyQSA.test(selector) ) {
+ old = true;
+ nid = expando;
+ newContext = context;
+ newSelector = nodeType === 9 && selector;
+
+ // qSA works strangely on Element-rooted queries
+ // We can work around this by specifying an extra ID on the root
+ // and working up from there (Thanks to Andrew Dupont for the technique)
+ // IE 8 doesn't work on object elements
+ if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+ groups = tokenize( selector );
+
+ if ( (old = context.getAttribute("id")) ) {
+ nid = old.replace( rescape, "\\$&" );
+ } else {
+ context.setAttribute( "id", nid );
+ }
+ nid = "[id='" + nid + "'] ";
+
+ i = groups.length;
+ while ( i-- ) {
+ groups[i] = nid + toSelector( groups[i] );
+ }
+ newContext = rsibling.test( selector ) && context.parentNode || context;
+ newSelector = groups.join(",");
+ }
+
+ if ( newSelector ) {
+ try {
+ push.apply( results, slice.call( newContext.querySelectorAll(
+ newSelector
+ ), 0 ) );
+ return results;
+ } catch(qsaError) {
+ } finally {
+ if ( !old ) {
+ context.removeAttribute("id");
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Detect xml
+ * @param {Element|Object} elem An element or a document
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // If no document and documentElement is available, return
+ if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Set our document
+ document = doc;
+ docElem = doc.documentElement;
+
+ // Support tests
+ documentIsXML = isXML( doc );
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.tagNameNoComments = assert(function( div ) {
+ div.appendChild( doc.createComment("") );
+ return !div.getElementsByTagName("*").length;
+ });
+
+ // Check if attributes should be retrieved by attribute nodes
+ support.attributes = assert(function( div ) {
+ div.innerHTML = "<select></select>";
+ var type = typeof div.lastChild.getAttribute("multiple");
+ // IE8 returns a string for some attributes even when not present
+ return type !== "boolean" && type !== "string";
+ });
+
+ // Check if getElementsByClassName can be trusted
+ support.getByClassName = assert(function( div ) {
+ // Opera can't find a second classname (in 9.6)
+ div.innerHTML = "<div class='hidden e'></div><div class='hidden'></div>";
+ if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) {
+ return false;
+ }
+
+ // Safari 3.2 caches class attributes and doesn't catch changes
+ div.lastChild.className = "e";
+ return div.getElementsByClassName("e").length === 2;
+ });
+
+ // Check if getElementById returns elements by name
+ // Check if getElementsByName privileges form controls or returns elements by ID
+ support.getByName = assert(function( div ) {
+ // Inject content
+ div.id = expando + 0;
+ div.innerHTML = "<a name='" + expando + "'></a><div name='" + expando + "'></div>";
+ docElem.insertBefore( div, docElem.firstChild );
+
+ // Test
+ var pass = doc.getElementsByName &&
+ // buggy browsers will return fewer than the correct 2
+ doc.getElementsByName( expando ).length === 2 +
+ // buggy browsers will return more than the correct 0
+ doc.getElementsByName( expando + 0 ).length;
+ support.getIdNotName = !doc.getElementById( expando );
+
+ // Cleanup
+ docElem.removeChild( div );
+
+ return pass;
+ });
+
+ // IE6/7 return modified attributes
+ Expr.attrHandle = assert(function( div ) {
+ div.innerHTML = "<a href='#'></a>";
+ return div.firstChild && typeof div.firstChild.getAttribute !== strundefined &&
+ div.firstChild.getAttribute("href") === "#";
+ }) ?
+ {} :
+ {
+ "href": function( elem ) {
+ return elem.getAttribute( "href", 2 );
+ },
+ "type": function( elem ) {
+ return elem.getAttribute("type");
+ }
+ };
+
+ // ID find and filter
+ if ( support.getIdNotName ) {
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== strundefined && !documentIsXML ) {
+ var m = context.getElementById( id );
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ return m && m.parentNode ? [m] : [];
+ }
+ };
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute("id") === attrId;
+ };
+ };
+ } else {
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== strundefined && !documentIsXML ) {
+ var m = context.getElementById( id );
+
+ return m ?
+ m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ?
+ [m] :
+ undefined :
+ [];
+ }
+ };
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+ return node && node.value === attrId;
+ };
+ };
+ }
+
+ // Tag
+ Expr.find["TAG"] = support.tagNameNoComments ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== strundefined ) {
+ return context.getElementsByTagName( tag );
+ }
+ } :
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Name
+ Expr.find["NAME"] = support.getByName && function( tag, context ) {
+ if ( typeof context.getElementsByName !== strundefined ) {
+ return context.getElementsByName( name );
+ }
+ };
+
+ // Class
+ Expr.find["CLASS"] = support.getByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21),
+ // no need to also add to buggyMatches since matches checks buggyQSA
+ // A support test would require too much code (would include document ready)
+ rbuggyQSA = [ ":focus" ];
+
+ if ( (support.qsa = isNative(doc.querySelectorAll)) ) {
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert(function( div ) {
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explictly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // http://bugs.jquery.com/ticket/12359
+ div.innerHTML = "<select><option selected=''></option></select>";
+
+ // IE8 - Some boolean attributes are not treated correctly
+ if ( !div.querySelectorAll("[selected]").length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" );
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":checked").length ) {
+ rbuggyQSA.push(":checked");
+ }
+ });
+
+ assert(function( div ) {
+
+ // Opera 10-12/IE8 - ^= $= *= and empty values
+ // Should not select anything
+ div.innerHTML = "<input type='hidden' i=''/>";
+ if ( div.querySelectorAll("[i^='']").length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":enabled").length ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ div.querySelectorAll("*,:x");
+ rbuggyQSA.push(",.*:");
+ });
+ }
+
+ if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.webkitMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector) )) ) {
+
+ assert(function( div ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( div, "div" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( div, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ });
+ }
+
+ rbuggyQSA = new RegExp( rbuggyQSA.join("|") );
+ rbuggyMatches = new RegExp( rbuggyMatches.join("|") );
+
+ // Element contains another
+ // Purposefully does not implement inclusive descendent
+ // As in, an element does not contain itself
+ contains = isNative(docElem.contains) || docElem.compareDocumentPosition ?
+ function( a, b ) {
+ var adown = a.nodeType === 9 ? a.documentElement : a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ));
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( (b = b.parentNode) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ // Document order sorting
+ sortOrder = docElem.compareDocumentPosition ?
+ function( a, b ) {
+ var compare;
+
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) {
+ if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) {
+ if ( a === doc || contains( preferredDoc, a ) ) {
+ return -1;
+ }
+ if ( b === doc || contains( preferredDoc, b ) ) {
+ return 1;
+ }
+ return 0;
+ }
+ return compare & 4 ? -1 : 1;
+ }
+
+ return a.compareDocumentPosition ? -1 : 1;
+ } :
+ function( a, b ) {
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+
+ // Parentless nodes are either documents or disconnected
+ } else if ( !aup || !bup ) {
+ return a === doc ? -1 :
+ b === doc ? 1 :
+ aup ? -1 :
+ bup ? 1 :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( (cur = cur.parentNode) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( (cur = cur.parentNode) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[i] === bp[i] ) {
+ i++;
+ }
+
+ return i ?
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[i], bp[i] ) :
+
+ // Otherwise nodes in our document sort first
+ ap[i] === preferredDoc ? -1 :
+ bp[i] === preferredDoc ? 1 :
+ 0;
+ };
+
+ // Always assume the presence of duplicates if sort doesn't
+ // pass them to our comparison function (as in Google Chrome).
+ hasDuplicate = false;
+ [0, 0].sort( sortOrder );
+ support.detectDuplicates = hasDuplicate;
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ // Make sure that attribute selectors are quoted
+ expr = expr.replace( rattributeQuotes, "='$1']" );
+
+ // rbuggyQSA always contains :focus, so no need for an existence check
+ if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) {
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch(e) {}
+ }
+
+ return Sizzle( expr, document, null, [elem] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+ // Set document vars if needed
+ if ( ( context.ownerDocument || context ) !== document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+ var val;
+
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ if ( !documentIsXML ) {
+ name = name.toLowerCase();
+ }
+ if ( (val = Expr.attrHandle[ name ]) ) {
+ return val( elem );
+ }
+ if ( documentIsXML || support.attributes ) {
+ return elem.getAttribute( name );
+ }
+ return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ?
+ name :
+ val && val.specified ? val.value : null;
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+// Document sorting and removing duplicates
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ i = 1,
+ j = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ for ( ; (elem = results[i]); i++ ) {
+ if ( elem === results[ i - 1 ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ return results;
+};
+
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE );
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( (cur = cur.nextSibling) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+// Returns a function to use in pseudos for input types
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+// Returns a function to use in pseudos for buttons
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && elem.type === type;
+ };
+}
+
+// Returns a function to use in pseudos for positionals
+function createPositionalPseudo( fn ) {
+ return markFunction(function( argument ) {
+ argument = +argument;
+ return markFunction(function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ (j = matchIndexes[i]) ] ) {
+ seed[j] = !(matches[j] = seed[j]);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+ // If no nodeType, this is expected to be an array
+ for ( ; (node = elem[i]); i++ ) {
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (see #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[1] = match[1].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape );
+
+ if ( match[2] === "~=" ) {
+ match[3] = " " + match[3] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[1] = match[1].toLowerCase();
+
+ if ( match[1].slice( 0, 3 ) === "nth" ) {
+ // nth-* requires argument
+ if ( !match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+ match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[5] && match[2];
+
+ if ( matchExpr["CHILD"].test( match[0] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[4] ) {
+ match[2] = match[4];
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+ // Get excess from tokenize (recursively)
+ (excess = tokenize( unquoted, true )) &&
+ // advance to the next closing parenthesis
+ (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+ // excess is a negative index
+ match[0] = match[0].slice( 0, excess );
+ match[2] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeName ) {
+ if ( nodeName === "*" ) {
+ return function() { return true; };
+ }
+
+ nodeName = nodeName.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );
+ });
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ };
+ },
+
+ "CHILD": function( type, what, argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, context, xml ) {
+ var cache, outerCache, node, diff, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( (node = node[ dir ]) ) {
+ if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
+ return false;
+ }
+ }
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+ // Seek `elem` from a previously-cached index
+ outerCache = parent[ expando ] || (parent[ expando ] = {});
+ cache = outerCache[ type ] || [];
+ nodeIndex = cache[0] === dirruns && cache[1];
+ diff = cache[0] === dirruns && cache[2];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ outerCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ // Use previously-cached element index if available
+ } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
+ diff = cache[1];
+
+ // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ } else {
+ // Use the same loop as above to seek `elem` from the start
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction(function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf.call( seed, matched[i] );
+ seed[ idx ] = !( matches[ idx ] = matched[i] );
+ }
+ }) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+ // Potentially complex pseudos
+ "not": markFunction(function( selector ) {
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction(function( seed, matches, context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( (elem = unmatched[i]) ) {
+ seed[i] = !(matches[i] = elem);
+ }
+ }
+ }) :
+ function( elem, context, xml ) {
+ input[0] = elem;
+ matcher( input, null, xml, results );
+ return !results.pop();
+ };
+ }),
+
+ "has": markFunction(function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ }),
+
+ "contains": markFunction(function( text ) {
+ return function( elem ) {
+ return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+ };
+ }),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+ // lang value must be a valid identifider
+ if ( !ridentifier.test(lang || "") ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( (elemLang = documentIsXML ?
+ elem.getAttribute("xml:lang") || elem.getAttribute("lang") :
+ elem.lang) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ return false;
+ };
+ }),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ },
+
+ // Boolean properties
+ "enabled": function( elem ) {
+ return elem.disabled === false;
+ },
+
+ "disabled": function( elem ) {
+ return elem.disabled === true;
+ },
+
+ "checked": function( elem ) {
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ },
+
+ "selected": function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
+ // not comment, processing instructions, or others
+ // Thanks to Diego Perini for the nodeName shortcut
+ // Greater than "@" means alpha characters (specifically not starting with "#" or "?")
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos["empty"]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+ // use getAttribute instead to test this case
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+ ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo(function() {
+ return [ 0 ];
+ }),
+
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
+ return [ length - 1 ];
+ }),
+
+ "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ }),
+
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ })
+ }
+};
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+function tokenize( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( match ) {
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
+ }
+ groups.push( tokens = [] );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( (match = rcombinators.exec( soFar )) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+ // Cast descendant combinators to space
+ type: match[0].replace( rtrim, " " )
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+ (match = preFilters[ type ]( match ))) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+ type: type,
+ matches: match
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+}
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[i].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ checkNonElements = base && dir === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var data, cache, outerCache,
+ dirkey = dirruns + " " + doneName;
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+ if ( xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || (elem[ expando ] = {});
+ if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {
+ if ( (data = cache[1]) === true || data === cachedruns ) {
+ return data === true;
+ }
+ } else {
+ cache = outerCache[ dir ] = [ dirkey ];
+ cache[1] = matcher( elem, context, xml ) || cachedruns;
+ if ( cache[1] === true ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[i]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[0];
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( (elem = unmatched[i]) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction(function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( (elem = temp[i]) ) {
+ matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ });
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[0].type ],
+ implicitRelative = leadingRelative || Expr.relative[" "],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf.call( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ (checkContext = context).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+ matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ } else {
+ matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[j].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ // A counter to specify which element is currently being matched
+ var matcherCachedRuns = 0,
+ bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, expandContext ) {
+ var elem, j, matcher,
+ setMatched = [],
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ outermost = expandContext != null,
+ contextBackup = outermostContext,
+ // We must always have either seed elements or context
+ elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1);
+
+ if ( outermost ) {
+ outermostContext = context !== document && context;
+ cachedruns = matcherCachedRuns;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Keep `i` a string if there are no elements so `matchedCount` will be "00" below
+ for ( ; (elem = elems[i]) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+ while ( (matcher = elementMatchers[j++]) ) {
+ if ( matcher( elem, context, xml ) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ cachedruns = ++matcherCachedRuns;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+ // They will have gone through all possible matchers
+ if ( (elem = !matcher && elem) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // Apply set filters to unmatched elements
+ matchedCount += i;
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( (matcher = setMatchers[j++]) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !(unmatched[i] || setMatched[i]) ) {
+ setMatched[i] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !group ) {
+ group = tokenize( selector );
+ }
+ i = group.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( group[i] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+ }
+ return cached;
+};
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[i], results );
+ }
+ return results;
+}
+
+function select( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ match = tokenize( selector );
+
+ if ( !seed ) {
+ // Try to minimize operations if there is only one group
+ if ( match.length === 1 ) {
+
+ // Take a shortcut and set the context if the root selector is an ID
+ tokens = match[0] = match[0].slice( 0 );
+ if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+ context.nodeType === 9 && !documentIsXML &&
+ Expr.relative[ tokens[1].type ] ) {
+
+ context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0];
+ if ( !context ) {
+ return results;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[i];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ (type = token.type) ] ) {
+ break;
+ }
+ if ( (find = Expr.find[ type ]) ) {
+ // Search, expanding context for leading sibling combinators
+ if ( (seed = find(
+ token.matches[0].replace( runescape, funescape ),
+ rsibling.test( tokens[0].type ) && context.parentNode || context
+ )) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, slice.call( seed, 0 ) );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function
+ // Provide `match` to avoid retokenization if we modified the selector above
+ compile( selector, match )(
+ seed,
+ context,
+ documentIsXML,
+ results,
+ rsibling.test( selector )
+ );
+ return results;
+}
+
+// Deprecated
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Easy API for creating new setFilters
+function setFilters() {}
+Expr.filters = setFilters.prototype = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+// Initialize with the default document
+setDocument();
+
+// Override sizzle attribute retrieval
+Sizzle.attr = jQuery.attr;
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.pseudos;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})( window );
+var runtil = /Until$/,
+ rparentsprev = /^(?:parents|prev(?:Until|All))/,
+ isSimple = /^.[^:#\[\.,]*$/,
+ rneedsContext = jQuery.expr.match.needsContext,
+ // methods guaranteed to produce a unique set when starting from a unique set
+ guaranteedUnique = {
+ children: true,
+ contents: true,
+ next: true,
+ prev: true
+ };
+
+jQuery.fn.extend({
+ find: function( selector ) {
+ var i, ret, self,
+ len = this.length;
+
+ if ( typeof selector !== "string" ) {
+ self = this;
+ return this.pushStack( jQuery( selector ).filter(function() {
+ for ( i = 0; i < len; i++ ) {
+ if ( jQuery.contains( self[ i ], this ) ) {
+ return true;
+ }
+ }
+ }) );
+ }
+
+ ret = [];
+ for ( i = 0; i < len; i++ ) {
+ jQuery.find( selector, this[ i ], ret );
+ }
+
+ // Needed because $( selector, context ) becomes $( context ).find( selector )
+ ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
+ ret.selector = ( this.selector ? this.selector + " " : "" ) + selector;
+ return ret;
+ },
+
+ has: function( target ) {
+ var i,
+ targets = jQuery( target, this ),
+ len = targets.length;
+
+ return this.filter(function() {
+ for ( i = 0; i < len; i++ ) {
+ if ( jQuery.contains( this, targets[i] ) ) {
+ return true;
+ }
+ }
+ });
+ },
+
+ not: function( selector ) {
+ return this.pushStack( winnow(this, selector, false) );
+ },
+
+ filter: function( selector ) {
+ return this.pushStack( winnow(this, selector, true) );
+ },
+
+ is: function( selector ) {
+ return !!selector && (
+ typeof selector === "string" ?
+ // If this is a positional/relative selector, check membership in the returned set
+ // so $("p:first").is("p:last") won't return true for a doc with two "p".
+ rneedsContext.test( selector ) ?
+ jQuery( selector, this.context ).index( this[0] ) >= 0 :
+ jQuery.filter( selector, this ).length > 0 :
+ this.filter( selector ).length > 0 );
+ },
+
+ closest: function( selectors, context ) {
+ var cur,
+ i = 0,
+ l = this.length,
+ ret = [],
+ pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+ jQuery( selectors, context || this.context ) :
+ 0;
+
+ for ( ; i < l; i++ ) {
+ cur = this[i];
+
+ while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) {
+ if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+ ret.push( cur );
+ break;
+ }
+ cur = cur.parentNode;
+ }
+ }
+
+ return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret );
+ },
+
+ // Determine the position of an element within
+ // the matched set of elements
+ index: function( elem ) {
+
+ // No argument, return index in parent
+ if ( !elem ) {
+ return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1;
+ }
+
+ // index in selector
+ if ( typeof elem === "string" ) {
+ return jQuery.inArray( this[0], jQuery( elem ) );
+ }
+
+ // Locate the position of the desired element
+ return jQuery.inArray(
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[0] : elem, this );
+ },
+
+ add: function( selector, context ) {
+ var set = typeof selector === "string" ?
+ jQuery( selector, context ) :
+ jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+ all = jQuery.merge( this.get(), set );
+
+ return this.pushStack( jQuery.unique(all) );
+ },
+
+ addBack: function( selector ) {
+ return this.add( selector == null ?
+ this.prevObject : this.prevObject.filter(selector)
+ );
+ }
+});
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+function sibling( cur, dir ) {
+ do {
+ cur = cur[ dir ];
+ } while ( cur && cur.nodeType !== 1 );
+
+ return cur;
+}
+
+jQuery.each({
+ parent: function( elem ) {
+ var parent = elem.parentNode;
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+ parents: function( elem ) {
+ return jQuery.dir( elem, "parentNode" );
+ },
+ parentsUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "parentNode", until );
+ },
+ next: function( elem ) {
+ return sibling( elem, "nextSibling" );
+ },
+ prev: function( elem ) {
+ return sibling( elem, "previousSibling" );
+ },
+ nextAll: function( elem ) {
+ return jQuery.dir( elem, "nextSibling" );
+ },
+ prevAll: function( elem ) {
+ return jQuery.dir( elem, "previousSibling" );
+ },
+ nextUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "nextSibling", until );
+ },
+ prevUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "previousSibling", until );
+ },
+ siblings: function( elem ) {
+ return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
+ },
+ children: function( elem ) {
+ return jQuery.sibling( elem.firstChild );
+ },
+ contents: function( elem ) {
+ return jQuery.nodeName( elem, "iframe" ) ?
+ elem.contentDocument || elem.contentWindow.document :
+ jQuery.merge( [], elem.childNodes );
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function( until, selector ) {
+ var ret = jQuery.map( this, fn, until );
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( this.length > 1 && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret );
+ };
+});
+
+jQuery.extend({
+ filter: function( expr, elems, not ) {
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ return elems.length === 1 ?
+ jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+ jQuery.find.matches(expr, elems);
+ },
+
+ dir: function( elem, dir, until ) {
+ var matched = [],
+ cur = elem[ dir ];
+
+ while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+ if ( cur.nodeType === 1 ) {
+ matched.push( cur );
+ }
+ cur = cur[dir];
+ }
+ return matched;
+ },
+
+ sibling: function( n, elem ) {
+ var r = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ r.push( n );
+ }
+ }
+
+ return r;
+ }
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, keep ) {
+
+ // Can't pass null or undefined to indexOf in Firefox 4
+ // Set to 0 to skip string check
+ qualifier = qualifier || 0;
+
+ if ( jQuery.isFunction( qualifier ) ) {
+ return jQuery.grep(elements, function( elem, i ) {
+ var retVal = !!qualifier.call( elem, i, elem );
+ return retVal === keep;
+ });
+
+ } else if ( qualifier.nodeType ) {
+ return jQuery.grep(elements, function( elem ) {
+ return ( elem === qualifier ) === keep;
+ });
+
+ } else if ( typeof qualifier === "string" ) {
+ var filtered = jQuery.grep(elements, function( elem ) {
+ return elem.nodeType === 1;
+ });
+
+ if ( isSimple.test( qualifier ) ) {
+ return jQuery.filter(qualifier, filtered, !keep);
+ } else {
+ qualifier = jQuery.filter( qualifier, filtered );
+ }
+ }
+
+ return jQuery.grep(elements, function( elem ) {
+ return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;
+ });
+}
+function createSafeFragment( document ) {
+ var list = nodeNames.split( "|" ),
+ safeFrag = document.createDocumentFragment();
+
+ if ( safeFrag.createElement ) {
+ while ( list.length ) {
+ safeFrag.createElement(
+ list.pop()
+ );
+ }
+ }
+ return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
+ "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+ rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
+ rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
+ rleadingWhitespace = /^\s+/,
+ rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+ rtagName = /<([\w:]+)/,
+ rtbody = /<tbody/i,
+ rhtml = /<|&#?\w+;/,
+ rnoInnerhtml = /<(?:script|style|link)/i,
+ manipulation_rcheckableType = /^(?:checkbox|radio)$/i,
+ // checked="checked" or checked
+ rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+ rscriptType = /^$|\/(?:java|ecma)script/i,
+ rscriptTypeMasked = /^true\/(.*)/,
+ rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
+
+ // We have to close these tags to support XHTML (#13200)
+ wrapMap = {
+ option: [ 1, "<select multiple='multiple'>", "</select>" ],
+ legend: [ 1, "<fieldset>", "</fieldset>" ],
+ area: [ 1, "<map>", "</map>" ],
+ param: [ 1, "<object>", "</object>" ],
+ thead: [ 1, "<table>", "</table>" ],
+ tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+ col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+ td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+ // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
+ // unless wrapped in a div with non-breaking characters in front of it.
+ _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ]
+ },
+ safeFragment = createSafeFragment( document ),
+ fragmentDiv = safeFragment.appendChild( document.createElement("div") );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+jQuery.fn.extend({
+ text: function( value ) {
+ return jQuery.access( this, function( value ) {
+ return value === undefined ?
+ jQuery.text( this ) :
+ this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
+ }, null, value, arguments.length );
+ },
+
+ wrapAll: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapAll( html.call(this, i) );
+ });
+ }
+
+ if ( this[0] ) {
+ // The elements to wrap the target around
+ var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+ if ( this[0].parentNode ) {
+ wrap.insertBefore( this[0] );
+ }
+
+ wrap.map(function() {
+ var elem = this;
+
+ while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+ elem = elem.firstChild;
+ }
+
+ return elem;
+ }).append( this );
+ }
+
+ return this;
+ },
+
+ wrapInner: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapInner( html.call(this, i) );
+ });
+ }
+
+ return this.each(function() {
+ var self = jQuery( this ),
+ contents = self.contents();
+
+ if ( contents.length ) {
+ contents.wrapAll( html );
+
+ } else {
+ self.append( html );
+ }
+ });
+ },
+
+ wrap: function( html ) {
+ var isFunction = jQuery.isFunction( html );
+
+ return this.each(function(i) {
+ jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+ });
+ },
+
+ unwrap: function() {
+ return this.parent().each(function() {
+ if ( !jQuery.nodeName( this, "body" ) ) {
+ jQuery( this ).replaceWith( this.childNodes );
+ }
+ }).end();
+ },
+
+ append: function() {
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ this.appendChild( elem );
+ }
+ });
+ },
+
+ prepend: function() {
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ this.insertBefore( elem, this.firstChild );
+ }
+ });
+ },
+
+ before: function() {
+ return this.domManip( arguments, false, function( elem ) {
+ if ( this.parentNode ) {
+ this.parentNode.insertBefore( elem, this );
+ }
+ });
+ },
+
+ after: function() {
+ return this.domManip( arguments, false, function( elem ) {
+ if ( this.parentNode ) {
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ }
+ });
+ },
+
+ // keepData is for internal use only--do not document
+ remove: function( selector, keepData ) {
+ var elem,
+ i = 0;
+
+ for ( ; (elem = this[i]) != null; i++ ) {
+ if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) {
+ if ( !keepData && elem.nodeType === 1 ) {
+ jQuery.cleanData( getAll( elem ) );
+ }
+
+ if ( elem.parentNode ) {
+ if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
+ setGlobalEval( getAll( elem, "script" ) );
+ }
+ elem.parentNode.removeChild( elem );
+ }
+ }
+ }
+
+ return this;
+ },
+
+ empty: function() {
+ var elem,
+ i = 0;
+
+ for ( ; (elem = this[i]) != null; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( elem.nodeType === 1 ) {
+ jQuery.cleanData( getAll( elem, false ) );
+ }
+
+ // Remove any remaining nodes
+ while ( elem.firstChild ) {
+ elem.removeChild( elem.firstChild );
+ }
+
+ // If this is a select, ensure that it displays empty (#12336)
+ // Support: IE<9
+ if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
+ elem.options.length = 0;
+ }
+ }
+
+ return this;
+ },
+
+ clone: function( dataAndEvents, deepDataAndEvents ) {
+ dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+ deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+ return this.map( function () {
+ return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+ });
+ },
+
+ html: function( value ) {
+ return jQuery.access( this, function( value ) {
+ var elem = this[0] || {},
+ i = 0,
+ l = this.length;
+
+ if ( value === undefined ) {
+ return elem.nodeType === 1 ?
+ elem.innerHTML.replace( rinlinejQuery, "" ) :
+ undefined;
+ }
+
+ // See if we can take a shortcut and just use innerHTML
+ if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+ ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) &&
+ ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
+ !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
+
+ value = value.replace( rxhtmlTag, "<$1></$2>" );
+
+ try {
+ for (; i < l; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ elem = this[i] || {};
+ if ( elem.nodeType === 1 ) {
+ jQuery.cleanData( getAll( elem, false ) );
+ elem.innerHTML = value;
+ }
+ }
+
+ elem = 0;
+
+ // If using innerHTML throws an exception, use the fallback method
+ } catch(e) {}
+ }
+
+ if ( elem ) {
+ this.empty().append( value );
+ }
+ }, null, value, arguments.length );
+ },
+
+ replaceWith: function( value ) {
+ var isFunc = jQuery.isFunction( value );
+
+ // Make sure that the elements are removed from the DOM before they are inserted
+ // this can help fix replacing a parent with child elements
+ if ( !isFunc && typeof value !== "string" ) {
+ value = jQuery( value ).not( this ).detach();
+ }
+
+ return this.domManip( [ value ], true, function( elem ) {
+ var next = this.nextSibling,
+ parent = this.parentNode;
+
+ if ( parent ) {
+ jQuery( this ).remove();
+ parent.insertBefore( elem, next );
+ }
+ });
+ },
+
+ detach: function( selector ) {
+ return this.remove( selector, true );
+ },
+
+ domManip: function( args, table, callback ) {
+
+ // Flatten any nested arrays
+ args = core_concat.apply( [], args );
+
+ var first, node, hasScripts,
+ scripts, doc, fragment,
+ i = 0,
+ l = this.length,
+ set = this,
+ iNoClone = l - 1,
+ value = args[0],
+ isFunction = jQuery.isFunction( value );
+
+ // We can't cloneNode fragments that contain checked, in WebKit
+ if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {
+ return this.each(function( index ) {
+ var self = set.eq( index );
+ if ( isFunction ) {
+ args[0] = value.call( this, index, table ? self.html() : undefined );
+ }
+ self.domManip( args, table, callback );
+ });
+ }
+
+ if ( l ) {
+ fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
+ first = fragment.firstChild;
+
+ if ( fragment.childNodes.length === 1 ) {
+ fragment = first;
+ }
+
+ if ( first ) {
+ table = table && jQuery.nodeName( first, "tr" );
+ scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+ hasScripts = scripts.length;
+
+ // Use the original fragment for the last item instead of the first because it can end up
+ // being emptied incorrectly in certain situations (#8070).
+ for ( ; i < l; i++ ) {
+ node = fragment;
+
+ if ( i !== iNoClone ) {
+ node = jQuery.clone( node, true, true );
+
+ // Keep references to cloned scripts for later restoration
+ if ( hasScripts ) {
+ jQuery.merge( scripts, getAll( node, "script" ) );
+ }
+ }
+
+ callback.call(
+ table && jQuery.nodeName( this[i], "table" ) ?
+ findOrAppend( this[i], "tbody" ) :
+ this[i],
+ node,
+ i
+ );
+ }
+
+ if ( hasScripts ) {
+ doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+ // Reenable scripts
+ jQuery.map( scripts, restoreScript );
+
+ // Evaluate executable scripts on first document insertion
+ for ( i = 0; i < hasScripts; i++ ) {
+ node = scripts[ i ];
+ if ( rscriptType.test( node.type || "" ) &&
+ !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
+
+ if ( node.src ) {
+ // Hope ajax is available...
+ jQuery.ajax({
+ url: node.src,
+ type: "GET",
+ dataType: "script",
+ async: false,
+ global: false,
+ "throws": true
+ });
+ } else {
+ jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) );
+ }
+ }
+ }
+ }
+
+ // Fix #11809: Avoid leaking memory
+ fragment = first = null;
+ }
+ }
+
+ return this;
+ }
+});
+
+function findOrAppend( elem, tag ) {
+ return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) );
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+ var attr = elem.getAttributeNode("type");
+ elem.type = ( attr && attr.specified ) + "/" + elem.type;
+ return elem;
+}
+function restoreScript( elem ) {
+ var match = rscriptTypeMasked.exec( elem.type );
+ if ( match ) {
+ elem.type = match[1];
+ } else {
+ elem.removeAttribute("type");
+ }
+ return elem;
+}
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+ var elem,
+ i = 0;
+ for ( ; (elem = elems[i]) != null; i++ ) {
+ jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) );
+ }
+}
+
+function cloneCopyEvent( src, dest ) {
+
+ if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+ return;
+ }
+
+ var type, i, l,
+ oldData = jQuery._data( src ),
+ curData = jQuery._data( dest, oldData ),
+ events = oldData.events;
+
+ if ( events ) {
+ delete curData.handle;
+ curData.events = {};
+
+ for ( type in events ) {
+ for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+ jQuery.event.add( dest, type, events[ type ][ i ] );
+ }
+ }
+ }
+
+ // make the cloned public data object a copy from the original
+ if ( curData.data ) {
+ curData.data = jQuery.extend( {}, curData.data );
+ }
+}
+
+function fixCloneNodeIssues( src, dest ) {
+ var nodeName, e, data;
+
+ // We do not need to do anything for non-Elements
+ if ( dest.nodeType !== 1 ) {
+ return;
+ }
+
+ nodeName = dest.nodeName.toLowerCase();
+
+ // IE6-8 copies events bound via attachEvent when using cloneNode.
+ if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) {
+ data = jQuery._data( dest );
+
+ for ( e in data.events ) {
+ jQuery.removeEvent( dest, e, data.handle );
+ }
+
+ // Event data gets referenced instead of copied if the expando gets copied too
+ dest.removeAttribute( jQuery.expando );
+ }
+
+ // IE blanks contents when cloning scripts, and tries to evaluate newly-set text
+ if ( nodeName === "script" && dest.text !== src.text ) {
+ disableScript( dest ).text = src.text;
+ restoreScript( dest );
+
+ // IE6-10 improperly clones children of object elements using classid.
+ // IE10 throws NoModificationAllowedError if parent is null, #12132.
+ } else if ( nodeName === "object" ) {
+ if ( dest.parentNode ) {
+ dest.outerHTML = src.outerHTML;
+ }
+
+ // This path appears unavoidable for IE9. When cloning an object
+ // element in IE9, the outerHTML strategy above is not sufficient.
+ // If the src has innerHTML and the destination does not,
+ // copy the src.innerHTML into the dest.innerHTML. #10324
+ if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) {
+ dest.innerHTML = src.innerHTML;
+ }
+
+ } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) {
+ // IE6-8 fails to persist the checked state of a cloned checkbox
+ // or radio button. Worse, IE6-7 fail to give the cloned element
+ // a checked appearance if the defaultChecked value isn't also set
+
+ dest.defaultChecked = dest.checked = src.checked;
+
+ // IE6-7 get confused and end up setting the value of a cloned
+ // checkbox/radio button to an empty string instead of "on"
+ if ( dest.value !== src.value ) {
+ dest.value = src.value;
+ }
+
+ // IE6-8 fails to return the selected option to the default selected
+ // state when cloning options
+ } else if ( nodeName === "option" ) {
+ dest.defaultSelected = dest.selected = src.defaultSelected;
+
+ // IE6-8 fails to set the defaultValue to the correct value when
+ // cloning other types of input fields
+ } else if ( nodeName === "input" || nodeName === "textarea" ) {
+ dest.defaultValue = src.defaultValue;
+ }
+}
+
+jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var elems,
+ i = 0,
+ ret = [],
+ insert = jQuery( selector ),
+ last = insert.length - 1;
+
+ for ( ; i <= last; i++ ) {
+ elems = i === last ? this : this.clone(true);
+ jQuery( insert[i] )[ original ]( elems );
+
+ // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get()
+ core_push.apply( ret, elems.get() );
+ }
+
+ return this.pushStack( ret );
+ };
+});
+
+function getAll( context, tag ) {
+ var elems, elem,
+ i = 0,
+ found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) :
+ typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) :
+ undefined;
+
+ if ( !found ) {
+ for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {
+ if ( !tag || jQuery.nodeName( elem, tag ) ) {
+ found.push( elem );
+ } else {
+ jQuery.merge( found, getAll( elem, tag ) );
+ }
+ }
+ }
+
+ return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
+ jQuery.merge( [ context ], found ) :
+ found;
+}
+
+// Used in buildFragment, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+ if ( manipulation_rcheckableType.test( elem.type ) ) {
+ elem.defaultChecked = elem.checked;
+ }
+}
+
+jQuery.extend({
+ clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+ var destElements, node, clone, i, srcElements,
+ inPage = jQuery.contains( elem.ownerDocument, elem );
+
+ if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+ clone = elem.cloneNode( true );
+
+ // IE<=8 does not properly clone detached, unknown element nodes
+ } else {
+ fragmentDiv.innerHTML = elem.outerHTML;
+ fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
+ }
+
+ if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+ (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+
+ // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
+ destElements = getAll( clone );
+ srcElements = getAll( elem );
+
+ // Fix all IE cloning issues
+ for ( i = 0; (node = srcElements[i]) != null; ++i ) {
+ // Ensure that the destination node is not null; Fixes #9587
+ if ( destElements[i] ) {
+ fixCloneNodeIssues( node, destElements[i] );
+ }
+ }
+ }
+
+ // Copy the events from the original to the clone
+ if ( dataAndEvents ) {
+ if ( deepDataAndEvents ) {
+ srcElements = srcElements || getAll( elem );
+ destElements = destElements || getAll( clone );
+
+ for ( i = 0; (node = srcElements[i]) != null; i++ ) {
+ cloneCopyEvent( node, destElements[i] );
+ }
+ } else {
+ cloneCopyEvent( elem, clone );
+ }
+ }
+
+ // Preserve script evaluation history
+ destElements = getAll( clone, "script" );
+ if ( destElements.length > 0 ) {
+ setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+ }
+
+ destElements = srcElements = node = null;
+
+ // Return the cloned set
+ return clone;
+ },
+
+ buildFragment: function( elems, context, scripts, selection ) {
+ var j, elem, contains,
+ tmp, tag, tbody, wrap,
+ l = elems.length,
+
+ // Ensure a safe fragment
+ safe = createSafeFragment( context ),
+
+ nodes = [],
+ i = 0;
+
+ for ( ; i < l; i++ ) {
+ elem = elems[ i ];
+
+ if ( elem || elem === 0 ) {
+
+ // Add nodes directly
+ if ( jQuery.type( elem ) === "object" ) {
+ jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+ // Convert non-html into a text node
+ } else if ( !rhtml.test( elem ) ) {
+ nodes.push( context.createTextNode( elem ) );
+
+ // Convert html into DOM nodes
+ } else {
+ tmp = tmp || safe.appendChild( context.createElement("div") );
+
+ // Deserialize a standard representation
+ tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
+ wrap = wrapMap[ tag ] || wrapMap._default;
+
+ tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2];
+
+ // Descend through wrappers to the right content
+ j = wrap[0];
+ while ( j-- ) {
+ tmp = tmp.lastChild;
+ }
+
+ // Manually add leading whitespace removed by IE
+ if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+ nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) );
+ }
+
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( !jQuery.support.tbody ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ elem = tag === "table" && !rtbody.test( elem ) ?
+ tmp.firstChild :
+
+ // String was a bare <thead> or <tfoot>
+ wrap[1] === "<table>" && !rtbody.test( elem ) ?
+ tmp :
+ 0;
+
+ j = elem && elem.childNodes.length;
+ while ( j-- ) {
+ if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) {
+ elem.removeChild( tbody );
+ }
+ }
+ }
+
+ jQuery.merge( nodes, tmp.childNodes );
+
+ // Fix #12392 for WebKit and IE > 9
+ tmp.textContent = "";
+
+ // Fix #12392 for oldIE
+ while ( tmp.firstChild ) {
+ tmp.removeChild( tmp.firstChild );
+ }
+
+ // Remember the top-level container for proper cleanup
+ tmp = safe.lastChild;
+ }
+ }
+ }
+
+ // Fix #11356: Clear elements from fragment
+ if ( tmp ) {
+ safe.removeChild( tmp );
+ }
+
+ // Reset defaultChecked for any radios and checkboxes
+ // about to be appended to the DOM in IE 6/7 (#8060)
+ if ( !jQuery.support.appendChecked ) {
+ jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked );
+ }
+
+ i = 0;
+ while ( (elem = nodes[ i++ ]) ) {
+
+ // #4087 - If origin and destination elements are the same, and this is
+ // that element, do not do anything
+ if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
+ continue;
+ }
+
+ contains = jQuery.contains( elem.ownerDocument, elem );
+
+ // Append to fragment
+ tmp = getAll( safe.appendChild( elem ), "script" );
+
+ // Preserve script evaluation history
+ if ( contains ) {
+ setGlobalEval( tmp );
+ }
+
+ // Capture executables
+ if ( scripts ) {
+ j = 0;
+ while ( (elem = tmp[ j++ ]) ) {
+ if ( rscriptType.test( elem.type || "" ) ) {
+ scripts.push( elem );
+ }
+ }
+ }
+ }
+
+ tmp = null;
+
+ return safe;
+ },
+
+ cleanData: function( elems, /* internal */ acceptData ) {
+ var elem, type, id, data,
+ i = 0,
+ internalKey = jQuery.expando,
+ cache = jQuery.cache,
+ deleteExpando = jQuery.support.deleteExpando,
+ special = jQuery.event.special;
+
+ for ( ; (elem = elems[i]) != null; i++ ) {
+
+ if ( acceptData || jQuery.acceptData( elem ) ) {
+
+ id = elem[ internalKey ];
+ data = id && cache[ id ];
+
+ if ( data ) {
+ if ( data.events ) {
+ for ( type in data.events ) {
+ if ( special[ type ] ) {
+ jQuery.event.remove( elem, type );
+
+ // This is a shortcut to avoid jQuery.event.remove's overhead
+ } else {
+ jQuery.removeEvent( elem, type, data.handle );
+ }
+ }
+ }
+
+ // Remove cache only if it was not already removed by jQuery.event.remove
+ if ( cache[ id ] ) {
+
+ delete cache[ id ];
+
+ // IE does not allow us to delete expando properties from nodes,
+ // nor does it have a removeAttribute function on Document nodes;
+ // we must handle all of these cases
+ if ( deleteExpando ) {
+ delete elem[ internalKey ];
+
+ } else if ( typeof elem.removeAttribute !== core_strundefined ) {
+ elem.removeAttribute( internalKey );
+
+ } else {
+ elem[ internalKey ] = null;
+ }
+
+ core_deletedIds.push( id );
+ }
+ }
+ }
+ }
+ }
+});
+var iframe, getStyles, curCSS,
+ ralpha = /alpha\([^)]*\)/i,
+ ropacity = /opacity\s*=\s*([^)]*)/,
+ rposition = /^(top|right|bottom|left)$/,
+ // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
+ // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+ rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+ rmargin = /^margin/,
+ rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
+ rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
+ rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ),
+ elemdisplay = { BODY: "block" },
+
+ cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+ cssNormalTransform = {
+ letterSpacing: 0,
+ fontWeight: 400
+ },
+
+ cssExpand = [ "Top", "Right", "Bottom", "Left" ],
+ cssPrefixes = [ "Webkit", "O", "Moz", "ms" ];
+
+// return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( style, name ) {
+
+ // shortcut for names that are not vendor prefixed
+ if ( name in style ) {
+ return name;
+ }
+
+ // check for vendor prefixed names
+ var capName = name.charAt(0).toUpperCase() + name.slice(1),
+ origName = name,
+ i = cssPrefixes.length;
+
+ while ( i-- ) {
+ name = cssPrefixes[ i ] + capName;
+ if ( name in style ) {
+ return name;
+ }
+ }
+
+ return origName;
+}
+
+function isHidden( elem, el ) {
+ // isHidden might be called from jQuery#filter function;
+ // in that case, element will be second argument
+ elem = el || elem;
+ return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
+}
+
+function showHide( elements, show ) {
+ var display, elem, hidden,
+ values = [],
+ index = 0,
+ length = elements.length;
+
+ for ( ; index < length; index++ ) {
+ elem = elements[ index ];
+ if ( !elem.style ) {
+ continue;
+ }
+
+ values[ index ] = jQuery._data( elem, "olddisplay" );
+ display = elem.style.display;
+ if ( show ) {
+ // Reset the inline display of this element to learn if it is
+ // being hidden by cascaded rules or not
+ if ( !values[ index ] && display === "none" ) {
+ elem.style.display = "";
+ }
+
+ // Set elements which have been overridden with display: none
+ // in a stylesheet to whatever the default browser style is
+ // for such an element
+ if ( elem.style.display === "" && isHidden( elem ) ) {
+ values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
+ }
+ } else {
+
+ if ( !values[ index ] ) {
+ hidden = isHidden( elem );
+
+ if ( display && display !== "none" || !hidden ) {
+ jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) );
+ }
+ }
+ }
+ }
+
+ // Set the display of most of the elements in a second loop
+ // to avoid the constant reflow
+ for ( index = 0; index < length; index++ ) {
+ elem = elements[ index ];
+ if ( !elem.style ) {
+ continue;
+ }
+ if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+ elem.style.display = show ? values[ index ] || "" : "none";
+ }
+ }
+
+ return elements;
+}
+
+jQuery.fn.extend({
+ css: function( name, value ) {
+ return jQuery.access( this, function( elem, name, value ) {
+ var len, styles,
+ map = {},
+ i = 0;
+
+ if ( jQuery.isArray( name ) ) {
+ styles = getStyles( elem );
+ len = name.length;
+
+ for ( ; i < len; i++ ) {
+ map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+ }
+
+ return map;
+ }
+
+ return value !== undefined ?
+ jQuery.style( elem, name, value ) :
+ jQuery.css( elem, name );
+ }, name, value, arguments.length > 1 );
+ },
+ show: function() {
+ return showHide( this, true );
+ },
+ hide: function() {
+ return showHide( this );
+ },
+ toggle: function( state ) {
+ var bool = typeof state === "boolean";
+
+ return this.each(function() {
+ if ( bool ? state : isHidden( this ) ) {
+ jQuery( this ).show();
+ } else {
+ jQuery( this ).hide();
+ }
+ });
+ }
+});
+
+jQuery.extend({
+ // Add in style property hooks for overriding the default
+ // behavior of getting and setting a style property
+ cssHooks: {
+ opacity: {
+ get: function( elem, computed ) {
+ if ( computed ) {
+ // We should always get a number back from opacity
+ var ret = curCSS( elem, "opacity" );
+ return ret === "" ? "1" : ret;
+ }
+ }
+ }
+ },
+
+ // Exclude the following css properties to add px
+ cssNumber: {
+ "columnCount": true,
+ "fillOpacity": true,
+ "fontWeight": true,
+ "lineHeight": true,
+ "opacity": true,
+ "orphans": true,
+ "widows": true,
+ "zIndex": true,
+ "zoom": true
+ },
+
+ // Add in properties whose names you wish to fix before
+ // setting or getting the value
+ cssProps: {
+ // normalize float css property
+ "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+ },
+
+ // Get and set the style property on a DOM Node
+ style: function( elem, name, value, extra ) {
+ // Don't set styles on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+ return;
+ }
+
+ // Make sure that we're working with the right name
+ var ret, type, hooks,
+ origName = jQuery.camelCase( name ),
+ style = elem.style;
+
+ name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
+
+ // gets hook for the prefixed version
+ // followed by the unprefixed version
+ hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+ // Check if we're setting a value
+ if ( value !== undefined ) {
+ type = typeof value;
+
+ // convert relative number strings (+= or -=) to relative numbers. #7345
+ if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+ value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
+ // Fixes bug #9237
+ type = "number";
+ }
+
+ // Make sure that NaN and null values aren't set. See: #7116
+ if ( value == null || type === "number" && isNaN( value ) ) {
+ return;
+ }
+
+ // If a number was passed in, add 'px' to the (except for certain CSS properties)
+ if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+ value += "px";
+ }
+
+ // Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
+ // but it would mean to define eight (for every problematic property) identical functions
+ if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) {
+ style[ name ] = "inherit";
+ }
+
+ // If a hook was provided, use that value, otherwise just set the specified value
+ if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+
+ // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+ // Fixes bug #5509
+ try {
+ style[ name ] = value;
+ } catch(e) {}
+ }
+
+ } else {
+ // If a hook was provided get the non-computed value from there
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+ return ret;
+ }
+
+ // Otherwise just get the value from the style object
+ return style[ name ];
+ }
+ },
+
+ css: function( elem, name, extra, styles ) {
+ var num, val, hooks,
+ origName = jQuery.camelCase( name );
+
+ // Make sure that we're working with the right name
+ name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
+
+ // gets hook for the prefixed version
+ // followed by the unprefixed version
+ hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+ // If a hook was provided get the computed value from there
+ if ( hooks && "get" in hooks ) {
+ val = hooks.get( elem, true, extra );
+ }
+
+ // Otherwise, if a way to get the computed value exists, use that
+ if ( val === undefined ) {
+ val = curCSS( elem, name, styles );
+ }
+
+ //convert "normal" to computed value
+ if ( val === "normal" && name in cssNormalTransform ) {
+ val = cssNormalTransform[ name ];
+ }
+
+ // Return, converting to number if forced or a qualifier was provided and val looks numeric
+ if ( extra === "" || extra ) {
+ num = parseFloat( val );
+ return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
+ }
+ return val;
+ },
+
+ // A method for quickly swapping in/out CSS properties to get correct calculations
+ swap: function( elem, options, callback, args ) {
+ var ret, name,
+ old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ ret = callback.apply( elem, args || [] );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+
+ return ret;
+ }
+});
+
+// NOTE: we've included the "window" in window.getComputedStyle
+// because jsdom on node.js will break without it.
+if ( window.getComputedStyle ) {
+ getStyles = function( elem ) {
+ return window.getComputedStyle( elem, null );
+ };
+
+ curCSS = function( elem, name, _computed ) {
+ var width, minWidth, maxWidth,
+ computed = _computed || getStyles( elem ),
+
+ // getPropertyValue is only needed for .css('filter') in IE9, see #12537
+ ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined,
+ style = elem.style;
+
+ if ( computed ) {
+
+ if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+ ret = jQuery.style( elem, name );
+ }
+
+ // A tribute to the "awesome hack by Dean Edwards"
+ // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
+ // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
+ // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
+ if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+
+ // Remember the original values
+ width = style.width;
+ minWidth = style.minWidth;
+ maxWidth = style.maxWidth;
+
+ // Put in the new values to get a computed value out
+ style.minWidth = style.maxWidth = style.width = ret;
+ ret = computed.width;
+
+ // Revert the changed values
+ style.width = width;
+ style.minWidth = minWidth;
+ style.maxWidth = maxWidth;
+ }
+ }
+
+ return ret;
+ };
+} else if ( document.documentElement.currentStyle ) {
+ getStyles = function( elem ) {
+ return elem.currentStyle;
+ };
+
+ curCSS = function( elem, name, _computed ) {
+ var left, rs, rsLeft,
+ computed = _computed || getStyles( elem ),
+ ret = computed ? computed[ name ] : undefined,
+ style = elem.style;
+
+ // Avoid setting ret to empty string here
+ // so we don't default to auto
+ if ( ret == null && style && style[ name ] ) {
+ ret = style[ name ];
+ }
+
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+ // If we're not dealing with a regular pixel number
+ // but a number that has a weird ending, we need to convert it to pixels
+ // but not position css attributes, as those are proportional to the parent element instead
+ // and we can't measure the parent instead because it might trigger a "stacking dolls" problem
+ if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
+
+ // Remember the original values
+ left = style.left;
+ rs = elem.runtimeStyle;
+ rsLeft = rs && rs.left;
+
+ // Put in the new values to get a computed value out
+ if ( rsLeft ) {
+ rs.left = elem.currentStyle.left;
+ }
+ style.left = name === "fontSize" ? "1em" : ret;
+ ret = style.pixelLeft + "px";
+
+ // Revert the changed values
+ style.left = left;
+ if ( rsLeft ) {
+ rs.left = rsLeft;
+ }
+ }
+
+ return ret === "" ? "auto" : ret;
+ };
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+ var matches = rnumsplit.exec( value );
+ return matches ?
+ // Guard against undefined "subtract", e.g., when used as in cssHooks
+ Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
+ value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
+ var i = extra === ( isBorderBox ? "border" : "content" ) ?
+ // If we already have the right measurement, avoid augmentation
+ 4 :
+ // Otherwise initialize for horizontal or vertical properties
+ name === "width" ? 1 : 0,
+
+ val = 0;
+
+ for ( ; i < 4; i += 2 ) {
+ // both box models exclude margin, so add it if we want it
+ if ( extra === "margin" ) {
+ val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
+ }
+
+ if ( isBorderBox ) {
+ // border-box includes padding, so remove it if we want content
+ if ( extra === "content" ) {
+ val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+ }
+
+ // at this point, extra isn't border nor margin, so remove border
+ if ( extra !== "margin" ) {
+ val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ }
+ } else {
+ // at this point, extra isn't content, so add padding
+ val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+ // at this point, extra isn't content nor padding, so add border
+ if ( extra !== "padding" ) {
+ val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ }
+ }
+ }
+
+ return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+ // Start with offset property, which is equivalent to the border-box value
+ var valueIsBorderBox = true,
+ val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+ styles = getStyles( elem ),
+ isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+ // some non-html elements return undefined for offsetWidth, so check for null/undefined
+ // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+ // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+ if ( val <= 0 || val == null ) {
+ // Fall back to computed then uncomputed css if necessary
+ val = curCSS( elem, name, styles );
+ if ( val < 0 || val == null ) {
+ val = elem.style[ name ];
+ }
+
+ // Computed unit is not pixels. Stop here and return.
+ if ( rnumnonpx.test(val) ) {
+ return val;
+ }
+
+ // we need the check for style in case a browser which returns unreliable values
+ // for getComputedStyle silently falls back to the reliable elem.style
+ valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
+
+ // Normalize "", auto, and prepare for extra
+ val = parseFloat( val ) || 0;
+ }
+
+ // use the active box-sizing model to add/subtract irrelevant styles
+ return ( val +
+ augmentWidthOrHeight(
+ elem,
+ name,
+ extra || ( isBorderBox ? "border" : "content" ),
+ valueIsBorderBox,
+ styles
+ )
+ ) + "px";
+}
+
+// Try to determine the default display value of an element
+function css_defaultDisplay( nodeName ) {
+ var doc = document,
+ display = elemdisplay[ nodeName ];
+
+ if ( !display ) {
+ display = actualDisplay( nodeName, doc );
+
+ // If the simple way fails, read from inside an iframe
+ if ( display === "none" || !display ) {
+ // Use the already-created iframe if possible
+ iframe = ( iframe ||
+ jQuery("<iframe frameborder='0' width='0' height='0'/>")
+ .css( "cssText", "display:block !important" )
+ ).appendTo( doc.documentElement );
+
+ // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
+ doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document;
+ doc.write("<!doctype html><html><body>");
+ doc.close();
+
+ display = actualDisplay( nodeName, doc );
+ iframe.detach();
+ }
+
+ // Store the correct default display
+ elemdisplay[ nodeName ] = display;
+ }
+
+ return display;
+}
+
+// Called ONLY from within css_defaultDisplay
+function actualDisplay( name, doc ) {
+ var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
+ display = jQuery.css( elem[0], "display" );
+ elem.remove();
+ return display;
+}
+
+jQuery.each([ "height", "width" ], function( i, name ) {
+ jQuery.cssHooks[ name ] = {
+ get: function( elem, computed, extra ) {
+ if ( computed ) {
+ // certain elements can have dimension info if we invisibly show them
+ // however, it must have a current display style that would benefit from this
+ return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ?
+ jQuery.swap( elem, cssShow, function() {
+ return getWidthOrHeight( elem, name, extra );
+ }) :
+ getWidthOrHeight( elem, name, extra );
+ }
+ },
+
+ set: function( elem, value, extra ) {
+ var styles = extra && getStyles( elem );
+ return setPositiveNumber( elem, value, extra ?
+ augmentWidthOrHeight(
+ elem,
+ name,
+ extra,
+ jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+ styles
+ ) : 0
+ );
+ }
+ };
+});
+
+if ( !jQuery.support.opacity ) {
+ jQuery.cssHooks.opacity = {
+ get: function( elem, computed ) {
+ // IE uses filters for opacity
+ return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+ ( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
+ computed ? "1" : "";
+ },
+
+ set: function( elem, value ) {
+ var style = elem.style,
+ currentStyle = elem.currentStyle,
+ opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+ filter = currentStyle && currentStyle.filter || style.filter || "";
+
+ // IE has trouble with opacity if it does not have layout
+ // Force it by setting the zoom level
+ style.zoom = 1;
+
+ // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+ // if value === "", then remove inline opacity #12685
+ if ( ( value >= 1 || value === "" ) &&
+ jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
+ style.removeAttribute ) {
+
+ // Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+ // if "filter:" is present at all, clearType is disabled, we want to avoid this
+ // style.removeAttribute is IE Only, but so apparently is this code path...
+ style.removeAttribute( "filter" );
+
+ // if there is no filter style applied in a css rule or unset inline opacity, we are done
+ if ( value === "" || currentStyle && !currentStyle.filter ) {
+ return;
+ }
+ }
+
+ // otherwise, set new filter values
+ style.filter = ralpha.test( filter ) ?
+ filter.replace( ralpha, opacity ) :
+ filter + " " + opacity;
+ }
+ };
+}
+
+// These hooks cannot be added until DOM ready because the support test
+// for it is not run until after DOM ready
+jQuery(function() {
+ if ( !jQuery.support.reliableMarginRight ) {
+ jQuery.cssHooks.marginRight = {
+ get: function( elem, computed ) {
+ if ( computed ) {
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ // Work around by temporarily setting element display to inline-block
+ return jQuery.swap( elem, { "display": "inline-block" },
+ curCSS, [ elem, "marginRight" ] );
+ }
+ }
+ };
+ }
+
+ // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+ // getComputedStyle returns percent when specified for top/left/bottom/right
+ // rather than make the css module depend on the offset module, we just check for it here
+ if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
+ jQuery.each( [ "top", "left" ], function( i, prop ) {
+ jQuery.cssHooks[ prop ] = {
+ get: function( elem, computed ) {
+ if ( computed ) {
+ computed = curCSS( elem, prop );
+ // if curCSS returns percentage, fallback to offset
+ return rnumnonpx.test( computed ) ?
+ jQuery( elem ).position()[ prop ] + "px" :
+ computed;
+ }
+ }
+ };
+ });
+ }
+
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.hidden = function( elem ) {
+ // Support: Opera <= 12.12
+ // Opera reports offsetWidths and offsetHeights less than zero on some elements
+ return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 ||
+ (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none");
+ };
+
+ jQuery.expr.filters.visible = function( elem ) {
+ return !jQuery.expr.filters.hidden( elem );
+ };
+}
+
+// These hooks are used by animate to expand properties
+jQuery.each({
+ margin: "",
+ padding: "",
+ border: "Width"
+}, function( prefix, suffix ) {
+ jQuery.cssHooks[ prefix + suffix ] = {
+ expand: function( value ) {
+ var i = 0,
+ expanded = {},
+
+ // assumes a single number if not a string
+ parts = typeof value === "string" ? value.split(" ") : [ value ];
+
+ for ( ; i < 4; i++ ) {
+ expanded[ prefix + cssExpand[ i ] + suffix ] =
+ parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+ }
+
+ return expanded;
+ }
+ };
+
+ if ( !rmargin.test( prefix ) ) {
+ jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+ }
+});
+var r20 = /%20/g,
+ rbracket = /\[\]$/,
+ rCRLF = /\r?\n/g,
+ rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+ rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+jQuery.fn.extend({
+ serialize: function() {
+ return jQuery.param( this.serializeArray() );
+ },
+ serializeArray: function() {
+ return this.map(function(){
+ // Can add propHook for "elements" to filter or add form elements
+ var elements = jQuery.prop( this, "elements" );
+ return elements ? jQuery.makeArray( elements ) : this;
+ })
+ .filter(function(){
+ var type = this.type;
+ // Use .is(":disabled") so that fieldset[disabled] works
+ return this.name && !jQuery( this ).is( ":disabled" ) &&
+ rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+ ( this.checked || !manipulation_rcheckableType.test( type ) );
+ })
+ .map(function( i, elem ){
+ var val = jQuery( this ).val();
+
+ return val == null ?
+ null :
+ jQuery.isArray( val ) ?
+ jQuery.map( val, function( val ){
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ }) :
+ { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ }).get();
+ }
+});
+
+//Serialize an array of form elements or a set of
+//key/values into a query string
+jQuery.param = function( a, traditional ) {
+ var prefix,
+ s = [],
+ add = function( key, value ) {
+ // If value is a function, invoke it and return its value
+ value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+ s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+ };
+
+ // Set traditional to true for jQuery <= 1.3.2 behavior.
+ if ( traditional === undefined ) {
+ traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+ }
+
+ // If an array was passed in, assume that it is an array of form elements.
+ if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+ // Serialize the form elements
+ jQuery.each( a, function() {
+ add( this.name, this.value );
+ });
+
+ } else {
+ // If traditional, encode the "old" way (the way 1.3.2 or older
+ // did it), otherwise encode params recursively.
+ for ( prefix in a ) {
+ buildParams( prefix, a[ prefix ], traditional, add );
+ }
+ }
+
+ // Return the resulting serialization
+ return s.join( "&" ).replace( r20, "+" );
+};
+
+function buildParams( prefix, obj, traditional, add ) {
+ var name;
+
+ if ( jQuery.isArray( obj ) ) {
+ // Serialize array item.
+ jQuery.each( obj, function( i, v ) {
+ if ( traditional || rbracket.test( prefix ) ) {
+ // Treat each array item as a scalar.
+ add( prefix, v );
+
+ } else {
+ // Item is non-scalar (array or object), encode its numeric index.
+ buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
+ }
+ });
+
+ } else if ( !traditional && jQuery.type( obj ) === "object" ) {
+ // Serialize object item.
+ for ( name in obj ) {
+ buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+ }
+
+ } else {
+ // Serialize scalar item.
+ add( prefix, obj );
+ }
+}
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+ "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+ "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+ // Handle event binding
+ jQuery.fn[ name ] = function( data, fn ) {
+ return arguments.length > 0 ?
+ this.on( name, null, data, fn ) :
+ this.trigger( name );
+ };
+});
+
+jQuery.fn.hover = function( fnOver, fnOut ) {
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+};
+var
+ // Document location
+ ajaxLocParts,
+ ajaxLocation,
+ ajax_nonce = jQuery.now(),
+
+ ajax_rquery = /\?/,
+ rhash = /#.*$/,
+ rts = /([?&])_=[^&]*/,
+ rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+ // #7653, #8125, #8152: local protocol detection
+ rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+ rnoContent = /^(?:GET|HEAD)$/,
+ rprotocol = /^\/\//,
+ rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
+
+ // Keep a copy of the old load method
+ _load = jQuery.fn.load,
+
+ /* Prefilters
+ * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+ * 2) These are called:
+ * - BEFORE asking for a transport
+ * - AFTER param serialization (s.data is a string if s.processData is true)
+ * 3) key is the dataType
+ * 4) the catchall symbol "*" can be used
+ * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+ */
+ prefilters = {},
+
+ /* Transports bindings
+ * 1) key is the dataType
+ * 2) the catchall symbol "*" can be used
+ * 3) selection will start with transport dataType and THEN go to "*" if needed
+ */
+ transports = {},
+
+ // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+ allTypes = "*/".concat("*");
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+ ajaxLocation = location.href;
+} catch( e ) {
+ // Use the href attribute of an A element
+ // since IE will modify it given document.location
+ ajaxLocation = document.createElement( "a" );
+ ajaxLocation.href = "";
+ ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+ // dataTypeExpression is optional and defaults to "*"
+ return function( dataTypeExpression, func ) {
+
+ if ( typeof dataTypeExpression !== "string" ) {
+ func = dataTypeExpression;
+ dataTypeExpression = "*";
+ }
+
+ var dataType,
+ i = 0,
+ dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || [];
+
+ if ( jQuery.isFunction( func ) ) {
+ // For each dataType in the dataTypeExpression
+ while ( (dataType = dataTypes[i++]) ) {
+ // Prepend if requested
+ if ( dataType[0] === "+" ) {
+ dataType = dataType.slice( 1 ) || "*";
+ (structure[ dataType ] = structure[ dataType ] || []).unshift( func );
+
+ // Otherwise append
+ } else {
+ (structure[ dataType ] = structure[ dataType ] || []).push( func );
+ }
+ }
+ }
+ };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+ var inspected = {},
+ seekingTransport = ( structure === transports );
+
+ function inspect( dataType ) {
+ var selected;
+ inspected[ dataType ] = true;
+ jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+ var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+ if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+ options.dataTypes.unshift( dataTypeOrTransport );
+ inspect( dataTypeOrTransport );
+ return false;
+ } else if ( seekingTransport ) {
+ return !( selected = dataTypeOrTransport );
+ }
+ });
+ return selected;
+ }
+
+ return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+ var deep, key,
+ flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+ for ( key in src ) {
+ if ( src[ key ] !== undefined ) {
+ ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];
+ }
+ }
+ if ( deep ) {
+ jQuery.extend( true, target, deep );
+ }
+
+ return target;
+}
+
+jQuery.fn.load = function( url, params, callback ) {
+ if ( typeof url !== "string" && _load ) {
+ return _load.apply( this, arguments );
+ }
+
+ var selector, response, type,
+ self = this,
+ off = url.indexOf(" ");
+
+ if ( off >= 0 ) {
+ selector = url.slice( off, url.length );
+ url = url.slice( 0, off );
+ }
+
+ // If it's a function
+ if ( jQuery.isFunction( params ) ) {
+
+ // We assume that it's the callback
+ callback = params;
+ params = undefined;
+
+ // Otherwise, build a param string
+ } else if ( params && typeof params === "object" ) {
+ type = "POST";
+ }
+
+ // If we have elements to modify, make the request
+ if ( self.length > 0 ) {
+ jQuery.ajax({
+ url: url,
+
+ // if "type" variable is undefined, then "GET" method will be used
+ type: type,
+ dataType: "html",
+ data: params
+ }).done(function( responseText ) {
+
+ // Save response for use in complete callback
+ response = arguments;
+
+ self.html( selector ?
+
+ // If a selector was specified, locate the right elements in a dummy div
+ // Exclude scripts to avoid IE 'Permission Denied' errors
+ jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+ // Otherwise use the full result
+ responseText );
+
+ }).complete( callback && function( jqXHR, status ) {
+ self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+ });
+ }
+
+ return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){
+ jQuery.fn[ type ] = function( fn ){
+ return this.on( type, fn );
+ };
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+ jQuery[ method ] = function( url, data, callback, type ) {
+ // shift arguments if data argument was omitted
+ if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ return jQuery.ajax({
+ url: url,
+ type: method,
+ dataType: type,
+ data: data,
+ success: callback
+ });
+ };
+});
+
+jQuery.extend({
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ etag: {},
+
+ ajaxSettings: {
+ url: ajaxLocation,
+ type: "GET",
+ isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+ global: true,
+ processData: true,
+ async: true,
+ contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+ /*
+ timeout: 0,
+ data: null,
+ dataType: null,
+ username: null,
+ password: null,
+ cache: null,
+ throws: false,
+ traditional: false,
+ headers: {},
+ */
+
+ accepts: {
+ "*": allTypes,
+ text: "text/plain",
+ html: "text/html",
+ xml: "application/xml, text/xml",
+ json: "application/json, text/javascript"
+ },
+
+ contents: {
+ xml: /xml/,
+ html: /html/,
+ json: /json/
+ },
+
+ responseFields: {
+ xml: "responseXML",
+ text: "responseText"
+ },
+
+ // Data converters
+ // Keys separate source (or catchall "*") and destination types with a single space
+ converters: {
+
+ // Convert anything to text
+ "* text": window.String,
+
+ // Text to html (true = no transformation)
+ "text html": true,
+
+ // Evaluate text as a json expression
+ "text json": jQuery.parseJSON,
+
+ // Parse text as xml
+ "text xml": jQuery.parseXML
+ },
+
+ // For options that shouldn't be deep extended:
+ // you can add your own custom options here if
+ // and when you create one that shouldn't be
+ // deep extended (see ajaxExtend)
+ flatOptions: {
+ url: true,
+ context: true
+ }
+ },
+
+ // Creates a full fledged settings object into target
+ // with both ajaxSettings and settings fields.
+ // If target is omitted, writes into ajaxSettings.
+ ajaxSetup: function( target, settings ) {
+ return settings ?
+
+ // Building a settings object
+ ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+ // Extending ajaxSettings
+ ajaxExtend( jQuery.ajaxSettings, target );
+ },
+
+ ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+ ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+ // Main method
+ ajax: function( url, options ) {
+
+ // If url is an object, simulate pre-1.5 signature
+ if ( typeof url === "object" ) {
+ options = url;
+ url = undefined;
+ }
+
+ // Force options to be an object
+ options = options || {};
+
+ var // Cross-domain detection vars
+ parts,
+ // Loop variable
+ i,
+ // URL without anti-cache param
+ cacheURL,
+ // Response headers as string
+ responseHeadersString,
+ // timeout handle
+ timeoutTimer,
+
+ // To know if global events are to be dispatched
+ fireGlobals,
+
+ transport,
+ // Response headers
+ responseHeaders,
+ // Create the final options object
+ s = jQuery.ajaxSetup( {}, options ),
+ // Callbacks context
+ callbackContext = s.context || s,
+ // Context for global events is callbackContext if it is a DOM node or jQuery collection
+ globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
+ jQuery( callbackContext ) :
+ jQuery.event,
+ // Deferreds
+ deferred = jQuery.Deferred(),
+ completeDeferred = jQuery.Callbacks("once memory"),
+ // Status-dependent callbacks
+ statusCode = s.statusCode || {},
+ // Headers (they are sent all at once)
+ requestHeaders = {},
+ requestHeadersNames = {},
+ // The jqXHR state
+ state = 0,
+ // Default abort message
+ strAbort = "canceled",
+ // Fake xhr
+ jqXHR = {
+ readyState: 0,
+
+ // Builds headers hashtable if needed
+ getResponseHeader: function( key ) {
+ var match;
+ if ( state === 2 ) {
+ if ( !responseHeaders ) {
+ responseHeaders = {};
+ while ( (match = rheaders.exec( responseHeadersString )) ) {
+ responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+ }
+ }
+ match = responseHeaders[ key.toLowerCase() ];
+ }
+ return match == null ? null : match;
+ },
+
+ // Raw string
+ getAllResponseHeaders: function() {
+ return state === 2 ? responseHeadersString : null;
+ },
+
+ // Caches the header
+ setRequestHeader: function( name, value ) {
+ var lname = name.toLowerCase();
+ if ( !state ) {
+ name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+ requestHeaders[ name ] = value;
+ }
+ return this;
+ },
+
+ // Overrides response content-type header
+ overrideMimeType: function( type ) {
+ if ( !state ) {
+ s.mimeType = type;
+ }
+ return this;
+ },
+
+ // Status-dependent callbacks
+ statusCode: function( map ) {
+ var code;
+ if ( map ) {
+ if ( state < 2 ) {
+ for ( code in map ) {
+ // Lazy-add the new callback in a way that preserves old ones
+ statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+ }
+ } else {
+ // Execute the appropriate callbacks
+ jqXHR.always( map[ jqXHR.status ] );
+ }
+ }
+ return this;
+ },
+
+ // Cancel the request
+ abort: function( statusText ) {
+ var finalText = statusText || strAbort;
+ if ( transport ) {
+ transport.abort( finalText );
+ }
+ done( 0, finalText );
+ return this;
+ }
+ };
+
+ // Attach deferreds
+ deferred.promise( jqXHR ).complete = completeDeferred.add;
+ jqXHR.success = jqXHR.done;
+ jqXHR.error = jqXHR.fail;
+
+ // Remove hash character (#7531: and string promotion)
+ // Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+ // Handle falsy url in the settings object (#10093: consistency with old signature)
+ // We also use the url parameter if available
+ s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+ // Alias method option to type as per ticket #12004
+ s.type = options.method || options.type || s.method || s.type;
+
+ // Extract dataTypes list
+ s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""];
+
+ // A cross-domain request is in order when we have a protocol:host:port mismatch
+ if ( s.crossDomain == null ) {
+ parts = rurl.exec( s.url.toLowerCase() );
+ s.crossDomain = !!( parts &&
+ ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
+ ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+ ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+ );
+ }
+
+ // Convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
+
+ // Apply prefilters
+ inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+ // If request was aborted inside a prefilter, stop there
+ if ( state === 2 ) {
+ return jqXHR;
+ }
+
+ // We can fire global events as of now if asked to
+ fireGlobals = s.global;
+
+ // Watch for a new set of requests
+ if ( fireGlobals && jQuery.active++ === 0 ) {
+ jQuery.event.trigger("ajaxStart");
+ }
+
+ // Uppercase the type
+ s.type = s.type.toUpperCase();
+
+ // Determine if request has content
+ s.hasContent = !rnoContent.test( s.type );
+
+ // Save the URL in case we're toying with the If-Modified-Since
+ // and/or If-None-Match header later on
+ cacheURL = s.url;
+
+ // More options handling for requests with no content
+ if ( !s.hasContent ) {
+
+ // If data is available, append data to url
+ if ( s.data ) {
+ cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
+ // #9682: remove data so that it's not used in an eventual retry
+ delete s.data;
+ }
+
+ // Add anti-cache in url if needed
+ if ( s.cache === false ) {
+ s.url = rts.test( cacheURL ) ?
+
+ // If there is already a '_' parameter, set its value
+ cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :
+
+ // Otherwise add one to the end
+ cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
+ }
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ if ( jQuery.lastModified[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+ }
+ if ( jQuery.etag[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+ }
+ }
+
+ // Set the correct header, if data is being sent
+ if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+ jqXHR.setRequestHeader( "Content-Type", s.contentType );
+ }
+
+ // Set the Accepts header for the server, depending on the dataType
+ jqXHR.setRequestHeader(
+ "Accept",
+ s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+ s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+ s.accepts[ "*" ]
+ );
+
+ // Check for headers option
+ for ( i in s.headers ) {
+ jqXHR.setRequestHeader( i, s.headers[ i ] );
+ }
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+ // Abort if not done already and return
+ return jqXHR.abort();
+ }
+
+ // aborting is no longer a cancellation
+ strAbort = "abort";
+
+ // Install callbacks on deferreds
+ for ( i in { success: 1, error: 1, complete: 1 } ) {
+ jqXHR[ i ]( s[ i ] );
+ }
+
+ // Get transport
+ transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+ // If no transport, we auto-abort
+ if ( !transport ) {
+ done( -1, "No Transport" );
+ } else {
+ jqXHR.readyState = 1;
+
+ // Send global event
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+ }
+ // Timeout
+ if ( s.async && s.timeout > 0 ) {
+ timeoutTimer = setTimeout(function() {
+ jqXHR.abort("timeout");
+ }, s.timeout );
+ }
+
+ try {
+ state = 1;
+ transport.send( requestHeaders, done );
+ } catch ( e ) {
+ // Propagate exception as error if not done
+ if ( state < 2 ) {
+ done( -1, e );
+ // Simply rethrow otherwise
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ // Callback for when everything is done
+ function done( status, nativeStatusText, responses, headers ) {
+ var isSuccess, success, error, response, modified,
+ statusText = nativeStatusText;
+
+ // Called once
+ if ( state === 2 ) {
+ return;
+ }
+
+ // State is "done" now
+ state = 2;
+
+ // Clear timeout if it exists
+ if ( timeoutTimer ) {
+ clearTimeout( timeoutTimer );
+ }
+
+ // Dereference transport for early garbage collection
+ // (no matter how long the jqXHR object will be used)
+ transport = undefined;
+
+ // Cache response headers
+ responseHeadersString = headers || "";
+
+ // Set readyState
+ jqXHR.readyState = status > 0 ? 4 : 0;
+
+ // Get response data
+ if ( responses ) {
+ response = ajaxHandleResponses( s, jqXHR, responses );
+ }
+
+ // If successful, handle type chaining
+ if ( status >= 200 && status < 300 || status === 304 ) {
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ modified = jqXHR.getResponseHeader("Last-Modified");
+ if ( modified ) {
+ jQuery.lastModified[ cacheURL ] = modified;
+ }
+ modified = jqXHR.getResponseHeader("etag");
+ if ( modified ) {
+ jQuery.etag[ cacheURL ] = modified;
+ }
+ }
+
+ // if no content
+ if ( status === 204 ) {
+ isSuccess = true;
+ statusText = "nocontent";
+
+ // if not modified
+ } else if ( status === 304 ) {
+ isSuccess = true;
+ statusText = "notmodified";
+
+ // If we have data, let's convert it
+ } else {
+ isSuccess = ajaxConvert( s, response );
+ statusText = isSuccess.state;
+ success = isSuccess.data;
+ error = isSuccess.error;
+ isSuccess = !error;
+ }
+ } else {
+ // We extract error from statusText
+ // then normalize statusText and status for non-aborts
+ error = statusText;
+ if ( status || !statusText ) {
+ statusText = "error";
+ if ( status < 0 ) {
+ status = 0;
+ }
+ }
+ }
+
+ // Set data for the fake xhr object
+ jqXHR.status = status;
+ jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+ // Success/Error
+ if ( isSuccess ) {
+ deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+ } else {
+ deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+ }
+
+ // Status-dependent callbacks
+ jqXHR.statusCode( statusCode );
+ statusCode = undefined;
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+ [ jqXHR, s, isSuccess ? success : error ] );
+ }
+
+ // Complete
+ completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+ // Handle the global AJAX counter
+ if ( !( --jQuery.active ) ) {
+ jQuery.event.trigger("ajaxStop");
+ }
+ }
+ }
+
+ return jqXHR;
+ },
+
+ getScript: function( url, callback ) {
+ return jQuery.get( url, undefined, callback, "script" );
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get( url, data, callback, "json" );
+ }
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+ var firstDataType, ct, finalDataType, type,
+ contents = s.contents,
+ dataTypes = s.dataTypes,
+ responseFields = s.responseFields;
+
+ // Fill responseXXX fields
+ for ( type in responseFields ) {
+ if ( type in responses ) {
+ jqXHR[ responseFields[type] ] = responses[ type ];
+ }
+ }
+
+ // Remove auto dataType and get content-type in the process
+ while( dataTypes[ 0 ] === "*" ) {
+ dataTypes.shift();
+ if ( ct === undefined ) {
+ ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
+ }
+ }
+
+ // Check if we're dealing with a known content-type
+ if ( ct ) {
+ for ( type in contents ) {
+ if ( contents[ type ] && contents[ type ].test( ct ) ) {
+ dataTypes.unshift( type );
+ break;
+ }
+ }
+ }
+
+ // Check to see if we have a response for the expected dataType
+ if ( dataTypes[ 0 ] in responses ) {
+ finalDataType = dataTypes[ 0 ];
+ } else {
+ // Try convertible dataTypes
+ for ( type in responses ) {
+ if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+ finalDataType = type;
+ break;
+ }
+ if ( !firstDataType ) {
+ firstDataType = type;
+ }
+ }
+ // Or just use first one
+ finalDataType = finalDataType || firstDataType;
+ }
+
+ // If we found a dataType
+ // We add the dataType to the list if needed
+ // and return the corresponding response
+ if ( finalDataType ) {
+ if ( finalDataType !== dataTypes[ 0 ] ) {
+ dataTypes.unshift( finalDataType );
+ }
+ return responses[ finalDataType ];
+ }
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+ var conv2, current, conv, tmp,
+ converters = {},
+ i = 0,
+ // Work with a copy of dataTypes in case we need to modify it for conversion
+ dataTypes = s.dataTypes.slice(),
+ prev = dataTypes[ 0 ];
+
+ // Apply the dataFilter if provided
+ if ( s.dataFilter ) {
+ response = s.dataFilter( response, s.dataType );
+ }
+
+ // Create converters map with lowercased keys
+ if ( dataTypes[ 1 ] ) {
+ for ( conv in s.converters ) {
+ converters[ conv.toLowerCase() ] = s.converters[ conv ];
+ }
+ }
+
+ // Convert to each sequential dataType, tolerating list modification
+ for ( ; (current = dataTypes[++i]); ) {
+
+ // There's only work to do if current dataType is non-auto
+ if ( current !== "*" ) {
+
+ // Convert response if prev dataType is non-auto and differs from current
+ if ( prev !== "*" && prev !== current ) {
+
+ // Seek a direct converter
+ conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+ // If none found, seek a pair
+ if ( !conv ) {
+ for ( conv2 in converters ) {
+
+ // If conv2 outputs current
+ tmp = conv2.split(" ");
+ if ( tmp[ 1 ] === current ) {
+
+ // If prev can be converted to accepted input
+ conv = converters[ prev + " " + tmp[ 0 ] ] ||
+ converters[ "* " + tmp[ 0 ] ];
+ if ( conv ) {
+ // Condense equivalence converters
+ if ( conv === true ) {
+ conv = converters[ conv2 ];
+
+ // Otherwise, insert the intermediate dataType
+ } else if ( converters[ conv2 ] !== true ) {
+ current = tmp[ 0 ];
+ dataTypes.splice( i--, 0, current );
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Apply converter (if not an equivalence)
+ if ( conv !== true ) {
+
+ // Unless errors are allowed to bubble, catch and return them
+ if ( conv && s["throws"] ) {
+ response = conv( response );
+ } else {
+ try {
+ response = conv( response );
+ } catch ( e ) {
+ return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+ }
+ }
+ }
+ }
+
+ // Update prev for next iteration
+ prev = current;
+ }
+ }
+
+ return { state: "success", data: response };
+}
+// Install script dataType
+jQuery.ajaxSetup({
+ accepts: {
+ script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+ },
+ contents: {
+ script: /(?:java|ecma)script/
+ },
+ converters: {
+ "text script": function( text ) {
+ jQuery.globalEval( text );
+ return text;
+ }
+ }
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+ if ( s.cache === undefined ) {
+ s.cache = false;
+ }
+ if ( s.crossDomain ) {
+ s.type = "GET";
+ s.global = false;
+ }
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+ // This transport only deals with cross domain requests
+ if ( s.crossDomain ) {
+
+ var script,
+ head = document.head || jQuery("head")[0] || document.documentElement;
+
+ return {
+
+ send: function( _, callback ) {
+
+ script = document.createElement("script");
+
+ script.async = true;
+
+ if ( s.scriptCharset ) {
+ script.charset = s.scriptCharset;
+ }
+
+ script.src = s.url;
+
+ // Attach handlers for all browsers
+ script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+ if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+ // Handle memory leak in IE
+ script.onload = script.onreadystatechange = null;
+
+ // Remove the script
+ if ( script.parentNode ) {
+ script.parentNode.removeChild( script );
+ }
+
+ // Dereference the script
+ script = null;
+
+ // Callback if not abort
+ if ( !isAbort ) {
+ callback( 200, "success" );
+ }
+ }
+ };
+
+ // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
+ // Use native DOM manipulation to avoid our domManip AJAX trickery
+ head.insertBefore( script, head.firstChild );
+ },
+
+ abort: function() {
+ if ( script ) {
+ script.onload( undefined, true );
+ }
+ }
+ };
+ }
+});
+var oldCallbacks = [],
+ rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+ jsonp: "callback",
+ jsonpCallback: function() {
+ var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) );
+ this[ callback ] = true;
+ return callback;
+ }
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+ var callbackName, overwritten, responseContainer,
+ jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+ "url" :
+ typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
+ );
+
+ // Handle iff the expected data type is "jsonp" or we have a parameter to set
+ if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+ // Get callback name, remembering preexisting value associated with it
+ callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+ s.jsonpCallback() :
+ s.jsonpCallback;
+
+ // Insert callback into url or form data
+ if ( jsonProp ) {
+ s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+ } else if ( s.jsonp !== false ) {
+ s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+ }
+
+ // Use data converter to retrieve json after script execution
+ s.converters["script json"] = function() {
+ if ( !responseContainer ) {
+ jQuery.error( callbackName + " was not called" );
+ }
+ return responseContainer[ 0 ];
+ };
+
+ // force json dataType
+ s.dataTypes[ 0 ] = "json";
+
+ // Install callback
+ overwritten = window[ callbackName ];
+ window[ callbackName ] = function() {
+ responseContainer = arguments;
+ };
+
+ // Clean-up function (fires after converters)
+ jqXHR.always(function() {
+ // Restore preexisting value
+ window[ callbackName ] = overwritten;
+
+ // Save back as free
+ if ( s[ callbackName ] ) {
+ // make sure that re-using the options doesn't screw things around
+ s.jsonpCallback = originalSettings.jsonpCallback;
+
+ // save the callback name for future use
+ oldCallbacks.push( callbackName );
+ }
+
+ // Call if it was a function and we have a response
+ if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+ overwritten( responseContainer[ 0 ] );
+ }
+
+ responseContainer = overwritten = undefined;
+ });
+
+ // Delegate to script
+ return "script";
+ }
+});
+var xhrCallbacks, xhrSupported,
+ xhrId = 0,
+ // #5280: Internet Explorer will keep connections alive if we don't abort on unload
+ xhrOnUnloadAbort = window.ActiveXObject && function() {
+ // Abort all pending requests
+ var key;
+ for ( key in xhrCallbacks ) {
+ xhrCallbacks[ key ]( undefined, true );
+ }
+ };
+
+// Functions to create xhrs
+function createStandardXHR() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch( e ) {}
+}
+
+function createActiveXHR() {
+ try {
+ return new window.ActiveXObject("Microsoft.XMLHTTP");
+ } catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+ /* Microsoft failed to properly
+ * implement the XMLHttpRequest in IE7 (can't request local files),
+ * so we use the ActiveXObject when it is available
+ * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+ * we need a fallback.
+ */
+ function() {
+ return !this.isLocal && createStandardXHR() || createActiveXHR();
+ } :
+ // For all other browsers, use the standard XMLHttpRequest object
+ createStandardXHR;
+
+// Determine support properties
+xhrSupported = jQuery.ajaxSettings.xhr();
+jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+xhrSupported = jQuery.support.ajax = !!xhrSupported;
+
+// Create transport if the browser can provide an xhr
+if ( xhrSupported ) {
+
+ jQuery.ajaxTransport(function( s ) {
+ // Cross domain only allowed if supported through XMLHttpRequest
+ if ( !s.crossDomain || jQuery.support.cors ) {
+
+ var callback;
+
+ return {
+ send: function( headers, complete ) {
+
+ // Get a new xhr
+ var handle, i,
+ xhr = s.xhr();
+
+ // Open the socket
+ // Passing null username, generates a login popup on Opera (#2865)
+ if ( s.username ) {
+ xhr.open( s.type, s.url, s.async, s.username, s.password );
+ } else {
+ xhr.open( s.type, s.url, s.async );
+ }
+
+ // Apply custom fields if provided
+ if ( s.xhrFields ) {
+ for ( i in s.xhrFields ) {
+ xhr[ i ] = s.xhrFields[ i ];
+ }
+ }
+
+ // Override mime type if needed
+ if ( s.mimeType && xhr.overrideMimeType ) {
+ xhr.overrideMimeType( s.mimeType );
+ }
+
+ // X-Requested-With header
+ // For cross-domain requests, seeing as conditions for a preflight are
+ // akin to a jigsaw puzzle, we simply never set it to be sure.
+ // (it can always be set on a per-request basis or even using ajaxSetup)
+ // For same-domain requests, won't change header if already provided.
+ if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+ headers["X-Requested-With"] = "XMLHttpRequest";
+ }
+
+ // Need an extra try/catch for cross domain requests in Firefox 3
+ try {
+ for ( i in headers ) {
+ xhr.setRequestHeader( i, headers[ i ] );
+ }
+ } catch( err ) {}
+
+ // Do send the request
+ // This may raise an exception which is actually
+ // handled in jQuery.ajax (so no try/catch here)
+ xhr.send( ( s.hasContent && s.data ) || null );
+
+ // Listener
+ callback = function( _, isAbort ) {
+ var status, responseHeaders, statusText, responses;
+
+ // Firefox throws exceptions when accessing properties
+ // of an xhr when a network error occurred
+ // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+ try {
+
+ // Was never called and is aborted or complete
+ if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+ // Only called once
+ callback = undefined;
+
+ // Do not keep as active anymore
+ if ( handle ) {
+ xhr.onreadystatechange = jQuery.noop;
+ if ( xhrOnUnloadAbort ) {
+ delete xhrCallbacks[ handle ];
+ }
+ }
+
+ // If it's an abort
+ if ( isAbort ) {
+ // Abort it manually if needed
+ if ( xhr.readyState !== 4 ) {
+ xhr.abort();
+ }
+ } else {
+ responses = {};
+ status = xhr.status;
+ responseHeaders = xhr.getAllResponseHeaders();
+
+ // When requesting binary data, IE6-9 will throw an exception
+ // on any attempt to access responseText (#11426)
+ if ( typeof xhr.responseText === "string" ) {
+ responses.text = xhr.responseText;
+ }
+
+ // Firefox throws an exception when accessing
+ // statusText for faulty cross-domain requests
+ try {
+ statusText = xhr.statusText;
+ } catch( e ) {
+ // We normalize with Webkit giving an empty statusText
+ statusText = "";
+ }
+
+ // Filter status for non standard behaviors
+
+ // If the request is local and we have data: assume a success
+ // (success with no data won't get notified, that's the best we
+ // can do given current implementations)
+ if ( !status && s.isLocal && !s.crossDomain ) {
+ status = responses.text ? 200 : 404;
+ // IE - #1450: sometimes returns 1223 when it should be 204
+ } else if ( status === 1223 ) {
+ status = 204;
+ }
+ }
+ }
+ } catch( firefoxAccessException ) {
+ if ( !isAbort ) {
+ complete( -1, firefoxAccessException );
+ }
+ }
+
+ // Call complete if needed
+ if ( responses ) {
+ complete( status, statusText, responses, responseHeaders );
+ }
+ };
+
+ if ( !s.async ) {
+ // if we're in sync mode we fire the callback
+ callback();
+ } else if ( xhr.readyState === 4 ) {
+ // (IE6 & IE7) if it's in cache and has been
+ // retrieved directly we need to fire the callback
+ setTimeout( callback );
+ } else {
+ handle = ++xhrId;
+ if ( xhrOnUnloadAbort ) {
+ // Create the active xhrs callbacks list if needed
+ // and attach the unload handler
+ if ( !xhrCallbacks ) {
+ xhrCallbacks = {};
+ jQuery( window ).unload( xhrOnUnloadAbort );
+ }
+ // Add to list of active xhrs callbacks
+ xhrCallbacks[ handle ] = callback;
+ }
+ xhr.onreadystatechange = callback;
+ }
+ },
+
+ abort: function() {
+ if ( callback ) {
+ callback( undefined, true );
+ }
+ }
+ };
+ }
+ });
+}
+var fxNow, timerId,
+ rfxtypes = /^(?:toggle|show|hide)$/,
+ rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+ rrun = /queueHooks$/,
+ animationPrefilters = [ defaultPrefilter ],
+ tweeners = {
+ "*": [function( prop, value ) {
+ var end, unit,
+ tween = this.createTween( prop, value ),
+ parts = rfxnum.exec( value ),
+ target = tween.cur(),
+ start = +target || 0,
+ scale = 1,
+ maxIterations = 20;
+
+ if ( parts ) {
+ end = +parts[2];
+ unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+
+ // We need to compute starting value
+ if ( unit !== "px" && start ) {
+ // Iteratively approximate from a nonzero starting point
+ // Prefer the current property, because this process will be trivial if it uses the same units
+ // Fallback to end or a simple constant
+ start = jQuery.css( tween.elem, prop, true ) || end || 1;
+
+ do {
+ // If previous iteration zeroed out, double until we get *something*
+ // Use a string for doubling factor so we don't accidentally see scale as unchanged below
+ scale = scale || ".5";
+
+ // Adjust and apply
+ start = start / scale;
+ jQuery.style( tween.elem, prop, start + unit );
+
+ // Update scale, tolerating zero or NaN from tween.cur()
+ // And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+ } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+ }
+
+ tween.unit = unit;
+ tween.start = start;
+ // If a +=/-= token was provided, we're doing a relative animation
+ tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end;
+ }
+ return tween;
+ }]
+ };
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+ setTimeout(function() {
+ fxNow = undefined;
+ });
+ return ( fxNow = jQuery.now() );
+}
+
+function createTweens( animation, props ) {
+ jQuery.each( props, function( prop, value ) {
+ var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+ index = 0,
+ length = collection.length;
+ for ( ; index < length; index++ ) {
+ if ( collection[ index ].call( animation, prop, value ) ) {
+
+ // we're done with this property
+ return;
+ }
+ }
+ });
+}
+
+function Animation( elem, properties, options ) {
+ var result,
+ stopped,
+ index = 0,
+ length = animationPrefilters.length,
+ deferred = jQuery.Deferred().always( function() {
+ // don't match elem in the :animated selector
+ delete tick.elem;
+ }),
+ tick = function() {
+ if ( stopped ) {
+ return false;
+ }
+ var currentTime = fxNow || createFxNow(),
+ remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+ // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
+ temp = remaining / animation.duration || 0,
+ percent = 1 - temp,
+ index = 0,
+ length = animation.tweens.length;
+
+ for ( ; index < length ; index++ ) {
+ animation.tweens[ index ].run( percent );
+ }
+
+ deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+ if ( percent < 1 && length ) {
+ return remaining;
+ } else {
+ deferred.resolveWith( elem, [ animation ] );
+ return false;
+ }
+ },
+ animation = deferred.promise({
+ elem: elem,
+ props: jQuery.extend( {}, properties ),
+ opts: jQuery.extend( true, { specialEasing: {} }, options ),
+ originalProperties: properties,
+ originalOptions: options,
+ startTime: fxNow || createFxNow(),
+ duration: options.duration,
+ tweens: [],
+ createTween: function( prop, end ) {
+ var tween = jQuery.Tween( elem, animation.opts, prop, end,
+ animation.opts.specialEasing[ prop ] || animation.opts.easing );
+ animation.tweens.push( tween );
+ return tween;
+ },
+ stop: function( gotoEnd ) {
+ var index = 0,
+ // if we are going to the end, we want to run all the tweens
+ // otherwise we skip this part
+ length = gotoEnd ? animation.tweens.length : 0;
+ if ( stopped ) {
+ return this;
+ }
+ stopped = true;
+ for ( ; index < length ; index++ ) {
+ animation.tweens[ index ].run( 1 );
+ }
+
+ // resolve when we played the last frame
+ // otherwise, reject
+ if ( gotoEnd ) {
+ deferred.resolveWith( elem, [ animation, gotoEnd ] );
+ } else {
+ deferred.rejectWith( elem, [ animation, gotoEnd ] );
+ }
+ return this;
+ }
+ }),
+ props = animation.props;
+
+ propFilter( props, animation.opts.specialEasing );
+
+ for ( ; index < length ; index++ ) {
+ result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+ if ( result ) {
+ return result;
+ }
+ }
+
+ createTweens( animation, props );
+
+ if ( jQuery.isFunction( animation.opts.start ) ) {
+ animation.opts.start.call( elem, animation );
+ }
+
+ jQuery.fx.timer(
+ jQuery.extend( tick, {
+ elem: elem,
+ anim: animation,
+ queue: animation.opts.queue
+ })
+ );
+
+ // attach callbacks from options
+ return animation.progress( animation.opts.progress )
+ .done( animation.opts.done, animation.opts.complete )
+ .fail( animation.opts.fail )
+ .always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+ var value, name, index, easing, hooks;
+
+ // camelCase, specialEasing and expand cssHook pass
+ for ( index in props ) {
+ name = jQuery.camelCase( index );
+ easing = specialEasing[ name ];
+ value = props[ index ];
+ if ( jQuery.isArray( value ) ) {
+ easing = value[ 1 ];
+ value = props[ index ] = value[ 0 ];
+ }
+
+ if ( index !== name ) {
+ props[ name ] = value;
+ delete props[ index ];
+ }
+
+ hooks = jQuery.cssHooks[ name ];
+ if ( hooks && "expand" in hooks ) {
+ value = hooks.expand( value );
+ delete props[ name ];
+
+ // not quite $.extend, this wont overwrite keys already present.
+ // also - reusing 'index' from above because we have the correct "name"
+ for ( index in value ) {
+ if ( !( index in props ) ) {
+ props[ index ] = value[ index ];
+ specialEasing[ index ] = easing;
+ }
+ }
+ } else {
+ specialEasing[ name ] = easing;
+ }
+ }
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+ tweener: function( props, callback ) {
+ if ( jQuery.isFunction( props ) ) {
+ callback = props;
+ props = [ "*" ];
+ } else {
+ props = props.split(" ");
+ }
+
+ var prop,
+ index = 0,
+ length = props.length;
+
+ for ( ; index < length ; index++ ) {
+ prop = props[ index ];
+ tweeners[ prop ] = tweeners[ prop ] || [];
+ tweeners[ prop ].unshift( callback );
+ }
+ },
+
+ prefilter: function( callback, prepend ) {
+ if ( prepend ) {
+ animationPrefilters.unshift( callback );
+ } else {
+ animationPrefilters.push( callback );
+ }
+ }
+});
+
+function defaultPrefilter( elem, props, opts ) {
+ /*jshint validthis:true */
+ var prop, index, length,
+ value, dataShow, toggle,
+ tween, hooks, oldfire,
+ anim = this,
+ style = elem.style,
+ orig = {},
+ handled = [],
+ hidden = elem.nodeType && isHidden( elem );
+
+ // handle queue: false promises
+ if ( !opts.queue ) {
+ hooks = jQuery._queueHooks( elem, "fx" );
+ if ( hooks.unqueued == null ) {
+ hooks.unqueued = 0;
+ oldfire = hooks.empty.fire;
+ hooks.empty.fire = function() {
+ if ( !hooks.unqueued ) {
+ oldfire();
+ }
+ };
+ }
+ hooks.unqueued++;
+
+ anim.always(function() {
+ // doing this makes sure that the complete handler will be called
+ // before this completes
+ anim.always(function() {
+ hooks.unqueued--;
+ if ( !jQuery.queue( elem, "fx" ).length ) {
+ hooks.empty.fire();
+ }
+ });
+ });
+ }
+
+ // height/width overflow pass
+ if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+ // Make sure that nothing sneaks out
+ // Record all 3 overflow attributes because IE does not
+ // change the overflow attribute when overflowX and
+ // overflowY are set to the same value
+ opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+ // Set display property to inline-block for height/width
+ // animations on inline elements that are having width/height animated
+ if ( jQuery.css( elem, "display" ) === "inline" &&
+ jQuery.css( elem, "float" ) === "none" ) {
+
+ // inline-level elements accept inline-block;
+ // block-level elements need to be inline with layout
+ if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+ style.display = "inline-block";
+
+ } else {
+ style.zoom = 1;
+ }
+ }
+ }
+
+ if ( opts.overflow ) {
+ style.overflow = "hidden";
+ if ( !jQuery.support.shrinkWrapBlocks ) {
+ anim.always(function() {
+ style.overflow = opts.overflow[ 0 ];
+ style.overflowX = opts.overflow[ 1 ];
+ style.overflowY = opts.overflow[ 2 ];
+ });
+ }
+ }
+
+
+ // show/hide pass
+ for ( index in props ) {
+ value = props[ index ];
+ if ( rfxtypes.exec( value ) ) {
+ delete props[ index ];
+ toggle = toggle || value === "toggle";
+ if ( value === ( hidden ? "hide" : "show" ) ) {
+ continue;
+ }
+ handled.push( index );
+ }
+ }
+
+ length = handled.length;
+ if ( length ) {
+ dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} );
+ if ( "hidden" in dataShow ) {
+ hidden = dataShow.hidden;
+ }
+
+ // store state if its toggle - enables .stop().toggle() to "reverse"
+ if ( toggle ) {
+ dataShow.hidden = !hidden;
+ }
+ if ( hidden ) {
+ jQuery( elem ).show();
+ } else {
+ anim.done(function() {
+ jQuery( elem ).hide();
+ });
+ }
+ anim.done(function() {
+ var prop;
+ jQuery._removeData( elem, "fxshow" );
+ for ( prop in orig ) {
+ jQuery.style( elem, prop, orig[ prop ] );
+ }
+ });
+ for ( index = 0 ; index < length ; index++ ) {
+ prop = handled[ index ];
+ tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 );
+ orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
+
+ if ( !( prop in dataShow ) ) {
+ dataShow[ prop ] = tween.start;
+ if ( hidden ) {
+ tween.end = tween.start;
+ tween.start = prop === "width" || prop === "height" ? 1 : 0;
+ }
+ }
+ }
+ }
+}
+
+function Tween( elem, options, prop, end, easing ) {
+ return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+ constructor: Tween,
+ init: function( elem, options, prop, end, easing, unit ) {
+ this.elem = elem;
+ this.prop = prop;
+ this.easing = easing || "swing";
+ this.options = options;
+ this.start = this.now = this.cur();
+ this.end = end;
+ this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+ },
+ cur: function() {
+ var hooks = Tween.propHooks[ this.prop ];
+
+ return hooks && hooks.get ?
+ hooks.get( this ) :
+ Tween.propHooks._default.get( this );
+ },
+ run: function( percent ) {
+ var eased,
+ hooks = Tween.propHooks[ this.prop ];
+
+ if ( this.options.duration ) {
+ this.pos = eased = jQuery.easing[ this.easing ](
+ percent, this.options.duration * percent, 0, 1, this.options.duration
+ );
+ } else {
+ this.pos = eased = percent;
+ }
+ this.now = ( this.end - this.start ) * eased + this.start;
+
+ if ( this.options.step ) {
+ this.options.step.call( this.elem, this.now, this );
+ }
+
+ if ( hooks && hooks.set ) {
+ hooks.set( this );
+ } else {
+ Tween.propHooks._default.set( this );
+ }
+ return this;
+ }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+ _default: {
+ get: function( tween ) {
+ var result;
+
+ if ( tween.elem[ tween.prop ] != null &&
+ (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+ return tween.elem[ tween.prop ];
+ }
+
+ // passing an empty string as a 3rd parameter to .css will automatically
+ // attempt a parseFloat and fallback to a string if the parse fails
+ // so, simple values such as "10px" are parsed to Float.
+ // complex values such as "rotate(1rad)" are returned as is.
+ result = jQuery.css( tween.elem, tween.prop, "" );
+ // Empty strings, null, undefined and "auto" are converted to 0.
+ return !result || result === "auto" ? 0 : result;
+ },
+ set: function( tween ) {
+ // use step hook for back compat - use cssHook if its there - use .style if its
+ // available and use plain properties where available
+ if ( jQuery.fx.step[ tween.prop ] ) {
+ jQuery.fx.step[ tween.prop ]( tween );
+ } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+ jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+ } else {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+ }
+};
+
+// Remove in 2.0 - this supports IE8's panic based approach
+// to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+ set: function( tween ) {
+ if ( tween.elem.nodeType && tween.elem.parentNode ) {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+ var cssFn = jQuery.fn[ name ];
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return speed == null || typeof speed === "boolean" ?
+ cssFn.apply( this, arguments ) :
+ this.animate( genFx( name, true ), speed, easing, callback );
+ };
+});
+
+jQuery.fn.extend({
+ fadeTo: function( speed, to, easing, callback ) {
+
+ // show any hidden elements after setting opacity to 0
+ return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+ // animate to the value specified
+ .end().animate({ opacity: to }, speed, easing, callback );
+ },
+ animate: function( prop, speed, easing, callback ) {
+ var empty = jQuery.isEmptyObject( prop ),
+ optall = jQuery.speed( speed, easing, callback ),
+ doAnimation = function() {
+ // Operate on a copy of prop so per-property easing won't be lost
+ var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+ doAnimation.finish = function() {
+ anim.stop( true );
+ };
+ // Empty animations, or finishing resolves immediately
+ if ( empty || jQuery._data( this, "finish" ) ) {
+ anim.stop( true );
+ }
+ };
+ doAnimation.finish = doAnimation;
+
+ return empty || optall.queue === false ?
+ this.each( doAnimation ) :
+ this.queue( optall.queue, doAnimation );
+ },
+ stop: function( type, clearQueue, gotoEnd ) {
+ var stopQueue = function( hooks ) {
+ var stop = hooks.stop;
+ delete hooks.stop;
+ stop( gotoEnd );
+ };
+
+ if ( typeof type !== "string" ) {
+ gotoEnd = clearQueue;
+ clearQueue = type;
+ type = undefined;
+ }
+ if ( clearQueue && type !== false ) {
+ this.queue( type || "fx", [] );
+ }
+
+ return this.each(function() {
+ var dequeue = true,
+ index = type != null && type + "queueHooks",
+ timers = jQuery.timers,
+ data = jQuery._data( this );
+
+ if ( index ) {
+ if ( data[ index ] && data[ index ].stop ) {
+ stopQueue( data[ index ] );
+ }
+ } else {
+ for ( index in data ) {
+ if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+ stopQueue( data[ index ] );
+ }
+ }
+ }
+
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+ timers[ index ].anim.stop( gotoEnd );
+ dequeue = false;
+ timers.splice( index, 1 );
+ }
+ }
+
+ // start the next in the queue if the last step wasn't forced
+ // timers currently will call their complete callbacks, which will dequeue
+ // but only if they were gotoEnd
+ if ( dequeue || !gotoEnd ) {
+ jQuery.dequeue( this, type );
+ }
+ });
+ },
+ finish: function( type ) {
+ if ( type !== false ) {
+ type = type || "fx";
+ }
+ return this.each(function() {
+ var index,
+ data = jQuery._data( this ),
+ queue = data[ type + "queue" ],
+ hooks = data[ type + "queueHooks" ],
+ timers = jQuery.timers,
+ length = queue ? queue.length : 0;
+
+ // enable finishing flag on private data
+ data.finish = true;
+
+ // empty the queue first
+ jQuery.queue( this, type, [] );
+
+ if ( hooks && hooks.cur && hooks.cur.finish ) {
+ hooks.cur.finish.call( this );
+ }
+
+ // look for any active animations, and finish them
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+ timers[ index ].anim.stop( true );
+ timers.splice( index, 1 );
+ }
+ }
+
+ // look for any animations in the old queue and finish them
+ for ( index = 0; index < length; index++ ) {
+ if ( queue[ index ] && queue[ index ].finish ) {
+ queue[ index ].finish.call( this );
+ }
+ }
+
+ // turn off finishing flag
+ delete data.finish;
+ });
+ }
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+ var which,
+ attrs = { height: type },
+ i = 0;
+
+ // if we include width, step value is 1 to do all cssExpand values,
+ // if we don't include width, step value is 2 to skip over Left and Right
+ includeWidth = includeWidth? 1 : 0;
+ for( ; i < 4 ; i += 2 - includeWidth ) {
+ which = cssExpand[ i ];
+ attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+ }
+
+ if ( includeWidth ) {
+ attrs.opacity = attrs.width = type;
+ }
+
+ return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+ slideDown: genFx("show"),
+ slideUp: genFx("hide"),
+ slideToggle: genFx("toggle"),
+ fadeIn: { opacity: "show" },
+ fadeOut: { opacity: "hide" },
+ fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return this.animate( props, speed, easing, callback );
+ };
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+ var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+ };
+
+ opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+ opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+ // normalize opt.queue - true/undefined/null -> "fx"
+ if ( opt.queue == null || opt.queue === true ) {
+ opt.queue = "fx";
+ }
+
+ // Queueing
+ opt.old = opt.complete;
+
+ opt.complete = function() {
+ if ( jQuery.isFunction( opt.old ) ) {
+ opt.old.call( this );
+ }
+
+ if ( opt.queue ) {
+ jQuery.dequeue( this, opt.queue );
+ }
+ };
+
+ return opt;
+};
+
+jQuery.easing = {
+ linear: function( p ) {
+ return p;
+ },
+ swing: function( p ) {
+ return 0.5 - Math.cos( p*Math.PI ) / 2;
+ }
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+ var timer,
+ timers = jQuery.timers,
+ i = 0;
+
+ fxNow = jQuery.now();
+
+ for ( ; i < timers.length; i++ ) {
+ timer = timers[ i ];
+ // Checks the timer has not already been removed
+ if ( !timer() && timers[ i ] === timer ) {
+ timers.splice( i--, 1 );
+ }
+ }
+
+ if ( !timers.length ) {
+ jQuery.fx.stop();
+ }
+ fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+ if ( timer() && jQuery.timers.push( timer ) ) {
+ jQuery.fx.start();
+ }
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.start = function() {
+ if ( !timerId ) {
+ timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+ }
+};
+
+jQuery.fx.stop = function() {
+ clearInterval( timerId );
+ timerId = null;
+};
+
+jQuery.fx.speeds = {
+ slow: 600,
+ fast: 200,
+ // Default speed
+ _default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.animated = function( elem ) {
+ return jQuery.grep(jQuery.timers, function( fn ) {
+ return elem === fn.elem;
+ }).length;
+ };
+}
+jQuery.fn.offset = function( options ) {
+ if ( arguments.length ) {
+ return options === undefined ?
+ this :
+ this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ var docElem, win,
+ box = { top: 0, left: 0 },
+ elem = this[ 0 ],
+ doc = elem && elem.ownerDocument;
+
+ if ( !doc ) {
+ return;
+ }
+
+ docElem = doc.documentElement;
+
+ // Make sure it's not a disconnected DOM node
+ if ( !jQuery.contains( docElem, elem ) ) {
+ return box;
+ }
+
+ // If we don't have gBCR, just use 0,0 rather than error
+ // BlackBerry 5, iOS 3 (original iPhone)
+ if ( typeof elem.getBoundingClientRect !== core_strundefined ) {
+ box = elem.getBoundingClientRect();
+ }
+ win = getWindow( doc );
+ return {
+ top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ),
+ left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
+ };
+};
+
+jQuery.offset = {
+
+ setOffset: function( elem, options, i ) {
+ var position = jQuery.css( elem, "position" );
+
+ // set position first, in-case top/left are set even on static elem
+ if ( position === "static" ) {
+ elem.style.position = "relative";
+ }
+
+ var curElem = jQuery( elem ),
+ curOffset = curElem.offset(),
+ curCSSTop = jQuery.css( elem, "top" ),
+ curCSSLeft = jQuery.css( elem, "left" ),
+ calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+ props = {}, curPosition = {}, curTop, curLeft;
+
+ // need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+ if ( calculatePosition ) {
+ curPosition = curElem.position();
+ curTop = curPosition.top;
+ curLeft = curPosition.left;
+ } else {
+ curTop = parseFloat( curCSSTop ) || 0;
+ curLeft = parseFloat( curCSSLeft ) || 0;
+ }
+
+ if ( jQuery.isFunction( options ) ) {
+ options = options.call( elem, i, curOffset );
+ }
+
+ if ( options.top != null ) {
+ props.top = ( options.top - curOffset.top ) + curTop;
+ }
+ if ( options.left != null ) {
+ props.left = ( options.left - curOffset.left ) + curLeft;
+ }
+
+ if ( "using" in options ) {
+ options.using.call( elem, props );
+ } else {
+ curElem.css( props );
+ }
+ }
+};
+
+
+jQuery.fn.extend({
+
+ position: function() {
+ if ( !this[ 0 ] ) {
+ return;
+ }
+
+ var offsetParent, offset,
+ parentOffset = { top: 0, left: 0 },
+ elem = this[ 0 ];
+
+ // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
+ if ( jQuery.css( elem, "position" ) === "fixed" ) {
+ // we assume that getBoundingClientRect is available when computed position is fixed
+ offset = elem.getBoundingClientRect();
+ } else {
+ // Get *real* offsetParent
+ offsetParent = this.offsetParent();
+
+ // Get correct offsets
+ offset = this.offset();
+ if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
+ parentOffset = offsetParent.offset();
+ }
+
+ // Add offsetParent borders
+ parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
+ parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
+ }
+
+ // Subtract parent offsets and element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ return {
+ top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+ left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)
+ };
+ },
+
+ offsetParent: function() {
+ return this.map(function() {
+ var offsetParent = this.offsetParent || document.documentElement;
+ while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent || document.documentElement;
+ });
+ }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+ var top = /Y/.test( prop );
+
+ jQuery.fn[ method ] = function( val ) {
+ return jQuery.access( this, function( elem, method, val ) {
+ var win = getWindow( elem );
+
+ if ( val === undefined ) {
+ return win ? (prop in win) ? win[ prop ] :
+ win.document.documentElement[ method ] :
+ elem[ method ];
+ }
+
+ if ( win ) {
+ win.scrollTo(
+ !top ? val : jQuery( win ).scrollLeft(),
+ top ? val : jQuery( win ).scrollTop()
+ );
+
+ } else {
+ elem[ method ] = val;
+ }
+ }, method, val, arguments.length, null );
+ };
+});
+
+function getWindow( elem ) {
+ return jQuery.isWindow( elem ) ?
+ elem :
+ elem.nodeType === 9 ?
+ elem.defaultView || elem.parentWindow :
+ false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+ jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+ // margin is only for outerHeight, outerWidth
+ jQuery.fn[ funcName ] = function( margin, value ) {
+ var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+ extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+ return jQuery.access( this, function( elem, type, value ) {
+ var doc;
+
+ if ( jQuery.isWindow( elem ) ) {
+ // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+ // isn't a whole lot we can do. See pull request at this URL for discussion:
+ // https://github.com/jquery/jquery/pull/764
+ return elem.document.documentElement[ "client" + name ];
+ }
+
+ // Get document width or height
+ if ( elem.nodeType === 9 ) {
+ doc = elem.documentElement;
+
+ // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+ // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+ return Math.max(
+ elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+ elem.body[ "offset" + name ], doc[ "offset" + name ],
+ doc[ "client" + name ]
+ );
+ }
+
+ return value === undefined ?
+ // Get width or height on the element, requesting but not forcing parseFloat
+ jQuery.css( elem, type, extra ) :
+
+ // Set width or height on the element
+ jQuery.style( elem, type, value, extra );
+ }, type, chainable ? margin : undefined, chainable, null );
+ };
+ });
+});
+// Limit scope pollution from any deprecated API
+// (function() {
+
+// })();
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+// Expose jQuery as an AMD module, but only for AMD loaders that
+// understand the issues with loading multiple versions of jQuery
+// in a page that all might call define(). The loader will indicate
+// they have special allowances for multiple jQuery versions by
+// specifying define.amd.jQuery = true. Register as a named module,
+// since jQuery can be concatenated with other files that may use define,
+// but not use a proper concatenation script that understands anonymous
+// AMD modules. A named AMD is safest and most robust way to register.
+// Lowercase jquery is used because AMD module names are derived from
+// file names, and jQuery is normally delivered in a lowercase file name.
+// Do this after creating the global so that if an AMD module wants to call
+// noConflict to hide this version of jQuery, it will work.
+if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
+ define( "jquery", [], function () { return jQuery; } );
+}
+
+})( window );
diff --git a/tipboard/static/js/tipboard.js b/tipboard/static/js/tipboard.js
new file mode 100644
index 0000000..3727608
--- /dev/null
+++ b/tipboard/static/js/tipboard.js
@@ -0,0 +1,783 @@
+/*jslint browser: true, devel: true, evil: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+function RendererFactory() { }
+RendererFactory.prototype.rendererName2renderObj = {
+ // core renderer
+ "axistickrenderer": $.jqplot.AxisTickRenderer,
+ "canvasgridrenderer": $.jqplot.CanvasGridRenderer,
+ "divtitlerenderer": $.jqplot.DivTitleRenderer,
+ "linearaxisrenderer": $.jqplot.LinearAxisRenderer,
+ "markerrenderer": $.jqplot.MarkerRenderer,
+ "shaperenderer": $.jqplot.ShapeRenderer,
+ "shadowrenderer": $.jqplot.ShadowRenderer,
+ "linerenderer": $.jqplot.LineRenderer,
+ "axislabelrenderer": $.jqplot.AxisLabelRenderer,
+ // plugins
+ "barrenderer": $.jqplot.BarRenderer,
+ "blockrenderer": $.jqplot.BlockRenderer,
+ "bubblerenderer": $.jqplot.BubbleRenderer,
+ "canvasaxislabelrenderer": $.jqplot.CanvasAxisLabelRenderer,
+ "canvasaxistickrenderer": $.jqplot.CanvasAxisTickRenderer,
+ "categoryaxisrenderer": $.jqplot.CategoryAxisRenderer,
+ "dateaxisrenderer": $.jqplot.DateAxisRenderer,
+ "donutrenderer": $.jqplot.DonutRenderer,
+ "enhancedlegendrenderer": $.jqplot.EnhancedLegendRenderer,
+ "funnelrenderer": $.jqplot.FunnelRenderer,
+ "logaxisrenderer": $.jqplot.LogAxisRenderer,
+ "mekkoaxisrenderer": $.jqplot.MekkoAxisRenderer,
+ "mekkorenderer": $.jqplot.MekkoRenderer,
+ "metergaugerenderer": $.jqplot.MeterGaugeRenderer,
+ "ohlcrenderer": $.jqplot.OHLCRenderer,
+ "pierenderer": $.jqplot.PieRenderer,
+ // others
+ "canvastextrenderer": $.jqplot.CanvasTextRenderer,
+ "donutlegendrenderer": $.jqplot.DonutLegendRenderer,
+ "pyramidaxisrenderer": $.jqplot.PyramidAxisRenderer,
+ "pyramidgridrenderer": $.jqplot.PyramidGridRenderer,
+ "pyramidrenderer": $.jqplot.PyramidRenderer
+};
+RendererFactory.prototype.createRenderer = function (rendererName) {
+ var lower = rendererName.toLowerCase();
+ var rendererClass = RendererFactory.prototype.rendererName2renderObj[lower];
+ if (rendererClass === undefined) {
+ throw new RendererFactory.prototype.UnknownRenderer(rendererName);
+ }
+ return rendererClass;
+};
+var UnknownRenderer = function (rendererName) {
+ this.name = "UnknownRederer";
+ this.message = "Renderer: '" + rendererName + "' not found";
+};
+UnknownRenderer.prototype = new Error();
+UnknownRenderer.prototype.constructor = UnknownRenderer;
+RendererFactory.prototype.UnknownRenderer = UnknownRenderer;
+
+function RenderersSwapper() { }
+RenderersSwapper.prototype.insertRendererClasses = function(obj, keys) {
+ $.each(keys, function (idx, key) {
+ var rendererName, rendererClass;
+ rendererName = null;
+ try {
+ rendererName = obj[key];
+ } catch (err) {
+ console.log('skipped', key);
+ }
+ if (typeof rendererName === "string") {
+ rendererClass = RendererFactory.prototype.createRenderer(rendererName);
+ obj[key] = rendererClass;
+ }
+ });
+ return obj;
+};
+
+RenderersSwapper.prototype.swap = function (config) {
+ /*
+ jqplot config gets renderers under these paths:
+ simple:
+ config.legend.renderer
+ config.title.renderer
+ config.grid.renderer
+ config.axesDefaults.<axisRenderer>
+ config.seriesDefaults.<seriesRenderer>
+ nested:
+ config.axes.<axis>.<axisRenderer>
+ <axis> can be these keys:
+ xaxis, x2axis, yaxis, y2axis, .. , y9axis
+ config.series.<series>.<seriesRenderer>
+ <series>: array
+
+ <axisRenderer> can be these keys:
+ - tickRenderer, labelRenderer, renderer
+ <seriesRenderer> can be these keys:
+ - markerRenderer, renderer
+ */
+ var simple, complex;
+ simple = {
+ 'legend': ['renderer'],
+ 'title': ['renderer'],
+ 'grid': ['renderer'],
+ 'axesDefaults': ["tickRenderer", "labelRenderer", "renderer"],
+ 'seriesDefaults': ["markerRenderer", "renderer"]
+ };
+ $.each(simple, function (key, rendererTypes) {
+ if (config[key]) {
+ config[key] = RenderersSwapper.prototype.insertRendererClasses(
+ config[key], rendererTypes
+ );
+ }
+ });
+ complex = {
+ 'axes': simple.axesDefaults,
+ 'series': simple.seriesDefaults
+ };
+ $.each(complex, function (key, rendererTypes) {
+ if (config[key]) {
+ $.each(config[key], function (subkey, values) {
+ config[key][subkey] = RenderersSwapper.prototype.insertRendererClasses(
+ config[key][subkey], rendererTypes
+ );
+ });
+ }
+ });
+ return config;
+};
+
+function getFlipTime(node) {
+ // TODO: make it Tipboard.Dashboard member
+ var classStr = $(node).attr('class');
+ var flipTime = 20000;
+ $.each(classStr.split(' '), function(idx, val) {
+ var groups = /flip-time-(\d+)/.exec(val);
+ if (Boolean(groups) && groups.length > 1) {
+ flipTime = groups[1];
+ flipTime = parseInt(flipTime, 10) * 1000;
+ return false;
+ }
+ });
+ return flipTime;
+}
+
+function verboseLog(level, msg) {
+ /*
+ This function is: DEPRECATED.
+
+ Available levels:
+ 1: silent
+ 2: debug
+ */
+ var threshold = 1;
+ if (level > threshold) {
+ console.log(msg);
+ }
+}
+
+var renderersSwapper = new RenderersSwapper();
+
+(function($) {
+ 'use strict';
+
+ if (!window.console) {
+ window.console = {
+ log: function() {}
+ };
+ }
+
+ window.Tipboard = {};
+
+ Tipboard.DisplayUtils = {
+
+ pallet: {
+ 'black': '#000000',
+ 'white': '#FFFFFF',
+ 'tile_background': '#25282d',
+ 'red': '#DC5945',
+ 'yellow': '#FF9618',
+ 'green': '#94C140',
+ 'blue': '#12B0C5',
+ 'violet': '#9C4274',
+ 'orange': '#EC663C',
+ 'naval': '#54C5C0',
+ },
+
+ getPalette: function(colors) {
+ // DEPRECATED, use paletteAsList instead
+ var palette = [];
+ for (var name in colors) {
+ if (['black', 'white', 'tile_background'].indexOf(name) < 0) {
+ palette.push(colors[name]);
+ }
+ }
+ return palette;
+ },
+
+ paletteAsList: function() {
+ var paletteList = [];
+ $.each(Tipboard.DisplayUtils.palette, function(name, value) {
+ if (['black', 'white', 'tile_background'].indexOf(name) < 0) {
+ paletteList.push(value);
+ }
+ });
+ return paletteList;
+ },
+
+ replaceFromPalette: function(color) {
+ $.each(this.palette, function(colorName, colorValue) {
+ if (color === colorName) {
+ color = colorValue;
+ return false;
+ }
+ });
+ return color;
+ },
+
+ paletteAsSeriesColors: function() {
+ var seriesColors = [];
+ $.each(
+ Tipboard.DisplayUtils.getPalette(Tipboard.DisplayUtils.palette),
+ function(idx, color) {
+ seriesColors.push({'color': color});
+ }
+ );
+ return seriesColors;
+ },
+
+ expandLastChild: function(container) {
+ /*
+ Autogrow height of node *body* in *container* node respecting of *header*
+ height.
+
+ container:
+ item1
+ [..]
+ itemLast
+ */
+ var siblingsHeight = 0;
+ var len = $(container).children().length;
+ $.each($(container).children(), function(index, element) {
+ if (index == len - 1) {
+ return false;
+ }
+ siblingsHeight += $(element).outerHeight(true);
+ });
+ if (len - 1 < 0) {
+ console.log('expanding stopped - container has no elements');
+ return false;
+ }
+ var body = $(container).children()[len - 1];
+ var border = $(body).outerHeight(true) - $(body).height();
+ var bodyHeight = $(container).height() - siblingsHeight - border;
+ $(body).height(bodyHeight);
+ },
+
+ showTileData: function (tile) {
+ $.each(['.tile-content'], function(idx, klass) {
+ var node = $(tile).find(klass);
+ if (node.length > 1) {
+ $(node[1]).remove();
+ $(node[0]).show();
+ }
+ });
+ },
+ showExcMsg: function (tile, msg) {
+ $.each(['.tile-content'], function(idx, klass) {
+ var nodes = $(tile).find(klass);
+ if (nodes.length === 1) {
+ var cloned = $(nodes).clone();
+ $(nodes).hide();
+ $(cloned).insertAfter(nodes);
+ $(cloned).addClass('exception-message');
+ $(cloned).show();
+ } else {
+ $(nodes[0]).hide();
+ $(nodes[1]).show();
+ }
+ nodes = $(tile).find('.tile-content');
+ $(nodes[1]).html(msg);
+ });
+ },
+
+ createGraph: function (tileId, plotData, config) {
+ var containerId, plot;
+ containerId = tileId + '-chart';
+ plot = $.jqplot(containerId, plotData, config);
+ Tipboard.Dashboard.chartsIds[tileId] = plot;
+ },
+
+ rescaleTile: function (tile) {
+ /* <tile>: DOM element representing tile */
+ Tipboard.DisplayUtils.expandLastChild(tile);
+ Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]);
+ },
+
+ applyHighlighterConfig: function (highlighterNode, color, fading) {
+ if (typeof color === "undefined" && typeof fading === "undefined") {
+ // clear settings
+ $(highlighterNode).css('background-color', 'rgba(0, 0, 0, 0)');
+ highlighterNode.removeClass('fading-background-color');
+ } else {
+ if (typeof color === "undefined") {
+ color = $(highlighterNode).css('background-color', color);
+ }
+ color = Tipboard.DisplayUtils.replaceFromPalette(color);
+ $(highlighterNode).css('background-color', color);
+
+ if (fading === true) {
+ highlighterNode.addClass('fading-background-color');
+ }
+ if (fading === false) {
+ highlighterNode.removeClass('fading-background-color');
+ }
+ }
+ highlighterNode.before(highlighterNode.clone(true)).remove();
+ },
+
+ highlightAsOk: function(element) {
+ $(element).addClass('highlighted-green');
+ $(element).removeClass('highlighted-red');
+ },
+
+ highlightAsFail: function(element) {
+ $(element).removeClass('highlighted-green');
+ $(element).addClass('highlighted-red');
+ },
+
+ arrowUp: function(element) {
+ $(element).addClass('arrow-up');
+ $(element).removeClass('arrow-down');
+ },
+
+ arrowDown: function(element) {
+ $(element).removeClass('arrow-up');
+ $(element).addClass('arrow-down');
+ },
+
+ changeFontSize: function() {
+ var windowHeight = $(window).height();
+ if (windowHeight > $(window).width()) {
+ windowHeight = $(window).width();
+ }
+ if (windowHeight <= 768) {
+ return;
+ }
+ var newFontSize = Math.max(Math.floor(12 * windowHeight / 768) + 1, 12);
+ $('body').css({
+ 'font-size': newFontSize
+ });
+ console.log('Changed font size to ' + newFontSize + '.');
+ },
+
+ makeTilesMargins: function() {
+ console.log('Updating tile sizes.');
+ $('.tile').each(function() {
+ var tileWidth = $(this).parent().width() - 20;
+ var tileHeight = $(this).parent().height() - 20;
+ $(this).css({
+ 'width': tileWidth,
+ 'height': tileHeight,
+ 'top': 10,
+ 'left': 10
+ });
+ });
+ },
+
+ changeChartsSize: function() {
+ console.log('Updating chart sizes.');
+ $('.chart').each(function() {
+ var tileHeaderHeight = $(this).parents('.tile').find('.tile-header').height();
+ var tileContentHeight = 32;
+
+ function increaseContentHeight() {
+ if ($(this).hasClass('small-padding')) {
+ tileContentHeight += $(this).height() * 1.5;
+ } else {
+ tileContentHeight += $(this).height();
+ }
+ }
+ if (!$(this).hasClass('chart-only')) {
+ $(this).parents('.tile').find('.tile-content .result').each(increaseContentHeight);
+ $(this).parents('.tile').find('.tile-content h2').each(increaseContentHeight);
+ }
+ var chartHeight = $(this).parents('.tile').height() - tileHeaderHeight - tileContentHeight;
+ $(this).css({
+ height: chartHeight
+ });
+ // replot
+ var tileId = $(this).parents('div.tile').attr('id');
+ var plot = Tipboard.Dashboard.chartsIds[tileId];
+ if (Boolean(plot)) {
+ plot.replot( { resetAxes: true } );
+ }
+ });
+ },
+
+ resizeWindowEvent: function() {
+ this.makeTilesMargins();
+ this.changeFontSize();
+ this.changeChartsSize();
+ },
+
+ flipFlippablesIn: function(container) {
+ /*
+ * pass class *flippedforward* to next node with class
+ * *flippable* within *container*, if last element then wrap it
+ * and pass *flippedforward* to the first element with class
+ * *flippable* within the *container*
+ */
+ var nextFlipIdx;
+ var containerFlips = $(container).find('.flippable');
+ $(containerFlips).each(function(index, tile) {
+ if ($(tile).hasClass("flippedforward")) {
+ nextFlipIdx = (index + 1) % containerFlips.length;
+ $(tile).removeClass("flippedforward");
+ return false; // break
+ }
+ });
+ if (typeof nextFlipIdx !== undefined) {
+ var tileToFlip = containerFlips[nextFlipIdx];
+ $(tileToFlip).addClass("flippedforward");
+ }
+ },
+
+ reloadPage: function() {
+ var http = new XMLHttpRequest();
+ http.open('HEAD', window.location.href);
+ http.onreadystatechange = function() {
+ if (this.readyState === this.DONE && this.status === 200) {
+ window.location.reload();
+ }
+ };
+ http.send();
+ }
+ };
+
+ Tipboard.TileDisplayDecorator = {
+ fitTile: function(tile) {
+ Tipboard.DisplayUtils.expandLastChild(tile);
+ },
+ highlightResult: function(tile) {
+ $(tile).find('.highlighted-result [data-source-key]').each(function() {
+ var elementValue = parseInt($(this).html(), 10);
+ var parentContainer = $(this).parent('.highlighted-result');
+ var threshold = $(this).attr('data-threshold');
+ var isOK = false;
+ var thresholdFloat = parseFloat(threshold, 10);
+ if (!isNaN(thresholdFloat)) {
+ isOK = elementValue > thresholdFloat;
+ } else {
+ try {
+ isOK = eval(threshold);
+ } catch (TypeError) {
+ isOK = elementValue > 50;
+ }
+ }
+ if (isOK) {
+ Tipboard.DisplayUtils.highlightAsOk(parentContainer);
+ } else {
+ Tipboard.DisplayUtils.highlightAsFail(parentContainer);
+ }
+ });
+ },
+
+ drawArrow: function(tile) {
+ $(tile).find('.with-arrow-result [data-source-key]').each(function() {
+ var elementValue = parseInt($(this).html(), 10);
+ var parentContainer = $(this).parent('.with-arrow-result');
+ var arrowContainer = $(parentContainer).find('span.arrow');
+ if (elementValue < 0) {
+ Tipboard.DisplayUtils.arrowDown(arrowContainer);
+ } else {
+ Tipboard.DisplayUtils.arrowUp(arrowContainer);
+ }
+ });
+ },
+
+ runAllDecorators: function(tile) {
+ var item = void 0;
+ for (item in Tipboard.TileDisplayDecorator) {
+ if (typeof Tipboard.TileDisplayDecorator[item] === "function" && item !== "runAllDecorators") {
+ Tipboard.TileDisplayDecorator[item](tile);
+ }
+ }
+ }
+ };
+
+ Tipboard.WebSocketManager = {
+ onOpen: function(evt) {
+ console.log("Web socket opened.");
+ this.websocket.send("update");
+ },
+
+ onClose: function(evt) {
+ console.log("Web socket closed. Restarting...");
+ this.websocket = void 0;
+ setTimeout(Tipboard.WebSocketManager.init.bind(this), 1000);
+ },
+
+ onMessage: function(evt) {
+ var tileData = JSON.parse(evt.data);
+ console.log("Web socket received data: ", tileData);
+ // FIXME: pass colors in more suitable place
+ Tipboard.DisplayUtils.palette = $.extend(
+ true, this.palette, tileData.tipboard.color
+ );
+ var tileId = Tipboard.Dashboard.escapeId(tileData.id);
+ Tipboard.Dashboard.updateTile(
+ tileId,
+ tileData.tile_template,
+ tileData.data,
+ tileData.meta,
+ tileData.tipboard,
+ tileData.modified
+ );
+ },
+
+ onError: function(evt) {
+ console.log("WebSocket error: " + evt.data);
+ },
+
+ init: function() {
+ if ((this.websocket !== void 0) && !(this.websocket.readyState === WebSocket.CLOSED || this.websocket.readyState === WebSocket.CLOSING)) {
+ console.log("Closing outdated Web socket.");
+ this.websocket.close();
+ return; // the rest will be handled in onClose()
+ }
+ console.log("Initializing a new Web socket manager.");
+ this.websocket = new WebSocket(
+ "ws://" + window.location.host + "/communication/websocket"
+ );
+ this.websocket.onopen = function(evt) {
+ Tipboard.WebSocketManager.onOpen(evt);
+ };
+ this.websocket.onclose = function(evt) {
+ Tipboard.WebSocketManager.onClose(evt);
+ };
+ this.websocket.onmessage = function(evt) {
+ Tipboard.WebSocketManager.onMessage(evt);
+ };
+ this.websocket.onerror = function(evt) {
+ Tipboard.WebSocketManager.onError(evt);
+ };
+ }
+ };
+
+ Tipboard.Dashboard = {
+ webSocketResetInterval: 900000,
+ flipIds: [],
+ updateFunctions: {},
+ chartsIds: {},
+ };
+
+ // exceptions definition
+ var TimeoutBadFormat = function (timeout) {
+ this.name = "TimeoutBadFormat";
+ this.message = "Timeout consists non-digits: '" + timeout + "'";
+ };
+ TimeoutBadFormat.prototype = new Error();
+ TimeoutBadFormat.prototype.constructor = TimeoutBadFormat;
+ Tipboard.Dashboard.TimeoutBadFormat = TimeoutBadFormat;
+
+ var UnknownUpdateFunction = function (tileType) {
+ this.name = "UnknownUpdateFunction";
+ this.message = "Couldn't find update function for: " + tileType;
+ };
+ UnknownUpdateFunction.prototype = new Error();
+ UnknownUpdateFunction.prototype.constructor = UnknownUpdateFunction;
+ Tipboard.Dashboard.UnknownUpdateFunction = UnknownUpdateFunction;
+
+ Tipboard.Dashboard.id2node = function(id) {
+ var tile = $('#' + id)[0];
+ return tile;
+ };
+
+ Tipboard.Dashboard.tile2id = function(tileNode) {
+ return $(tileNode).attr('id');
+ };
+
+ Tipboard.Dashboard.escapeId = function(id) {
+ /*
+ the Tipboard application allows user to use eg. '.' in tiles' ids
+ jquery requires such chars to be escaped
+ */
+ // XXX: backslash MUST BE FIRST, otherwise this convertions is
+ // broken (escaping chars which meant to be escapers)
+ var charsToEscape = "\\!\"#$%&'()*+,./:;<=>?@[]^`{|}~";
+ for(var i=0; i<charsToEscape.length; i++) {
+ var _char = charsToEscape[i];
+ id = id.replace(_char, '\\' + _char);
+ }
+ return id;
+ };
+
+ Tipboard.Dashboard.setDataByKeys = function(
+ tileId, dataToPut, keysToUse
+ ) {
+ /*
+ *keysToUse*: list of keys, or string 'all', if 'all' then all keys
+ used from *dataToPut*
+ */
+ if (keysToUse === 'all') {
+ var allKeys = [];
+ for(var k in dataToPut) allKeys.push(k);
+ keysToUse = allKeys;
+ }
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ $.each(keysToUse, function(idx, key) {
+ var value = dataToPut[key];
+ if (value === void 0) {
+ var msg = 'WARN: No key "' + key + '" in data'
+ console.log(msg, dataToPut);
+ } else {
+ var dstId = '#' + tileId + '-' + key;
+ var dst = $(tile).find(dstId)[0];
+ if (typeof dst === 'undefined') {
+ var msg = 'WARN: Not found node with id: ' + dstId;
+ console.log(msg);
+ } else {
+ $(dst).text(value);
+ }
+ }
+ });
+ };
+
+ Tipboard.Dashboard.updateTile = function(
+ tileId, tileType, data, meta, tipboard, lastMod
+ ) {
+ console.log('Update tile: ', tileId);
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ // destroy old graph
+ var chartObj = Tipboard.Dashboard.chartsIds[tileId];
+ if (typeof chartObj === "object") {
+ Tipboard.Dashboard.chartsIds[tileId].destroy();
+ }
+ try {
+ var fn = Tipboard.Dashboard.getUpdateFunction(tileType);
+ fn(tileId, data, meta, tipboard);
+ $('#' + tileId + '-lastModified').val(lastMod);
+ Tipboard.DisplayUtils.showTileData(tile);
+ } catch (err) {
+ console.log('ERROR: ', tileId, err);
+ var msg = [
+ 'Tile ' + tileId + ' configuration error:',
+ err.name || 'error name: n/a',
+ err.message || 'error message: n/a',
+ ].join('<br>');
+ Tipboard.DisplayUtils.showExcMsg(tile, msg);
+ }
+ };
+
+ Tipboard.Dashboard.getUpdateFunction = function(tileType) {
+ var fn = this.updateFunctions[tileType];
+ if (typeof fn !== 'function') {
+ throw new Tipboard.Dashboard.UnknownUpdateFunction(tileType);
+ }
+ return fn;
+ };
+
+ Tipboard.Dashboard.registerUpdateFunction = function(name, fn) {
+ this.updateFunctions[name] = fn;
+ };
+
+ Tipboard.Dashboard.isTileRenderedSuccessful = function(tile) {
+ var suc = $(tile).find('.exception-message').length === 0 ? true : false;
+ return suc;
+ };
+
+ Tipboard.Dashboard.isValidDatetimeFormat = function(dateString) {
+ // valid dateString should be in ISO-8601 format, e.g.:
+ // "2014-01-10T15:27:34+02:00"
+ var regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/;
+ var valid = regex.test(dateString);
+ return valid;
+ };
+
+ Tipboard.Dashboard.isDataExpired = function(lastMod, timeout) {
+ var reason = "";
+ if (timeout === "") {
+ // backward compability, no timeout then skip checking
+ return;
+ }
+ if (!/^\d+$/.test(timeout)) {
+ throw new Tipboard.Dashboard.TimeoutBadFormat(timeout);
+ }
+ if (lastMod === "") {
+ reason = [
+ 'Could not find the timestamp.',
+ '(maybe your data is stalled?)'
+ ].join('<br>');
+ } else {
+ if (Tipboard.Dashboard.isValidDatetimeFormat(lastMod)) {
+ var now = new Date();
+ var lastDateTime = new Date(lastMod);
+ var dataAge = (now - lastDateTime)/1000; // dataAge is in seconds
+ if (dataAge >= timeout) {
+ reason = "Tile's data EXPIRED.";
+ }
+ } else {
+ reason = [
+ 'Timestamp found has bad format.',
+ 'found: ' + lastMod,
+ 'vs.',
+ 'expected: "yyyy-mm-dd hh:mm:ss"',
+ ].join('<br>');
+ }
+ }
+ return reason;
+ };
+
+ Tipboard.Dashboard.autoAddFlipClasses = function(flippingContainer) {
+ $.each($(flippingContainer).find('.tile'), function(idx, elem) {
+ if (idx === 0) {
+ $(elem).addClass('flippedforward');
+ }
+ $(elem).addClass('flippable');
+ });
+ };
+
+ Tipboard.Dashboard.addTilesCounter = function(col) {
+ var tilesTotal = $(col).children('div.tile').length;
+ if (tilesTotal > 1) {
+ $.each($(col).children('div'), function(tileIdx, tile) {
+ var container = $(tile).find('.tile-header');
+ var title = $(container).children()[0];
+ $(title).addClass('flip-tile-counter');
+ var counter = (tileIdx + 1) + '/' + tilesTotal;
+ $(container).append('<span class="tile-counter">' + counter + '</span>');
+ $(container).append('<div style="clear:both"></div>');
+ });
+ }
+ };
+
+ $(document).ready(function() {
+ $.jqplot.config.enablePlugins = true;
+ // resize events
+ $(window).bind("resize fullscreen-off", function() {
+ Tipboard.DisplayUtils.resizeWindowEvent();
+ });
+ // websocket management
+ Tipboard.WebSocketManager.init();
+ setInterval(
+ Tipboard.WebSocketManager.init.bind(Tipboard.WebSocketManager),
+ Tipboard.Dashboard.webSocketResetInterval
+ );
+ // flipping tiles
+ var flipContainers = $('div[class*="flip-time-"]');
+ $.each(flipContainers, function(idx, flippingContainer) {
+ Tipboard.Dashboard.autoAddFlipClasses(flippingContainer);
+ var flipInterval = getFlipTime(flippingContainer);
+ var flipIntervalId = setInterval(function() {
+ Tipboard.DisplayUtils.flipFlippablesIn(flippingContainer);
+ }, flipInterval);
+ Tipboard.Dashboard.flipIds.push(flipIntervalId);
+ });
+ $.each($("body > div"), function(rowIdx, row) {
+ // show tiles number (like: 1/3)
+ $.each($(row).children('div'), function(colIdx, col) {
+ Tipboard.Dashboard.addTilesCounter(col);
+ });
+ });
+ // watchdog
+ setInterval(Tipboard.DisplayUtils.reloadPage, 3600000);
+
+ // check if all tiles have fresh data
+ setInterval(function() {
+ var tiles = $('.tile');
+ $.each(tiles, function(idx, tile) {
+ if (Tipboard.Dashboard.isTileRenderedSuccessful(tile)) {
+ var tileId = Tipboard.Dashboard.tile2id(tile);
+ var lastMod = $('#' + tileId + '-lastModified').val();
+ var timeout = $('#' + tileId + '-timeout').val();
+ var reason = Tipboard.Dashboard.isDataExpired(
+ lastMod, timeout
+ );
+ if (reason) {
+ Tipboard.DisplayUtils.showExcMsg(tile, reason);
+ } else {
+ Tipboard.DisplayUtils.showTileData(tile);
+ }
+ }
+ });
+ }, 5000);
+ Tipboard.DisplayUtils.resizeWindowEvent();
+ });
+}($));
diff --git a/tipboard/templates/base.html b/tipboard/templates/base.html
new file mode 100644
index 0000000..950a3ff
--- /dev/null
+++ b/tipboard/templates/base.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en" ng-app="dashboard">
+ <head>
+ <meta charset="utf-8">
+ <title>{% block page_title %}Dashboard{% end %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ {% for file in tipboard_css %}
+ <link href="{{ static_url(file) }}" rel="stylesheet" media="screen">
+ {% end %}
+ {% for file in tipboard_js %}
+ <script src="{{ static_url(file) }}"></script>
+ {% end %}
+
+ <!--[if lt IE 9]>
+ <script src="{{ static_url("js/lib/html5shiv.js") }}"></script>
+ <script src="{{ static_url("js/lib/jqplot/excanvas.min.js") }}"></script>
+ <![endif]-->
+
+ {% block head %} {% end %}
+ </head>
+ {% block put_body %} <body></body> {% end %}
+</html>
diff --git a/tipboard/templates/flipboard.html b/tipboard/templates/flipboard.html
new file mode 100644
index 0000000..385b309
--- /dev/null
+++ b/tipboard/templates/flipboard.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+
+{% block page_title %}{{ page_title }}{% end %}
+
+{% block put_body %}
+
+<body class="flipboard">
+
+ <div class="error-wrapper">
+ <p class="error-message"></p>
+ </div>
+
+ <iframe src="" data-fliptime-interval="{{flipboard_interval}}" >
+ <p>Your browser does not support iframes.</p>
+ </iframe>
+
+</body>
+
+{% end %}
+
+
diff --git a/tipboard/templates/layout.html b/tipboard/templates/layout.html
new file mode 100644
index 0000000..bd31039
--- /dev/null
+++ b/tipboard/templates/layout.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+
+{% block page_title %}{{ details['page_title'] }}{% end %}
+
+{% block head %}
+ {% for file in tiles_css %}
+ <link href="{{ static_url(file) }}" rel="stylesheet" media="screen">
+ {% end %}
+ {% for file in tiles_js %}
+ <script src="{{ static_url(file) }}"></script>
+ {% end %}
+{% end %}
+
+{% block put_body %}
+
+<body>
+
+ {% for rows in layout %}
+ {% for row in rows.keys() %}
+ {# ROWS #}
+ <div class="row {{ row }}">
+ {% for item in rows[row] %}
+ {# COLS #}
+ {% set col=item.keys().pop() %}
+ <div class="col {{ col }}">
+ {% for tile in item.values().pop() %}
+ {# TILE #}
+ {% module Template(
+ tile_path(tile['tile_template']),
+ tile_id=tile['tile_id'],
+ title=tile['title'],
+ timeout=tile.get('timeout', ''),
+ ) %}
+ <input id='{{tile['tile_id']}}-lastModified' type="hidden"
+ name="modified" value="">
+ <input id='{{tile['tile_id']}}-timeout' type="hidden"
+ name="timeout" value="{{tile.get('timeout', '')}}">
+ {% end %}
+ </div>
+ {% end %}
+ </div>
+ {% end %}
+ {% end %}
+
+</body>
+
+{% end %}
+
+
diff --git a/tipboard/tests/__init__.py b/tipboard/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tipboard/tests/__init__.py
diff --git a/tipboard/tests/frontend/SpecRunner.html b/tipboard/tests/frontend/SpecRunner.html
new file mode 100644
index 0000000..b62052d
--- /dev/null
+++ b/tipboard/tests/frontend/SpecRunner.html
@@ -0,0 +1,397 @@
+<!DOCTYPE html>
+<html lang="en" ng-app="dashboard">
+<head>
+ <meta charset="utf-8">
+ <title>Sample Dashboard</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+ <!-- jasmine -->
+ <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.0.0/jasmine_favicon.png">
+ <link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/jasmine.css">
+
+ <script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
+ <script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
+ <script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
+ <!-- end jasmine -->
+
+
+ <link href="../../static/css/reset.css" rel="stylesheet" media="screen">
+ <link href="../../static/css/jquery.jqplot.css" rel="stylesheet" media="screen">
+ <link href="../../static/css/layout.css" rel="stylesheet" media="screen">
+
+ <script src="../../static/js/lib/jquery.js"></script>
+ <script src="../../static/js/lib/jquery.fullscreen.js"></script>
+ <script src="../../static/js/lib/jqplot/jquery.jqplot.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.trendline.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.canvasAxisTickRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.canvasTextRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.categoryAxisRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.barRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.pointLabels.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.highlighter.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.dateAxisRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.pieRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.blockRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.bubbleRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.canvasOverlay.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.ciParser.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.cursor.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.donutRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.dragable.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.enhancedLegendRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.funnelRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.json2.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.logAxisRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.mekkoAxisRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.mekkoRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.meterGaugeRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.mobile.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.ohlcRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.pyramidAxisRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.pyramidGridRenderer.js"></script>
+ <script src="../../static/js/lib/jqplot/plugins/jqplot.pyramidRenderer.js"></script>
+ <script src="../../static/js/tipboard.js"></script>
+ <script src="../../static/js/flipboard.js"></script>
+
+ <!--[if lt IE 9]>
+ <script src="/static/js/lib/html5shiv.js"></script>
+ <script src="/static/js/lib/jqplot/excanvas.min.js"></script>
+ <![endif]-->
+
+
+ <link href="../../tiles/just_value.css" rel="stylesheet" media="screen">
+ <link href="../../tiles/big_value.css" rel="stylesheet" media="screen">
+ <link href="../../tiles/cumulative_flow.css" rel="stylesheet" media="screen">
+ <link href="../../tiles/simple_percentage.css" rel="stylesheet" media="screen">
+ <link href="../../tiles/listing.css" rel="stylesheet" media="screen">
+ <link href="../../tiles/bar_chart.css" rel="stylesheet" media="screen">
+ <link href="../../tiles/fancy_listing.css" rel="stylesheet" media="screen">
+
+ <script src="../../tiles/just_value.js"></script>
+ <script src="../../tiles/text.js"></script>
+ <script src="../../tiles/pie_chart.js"></script>
+ <script src="../../tiles/big_value.js"></script>
+ <script src="../../tiles/norm_chart.js"></script>
+ <script src="../../tiles/line_chart.js"></script>
+ <script src="../../tiles/cumulative_flow.js"></script>
+ <script src="../../tiles/simple_percentage.js"></script>
+ <script src="../../tiles/listing.js"></script>
+ <script src="../../tiles/bar_chart.js"></script>
+ <script src="../../tiles/fancy_listing.js"></script>
+
+ <!-- include spec files here...
+ <script type="text/javascript" src=""></script>
+ -->
+ <script type="text/javascript" src="spec/tipboardSpec.js"></script>
+
+</head>
+
+<body>
+
+
+
+<div class="row row_1_of_2">
+
+
+
+<div class="col col_1_of_4 flip-time-3">
+
+
+<div class="tile highlighter-tile just-value-tile" id="example_just_value">
+<div class="tile-header">
+<h3>Example &quot;Just Value&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_just_value-title" class="result big-result fixed-height"></h2>
+<h3
+id="example_just_value-description"
+class="result label fixed-height"
+></h3>
+<div class="result big-result highlighted-result just-value-div" >
+<span id="example_just_value-just-value" class="just-value" > </span>
+</div>
+</div>
+</div>
+
+<input id='example_just_value-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_just_value-timeout' type="hidden"
+name="timeout" value="5">
+
+
+<div class="tile" id="example_text">
+<div class="tile-header">
+<h3>Example &quot;Text&quot;</h3>
+</div>
+<div class="tile-content">
+<span class="text-container"></span>
+</div>
+</div>
+
+<input id='example_text-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_text-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+
+
+<div class="col col_1_of_4 flip-time-5">
+
+
+<div class="tile pie-chart-tile" id="example_pie_chart">
+<div class="tile-header">
+<h3>Example &quot;Pie Chart&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_pie_chart-title" class="result big-result"> </h2>
+<h3 id="example_pie_chart-description" class="result label"> </h3>
+<div id="example_pie_chart-chart" class='chart round-legend'> </div>
+</div>
+</div>
+
+<input id='example_pie_chart-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_pie_chart-timeout' type="hidden"
+name="timeout" value="">
+
+
+<div class="tile highlighter-tile big_value_tile" id="example_big_value">
+<div class="tile-header">
+<h3 >Example &quot;Big Value&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_big_value-title" class="result big-result fixed-height"></h2>
+<h3 id="example_big_value-description" class="result label fixed-height"
+></h3>
+<div class="result big-value-container">
+<span id="example_big_value-big-value" class="big-value"></span>
+</div>
+<div class="result-row highlight-footer">
+<div class="result-half-col">
+<div class="result">
+<span id="example_big_value-upper-left-label" class="label"></span>
+<span id="example_big_value-upper-left-value"></span>
+</div>
+<div class="result">
+<span id="example_big_value-lower-left-label" class="label"></span>
+<span id="example_big_value-lower-left-value"></span>
+</div>
+</div>
+<div class="result-half-col">
+<div class="result">
+<span id="example_big_value-upper-right-label" class="label"></span>
+<span id="example_big_value-upper-right-value"></span>
+</div>
+<div class="result">
+<span id="example_big_value-lower-right-label" class="label"></span>
+<span id="example_big_value-lower-right-value"></span>
+</div>
+</div>
+</div>
+</div>
+</div>
+
+<input id='example_big_value-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_big_value-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+
+
+<div class="col col_1_of_4">
+
+
+<div class="tile norm-chart-tile" id="example_norm_chart">
+<div class="tile-header">
+<h3 >Empty &quot;Norm Chart&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_norm_chart-title" class="result big-result"></h2>
+<h3 id="example_norm_chart-description" class="result label"> </h3>
+<div id="example_norm_chart-chart" class="chart round-legend"> </div>
+</div>
+</div>
+
+<input id='example_norm_chart-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_norm_chart-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+
+
+<div class="col col_1_of_4 flip-time-20">
+
+
+<div class="tile line-chart-tile" id="example_line_chart">
+<div class="tile-header">
+<h3>Example &quot;Line Chart&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_line_chart-subtitle" class="result big-result"> </h2>
+<h3 id="example_line_chart-description" class="result label"> </h3>
+<div id="example_line_chart-chart" class='chart'> </div>
+</div>
+</div>
+
+<input id='example_line_chart-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_line_chart-timeout' type="hidden"
+name="timeout" value="">
+
+
+<div class="tile" id="example_cumulative_flow">
+<div class="tile-header">
+<h3>Empty &quot;Cumulative Flow&quot;</h3>
+</div>
+<div class="tile-content">
+<div id="example_cumulative_flow-chart" class="chart cumulative-flow-chart chart-only"> </div>
+<div class="plot-labels">
+<h2 id="example_cumulative_flow-title" class="labels-title"></h2>
+<!--
+js appends labels dynamically here
+-->
+</div>
+</div>
+</div>
+
+<input id='example_cumulative_flow-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_cumulative_flow-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+</div>
+
+
+
+
+<div class="row row_1_of_2">
+
+
+
+<div class="col col_1_of_4">
+
+
+<div class="tile highlighter-tile simple-percentage-tile" id="example_simple_percentage">
+<div class="tile-header">
+<h3>Example &quot;Simple Percentage&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_simple_percentage-title" class="result big-result fixed-height"></h2>
+<h3 id="example_simple_percentage-subtitle" class="result subtitle label fixed-height"></h3>
+<div class="result big-value-container">
+<span id="example_simple_percentage-big_value"></span>
+</div>
+<div class="result-row highlight-footer">
+<div class="result-col with-right-border">
+<div id="example_simple_percentage-left_label" class="result label"></div>
+<div id="example_simple_percentage-left_value" class="result medium-result"><span></span></div>
+</div>
+<div class="result-col">
+<div id="example_simple_percentage-right_label" class="result label"></div>
+<div id="example_simple_percentage-right_value" class="result medium-result"><span></span></div>
+</div>
+</div>
+</div>
+</div>
+
+<input id='example_simple_percentage-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_simple_percentage-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+
+
+<div class="col col_1_of_4">
+
+
+<div class="tile listing-tile" id="example_listing">
+<div class="tile-header">
+<h3>Example &quot;Listing&quot;</h3>
+</div>
+<div class="tile-content">
+<ul class="listing">
+<!--
+listing items added here by js
+-->
+</ul>
+</div>
+</div>
+
+<input id='example_listing-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_listing-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+
+
+<div class="col col_1_of_4">
+
+
+<div class="tile" id="example_bar_chart">
+<div class="tile-header">
+<h3>Example &quot;Bar Chart&quot;</h3>
+</div>
+<div class="tile-content">
+<h2 id="example_bar_chart-title" class="result big-result"></h2>
+<h3 id="example_bar_chart-subtitle" class="result label" ></h3>
+<div id="example_bar_chart-chart" class="chart bar-chart" > </div>
+</div>
+</div>
+
+<input id='example_bar_chart-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_bar_chart-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+
+
+<div class="col col_1_of_4">
+
+
+<div class="tile fancy-listing" id="example_fancy_listing">
+<div class="tile-header">
+<h3>Example &quot;Fancy Listing&quot;</h3>
+</div>
+<div class="tile-content overflow-proof">
+<div id="example_fancy_listing-fancy-listing-item" class="fancy-listing-item">
+<div class="float-wrapper">
+<div class="fancy-listing-label centerbox">
+<span class="fancy-listing-label-inside"></span>
+</div>
+</div>
+<div class="fancy-listing-def">
+<h3 class="fancy-listing-term"></h3>
+<p class="fancy-listing-desc"></p>
+</div>
+</div>
+</div>
+</div>
+
+<input id='example_fancy_listing-lastModified' type="hidden"
+name="modified" value="">
+<input id='example_fancy_listing-timeout' type="hidden"
+name="timeout" value="">
+
+</div>
+
+</div>
+
+
+</body>
+
+</html>
diff --git a/tipboard/tests/frontend/lib/jasmine-2.0.0/boot.js b/tipboard/tests/frontend/lib/jasmine-2.0.0/boot.js
new file mode 100644
index 0000000..ec8baa0
--- /dev/null
+++ b/tipboard/tests/frontend/lib/jasmine-2.0.0/boot.js
@@ -0,0 +1,181 @@
+/**
+ Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
+
+ If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
+
+ The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
+
+ [jasmine-gem]: http://github.com/pivotal/jasmine-gem
+ */
+
+(function() {
+
+ /**
+ * ## Require &amp; Instantiate
+ *
+ * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
+ */
+ window.jasmine = jasmineRequire.core(jasmineRequire);
+
+ /**
+ * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
+ */
+ jasmineRequire.html(jasmine);
+
+ /**
+ * Create the Jasmine environment. This is used to run all specs in a project.
+ */
+ var env = jasmine.getEnv();
+
+ /**
+ * ## The Global Interface
+ *
+ * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
+ */
+ var jasmineInterface = {
+ describe: function(description, specDefinitions) {
+ return env.describe(description, specDefinitions);
+ },
+
+ xdescribe: function(description, specDefinitions) {
+ return env.xdescribe(description, specDefinitions);
+ },
+
+ it: function(desc, func) {
+ return env.it(desc, func);
+ },
+
+ xit: function(desc, func) {
+ return env.xit(desc, func);
+ },
+
+ beforeEach: function(beforeEachFunction) {
+ return env.beforeEach(beforeEachFunction);
+ },
+
+ afterEach: function(afterEachFunction) {
+ return env.afterEach(afterEachFunction);
+ },
+
+ expect: function(actual) {
+ return env.expect(actual);
+ },
+
+ pending: function() {
+ return env.pending();
+ },
+
+ spyOn: function(obj, methodName) {
+ return env.spyOn(obj, methodName);
+ },
+
+ jsApiReporter: new jasmine.JsApiReporter({
+ timer: new jasmine.Timer()
+ })
+ };
+
+ /**
+ * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
+ */
+ if (typeof window == "undefined" && typeof exports == "object") {
+ extend(exports, jasmineInterface);
+ } else {
+ extend(window, jasmineInterface);
+ }
+
+ /**
+ * Expose the interface for adding custom equality testers.
+ */
+ jasmine.addCustomEqualityTester = function(tester) {
+ env.addCustomEqualityTester(tester);
+ };
+
+ /**
+ * Expose the interface for adding custom expectation matchers
+ */
+ jasmine.addMatchers = function(matchers) {
+ return env.addMatchers(matchers);
+ };
+
+ /**
+ * Expose the mock interface for the JavaScript timeout functions
+ */
+ jasmine.clock = function() {
+ return env.clock;
+ };
+
+ /**
+ * ## Runner Parameters
+ *
+ * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
+ */
+
+ var queryString = new jasmine.QueryString({
+ getWindowLocation: function() { return window.location; }
+ });
+
+ var catchingExceptions = queryString.getParam("catch");
+ env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
+
+ /**
+ * ## Reporters
+ * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
+ */
+ var htmlReporter = new jasmine.HtmlReporter({
+ env: env,
+ onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); },
+ getContainer: function() { return document.body; },
+ createElement: function() { return document.createElement.apply(document, arguments); },
+ createTextNode: function() { return document.createTextNode.apply(document, arguments); },
+ timer: new jasmine.Timer()
+ });
+
+ /**
+ * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
+ */
+ env.addReporter(jasmineInterface.jsApiReporter);
+ env.addReporter(htmlReporter);
+
+ /**
+ * Filter which specs will be run by matching the start of the full name against the `spec` query param.
+ */
+ var specFilter = new jasmine.HtmlSpecFilter({
+ filterString: function() { return queryString.getParam("spec"); }
+ });
+
+ env.specFilter = function(spec) {
+ return specFilter.matches(spec.getFullName());
+ };
+
+ /**
+ * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack.
+ */
+ window.setTimeout = window.setTimeout;
+ window.setInterval = window.setInterval;
+ window.clearTimeout = window.clearTimeout;
+ window.clearInterval = window.clearInterval;
+
+ /**
+ * ## Execution
+ *
+ * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
+ */
+ var currentWindowOnload = window.onload;
+
+ window.onload = function() {
+ if (currentWindowOnload) {
+ currentWindowOnload();
+ }
+ htmlReporter.initialize();
+ env.execute();
+ };
+
+ /**
+ * Helper function for readability above.
+ */
+ function extend(destination, source) {
+ for (var property in source) destination[property] = source[property];
+ return destination;
+ }
+
+}());
diff --git a/tipboard/tests/frontend/lib/jasmine-2.0.0/console.js b/tipboard/tests/frontend/lib/jasmine-2.0.0/console.js
new file mode 100644
index 0000000..33c1698
--- /dev/null
+++ b/tipboard/tests/frontend/lib/jasmine-2.0.0/console.js
@@ -0,0 +1,160 @@
+/*
+Copyright (c) 2008-2013 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+function getJasmineRequireObj() {
+ if (typeof module !== "undefined" && module.exports) {
+ return exports;
+ } else {
+ window.jasmineRequire = window.jasmineRequire || {};
+ return window.jasmineRequire;
+ }
+}
+
+getJasmineRequireObj().console = function(jRequire, j$) {
+ j$.ConsoleReporter = jRequire.ConsoleReporter();
+};
+
+getJasmineRequireObj().ConsoleReporter = function() {
+
+ var noopTimer = {
+ start: function(){},
+ elapsed: function(){ return 0; }
+ };
+
+ function ConsoleReporter(options) {
+ var print = options.print,
+ showColors = options.showColors || false,
+ onComplete = options.onComplete || function() {},
+ timer = options.timer || noopTimer,
+ specCount,
+ failureCount,
+ failedSpecs = [],
+ pendingCount,
+ ansi = {
+ green: '\x1B[32m',
+ red: '\x1B[31m',
+ yellow: '\x1B[33m',
+ none: '\x1B[0m'
+ };
+
+ this.jasmineStarted = function() {
+ specCount = 0;
+ failureCount = 0;
+ pendingCount = 0;
+ print("Started");
+ printNewline();
+ timer.start();
+ };
+
+ this.jasmineDone = function() {
+ printNewline();
+ for (var i = 0; i < failedSpecs.length; i++) {
+ specFailureDetails(failedSpecs[i]);
+ }
+
+ printNewline();
+ var specCounts = specCount + " " + plural("spec", specCount) + ", " +
+ failureCount + " " + plural("failure", failureCount);
+
+ if (pendingCount) {
+ specCounts += ", " + pendingCount + " pending " + plural("spec", pendingCount);
+ }
+
+ print(specCounts);
+
+ printNewline();
+ var seconds = timer.elapsed() / 1000;
+ print("Finished in " + seconds + " " + plural("second", seconds));
+
+ printNewline();
+
+ onComplete(failureCount === 0);
+ };
+
+ this.specDone = function(result) {
+ specCount++;
+
+ if (result.status == "pending") {
+ pendingCount++;
+ print(colored("yellow", "*"));
+ return;
+ }
+
+ if (result.status == "passed") {
+ print(colored("green", '.'));
+ return;
+ }
+
+ if (result.status == "failed") {
+ failureCount++;
+ failedSpecs.push(result);
+ print(colored("red", 'F'));
+ }
+ };
+
+ return this;
+
+ function printNewline() {
+ print("\n");
+ }
+
+ function colored(color, str) {
+ return showColors ? (ansi[color] + str + ansi.none) : str;
+ }
+
+ function plural(str, count) {
+ return count == 1 ? str : str + "s";
+ }
+
+ function repeat(thing, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(thing);
+ }
+ return arr;
+ }
+
+ function indent(str, spaces) {
+ var lines = (str || '').split("\n");
+ var newArr = [];
+ for (var i = 0; i < lines.length; i++) {
+ newArr.push(repeat(" ", spaces).join("") + lines[i]);
+ }
+ return newArr.join("\n");
+ }
+
+ function specFailureDetails(result) {
+ printNewline();
+ print(result.fullName);
+
+ for (var i = 0; i < result.failedExpectations.length; i++) {
+ var failedExpectation = result.failedExpectations[i];
+ printNewline();
+ print(indent(failedExpectation.stack, 2));
+ }
+
+ printNewline();
+ }
+ }
+
+ return ConsoleReporter;
+};
diff --git a/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine-html.js b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine-html.js
new file mode 100644
index 0000000..985d0d1
--- /dev/null
+++ b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine-html.js
@@ -0,0 +1,359 @@
+/*
+Copyright (c) 2008-2013 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+jasmineRequire.html = function(j$) {
+ j$.ResultsNode = jasmineRequire.ResultsNode();
+ j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
+ j$.QueryString = jasmineRequire.QueryString();
+ j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
+};
+
+jasmineRequire.HtmlReporter = function(j$) {
+
+ var noopTimer = {
+ start: function() {},
+ elapsed: function() { return 0; }
+ };
+
+ function HtmlReporter(options) {
+ var env = options.env || {},
+ getContainer = options.getContainer,
+ createElement = options.createElement,
+ createTextNode = options.createTextNode,
+ onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {},
+ timer = options.timer || noopTimer,
+ results = [],
+ specsExecuted = 0,
+ failureCount = 0,
+ pendingSpecCount = 0,
+ htmlReporterMain,
+ symbols;
+
+ this.initialize = function() {
+ htmlReporterMain = createDom("div", {className: "html-reporter"},
+ createDom("div", {className: "banner"},
+ createDom("span", {className: "title"}, "Jasmine"),
+ createDom("span", {className: "version"}, j$.version)
+ ),
+ createDom("ul", {className: "symbol-summary"}),
+ createDom("div", {className: "alert"}),
+ createDom("div", {className: "results"},
+ createDom("div", {className: "failures"})
+ )
+ );
+ getContainer().appendChild(htmlReporterMain);
+
+ symbols = find(".symbol-summary");
+ };
+
+ var totalSpecsDefined;
+ this.jasmineStarted = function(options) {
+ totalSpecsDefined = options.totalSpecsDefined || 0;
+ timer.start();
+ };
+
+ var summary = createDom("div", {className: "summary"});
+
+ var topResults = new j$.ResultsNode({}, "", null),
+ currentParent = topResults;
+
+ this.suiteStarted = function(result) {
+ currentParent.addChild(result, "suite");
+ currentParent = currentParent.last();
+ };
+
+ this.suiteDone = function(result) {
+ if (currentParent == topResults) {
+ return;
+ }
+
+ currentParent = currentParent.parent;
+ };
+
+ this.specStarted = function(result) {
+ currentParent.addChild(result, "spec");
+ };
+
+ var failures = [];
+ this.specDone = function(result) {
+ if (result.status != "disabled") {
+ specsExecuted++;
+ }
+
+ symbols.appendChild(createDom("li", {
+ className: result.status,
+ id: "spec_" + result.id,
+ title: result.fullName
+ }
+ ));
+
+ if (result.status == "failed") {
+ failureCount++;
+
+ var failure =
+ createDom("div", {className: "spec-detail failed"},
+ createDom("div", {className: "description"},
+ createDom("a", {title: result.fullName, href: specHref(result)}, result.fullName)
+ ),
+ createDom("div", {className: "messages"})
+ );
+ var messages = failure.childNodes[1];
+
+ for (var i = 0; i < result.failedExpectations.length; i++) {
+ var expectation = result.failedExpectations[i];
+ messages.appendChild(createDom("div", {className: "result-message"}, expectation.message));
+ messages.appendChild(createDom("div", {className: "stack-trace"}, expectation.stack));
+ }
+
+ failures.push(failure);
+ }
+
+ if (result.status == "pending") {
+ pendingSpecCount++;
+ }
+ };
+
+ this.jasmineDone = function() {
+ var banner = find(".banner");
+ banner.appendChild(createDom("span", {className: "duration"}, "finished in " + timer.elapsed() / 1000 + "s"));
+
+ var alert = find(".alert");
+
+ alert.appendChild(createDom("span", { className: "exceptions" },
+ createDom("label", { className: "label", 'for': "raise-exceptions" }, "raise exceptions"),
+ createDom("input", {
+ className: "raise",
+ id: "raise-exceptions",
+ type: "checkbox"
+ })
+ ));
+ var checkbox = find("input");
+
+ checkbox.checked = !env.catchingExceptions();
+ checkbox.onclick = onRaiseExceptionsClick;
+
+ if (specsExecuted < totalSpecsDefined) {
+ var skippedMessage = "Ran " + specsExecuted + " of " + totalSpecsDefined + " specs - run all";
+ alert.appendChild(
+ createDom("span", {className: "bar skipped"},
+ createDom("a", {href: "?", title: "Run all specs"}, skippedMessage)
+ )
+ );
+ }
+ var statusBarMessage = "" + pluralize("spec", specsExecuted) + ", " + pluralize("failure", failureCount);
+ if (pendingSpecCount) { statusBarMessage += ", " + pluralize("pending spec", pendingSpecCount); }
+
+ var statusBarClassName = "bar " + ((failureCount > 0) ? "failed" : "passed");
+ alert.appendChild(createDom("span", {className: statusBarClassName}, statusBarMessage));
+
+ var results = find(".results");
+ results.appendChild(summary);
+
+ summaryList(topResults, summary);
+
+ function summaryList(resultsTree, domParent) {
+ var specListNode;
+ for (var i = 0; i < resultsTree.children.length; i++) {
+ var resultNode = resultsTree.children[i];
+ if (resultNode.type == "suite") {
+ var suiteListNode = createDom("ul", {className: "suite", id: "suite-" + resultNode.result.id},
+ createDom("li", {className: "suite-detail"},
+ createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description)
+ )
+ );
+
+ summaryList(resultNode, suiteListNode);
+ domParent.appendChild(suiteListNode);
+ }
+ if (resultNode.type == "spec") {
+ if (domParent.getAttribute("class") != "specs") {
+ specListNode = createDom("ul", {className: "specs"});
+ domParent.appendChild(specListNode);
+ }
+ specListNode.appendChild(
+ createDom("li", {
+ className: resultNode.result.status,
+ id: "spec-" + resultNode.result.id
+ },
+ createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description)
+ )
+ );
+ }
+ }
+ }
+
+ if (failures.length) {
+ alert.appendChild(
+ createDom('span', {className: "menu bar spec-list"},
+ createDom("span", {}, "Spec List | "),
+ createDom('a', {className: "failures-menu", href: "#"}, "Failures")));
+ alert.appendChild(
+ createDom('span', {className: "menu bar failure-list"},
+ createDom('a', {className: "spec-list-menu", href: "#"}, "Spec List"),
+ createDom("span", {}, " | Failures ")));
+
+ find(".failures-menu").onclick = function() {
+ setMenuModeTo('failure-list');
+ };
+ find(".spec-list-menu").onclick = function() {
+ setMenuModeTo('spec-list');
+ };
+
+ setMenuModeTo('failure-list');
+
+ var failureNode = find(".failures");
+ for (var i = 0; i < failures.length; i++) {
+ failureNode.appendChild(failures[i]);
+ }
+ }
+ };
+
+ return this;
+
+ function find(selector) {
+ return getContainer().querySelector(selector);
+ }
+
+ function createDom(type, attrs, childrenVarArgs) {
+ var el = createElement(type);
+
+ for (var i = 2; i < arguments.length; i++) {
+ var child = arguments[i];
+
+ if (typeof child === 'string') {
+ el.appendChild(createTextNode(child));
+ } else {
+ if (child) {
+ el.appendChild(child);
+ }
+ }
+ }
+
+ for (var attr in attrs) {
+ if (attr == "className") {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr]);
+ }
+ }
+
+ return el;
+ }
+
+ function pluralize(singular, count) {
+ var word = (count == 1 ? singular : singular + "s");
+
+ return "" + count + " " + word;
+ }
+
+ function specHref(result) {
+ return "?spec=" + encodeURIComponent(result.fullName);
+ }
+
+ function setMenuModeTo(mode) {
+ htmlReporterMain.setAttribute("class", "html-reporter " + mode);
+ }
+ }
+
+ return HtmlReporter;
+};
+
+jasmineRequire.HtmlSpecFilter = function() {
+ function HtmlSpecFilter(options) {
+ var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ var filterPattern = new RegExp(filterString);
+
+ this.matches = function(specName) {
+ return filterPattern.test(specName);
+ };
+ }
+
+ return HtmlSpecFilter;
+};
+
+jasmineRequire.ResultsNode = function() {
+ function ResultsNode(result, type, parent) {
+ this.result = result;
+ this.type = type;
+ this.parent = parent;
+
+ this.children = [];
+
+ this.addChild = function(result, type) {
+ this.children.push(new ResultsNode(result, type, this));
+ };
+
+ this.last = function() {
+ return this.children[this.children.length - 1];
+ };
+ }
+
+ return ResultsNode;
+};
+
+jasmineRequire.QueryString = function() {
+ function QueryString(options) {
+
+ this.setParam = function(key, value) {
+ var paramMap = queryStringToParamMap();
+ paramMap[key] = value;
+ options.getWindowLocation().search = toQueryString(paramMap);
+ };
+
+ this.getParam = function(key) {
+ return queryStringToParamMap()[key];
+ };
+
+ return this;
+
+ function toQueryString(paramMap) {
+ var qStrPairs = [];
+ for (var prop in paramMap) {
+ qStrPairs.push(encodeURIComponent(prop) + "=" + encodeURIComponent(paramMap[prop]));
+ }
+ return "?" + qStrPairs.join('&');
+ }
+
+ function queryStringToParamMap() {
+ var paramStr = options.getWindowLocation().search.substring(1),
+ params = [],
+ paramMap = {};
+
+ if (paramStr.length > 0) {
+ params = paramStr.split('&');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i].split('=');
+ var value = decodeURIComponent(p[1]);
+ if (value === "true" || value === "false") {
+ value = JSON.parse(value);
+ }
+ paramMap[decodeURIComponent(p[0])] = value;
+ }
+ }
+
+ return paramMap;
+ }
+
+ }
+
+ return QueryString;
+};
diff --git a/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.css b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.css
new file mode 100644
index 0000000..f4d35b6
--- /dev/null
+++ b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.css
@@ -0,0 +1,55 @@
+body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
+
+.html-reporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
+.html-reporter a { text-decoration: none; }
+.html-reporter a:hover { text-decoration: underline; }
+.html-reporter p, .html-reporter h1, .html-reporter h2, .html-reporter h3, .html-reporter h4, .html-reporter h5, .html-reporter h6 { margin: 0; line-height: 14px; }
+.html-reporter .banner, .html-reporter .symbol-summary, .html-reporter .summary, .html-reporter .result-message, .html-reporter .spec .description, .html-reporter .spec-detail .description, .html-reporter .alert .bar, .html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; }
+.html-reporter .banner .version { margin-left: 14px; }
+.html-reporter #jasmine_content { position: fixed; right: 100%; }
+.html-reporter .version { color: #aaaaaa; }
+.html-reporter .banner { margin-top: 14px; }
+.html-reporter .duration { color: #aaaaaa; float: right; }
+.html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; }
+.html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; }
+.html-reporter .symbol-summary li.passed { font-size: 14px; }
+.html-reporter .symbol-summary li.passed:before { color: #5e7d00; content: "\02022"; }
+.html-reporter .symbol-summary li.failed { line-height: 9px; }
+.html-reporter .symbol-summary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
+.html-reporter .symbol-summary li.disabled { font-size: 14px; }
+.html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; }
+.html-reporter .symbol-summary li.pending { line-height: 17px; }
+.html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; }
+.html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
+.html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
+.html-reporter .bar.failed { background-color: #b03911; }
+.html-reporter .bar.passed { background-color: #a6b779; }
+.html-reporter .bar.skipped { background-color: #bababa; }
+.html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; }
+.html-reporter .bar.menu a { color: #333333; }
+.html-reporter .bar a { color: white; }
+.html-reporter.spec-list .bar.menu.failure-list, .html-reporter.spec-list .results .failures { display: none; }
+.html-reporter.failure-list .bar.menu.spec-list, .html-reporter.failure-list .summary { display: none; }
+.html-reporter .running-alert { background-color: #666666; }
+.html-reporter .results { margin-top: 14px; }
+.html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
+.html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
+.html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
+.html-reporter.showDetails .summary { display: none; }
+.html-reporter.showDetails #details { display: block; }
+.html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
+.html-reporter .summary { margin-top: 14px; }
+.html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; }
+.html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; }
+.html-reporter .summary li.passed a { color: #5e7d00; }
+.html-reporter .summary li.failed a { color: #b03911; }
+.html-reporter .summary li.pending a { color: #ba9d37; }
+.html-reporter .description + .suite { margin-top: 0; }
+.html-reporter .suite { margin-top: 14px; }
+.html-reporter .suite a { color: #333333; }
+.html-reporter .failures .spec-detail { margin-bottom: 28px; }
+.html-reporter .failures .spec-detail .description { background-color: #b03911; }
+.html-reporter .failures .spec-detail .description a { color: white; }
+.html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; }
+.html-reporter .result-message span.result { display: block; }
+.html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
diff --git a/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.js b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.js
new file mode 100644
index 0000000..24463ec
--- /dev/null
+++ b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine.js
@@ -0,0 +1,2402 @@
+/*
+Copyright (c) 2008-2013 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+function getJasmineRequireObj() {
+ if (typeof module !== "undefined" && module.exports) {
+ return exports;
+ } else {
+ window.jasmineRequire = window.jasmineRequire || {};
+ return window.jasmineRequire;
+ }
+}
+
+getJasmineRequireObj().core = function(jRequire) {
+ var j$ = {};
+
+ jRequire.base(j$);
+ j$.util = jRequire.util();
+ j$.Any = jRequire.Any();
+ j$.CallTracker = jRequire.CallTracker();
+ j$.Clock = jRequire.Clock();
+ j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler();
+ j$.Env = jRequire.Env(j$);
+ j$.ExceptionFormatter = jRequire.ExceptionFormatter();
+ j$.Expectation = jRequire.Expectation();
+ j$.buildExpectationResult = jRequire.buildExpectationResult();
+ j$.JsApiReporter = jRequire.JsApiReporter();
+ j$.matchersUtil = jRequire.matchersUtil(j$);
+ j$.ObjectContaining = jRequire.ObjectContaining(j$);
+ j$.pp = jRequire.pp(j$);
+ j$.QueueRunner = jRequire.QueueRunner();
+ j$.ReportDispatcher = jRequire.ReportDispatcher();
+ j$.Spec = jRequire.Spec(j$);
+ j$.SpyStrategy = jRequire.SpyStrategy();
+ j$.Suite = jRequire.Suite();
+ j$.Timer = jRequire.Timer();
+ j$.version = jRequire.version();
+
+ j$.matchers = jRequire.requireMatchers(jRequire, j$);
+
+ return j$;
+};
+
+getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
+ var availableMatchers = [
+ "toBe",
+ "toBeCloseTo",
+ "toBeDefined",
+ "toBeFalsy",
+ "toBeGreaterThan",
+ "toBeLessThan",
+ "toBeNaN",
+ "toBeNull",
+ "toBeTruthy",
+ "toBeUndefined",
+ "toContain",
+ "toEqual",
+ "toHaveBeenCalled",
+ "toHaveBeenCalledWith",
+ "toMatch",
+ "toThrow",
+ "toThrowError"
+ ],
+ matchers = {};
+
+ for (var i = 0; i < availableMatchers.length; i++) {
+ var name = availableMatchers[i];
+ matchers[name] = jRequire[name](j$);
+ }
+
+ return matchers;
+};
+
+getJasmineRequireObj().base = function(j$) {
+ j$.unimplementedMethod_ = function() {
+ throw new Error("unimplemented method");
+ };
+
+ j$.MAX_PRETTY_PRINT_DEPTH = 40;
+ j$.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+ j$.getGlobal = (function() {
+ var jasmineGlobal = eval.call(null, "this");
+ return function() {
+ return jasmineGlobal;
+ };
+ })();
+
+ j$.getEnv = function(options) {
+ var env = j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options);
+ //jasmine. singletons in here (setTimeout blah blah).
+ return env;
+ };
+
+ j$.isArray_ = function(value) {
+ return j$.isA_("Array", value);
+ };
+
+ j$.isString_ = function(value) {
+ return j$.isA_("String", value);
+ };
+
+ j$.isNumber_ = function(value) {
+ return j$.isA_("Number", value);
+ };
+
+ j$.isA_ = function(typeName, value) {
+ return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+ };
+
+ j$.isDomNode = function(obj) {
+ return obj.nodeType > 0;
+ };
+
+ j$.any = function(clazz) {
+ return new j$.Any(clazz);
+ };
+
+ j$.objectContaining = function(sample) {
+ return new j$.ObjectContaining(sample);
+ };
+
+ j$.createSpy = function(name, originalFn) {
+
+ var spyStrategy = new j$.SpyStrategy({
+ name: name,
+ fn: originalFn,
+ getSpy: function() { return spy; }
+ }),
+ callTracker = new j$.CallTracker(),
+ spy = function() {
+ callTracker.track({
+ object: this,
+ args: Array.prototype.slice.apply(arguments)
+ });
+ return spyStrategy.exec.apply(this, arguments);
+ };
+
+ for (var prop in originalFn) {
+ if (prop === 'and' || prop === 'calls') {
+ throw new Error("Jasmine spies would overwrite the 'and' and 'calls' properties on the object being spied upon");
+ }
+
+ spy[prop] = originalFn[prop];
+ }
+
+ spy.and = spyStrategy;
+ spy.calls = callTracker;
+
+ return spy;
+ };
+
+ j$.isSpy = function(putativeSpy) {
+ if (!putativeSpy) {
+ return false;
+ }
+ return putativeSpy.and instanceof j$.SpyStrategy &&
+ putativeSpy.calls instanceof j$.CallTracker;
+ };
+
+ j$.createSpyObj = function(baseName, methodNames) {
+ if (!j$.isArray_(methodNames) || methodNames.length === 0) {
+ throw "createSpyObj requires a non-empty array of method names to create spies for";
+ }
+ var obj = {};
+ for (var i = 0; i < methodNames.length; i++) {
+ obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]);
+ }
+ return obj;
+ };
+};
+
+getJasmineRequireObj().util = function() {
+
+ var util = {};
+
+ util.inherit = function(childClass, parentClass) {
+ var Subclass = function() {
+ };
+ Subclass.prototype = parentClass.prototype;
+ childClass.prototype = new Subclass();
+ };
+
+ util.htmlEscape = function(str) {
+ if (!str) {
+ return str;
+ }
+ return str.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ };
+
+ util.argsToArray = function(args) {
+ var arrayOfArgs = [];
+ for (var i = 0; i < args.length; i++) {
+ arrayOfArgs.push(args[i]);
+ }
+ return arrayOfArgs;
+ };
+
+ util.isUndefined = function(obj) {
+ return obj === void 0;
+ };
+
+ return util;
+};
+
+getJasmineRequireObj().Spec = function(j$) {
+ function Spec(attrs) {
+ this.expectationFactory = attrs.expectationFactory;
+ this.resultCallback = attrs.resultCallback || function() {};
+ this.id = attrs.id;
+ this.description = attrs.description || '';
+ this.fn = attrs.fn;
+ this.beforeFns = attrs.beforeFns || function() { return []; };
+ this.afterFns = attrs.afterFns || function() { return []; };
+ this.onStart = attrs.onStart || function() {};
+ this.exceptionFormatter = attrs.exceptionFormatter || function() {};
+ this.getSpecName = attrs.getSpecName || function() { return ''; };
+ this.expectationResultFactory = attrs.expectationResultFactory || function() { };
+ this.queueRunnerFactory = attrs.queueRunnerFactory || function() {};
+ this.catchingExceptions = attrs.catchingExceptions || function() { return true; };
+
+ this.timer = attrs.timer || {setTimeout: setTimeout, clearTimeout: clearTimeout};
+
+ if (!this.fn) {
+ this.pend();
+ }
+
+ this.result = {
+ id: this.id,
+ description: this.description,
+ fullName: this.getFullName(),
+ failedExpectations: []
+ };
+ }
+
+ Spec.prototype.addExpectationResult = function(passed, data) {
+ if (passed) {
+ return;
+ }
+ this.result.failedExpectations.push(this.expectationResultFactory(data));
+ };
+
+ Spec.prototype.expect = function(actual) {
+ return this.expectationFactory(actual, this);
+ };
+
+ Spec.prototype.execute = function(onComplete) {
+ var self = this,
+ timeout;
+
+ this.onStart(this);
+
+ if (this.markedPending || this.disabled) {
+ complete();
+ return;
+ }
+
+ function timeoutable(fn) {
+ return function(done) {
+ timeout = Function.prototype.apply.apply(self.timer.setTimeout, [j$.getGlobal(), [function() {
+ onException(new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'));
+ done();
+ }, j$.DEFAULT_TIMEOUT_INTERVAL]]);
+
+ var callDone = function() {
+ clearTimeoutable();
+ done();
+ };
+
+ fn.call(this, callDone); //TODO: do we care about more than 1 arg?
+ };
+ }
+
+ function clearTimeoutable() {
+ Function.prototype.apply.apply(self.timer.clearTimeout, [j$.getGlobal(), [timeout]]);
+ timeout = void 0;
+ }
+
+ var allFns = this.beforeFns().concat(this.fn).concat(this.afterFns()),
+ allTimeoutableFns = [];
+ for (var i = 0; i < allFns.length; i++) {
+ var fn = allFns[i];
+ allTimeoutableFns.push(fn.length > 0 ? timeoutable(fn) : fn);
+ }
+
+ this.queueRunnerFactory({
+ fns: allTimeoutableFns,
+ onException: onException,
+ onComplete: complete
+ });
+
+ function onException(e) {
+ clearTimeoutable();
+ if (Spec.isPendingSpecException(e)) {
+ self.pend();
+ return;
+ }
+
+ self.addExpectationResult(false, {
+ matcherName: "",
+ passed: false,
+ expected: "",
+ actual: "",
+ error: e
+ });
+ }
+
+ function complete() {
+ self.result.status = self.status();
+ self.resultCallback(self.result);
+
+ if (onComplete) {
+ onComplete();
+ }
+ }
+ };
+
+ Spec.prototype.disable = function() {
+ this.disabled = true;
+ };
+
+ Spec.prototype.pend = function() {
+ this.markedPending = true;
+ };
+
+ Spec.prototype.status = function() {
+ if (this.disabled) {
+ return 'disabled';
+ }
+
+ if (this.markedPending) {
+ return 'pending';
+ }
+
+ if (this.result.failedExpectations.length > 0) {
+ return 'failed';
+ } else {
+ return 'passed';
+ }
+ };
+
+ Spec.prototype.getFullName = function() {
+ return this.getSpecName(this);
+ };
+
+ Spec.pendingSpecExceptionMessage = "=> marked Pending";
+
+ Spec.isPendingSpecException = function(e) {
+ return e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1;
+ };
+
+ return Spec;
+};
+
+if (typeof window == void 0 && typeof exports == "object") {
+ exports.Spec = jasmineRequire.Spec;
+}
+
+getJasmineRequireObj().Env = function(j$) {
+ function Env(options) {
+ options = options || {};
+
+ var self = this;
+ var global = options.global || j$.getGlobal();
+
+ var totalSpecsDefined = 0;
+
+ var catchExceptions = true;
+
+ var realSetTimeout = j$.getGlobal().setTimeout;
+ var realClearTimeout = j$.getGlobal().clearTimeout;
+ this.clock = new j$.Clock(global, new j$.DelayedFunctionScheduler());
+
+ var runnableLookupTable = {};
+
+ var spies = [];
+
+ var currentSpec = null;
+ var currentSuite = null;
+
+ var reporter = new j$.ReportDispatcher([
+ "jasmineStarted",
+ "jasmineDone",
+ "suiteStarted",
+ "suiteDone",
+ "specStarted",
+ "specDone"
+ ]);
+
+ this.specFilter = function() {
+ return true;
+ };
+
+ var equalityTesters = [];
+
+ var customEqualityTesters = [];
+ this.addCustomEqualityTester = function(tester) {
+ customEqualityTesters.push(tester);
+ };
+
+ j$.Expectation.addCoreMatchers(j$.matchers);
+
+ var nextSpecId = 0;
+ var getNextSpecId = function() {
+ return 'spec' + nextSpecId++;
+ };
+
+ var nextSuiteId = 0;
+ var getNextSuiteId = function() {
+ return 'suite' + nextSuiteId++;
+ };
+
+ var expectationFactory = function(actual, spec) {
+ return j$.Expectation.Factory({
+ util: j$.matchersUtil,
+ customEqualityTesters: customEqualityTesters,
+ actual: actual,
+ addExpectationResult: addExpectationResult
+ });
+
+ function addExpectationResult(passed, result) {
+ return spec.addExpectationResult(passed, result);
+ }
+ };
+
+ var specStarted = function(spec) {
+ currentSpec = spec;
+ reporter.specStarted(spec.result);
+ };
+
+ var beforeFns = function(suite) {
+ return function() {
+ var befores = [];
+ while(suite) {
+ befores = befores.concat(suite.beforeFns);
+ suite = suite.parentSuite;
+ }
+ return befores.reverse();
+ };
+ };
+
+ var afterFns = function(suite) {
+ return function() {
+ var afters = [];
+ while(suite) {
+ afters = afters.concat(suite.afterFns);
+ suite = suite.parentSuite;
+ }
+ return afters;
+ };
+ };
+
+ var getSpecName = function(spec, suite) {
+ return suite.getFullName() + ' ' + spec.description;
+ };
+
+ // TODO: we may just be able to pass in the fn instead of wrapping here
+ var buildExpectationResult = j$.buildExpectationResult,
+ exceptionFormatter = new j$.ExceptionFormatter(),
+ expectationResultFactory = function(attrs) {
+ attrs.messageFormatter = exceptionFormatter.message;
+ attrs.stackFormatter = exceptionFormatter.stack;
+
+ return buildExpectationResult(attrs);
+ };
+
+ // TODO: fix this naming, and here's where the value comes in
+ this.catchExceptions = function(value) {
+ catchExceptions = !!value;
+ return catchExceptions;
+ };
+
+ this.catchingExceptions = function() {
+ return catchExceptions;
+ };
+
+ var maximumSpecCallbackDepth = 20;
+ var currentSpecCallbackDepth = 0;
+
+ function clearStack(fn) {
+ currentSpecCallbackDepth++;
+ if (currentSpecCallbackDepth >= maximumSpecCallbackDepth) {
+ currentSpecCallbackDepth = 0;
+ realSetTimeout(fn, 0);
+ } else {
+ fn();
+ }
+ }
+
+ var catchException = function(e) {
+ return j$.Spec.isPendingSpecException(e) || catchExceptions;
+ };
+
+ var queueRunnerFactory = function(options) {
+ options.catchException = catchException;
+ options.clearStack = options.clearStack || clearStack;
+
+ new j$.QueueRunner(options).execute();
+ };
+
+ var topSuite = new j$.Suite({
+ env: this,
+ id: getNextSuiteId(),
+ description: 'Jasmine__TopLevel__Suite',
+ queueRunner: queueRunnerFactory,
+ resultCallback: function() {} // TODO - hook this up
+ });
+ runnableLookupTable[topSuite.id] = topSuite;
+ currentSuite = topSuite;
+
+ this.topSuite = function() {
+ return topSuite;
+ };
+
+ this.execute = function(runnablesToRun) {
+ runnablesToRun = runnablesToRun || [topSuite.id];
+
+ var allFns = [];
+ for(var i = 0; i < runnablesToRun.length; i++) {
+ var runnable = runnableLookupTable[runnablesToRun[i]];
+ allFns.push((function(runnable) { return function(done) { runnable.execute(done); }; })(runnable));
+ }
+
+ reporter.jasmineStarted({
+ totalSpecsDefined: totalSpecsDefined
+ });
+
+ queueRunnerFactory({fns: allFns, onComplete: reporter.jasmineDone});
+ };
+
+ this.addReporter = function(reporterToAdd) {
+ reporter.addReporter(reporterToAdd);
+ };
+
+ this.addMatchers = function(matchersToAdd) {
+ j$.Expectation.addMatchers(matchersToAdd);
+ };
+
+ this.spyOn = function(obj, methodName) {
+ if (j$.util.isUndefined(obj)) {
+ throw new Error("spyOn could not find an object to spy upon for " + methodName + "()");
+ }
+
+ if (j$.util.isUndefined(obj[methodName])) {
+ throw new Error(methodName + '() method does not exist');
+ }
+
+ if (obj[methodName] && j$.isSpy(obj[methodName])) {
+ //TODO?: should this return the current spy? Downside: may cause user confusion about spy state
+ throw new Error(methodName + ' has already been spied upon');
+ }
+
+ var spy = j$.createSpy(methodName, obj[methodName]);
+
+ spies.push({
+ spy: spy,
+ baseObj: obj,
+ methodName: methodName,
+ originalValue: obj[methodName]
+ });
+
+ obj[methodName] = spy;
+
+ return spy;
+ };
+
+ var suiteFactory = function(description) {
+ var suite = new j$.Suite({
+ env: self,
+ id: getNextSuiteId(),
+ description: description,
+ parentSuite: currentSuite,
+ queueRunner: queueRunnerFactory,
+ onStart: suiteStarted,
+ resultCallback: function(attrs) {
+ reporter.suiteDone(attrs);
+ }
+ });
+
+ runnableLookupTable[suite.id] = suite;
+ return suite;
+ };
+
+ this.describe = function(description, specDefinitions) {
+ var suite = suiteFactory(description);
+
+ var parentSuite = currentSuite;
+ parentSuite.addChild(suite);
+ currentSuite = suite;
+
+ var declarationError = null;
+ try {
+ specDefinitions.call(suite);
+ } catch (e) {
+ declarationError = e;
+ }
+
+ if (declarationError) {
+ this.it("encountered a declaration exception", function() {
+ throw declarationError;
+ });
+ }
+
+ currentSuite = parentSuite;
+
+ return suite;
+ };
+
+ this.xdescribe = function(description, specDefinitions) {
+ var suite = this.describe(description, specDefinitions);
+ suite.disable();
+ return suite;
+ };
+
+ var specFactory = function(description, fn, suite) {
+ totalSpecsDefined++;
+
+ var spec = new j$.Spec({
+ id: getNextSpecId(),
+ beforeFns: beforeFns(suite),
+ afterFns: afterFns(suite),
+ expectationFactory: expectationFactory,
+ exceptionFormatter: exceptionFormatter,
+ resultCallback: specResultCallback,
+ getSpecName: function(spec) {
+ return getSpecName(spec, suite);
+ },
+ onStart: specStarted,
+ description: description,
+ expectationResultFactory: expectationResultFactory,
+ queueRunnerFactory: queueRunnerFactory,
+ fn: fn,
+ timer: {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}
+ });
+
+ runnableLookupTable[spec.id] = spec;
+
+ if (!self.specFilter(spec)) {
+ spec.disable();
+ }
+
+ return spec;
+
+ function removeAllSpies() {
+ for (var i = 0; i < spies.length; i++) {
+ var spyEntry = spies[i];
+ spyEntry.baseObj[spyEntry.methodName] = spyEntry.originalValue;
+ }
+ spies = [];
+ }
+
+ function specResultCallback(result) {
+ removeAllSpies();
+ j$.Expectation.resetMatchers();
+ customEqualityTesters = [];
+ currentSpec = null;
+ reporter.specDone(result);
+ }
+ };
+
+ var suiteStarted = function(suite) {
+ reporter.suiteStarted(suite.result);
+ };
+
+ this.it = function(description, fn) {
+ var spec = specFactory(description, fn, currentSuite);
+ currentSuite.addChild(spec);
+ return spec;
+ };
+
+ this.xit = function(description, fn) {
+ var spec = this.it(description, fn);
+ spec.pend();
+ return spec;
+ };
+
+ this.expect = function(actual) {
+ return currentSpec.expect(actual);
+ };
+
+ this.beforeEach = function(beforeEachFunction) {
+ currentSuite.beforeEach(beforeEachFunction);
+ };
+
+ this.afterEach = function(afterEachFunction) {
+ currentSuite.afterEach(afterEachFunction);
+ };
+
+ this.pending = function() {
+ throw j$.Spec.pendingSpecExceptionMessage;
+ };
+ }
+
+ return Env;
+};
+
+getJasmineRequireObj().JsApiReporter = function() {
+
+ var noopTimer = {
+ start: function(){},
+ elapsed: function(){ return 0; }
+ };
+
+ function JsApiReporter(options) {
+ var timer = options.timer || noopTimer,
+ status = "loaded";
+
+ this.started = false;
+ this.finished = false;
+
+ this.jasmineStarted = function() {
+ this.started = true;
+ status = 'started';
+ timer.start();
+ };
+
+ var executionTime;
+
+ this.jasmineDone = function() {
+ this.finished = true;
+ executionTime = timer.elapsed();
+ status = 'done';
+ };
+
+ this.status = function() {
+ return status;
+ };
+
+ var suites = {};
+
+ this.suiteStarted = function(result) {
+ storeSuite(result);
+ };
+
+ this.suiteDone = function(result) {
+ storeSuite(result);
+ };
+
+ function storeSuite(result) {
+ suites[result.id] = result;
+ }
+
+ this.suites = function() {
+ return suites;
+ };
+
+ var specs = [];
+ this.specStarted = function(result) { };
+
+ this.specDone = function(result) {
+ specs.push(result);
+ };
+
+ this.specResults = function(index, length) {
+ return specs.slice(index, index + length);
+ };
+
+ this.specs = function() {
+ return specs;
+ };
+
+ this.executionTime = function() {
+ return executionTime;
+ };
+
+ }
+
+ return JsApiReporter;
+};
+
+getJasmineRequireObj().Any = function() {
+
+ function Any(expectedObject) {
+ this.expectedObject = expectedObject;
+ }
+
+ Any.prototype.jasmineMatches = function(other) {
+ if (this.expectedObject == String) {
+ return typeof other == 'string' || other instanceof String;
+ }
+
+ if (this.expectedObject == Number) {
+ return typeof other == 'number' || other instanceof Number;
+ }
+
+ if (this.expectedObject == Function) {
+ return typeof other == 'function' || other instanceof Function;
+ }
+
+ if (this.expectedObject == Object) {
+ return typeof other == 'object';
+ }
+
+ if (this.expectedObject == Boolean) {
+ return typeof other == 'boolean';
+ }
+
+ return other instanceof this.expectedObject;
+ };
+
+ Any.prototype.jasmineToString = function() {
+ return '<jasmine.any(' + this.expectedClass + ')>';
+ };
+
+ return Any;
+};
+
+getJasmineRequireObj().CallTracker = function() {
+
+ function CallTracker() {
+ var calls = [];
+
+ this.track = function(context) {
+ calls.push(context);
+ };
+
+ this.any = function() {
+ return !!calls.length;
+ };
+
+ this.count = function() {
+ return calls.length;
+ };
+
+ this.argsFor = function(index) {
+ var call = calls[index];
+ return call ? call.args : [];
+ };
+
+ this.all = function() {
+ return calls;
+ };
+
+ this.allArgs = function() {
+ var callArgs = [];
+ for(var i = 0; i < calls.length; i++){
+ callArgs.push(calls[i].args);
+ }
+
+ return callArgs;
+ };
+
+ this.first = function() {
+ return calls[0];
+ };
+
+ this.mostRecent = function() {
+ return calls[calls.length - 1];
+ };
+
+ this.reset = function() {
+ calls = [];
+ };
+ }
+
+ return CallTracker;
+};
+
+getJasmineRequireObj().Clock = function() {
+ function Clock(global, delayedFunctionScheduler) {
+ var self = this,
+ realTimingFunctions = {
+ setTimeout: global.setTimeout,
+ clearTimeout: global.clearTimeout,
+ setInterval: global.setInterval,
+ clearInterval: global.clearInterval
+ },
+ fakeTimingFunctions = {
+ setTimeout: setTimeout,
+ clearTimeout: clearTimeout,
+ setInterval: setInterval,
+ clearInterval: clearInterval
+ },
+ installed = false,
+ timer;
+
+ self.install = function() {
+ replace(global, fakeTimingFunctions);
+ timer = fakeTimingFunctions;
+ installed = true;
+ };
+
+ self.uninstall = function() {
+ delayedFunctionScheduler.reset();
+ replace(global, realTimingFunctions);
+ timer = realTimingFunctions;
+ installed = false;
+ };
+
+ self.setTimeout = function(fn, delay, params) {
+ if (legacyIE()) {
+ if (arguments.length > 2) {
+ throw new Error("IE < 9 cannot support extra params to setTimeout without a polyfill");
+ }
+ return timer.setTimeout(fn, delay);
+ }
+ return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]);
+ };
+
+ self.setInterval = function(fn, delay, params) {
+ if (legacyIE()) {
+ if (arguments.length > 2) {
+ throw new Error("IE < 9 cannot support extra params to setInterval without a polyfill");
+ }
+ return timer.setInterval(fn, delay);
+ }
+ return Function.prototype.apply.apply(timer.setInterval, [global, arguments]);
+ };
+
+ self.clearTimeout = function(id) {
+ return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
+ };
+
+ self.clearInterval = function(id) {
+ return Function.prototype.call.apply(timer.clearInterval, [global, id]);
+ };
+
+ self.tick = function(millis) {
+ if (installed) {
+ delayedFunctionScheduler.tick(millis);
+ } else {
+ throw new Error("Mock clock is not installed, use jasmine.clock().install()");
+ }
+ };
+
+ return self;
+
+ function legacyIE() {
+ //if these methods are polyfilled, apply will be present
+ return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply;
+ }
+
+ function replace(dest, source) {
+ for (var prop in source) {
+ dest[prop] = source[prop];
+ }
+ }
+
+ function setTimeout(fn, delay) {
+ return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
+ }
+
+ function clearTimeout(id) {
+ return delayedFunctionScheduler.removeFunctionWithId(id);
+ }
+
+ function setInterval(fn, interval) {
+ return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
+ }
+
+ function clearInterval(id) {
+ return delayedFunctionScheduler.removeFunctionWithId(id);
+ }
+
+ function argSlice(argsObj, n) {
+ return Array.prototype.slice.call(argsObj, 2);
+ }
+ }
+
+ return Clock;
+};
+
+getJasmineRequireObj().DelayedFunctionScheduler = function() {
+ function DelayedFunctionScheduler() {
+ var self = this;
+ var scheduledLookup = [];
+ var scheduledFunctions = {};
+ var currentTime = 0;
+ var delayedFnCount = 0;
+
+ self.tick = function(millis) {
+ millis = millis || 0;
+ var endTime = currentTime + millis;
+
+ runScheduledFunctions(endTime);
+ currentTime = endTime;
+ };
+
+ self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
+ var f;
+ if (typeof(funcToCall) === 'string') {
+ /* jshint evil: true */
+ f = function() { return eval(funcToCall); };
+ /* jshint evil: false */
+ } else {
+ f = funcToCall;
+ }
+
+ millis = millis || 0;
+ timeoutKey = timeoutKey || ++delayedFnCount;
+ runAtMillis = runAtMillis || (currentTime + millis);
+
+ var funcToSchedule = {
+ runAtMillis: runAtMillis,
+ funcToCall: f,
+ recurring: recurring,
+ params: params,
+ timeoutKey: timeoutKey,
+ millis: millis
+ };
+
+ if (runAtMillis in scheduledFunctions) {
+ scheduledFunctions[runAtMillis].push(funcToSchedule);
+ } else {
+ scheduledFunctions[runAtMillis] = [funcToSchedule];
+ scheduledLookup.push(runAtMillis);
+ scheduledLookup.sort(function (a, b) {
+ return a - b;
+ });
+ }
+
+ return timeoutKey;
+ };
+
+ self.removeFunctionWithId = function(timeoutKey) {
+ for (var runAtMillis in scheduledFunctions) {
+ var funcs = scheduledFunctions[runAtMillis];
+ var i = indexOfFirstToPass(funcs, function (func) {
+ return func.timeoutKey === timeoutKey;
+ });
+
+ if (i > -1) {
+ if (funcs.length === 1) {
+ delete scheduledFunctions[runAtMillis];
+ deleteFromLookup(runAtMillis);
+ } else {
+ funcs.splice(i, 1);
+ }
+
+ // intervals get rescheduled when executed, so there's never more
+ // than a single scheduled function with a given timeoutKey
+ break;
+ }
+ }
+ };
+
+ self.reset = function() {
+ currentTime = 0;
+ scheduledLookup = [];
+ scheduledFunctions = {};
+ delayedFnCount = 0;
+ };
+
+ return self;
+
+ function indexOfFirstToPass(array, testFn) {
+ var index = -1;
+
+ for (var i = 0; i < array.length; ++i) {
+ if (testFn(array[i])) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ function deleteFromLookup(key) {
+ var value = Number(key);
+ var i = indexOfFirstToPass(scheduledLookup, function (millis) {
+ return millis === value;
+ });
+
+ if (i > -1) {
+ scheduledLookup.splice(i, 1);
+ }
+ }
+
+ function reschedule(scheduledFn) {
+ self.scheduleFunction(scheduledFn.funcToCall,
+ scheduledFn.millis,
+ scheduledFn.params,
+ true,
+ scheduledFn.timeoutKey,
+ scheduledFn.runAtMillis + scheduledFn.millis);
+ }
+
+ function runScheduledFunctions(endTime) {
+ if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
+ return;
+ }
+
+ do {
+ currentTime = scheduledLookup.shift();
+
+ var funcsToRun = scheduledFunctions[currentTime];
+ delete scheduledFunctions[currentTime];
+
+ for (var i = 0; i < funcsToRun.length; ++i) {
+ var funcToRun = funcsToRun[i];
+ funcToRun.funcToCall.apply(null, funcToRun.params || []);
+
+ if (funcToRun.recurring) {
+ reschedule(funcToRun);
+ }
+ }
+ } while (scheduledLookup.length > 0 &&
+ // checking first if we're out of time prevents setTimeout(0)
+ // scheduled in a funcToRun from forcing an extra iteration
+ currentTime !== endTime &&
+ scheduledLookup[0] <= endTime);
+ }
+ }
+
+ return DelayedFunctionScheduler;
+};
+
+getJasmineRequireObj().ExceptionFormatter = function() {
+ function ExceptionFormatter() {
+ this.message = function(error) {
+ var message = error.name +
+ ': ' +
+ error.message;
+
+ if (error.fileName || error.sourceURL) {
+ message += " in " + (error.fileName || error.sourceURL);
+ }
+
+ if (error.line || error.lineNumber) {
+ message += " (line " + (error.line || error.lineNumber) + ")";
+ }
+
+ return message;
+ };
+
+ this.stack = function(error) {
+ return error ? error.stack : null;
+ };
+ }
+
+ return ExceptionFormatter;
+};
+
+getJasmineRequireObj().Expectation = function() {
+
+ var matchers = {};
+
+ function Expectation(options) {
+ this.util = options.util || { buildFailureMessage: function() {} };
+ this.customEqualityTesters = options.customEqualityTesters || [];
+ this.actual = options.actual;
+ this.addExpectationResult = options.addExpectationResult || function(){};
+ this.isNot = options.isNot;
+
+ for (var matcherName in matchers) {
+ this[matcherName] = matchers[matcherName];
+ }
+ }
+
+ Expectation.prototype.wrapCompare = function(name, matcherFactory) {
+ return function() {
+ var args = Array.prototype.slice.call(arguments, 0),
+ expected = args.slice(0),
+ message = "";
+
+ args.unshift(this.actual);
+
+ var matcher = matcherFactory(this.util, this.customEqualityTesters),
+ matcherCompare = matcher.compare;
+
+ function defaultNegativeCompare() {
+ var result = matcher.compare.apply(null, args);
+ result.pass = !result.pass;
+ return result;
+ }
+
+ if (this.isNot) {
+ matcherCompare = matcher.negativeCompare || defaultNegativeCompare;
+ }
+
+ var result = matcherCompare.apply(null, args);
+
+ if (!result.pass) {
+ if (!result.message) {
+ args.unshift(this.isNot);
+ args.unshift(name);
+ message = this.util.buildFailureMessage.apply(null, args);
+ } else {
+ message = result.message;
+ }
+ }
+
+ if (expected.length == 1) {
+ expected = expected[0];
+ }
+
+ // TODO: how many of these params are needed?
+ this.addExpectationResult(
+ result.pass,
+ {
+ matcherName: name,
+ passed: result.pass,
+ message: message,
+ actual: this.actual,
+ expected: expected // TODO: this may need to be arrayified/sliced
+ }
+ );
+ };
+ };
+
+ Expectation.addCoreMatchers = function(matchers) {
+ var prototype = Expectation.prototype;
+ for (var matcherName in matchers) {
+ var matcher = matchers[matcherName];
+ prototype[matcherName] = prototype.wrapCompare(matcherName, matcher);
+ }
+ };
+
+ Expectation.addMatchers = function(matchersToAdd) {
+ for (var name in matchersToAdd) {
+ var matcher = matchersToAdd[name];
+ matchers[name] = Expectation.prototype.wrapCompare(name, matcher);
+ }
+ };
+
+ Expectation.resetMatchers = function() {
+ for (var name in matchers) {
+ delete matchers[name];
+ }
+ };
+
+ Expectation.Factory = function(options) {
+ options = options || {};
+
+ var expect = new Expectation(options);
+
+ // TODO: this would be nice as its own Object - NegativeExpectation
+ // TODO: copy instead of mutate options
+ options.isNot = true;
+ expect.not = new Expectation(options);
+
+ return expect;
+ };
+
+ return Expectation;
+};
+
+//TODO: expectation result may make more sense as a presentation of an expectation.
+getJasmineRequireObj().buildExpectationResult = function() {
+ function buildExpectationResult(options) {
+ var messageFormatter = options.messageFormatter || function() {},
+ stackFormatter = options.stackFormatter || function() {};
+
+ return {
+ matcherName: options.matcherName,
+ expected: options.expected,
+ actual: options.actual,
+ message: message(),
+ stack: stack(),
+ passed: options.passed
+ };
+
+ function message() {
+ if (options.passed) {
+ return "Passed.";
+ } else if (options.message) {
+ return options.message;
+ } else if (options.error) {
+ return messageFormatter(options.error);
+ }
+ return "";
+ }
+
+ function stack() {
+ if (options.passed) {
+ return "";
+ }
+
+ var error = options.error;
+ if (!error) {
+ try {
+ throw new Error(message());
+ } catch (e) {
+ error = e;
+ }
+ }
+ return stackFormatter(error);
+ }
+ }
+
+ return buildExpectationResult;
+};
+
+getJasmineRequireObj().ObjectContaining = function(j$) {
+
+ function ObjectContaining(sample) {
+ this.sample = sample;
+ }
+
+ ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
+ if (typeof(this.sample) !== "object") { throw new Error("You must provide an object to objectContaining, not '"+this.sample+"'."); }
+
+ mismatchKeys = mismatchKeys || [];
+ mismatchValues = mismatchValues || [];
+
+ var hasKey = function(obj, keyName) {
+ return obj !== null && !j$.util.isUndefined(obj[keyName]);
+ };
+
+ for (var property in this.sample) {
+ if (!hasKey(other, property) && hasKey(this.sample, property)) {
+ mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+ }
+ else if (!j$.matchersUtil.equals(this.sample[property], other[property])) {
+ mismatchValues.push("'" + property + "' was '" + (other[property] ? j$.util.htmlEscape(other[property].toString()) : other[property]) + "' in actual, but was '" + (this.sample[property] ? j$.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in expected.");
+ }
+ }
+
+ return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+ };
+
+ ObjectContaining.prototype.jasmineToString = function() {
+ return "<jasmine.objectContaining(" + j$.pp(this.sample) + ")>";
+ };
+
+ return ObjectContaining;
+};
+
+getJasmineRequireObj().pp = function(j$) {
+
+ function PrettyPrinter() {
+ this.ppNestLevel_ = 0;
+ }
+
+ PrettyPrinter.prototype.format = function(value) {
+ this.ppNestLevel_++;
+ try {
+ if (j$.util.isUndefined(value)) {
+ this.emitScalar('undefined');
+ } else if (value === null) {
+ this.emitScalar('null');
+ } else if (value === j$.getGlobal()) {
+ this.emitScalar('<global>');
+ } else if (value.jasmineToString) {
+ this.emitScalar(value.jasmineToString());
+ } else if (typeof value === 'string') {
+ this.emitString(value);
+ } else if (j$.isSpy(value)) {
+ this.emitScalar("spy on " + value.and.identity());
+ } else if (value instanceof RegExp) {
+ this.emitScalar(value.toString());
+ } else if (typeof value === 'function') {
+ this.emitScalar('Function');
+ } else if (typeof value.nodeType === 'number') {
+ this.emitScalar('HTMLNode');
+ } else if (value instanceof Date) {
+ this.emitScalar('Date(' + value + ')');
+ } else if (value.__Jasmine_been_here_before__) {
+ this.emitScalar('<circular reference: ' + (j$.isArray_(value) ? 'Array' : 'Object') + '>');
+ } else if (j$.isArray_(value) || j$.isA_('Object', value)) {
+ value.__Jasmine_been_here_before__ = true;
+ if (j$.isArray_(value)) {
+ this.emitArray(value);
+ } else {
+ this.emitObject(value);
+ }
+ delete value.__Jasmine_been_here_before__;
+ } else {
+ this.emitScalar(value.toString());
+ }
+ } finally {
+ this.ppNestLevel_--;
+ }
+ };
+
+ PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+ for (var property in obj) {
+ if (!obj.hasOwnProperty(property)) { continue; }
+ if (property == '__Jasmine_been_here_before__') { continue; }
+ fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) &&
+ obj.__lookupGetter__(property) !== null) : false);
+ }
+ };
+
+ PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_;
+ PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_;
+ PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_;
+ PrettyPrinter.prototype.emitString = j$.unimplementedMethod_;
+
+ function StringPrettyPrinter() {
+ PrettyPrinter.call(this);
+
+ this.string = '';
+ }
+
+ j$.util.inherit(StringPrettyPrinter, PrettyPrinter);
+
+ StringPrettyPrinter.prototype.emitScalar = function(value) {
+ this.append(value);
+ };
+
+ StringPrettyPrinter.prototype.emitString = function(value) {
+ this.append("'" + value + "'");
+ };
+
+ StringPrettyPrinter.prototype.emitArray = function(array) {
+ if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+ this.append("Array");
+ return;
+ }
+
+ this.append('[ ');
+ for (var i = 0; i < array.length; i++) {
+ if (i > 0) {
+ this.append(', ');
+ }
+ this.format(array[i]);
+ }
+ this.append(' ]');
+ };
+
+ StringPrettyPrinter.prototype.emitObject = function(obj) {
+ if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+ this.append("Object");
+ return;
+ }
+
+ var self = this;
+ this.append('{ ');
+ var first = true;
+
+ this.iterateObject(obj, function(property, isGetter) {
+ if (first) {
+ first = false;
+ } else {
+ self.append(', ');
+ }
+
+ self.append(property);
+ self.append(' : ');
+ if (isGetter) {
+ self.append('<getter>');
+ } else {
+ self.format(obj[property]);
+ }
+ });
+
+ this.append(' }');
+ };
+
+ StringPrettyPrinter.prototype.append = function(value) {
+ this.string += value;
+ };
+
+ return function(value) {
+ var stringPrettyPrinter = new StringPrettyPrinter();
+ stringPrettyPrinter.format(value);
+ return stringPrettyPrinter.string;
+ };
+};
+
+getJasmineRequireObj().QueueRunner = function() {
+
+ function QueueRunner(attrs) {
+ this.fns = attrs.fns || [];
+ this.onComplete = attrs.onComplete || function() {};
+ this.clearStack = attrs.clearStack || function(fn) {fn();};
+ this.onException = attrs.onException || function() {};
+ this.catchException = attrs.catchException || function() { return true; };
+ this.userContext = {};
+ }
+
+ QueueRunner.prototype.execute = function() {
+ this.run(this.fns, 0);
+ };
+
+ QueueRunner.prototype.run = function(fns, recursiveIndex) {
+ var length = fns.length,
+ self = this,
+ iterativeIndex;
+
+ for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) {
+ var fn = fns[iterativeIndex];
+ if (fn.length > 0) {
+ return attemptAsync(fn);
+ } else {
+ attemptSync(fn);
+ }
+ }
+
+ var runnerDone = iterativeIndex >= length;
+
+ if (runnerDone) {
+ this.clearStack(this.onComplete);
+ }
+
+ function attemptSync(fn) {
+ try {
+ fn.call(self.userContext);
+ } catch (e) {
+ handleException(e);
+ }
+ }
+
+ function attemptAsync(fn) {
+ var next = function () { self.run(fns, iterativeIndex + 1); };
+
+ try {
+ fn.call(self.userContext, next);
+ } catch (e) {
+ handleException(e);
+ next();
+ }
+ }
+
+ function handleException(e) {
+ self.onException(e);
+ if (!self.catchException(e)) {
+ //TODO: set a var when we catch an exception and
+ //use a finally block to close the loop in a nice way..
+ throw e;
+ }
+ }
+ };
+
+ return QueueRunner;
+};
+
+getJasmineRequireObj().ReportDispatcher = function() {
+ function ReportDispatcher(methods) {
+
+ var dispatchedMethods = methods || [];
+
+ for (var i = 0; i < dispatchedMethods.length; i++) {
+ var method = dispatchedMethods[i];
+ this[method] = (function(m) {
+ return function() {
+ dispatch(m, arguments);
+ };
+ }(method));
+ }
+
+ var reporters = [];
+
+ this.addReporter = function(reporter) {
+ reporters.push(reporter);
+ };
+
+ return this;
+
+ function dispatch(method, args) {
+ for (var i = 0; i < reporters.length; i++) {
+ var reporter = reporters[i];
+ if (reporter[method]) {
+ reporter[method].apply(reporter, args);
+ }
+ }
+ }
+ }
+
+ return ReportDispatcher;
+};
+
+
+getJasmineRequireObj().SpyStrategy = function() {
+
+ function SpyStrategy(options) {
+ options = options || {};
+
+ var identity = options.name || "unknown",
+ originalFn = options.fn || function() {},
+ getSpy = options.getSpy || function() {},
+ plan = function() {};
+
+ this.identity = function() {
+ return identity;
+ };
+
+ this.exec = function() {
+ return plan.apply(this, arguments);
+ };
+
+ this.callThrough = function() {
+ plan = originalFn;
+ return getSpy();
+ };
+
+ this.returnValue = function(value) {
+ plan = function() {
+ return value;
+ };
+ return getSpy();
+ };
+
+ this.throwError = function(something) {
+ var error = (something instanceof Error) ? something : new Error(something);
+ plan = function() {
+ throw error;
+ };
+ return getSpy();
+ };
+
+ this.callFake = function(fn) {
+ plan = fn;
+ return getSpy();
+ };
+
+ this.stub = function(fn) {
+ plan = function() {};
+ return getSpy();
+ };
+ }
+
+ return SpyStrategy;
+};
+
+getJasmineRequireObj().Suite = function() {
+ function Suite(attrs) {
+ this.env = attrs.env;
+ this.id = attrs.id;
+ this.parentSuite = attrs.parentSuite;
+ this.description = attrs.description;
+ this.onStart = attrs.onStart || function() {};
+ this.resultCallback = attrs.resultCallback || function() {};
+ this.clearStack = attrs.clearStack || function(fn) {fn();};
+
+ this.beforeFns = [];
+ this.afterFns = [];
+ this.queueRunner = attrs.queueRunner || function() {};
+ this.disabled = false;
+
+ this.children = [];
+
+ this.result = {
+ id: this.id,
+ status: this.disabled ? 'disabled' : '',
+ description: this.description,
+ fullName: this.getFullName()
+ };
+ }
+
+ Suite.prototype.getFullName = function() {
+ var fullName = this.description;
+ for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+ if (parentSuite.parentSuite) {
+ fullName = parentSuite.description + ' ' + fullName;
+ }
+ }
+ return fullName;
+ };
+
+ Suite.prototype.disable = function() {
+ this.disabled = true;
+ };
+
+ Suite.prototype.beforeEach = function(fn) {
+ this.beforeFns.unshift(fn);
+ };
+
+ Suite.prototype.afterEach = function(fn) {
+ this.afterFns.unshift(fn);
+ };
+
+ Suite.prototype.addChild = function(child) {
+ this.children.push(child);
+ };
+
+ Suite.prototype.execute = function(onComplete) {
+ var self = this;
+ if (this.disabled) {
+ complete();
+ return;
+ }
+
+ var allFns = [];
+
+ for (var i = 0; i < this.children.length; i++) {
+ allFns.push(wrapChildAsAsync(this.children[i]));
+ }
+
+ this.onStart(this);
+
+ this.queueRunner({
+ fns: allFns,
+ onComplete: complete
+ });
+
+ function complete() {
+ self.resultCallback(self.result);
+
+ if (onComplete) {
+ onComplete();
+ }
+ }
+
+ function wrapChildAsAsync(child) {
+ return function(done) { child.execute(done); };
+ }
+ };
+
+ return Suite;
+};
+
+if (typeof window == void 0 && typeof exports == "object") {
+ exports.Suite = jasmineRequire.Suite;
+}
+
+getJasmineRequireObj().Timer = function() {
+ function Timer(options) {
+ options = options || {};
+
+ var now = options.now || function() { return new Date().getTime(); },
+ startTime;
+
+ this.start = function() {
+ startTime = now();
+ };
+
+ this.elapsed = function() {
+ return now() - startTime;
+ };
+ }
+
+ return Timer;
+};
+
+getJasmineRequireObj().matchersUtil = function(j$) {
+ // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
+
+ return {
+ equals: function(a, b, customTesters) {
+ customTesters = customTesters || [];
+
+ return eq(a, b, [], [], customTesters);
+ },
+
+ contains: function(haystack, needle, customTesters) {
+ customTesters = customTesters || [];
+
+ if (Object.prototype.toString.apply(haystack) === "[object Array]") {
+ for (var i = 0; i < haystack.length; i++) {
+ if (eq(haystack[i], needle, [], [], customTesters)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return haystack.indexOf(needle) >= 0;
+ },
+
+ buildFailureMessage: function() {
+ var args = Array.prototype.slice.call(arguments, 0),
+ matcherName = args[0],
+ isNot = args[1],
+ actual = args[2],
+ expected = args.slice(3),
+ englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+
+ var message = "Expected " +
+ j$.pp(actual) +
+ (isNot ? " not " : " ") +
+ englishyPredicate;
+
+ if (expected.length > 0) {
+ for (var i = 0; i < expected.length; i++) {
+ if (i > 0) {
+ message += ",";
+ }
+ message += " " + j$.pp(expected[i]);
+ }
+ }
+
+ return message + ".";
+ }
+ };
+
+ // Equality function lovingly adapted from isEqual in
+ // [Underscore](http://underscorejs.org)
+ function eq(a, b, aStack, bStack, customTesters) {
+ var result = true;
+
+ for (var i = 0; i < customTesters.length; i++) {
+ var customTesterResult = customTesters[i](a, b);
+ if (!j$.util.isUndefined(customTesterResult)) {
+ return customTesterResult;
+ }
+ }
+
+ if (a instanceof j$.Any) {
+ result = a.jasmineMatches(b);
+ if (result) {
+ return true;
+ }
+ }
+
+ if (b instanceof j$.Any) {
+ result = b.jasmineMatches(a);
+ if (result) {
+ return true;
+ }
+ }
+
+ if (b instanceof j$.ObjectContaining) {
+ result = b.jasmineMatches(a);
+ if (result) {
+ return true;
+ }
+ }
+
+ if (a instanceof Error && b instanceof Error) {
+ return a.message == b.message;
+ }
+
+ // Identical objects are equal. `0 === -0`, but they aren't identical.
+ // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+ if (a === b) { return a !== 0 || 1 / a == 1 / b; }
+ // A strict comparison is necessary because `null == undefined`.
+ if (a === null || b === null) { return a === b; }
+ var className = Object.prototype.toString.call(a);
+ if (className != Object.prototype.toString.call(b)) { return false; }
+ switch (className) {
+ // Strings, numbers, dates, and booleans are compared by value.
+ case '[object String]':
+ // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+ // equivalent to `new String("5")`.
+ return a == String(b);
+ case '[object Number]':
+ // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+ // other numeric values.
+ return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+ case '[object Date]':
+ case '[object Boolean]':
+ // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+ // millisecond representations. Note that invalid dates with millisecond representations
+ // of `NaN` are not equivalent.
+ return +a == +b;
+ // RegExps are compared by their source patterns and flags.
+ case '[object RegExp]':
+ return a.source == b.source &&
+ a.global == b.global &&
+ a.multiline == b.multiline &&
+ a.ignoreCase == b.ignoreCase;
+ }
+ if (typeof a != 'object' || typeof b != 'object') { return false; }
+ // Assume equality for cyclic structures. The algorithm for detecting cyclic
+ // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+ var length = aStack.length;
+ while (length--) {
+ // Linear search. Performance is inversely proportional to the number of
+ // unique nested structures.
+ if (aStack[length] == a) { return bStack[length] == b; }
+ }
+ // Add the first object to the stack of traversed objects.
+ aStack.push(a);
+ bStack.push(b);
+ var size = 0;
+ // Recursively compare objects and arrays.
+ if (className == '[object Array]') {
+ // Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length;
+ result = size == b.length;
+ if (result) {
+ // Deep compare the contents, ignoring non-numeric properties.
+ while (size--) {
+ if (!(result = eq(a[size], b[size], aStack, bStack, customTesters))) { break; }
+ }
+ }
+ } else {
+ // Objects with different constructors are not equivalent, but `Object`s
+ // from different frames are.
+ var aCtor = a.constructor, bCtor = b.constructor;
+ if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
+ isFunction(bCtor) && (bCtor instanceof bCtor))) {
+ return false;
+ }
+ // Deep compare objects.
+ for (var key in a) {
+ if (has(a, key)) {
+ // Count the expected number of properties.
+ size++;
+ // Deep compare each member.
+ if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters))) { break; }
+ }
+ }
+ // Ensure that both objects contain the same number of properties.
+ if (result) {
+ for (key in b) {
+ if (has(b, key) && !(size--)) { break; }
+ }
+ result = !size;
+ }
+ }
+ // Remove the first object from the stack of traversed objects.
+ aStack.pop();
+ bStack.pop();
+
+ return result;
+
+ function has(obj, key) {
+ return obj.hasOwnProperty(key);
+ }
+
+ function isFunction(obj) {
+ return typeof obj === 'function';
+ }
+ }
+};
+
+getJasmineRequireObj().toBe = function() {
+ function toBe() {
+ return {
+ compare: function(actual, expected) {
+ return {
+ pass: actual === expected
+ };
+ }
+ };
+ }
+
+ return toBe;
+};
+
+getJasmineRequireObj().toBeCloseTo = function() {
+
+ function toBeCloseTo() {
+ return {
+ compare: function(actual, expected, precision) {
+ if (precision !== 0) {
+ precision = precision || 2;
+ }
+
+ return {
+ pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2)
+ };
+ }
+ };
+ }
+
+ return toBeCloseTo;
+};
+
+getJasmineRequireObj().toBeDefined = function() {
+ function toBeDefined() {
+ return {
+ compare: function(actual) {
+ return {
+ pass: (void 0 !== actual)
+ };
+ }
+ };
+ }
+
+ return toBeDefined;
+};
+
+getJasmineRequireObj().toBeFalsy = function() {
+ function toBeFalsy() {
+ return {
+ compare: function(actual) {
+ return {
+ pass: !!!actual
+ };
+ }
+ };
+ }
+
+ return toBeFalsy;
+};
+
+getJasmineRequireObj().toBeGreaterThan = function() {
+
+ function toBeGreaterThan() {
+ return {
+ compare: function(actual, expected) {
+ return {
+ pass: actual > expected
+ };
+ }
+ };
+ }
+
+ return toBeGreaterThan;
+};
+
+
+getJasmineRequireObj().toBeLessThan = function() {
+ function toBeLessThan() {
+ return {
+
+ compare: function(actual, expected) {
+ return {
+ pass: actual < expected
+ };
+ }
+ };
+ }
+
+ return toBeLessThan;
+};
+getJasmineRequireObj().toBeNaN = function(j$) {
+
+ function toBeNaN() {
+ return {
+ compare: function(actual) {
+ var result = {
+ pass: (actual !== actual)
+ };
+
+ if (result.pass) {
+ result.message = "Expected actual not to be NaN.";
+ } else {
+ result.message = "Expected " + j$.pp(actual) + " to be NaN.";
+ }
+
+ return result;
+ }
+ };
+ }
+
+ return toBeNaN;
+};
+
+getJasmineRequireObj().toBeNull = function() {
+
+ function toBeNull() {
+ return {
+ compare: function(actual) {
+ return {
+ pass: actual === null
+ };
+ }
+ };
+ }
+
+ return toBeNull;
+};
+
+getJasmineRequireObj().toBeTruthy = function() {
+
+ function toBeTruthy() {
+ return {
+ compare: function(actual) {
+ return {
+ pass: !!actual
+ };
+ }
+ };
+ }
+
+ return toBeTruthy;
+};
+
+getJasmineRequireObj().toBeUndefined = function() {
+
+ function toBeUndefined() {
+ return {
+ compare: function(actual) {
+ return {
+ pass: void 0 === actual
+ };
+ }
+ };
+ }
+
+ return toBeUndefined;
+};
+
+getJasmineRequireObj().toContain = function() {
+ function toContain(util, customEqualityTesters) {
+ customEqualityTesters = customEqualityTesters || [];
+
+ return {
+ compare: function(actual, expected) {
+
+ return {
+ pass: util.contains(actual, expected, customEqualityTesters)
+ };
+ }
+ };
+ }
+
+ return toContain;
+};
+
+getJasmineRequireObj().toEqual = function() {
+
+ function toEqual(util, customEqualityTesters) {
+ customEqualityTesters = customEqualityTesters || [];
+
+ return {
+ compare: function(actual, expected) {
+ var result = {
+ pass: false
+ };
+
+ result.pass = util.equals(actual, expected, customEqualityTesters);
+
+ return result;
+ }
+ };
+ }
+
+ return toEqual;
+};
+
+getJasmineRequireObj().toHaveBeenCalled = function(j$) {
+
+ function toHaveBeenCalled() {
+ return {
+ compare: function(actual) {
+ var result = {};
+
+ if (!j$.isSpy(actual)) {
+ throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.');
+ }
+
+ if (arguments.length > 1) {
+ throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+ }
+
+ result.pass = actual.calls.any();
+
+ result.message = result.pass ?
+ "Expected spy " + actual.and.identity() + " not to have been called." :
+ "Expected spy " + actual.and.identity() + " to have been called.";
+
+ return result;
+ }
+ };
+ }
+
+ return toHaveBeenCalled;
+};
+
+getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
+
+ function toHaveBeenCalledWith(util) {
+ return {
+ compare: function() {
+ var args = Array.prototype.slice.call(arguments, 0),
+ actual = args[0],
+ expectedArgs = args.slice(1),
+ result = { pass: false };
+
+ if (!j$.isSpy(actual)) {
+ throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.');
+ }
+
+ if (!actual.calls.any()) {
+ result.message = "Expected spy " + actual.and.identity() + " to have been called with " + j$.pp(expectedArgs) + " but it was never called.";
+ return result;
+ }
+
+ if (util.contains(actual.calls.allArgs(), expectedArgs)) {
+ result.pass = true;
+ result.message = "Expected spy " + actual.and.identity() + " not to have been called with " + j$.pp(expectedArgs) + " but it was.";
+ } else {
+ result.message = "Expected spy " + actual.and.identity() + " to have been called with " + j$.pp(expectedArgs) + " but actual calls were " + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + ".";
+ }
+
+ return result;
+ }
+ };
+ }
+
+ return toHaveBeenCalledWith;
+};
+
+getJasmineRequireObj().toMatch = function() {
+
+ function toMatch() {
+ return {
+ compare: function(actual, expected) {
+ var regexp = new RegExp(expected);
+
+ return {
+ pass: regexp.test(actual)
+ };
+ }
+ };
+ }
+
+ return toMatch;
+};
+
+getJasmineRequireObj().toThrow = function(j$) {
+
+ function toThrow(util) {
+ return {
+ compare: function(actual, expected) {
+ var result = { pass: false },
+ threw = false,
+ thrown;
+
+ if (typeof actual != "function") {
+ throw new Error("Actual is not a Function");
+ }
+
+ try {
+ actual();
+ } catch (e) {
+ threw = true;
+ thrown = e;
+ }
+
+ if (!threw) {
+ result.message = "Expected function to throw an exception.";
+ return result;
+ }
+
+ if (arguments.length == 1) {
+ result.pass = true;
+ result.message = "Expected function not to throw, but it threw " + j$.pp(thrown) + ".";
+
+ return result;
+ }
+
+ if (util.equals(thrown, expected)) {
+ result.pass = true;
+ result.message = "Expected function not to throw " + j$.pp(expected) + ".";
+ } else {
+ result.message = "Expected function to throw " + j$.pp(expected) + ", but it threw " + j$.pp(thrown) + ".";
+ }
+
+ return result;
+ }
+ };
+ }
+
+ return toThrow;
+};
+
+getJasmineRequireObj().toThrowError = function(j$) {
+ function toThrowError (util) {
+ return {
+ compare: function(actual) {
+ var threw = false,
+ thrown,
+ errorType,
+ message,
+ regexp,
+ name,
+ constructorName;
+
+ if (typeof actual != "function") {
+ throw new Error("Actual is not a Function");
+ }
+
+ extractExpectedParams.apply(null, arguments);
+
+ try {
+ actual();
+ } catch (e) {
+ threw = true;
+ thrown = e;
+ }
+
+ if (!threw) {
+ return fail("Expected function to throw an Error.");
+ }
+
+ if (!(thrown instanceof Error)) {
+ return fail("Expected function to throw an Error, but it threw " + thrown + ".");
+ }
+
+ if (arguments.length == 1) {
+ return pass("Expected function not to throw an Error, but it threw " + fnNameFor(thrown) + ".");
+ }
+
+ if (errorType) {
+ name = fnNameFor(errorType);
+ constructorName = fnNameFor(thrown.constructor);
+ }
+
+ if (errorType && message) {
+ if (thrown.constructor == errorType && util.equals(thrown.message, message)) {
+ return pass("Expected function not to throw " + name + " with message \"" + message + "\".");
+ } else {
+ return fail("Expected function to throw " + name + " with message \"" + message +
+ "\", but it threw " + constructorName + " with message \"" + thrown.message + "\".");
+ }
+ }
+
+ if (errorType && regexp) {
+ if (thrown.constructor == errorType && regexp.test(thrown.message)) {
+ return pass("Expected function not to throw " + name + " with message matching " + regexp + ".");
+ } else {
+ return fail("Expected function to throw " + name + " with message matching " + regexp +
+ ", but it threw " + constructorName + " with message \"" + thrown.message + "\".");
+ }
+ }
+
+ if (errorType) {
+ if (thrown.constructor == errorType) {
+ return pass("Expected function not to throw " + name + ".");
+ } else {
+ return fail("Expected function to throw " + name + ", but it threw " + constructorName + ".");
+ }
+ }
+
+ if (message) {
+ if (thrown.message == message) {
+ return pass("Expected function not to throw an exception with message " + j$.pp(message) + ".");
+ } else {
+ return fail("Expected function to throw an exception with message " + j$.pp(message) +
+ ", but it threw an exception with message " + j$.pp(thrown.message) + ".");
+ }
+ }
+
+ if (regexp) {
+ if (regexp.test(thrown.message)) {
+ return pass("Expected function not to throw an exception with a message matching " + j$.pp(regexp) + ".");
+ } else {
+ return fail("Expected function to throw an exception with a message matching " + j$.pp(regexp) +
+ ", but it threw an exception with message " + j$.pp(thrown.message) + ".");
+ }
+ }
+
+ function fnNameFor(func) {
+ return func.name || func.toString().match(/^\s*function\s*(\w*)\s*\(/)[1];
+ }
+
+ function pass(notMessage) {
+ return {
+ pass: true,
+ message: notMessage
+ };
+ }
+
+ function fail(message) {
+ return {
+ pass: false,
+ message: message
+ };
+ }
+
+ function extractExpectedParams() {
+ if (arguments.length == 1) {
+ return;
+ }
+
+ if (arguments.length == 2) {
+ var expected = arguments[1];
+
+ if (expected instanceof RegExp) {
+ regexp = expected;
+ } else if (typeof expected == "string") {
+ message = expected;
+ } else if (checkForAnErrorType(expected)) {
+ errorType = expected;
+ }
+
+ if (!(errorType || message || regexp)) {
+ throw new Error("Expected is not an Error, string, or RegExp.");
+ }
+ } else {
+ if (checkForAnErrorType(arguments[1])) {
+ errorType = arguments[1];
+ } else {
+ throw new Error("Expected error type is not an Error.");
+ }
+
+ if (arguments[2] instanceof RegExp) {
+ regexp = arguments[2];
+ } else if (typeof arguments[2] == "string") {
+ message = arguments[2];
+ } else {
+ throw new Error("Expected error message is not a string or RegExp.");
+ }
+ }
+ }
+
+ function checkForAnErrorType(type) {
+ if (typeof type !== "function") {
+ return false;
+ }
+
+ var Surrogate = function() {};
+ Surrogate.prototype = type.prototype;
+ return (new Surrogate()) instanceof Error;
+ }
+ }
+ };
+ }
+
+ return toThrowError;
+};
+
+getJasmineRequireObj().version = function() {
+ return "2.0.0";
+};
diff --git a/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine_favicon.png b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine_favicon.png
new file mode 100644
index 0000000..3562e27
--- /dev/null
+++ b/tipboard/tests/frontend/lib/jasmine-2.0.0/jasmine_favicon.png
Binary files differ
diff --git a/tipboard/tests/frontend/spec/tipboardSpec.js b/tipboard/tests/frontend/spec/tipboardSpec.js
new file mode 100644
index 0000000..804fb1d
--- /dev/null
+++ b/tipboard/tests/frontend/spec/tipboardSpec.js
@@ -0,0 +1,705 @@
+describe("Tipboard flip-tile mechanism", function() {
+ var getFlipTimeNodes = function() {
+ var flipTimeNodes = $('body > div:first-child > div');
+ return flipTimeNodes;
+ };
+
+ it("finds flip-time correctly", function() {
+ var expectedTimes = [3, 5, 20];
+ var flippingConatainers = getFlipTimeNodes();
+ $.each(expectedTimes, function(idx, expectedTime) {
+ var flippingContainer = flippingConatainers[idx];
+ console.log('flippingContainer', flippingContainer);
+ var foundTime = getFlipTime(flippingContainer);
+ expect(foundTime).toBe(expectedTime*1000);
+ });
+ });
+
+ it("adds classes (flippedforward, flipped) automatically", function() {
+ var flipTimeNodes = getFlipTimeNodes();
+ var noFlipNode = flipTimeNodes.splice(2,1);
+ expect(flipTimeNodes.length).toBe(3);
+ expect(noFlipNode.length).toBe(1);
+ expect($(noFlipNode[0]).hasClass('flippedforward')).toBe(false);
+ expect($(noFlipNode[0]).hasClass('flippable')).toBe(false);
+
+ console.log('flipTimeNodes', flipTimeNodes);
+ $.each(flipTimeNodes, function(idx, flipContainer) {
+ var containedTiles = $(flipContainer).find('div.tile');
+ expect(containedTiles.length).toBe(2);
+
+ $.each(containedTiles, function(tileIdx, tile) {
+ console.log('tile', tile);
+ if (tileIdx === 0) {
+ expect($(tile).hasClass('flippedforward')).toBe(true);
+ expect($(tile).hasClass('flippable')).toBe(true);
+ } else {
+ expect($(tile).hasClass('flippedforward')).toBe(false);
+ expect($(tile).hasClass('flippable')).toBe(true);
+ }
+ });
+ });
+ });
+
+ it("appends correct tiles counter", function() {
+ var flipTimeNodes = getFlipTimeNodes();
+ var noFlipNode = flipTimeNodes.splice(2,1);
+ Tipboard.Dashboard.addTilesCounter(noFlipNode);
+ expect(
+ $(noFlipNode).find('span.tile-counter').length
+ ).toBe(0);
+
+ $.each(flipTimeNodes, function(idx, flipContainer) {
+ var containedTiles = $(flipContainer).find('div.tile');
+ $.each(containedTiles, function(tileIdx, tile) {
+ console.log('tile-couter tile check', tile);
+ var counterSpans = $(tile).find('span.tile-counter');
+ expect(counterSpans.length).toBe(1);
+ var expected = [tileIdx + 1, containedTiles.length].join('/');
+ var foundText = $(counterSpans[0]).text();
+ expect(foundText).toBe(expected);
+ });
+ });
+ });
+
+});
+
+describe("Tipboard RenderersSwapper", function() {
+
+ var aRendererClass = $.jqplot.AxisTickRenderer;
+ var aRendererClassName = "AXISTICKRENDERER";
+
+ var getObjectAtPath = function(object, path) {
+ path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
+ path = path.replace(/^\./, ''); // strip a leading dot
+ var steps = path.split('.');
+ while (steps.length) {
+ var step = steps.shift();
+ if (step in object) {
+ object = object[step];
+ } else {
+ return;
+ }
+ }
+ return object;
+ };
+
+ var configWithRendererOnPossiblePaths = function(renderer) {
+ var simpleRenderer = {renderer: renderer};
+ var axisWithRenderer = {
+ tickRenderer: renderer,
+ labelRenderer: renderer,
+ renderer: renderer
+ };
+ var seriesWithRenderer = {
+ markerRenderer: renderer,
+ renderer: renderer
+ };
+ var config = {
+ 'legend': simpleRenderer,
+ 'title': simpleRenderer,
+ 'grid': simpleRenderer,
+ 'axesDefaults': axisWithRenderer,
+ 'axes': {
+ 'xaxis': axisWithRenderer,
+ 'x2axis': axisWithRenderer,
+ 'yaxis': axisWithRenderer,
+ 'y2axis': axisWithRenderer,
+ // ..
+ 'y9axis': axisWithRenderer,
+ },
+ 'seriesDefaults': seriesWithRenderer,
+ 'series': [
+ seriesWithRenderer,
+ seriesWithRenderer
+ //..
+ ]
+ };
+ return config;
+ };
+
+ it("has correct references to renderer classes", function() {
+ $.each(
+ RendererFactory.prototype.rendererName2renderObj,
+ function(rendererName, rendererClass) {
+ var rendererClassType = typeof rendererClass;
+ expect(rendererClassType).toBe("function");
+ }
+ );
+ });
+
+ it("replaces renderer case insensitively", function() {
+ var config = {
+ 'title': {
+ 'renderer': aRendererClassName
+ }
+ };
+ renderersSwapper.swap(config);
+ expect(config.title.renderer).toBe(aRendererClass);
+ });
+
+ it("replaces all defined renderers", function() {
+ var config = {
+ 'title': {
+ 'renderer': undefined,
+ }
+ };
+ var allRenderers = RendererFactory.prototype.rendererName2renderObj;
+ $.each(allRenderers, function(rendererName, rendererClass) {
+ config.title.renderer = rendererName;
+ renderersSwapper.swap(config);
+ expect(config.title.renderer).toBe(rendererClass);
+ });
+ });
+
+ it("replaces renderers on every possible path", function() {
+ var goodConfig = configWithRendererOnPossiblePaths(
+ aRendererClass
+ );
+ goodConfig = JSON.stringify(goodConfig);
+
+ var configToSwap = configWithRendererOnPossiblePaths(
+ aRendererClassName
+ );
+ renderersSwapper.swap(configToSwap);
+ var swappedConfig = JSON.stringify(configToSwap);
+ expect(swappedConfig).toBe(goodConfig);
+ });
+
+ it("raises error on undefined renderer name", function() {
+ var invalidRendererName = "invalidRendererName";
+ var configToSwap = configWithRendererOnPossiblePaths(
+ invalidRendererName
+ );
+ expect(
+ renderersSwapper.swap.bind(null, configToSwap)
+ ).toThrow(
+ new RendererFactory.prototype.UnknownRenderer(invalidRendererName)
+ );
+ });
+
+});
+
+describe("Tipboard palette functions", function() {
+
+ it("return palette as non-empty list", function() {
+ var paletteAsList = Tipboard.DisplayUtils.paletteAsList();
+ expect(paletteAsList.length).toBeGreaterThan(0);
+ var paletteType = Object.prototype.toString.call(paletteAsList);
+ expect(paletteType).toBe("[object Array]");
+ });
+
+ it("return the same color when color is not from palette", function() {
+ var notpaletteColors = ['#123456', 'pink', ''];
+ $.each(notpaletteColors, function(idx, rightColor) {
+ testingColor = Tipboard.DisplayUtils.replaceFromPalette(
+ rightColor
+ );
+ expect(testingColor).toBe(rightColor);
+ });
+ });
+
+ it("return the palette color when color is from palette", function() {
+ $.each(Tipboard.DisplayUtils.palette, function(colorName, goodColor) {
+ var testingColor = Tipboard.DisplayUtils.replaceFromPalette(
+ colorName
+ );
+ expect(testingColor).toBe(goodColor);
+ });
+ });
+
+ it("return palette for jqplot seriesColor", function() {
+ var seriesColors = Tipboard.DisplayUtils.paletteAsSeriesColors();
+ expect(seriesColors.length).toBeGreaterThan(0);
+ var seriesColorsType = Object.prototype.toString.call(seriesColors);
+ expect(seriesColorsType).toBe("[object Array]");
+ var childType = Object.prototype.toString.call(seriesColors[0]);
+ expect(childType).toBe("[object Object]");
+ });
+});
+
+describe("Tipboard isDataExpired function", function() {
+
+ describe("works correctly when arg", function() {
+
+ describe("lastMod is", function() {
+
+ _date2format = function(datetime) {
+ // toISOString returns UTC time, hence we need to convert it
+ // to user's timezone, e.g. "2014-01-10T15:27:34+02:00"
+ var date = datetime.toISOString().split('T')[0];
+ var time = datetime.toTimeString().split(' ')[0];
+ var tz = datetime.toTimeString().split(' ')[1].slice(-5);
+ tz = [tz.slice(0, 3), tz.slice(3)].join(':');
+ var formatted = [date, [time, tz].join('')].join('T');
+ return formatted;
+ };
+
+ it("empty string", function() {
+ var lastMod = "";
+ var timeout = 5;
+ var reason = Tipboard.Dashboard.isDataExpired(
+ lastMod, timeout
+ );
+ expect(reason).toMatch(/Could not find the timestamp./);
+ });
+
+ it("invalid format", function() {
+ var lastMod = new Date();
+ lastMod = 'fadfasdfdas';
+ var timeout = 5;
+ var reason = Tipboard.Dashboard.isDataExpired(
+ lastMod, timeout
+ );
+ expect(reason).toMatch(/Timestamp found has bad format./);
+ });
+
+ it("expired", function(){
+ var lastMod = new Date();
+ lastMod.setYear(lastMod.getYear() - 1);
+ lastMod = _date2format(lastMod);
+ var timeout = 10;
+ var reason = Tipboard.Dashboard.isDataExpired(
+ lastMod, timeout
+ );
+ expect(reason).toMatch(/Tile's data EXPIRED./);
+ });
+
+ it("not expired", function() {
+ var lastMod = new Date();
+ lastMod = _date2format(lastMod);
+ var timeout = 1000;
+ var reason = Tipboard.Dashboard.isDataExpired(
+ lastMod, timeout
+ );
+ expect(reason).toBe("");
+ });
+
+ });
+
+ describe("timout", function() {
+
+ it("is empty string", function() {
+ var lastMod = new Date();
+ var timeout = "";
+ var reason = Tipboard.Dashboard.isDataExpired(
+ lastMod, timeout
+ );
+ expect(reason).toBe(undefined);
+ });
+
+ it("contains non-digit", function() {
+ var lastMod = new Date();
+ var timeout = "5a";
+ expect(
+ Tipboard.Dashboard.isDataExpired.bind(null, lastMod, timeout)
+ ).toThrow(
+ new Tipboard.Dashboard.TimeoutBadFormat(timeout)
+ );
+ });
+ });
+ });
+
+});
+
+describe("Tipboard show-error mechanism", function() {
+
+ var getFirstTile = function() {
+ // XXX: html dom is not visible on *describe* level, so call it on
+ // each *it* as a function (then html dom is visible)
+ return $('.tile')[0];
+ };
+
+ var checkIfTileIsShown = function(tile) {
+ var tileHeader = tile.children[0];
+ console.log('tile.children', tile.children);
+ expect(tile.children.length).toBe(2);
+ var msgContainer = $(tile).children()[1];
+ var isError = $(msgContainer).hasClass('exception-message');
+ expect(isError).toBe(false);
+ var display = $(msgContainer).css('display');
+ expect(display).not.toBe('none');
+ };
+
+ var checkIfErrorIsShown = function(tile) {
+ var tileHeader = tile.children[0];
+ console.log('tile.children', tile.children);
+ expect(tile.children.length).toBe(3);
+
+ var msgContainer = $(tile).children()[1];
+ var isError = $(msgContainer).hasClass('exception-message');
+ expect(isError).toBe(false);
+ var display = $(msgContainer).css('display');
+ expect(display).toBe('none');
+
+ var errorContainer = $(tile).children()[2];
+ isError = $(errorContainer).hasClass('exception-message');
+ expect(isError).toBe(true);
+ display = $(errorContainer).css('display');
+ expect(display).not.toBe('none');
+ };
+
+ it("change tile to tile", function() {
+ var tile = getFirstTile();
+ Tipboard.DisplayUtils.showTileData(tile);
+ checkIfTileIsShown(tile);
+ });
+
+ it("change tile to error", function() {
+ var tile = getFirstTile();
+ Tipboard.DisplayUtils.showExcMsg(tile, "aMessage");
+ checkIfErrorIsShown(tile);
+ });
+
+ it("change error to error", function() {
+ var tile = getFirstTile();
+ Tipboard.DisplayUtils.showExcMsg(tile, "aMessage");
+ Tipboard.DisplayUtils.showExcMsg(tile, "aMessage");
+ checkIfErrorIsShown(tile);
+ });
+
+ it("change error to tile", function() {
+ var tile = getFirstTile();
+ Tipboard.DisplayUtils.showExcMsg(tile, "aMessage");
+ Tipboard.DisplayUtils.showTileData(tile);
+ checkIfTileIsShown(tile);
+ });
+
+});
+
+describe("Tipboard tiles API", function() {
+
+ var tileType = 'tileType';
+ var fnToRegister = function() {};
+ var registering = function() {
+ Tipboard.Dashboard.registerUpdateFunction(tileType, fnToRegister);
+ };
+
+ beforeEach(function() {
+ Tipboard.Dashboard.updateFunctions = {};
+ });
+
+
+ it("allows registering update-tile function", function() {
+ expect(registering).not.toThrow();
+ });
+
+ it("return registered update-tile function", function() {
+ registering();
+ var returnedFn = Tipboard.Dashboard.getUpdateFunction(tileType);
+ expect(returnedFn).toBe(fnToRegister);
+ });
+
+ it(
+ "raise exception when getting not registered update-tile function",
+ function() {
+ expect(
+ Tipboard.Dashboard.getUpdateFunction.bind(
+ Tipboard.Dashboard, tileType
+ )
+ ).toThrow(new Tipboard.Dashboard.UnknownUpdateFunction(tileType));
+ }
+ );
+
+ it("escapes tile id according to jquery-id restriction", function() {
+ var idToEscape = "\\!\"#$%&'()*+,./:;<=>?@[]^`{|}~";
+ var escaped = Tipboard.Dashboard.escapeId(idToEscape);
+ expect(escaped.length).toBe(idToEscape.length*2);
+ var idNotToEscape = "anExampleTileIdNotToEscape1234567890";
+ escaped = Tipboard.Dashboard.escapeId(idNotToEscape);
+ expect(escaped).toBe(idNotToEscape);
+ });
+
+ describe("applyHighlighterConfig function", function() {
+ var tileId = 'example_simple_percentage';
+ var _getHightlighterNode = function () {
+ var highlighterNode = $('#' + tileId + '-big_value').parent();
+ return highlighterNode;
+ };
+
+ it("sets color correctly", function() {
+ var highlighterNode = _getHightlighterNode();
+ var transparentColor = $(highlighterNode).parent().css(
+ 'background-color'
+ );
+ var DataToCheck = [{
+ 'colorToSet': undefined,
+ 'initColor': transparentColor,
+ 'correct': transparentColor
+ }, {
+ 'colorToSet': undefined,
+ 'initColor': 'rgb(18, 176, 197)',
+ 'correct': 'rgb(18, 176, 197)'
+ }, {
+ 'colorToSet': 'blue',
+ 'initColor': transparentColor,
+ 'correct': 'rgb(18, 176, 197)'
+ }, {
+ 'colorToSet': 'blue',
+ 'initColor': 'rgb(18, 176, 197)',
+ 'correct': 'rgb(18, 176, 197)'
+ }];
+ $.each(DataToCheck, function(idx, data) {
+ $(highlighterNode).css('background-color', data.initColor);
+ Tipboard.DisplayUtils.applyHighlighterConfig(
+ highlighterNode, data.colorToSet, false
+ );
+ var foundColor = $(highlighterNode).css('background-color');
+ console.log("found vs correct", foundColor, data.correct);
+ expect(foundColor).toBe(data.correct);
+ });
+ });
+
+ it("sets folding correctly", function() {
+ var highlighterNode = _getHightlighterNode();
+ var okResults = [
+ [false, false, true],
+ [true, false, true]
+ ];
+ $.each([false, true], function(initStateIdx, initState) {
+ $.each(["undefined", false, true], function(fadingIdx, fading) {
+ if (initState === true) {
+ highlighterNode.addClass('fading-background-color');
+ }
+ if (initState === false) {
+ highlighterNode.removeClass('fading-background-color');
+ }
+
+ Tipboard.DisplayUtils.applyHighlighterConfig(
+ highlighterNode, 'blue', fading
+ );
+ var hasClass = $(highlighterNode).hasClass(
+ 'fading-background-color'
+ );
+ var expected = okResults[initStateIdx][fadingIdx];
+ expect(hasClass).toBe(expected);
+ });
+ });
+ });
+
+ it("cleans highlighting no color and no fading", function() {
+ var highlighterNode = _getHightlighterNode();
+ var transparentColor = $(highlighterNode).parent().css(
+ 'background-color'
+ );
+ Tipboard.DisplayUtils.applyHighlighterConfig(highlighterNode);
+
+ var foundColor = $(highlighterNode).css('background-color');
+ expect(foundColor).toBe(transparentColor);
+
+ var hasClass = $(highlighterNode).hasClass(
+ 'fading-background-color'
+ );
+ expect(hasClass).toBe(false);
+ });
+
+ });
+
+ describe("setDataByKeys function", function() {
+ var _checkFields = function() {
+ $.each(dataToSet, function(fieldName, fieldVal) {
+ var fieldId = '#' + tileId + '-' + fieldName;
+ var got = $(fieldId).text();
+ console.log(got, '===', fieldName);
+ expect(got).toBe(fieldName);
+ });
+ };
+ var tileId = 'example_simple_percentage';
+ var fieldNames = [
+ 'title', 'subtitle', 'big_value', 'left_label', 'left_value',
+ 'right_label', 'right_value'
+ ];
+ var dataToSet = {};
+ $.each(fieldNames, function(idx, fieldName) {
+ dataToSet[fieldName] = fieldName;
+ });
+
+ afterEach(function() {
+ // reset fields texts
+ $.each(fieldNames, function(idx, fieldName) {
+ var fieldId = ['#' + tileId, fieldName].join('-');
+ $(fieldId).text('');
+ });
+ });
+
+ it("works with 'all' arg", function() {
+ Tipboard.Dashboard.setDataByKeys(
+ tileId, dataToSet, 'all'
+ );
+ _checkFields();
+ });
+
+ it("works when keys list passed", function() {
+ Tipboard.Dashboard.setDataByKeys(
+ tileId, dataToSet, fieldNames
+ );
+ _checkFields();
+ });
+
+ });
+
+});
+
+describe("Tipboard tile", function() {
+
+ describe("norm_chart", function() {
+
+ it("checks correclty if isRawNormsSet", function() {
+ var config = {};
+ result = NormChartTile.isRawNormsSet(config);
+ expect(result).toBe(false);
+
+ config.canvasOverlay = {
+ objects: {},
+ };
+ result = NormChartTile.isRawNormsSet(config);
+ expect(result).toBe(true);
+
+ config.canvasOverlay = {
+ objects: 'notObject',
+ };
+ result = NormChartTile.isRawNormsSet(config);
+ expect(result).toBe(false);
+ });
+
+ it("creates correctly raw config", function() {
+ var easyNorms = [
+ ['blue', 0, 1],
+ ['red', 5, 3]
+ ];
+ var colorMap = {
+ 'blue': '#12B0C5',
+ 'red': '#DC5945'
+ };
+ var config = {
+ easyNorms: easyNorms
+ };
+ NormChartTile.useEasyNorms(config);
+ expect(isRawNormSet).not.toBe(true);
+ var isRawNormSet = NormChartTile.isRawNormsSet(config);
+ expect(isRawNormSet).toBe(true);
+ $.each(config.canvasOverlay.objects, function(idx, data) {
+ var lineData = data.horizontalLine;
+ expect(lineData.color).toBe(colorMap[easyNorms[idx][0]]);
+ expect(lineData.y).toBe(easyNorms[idx][1]);
+ expect(lineData.lineWidth).toBe(easyNorms[idx][2]);
+ });
+ });
+
+ });
+
+ describe("fancy_listing", function() {
+ var getFancyTile = function() {
+ return $('#example_fancy_listing');
+ };
+ var data = [
+ {label: 'l1', text: 't1', description: 'd1'},
+ {label: 'l2', text: 't2', description: 'd2'},
+ {label: 'l3', text: 't3', description: 'd3'},
+ ];
+
+ afterEach(function() {
+ var tile = getFancyTile();
+ $(tile).find('.fancy-listing-item').slice(1).remove();
+ });
+
+ describe("initContainer", function() {
+
+ it("returns node to clone", function() {
+ var fancyTile = getFancyTile();
+ var nodeToClone = FancyListing.initContainer(fancyTile);
+ expect(typeof nodeToClone).toBe("object");
+ expect(nodeToClone.className).toBe("fancy-listing-item");
+ });
+
+ it("removes children beside first", function() {
+ var fancyTile = getFancyTile();
+ var itemContainer = $(fancyTile).find('.tile-content');
+ var nodeToClone = $(itemContainer).children()[0];
+ FancyListing.populateItems(fancyTile, nodeToClone, data);
+ expect(itemContainer.children().length).toBe(1 + data.length);
+ nodeToClone = FancyListing.initContainer(fancyTile);
+ var items = $(itemContainer).children();
+ expect(items.length).toBe(1);
+ });
+
+ });
+
+ xit("centers content vertically", function() {
+ // replace centering with css class instead of js, then do test
+ });
+
+ it("data is replaced correctly", function() {
+ var fancyTile = getFancyTile();
+ var itemContainer = $(fancyTile).find('.tile-content');
+ var nodeToClone = $(itemContainer).children()[0];
+ FancyListing.populateItems(fancyTile, nodeToClone, data);
+ var items = $(itemContainer).children().slice(1);
+ $.each(items, function(idx, item) {
+ itemLabel = $(item).find('.fancy-listing-label-inside').text();
+ itemTerm = $(item).find('.fancy-listing-term').text();
+ itemDesc = $(item).find('.fancy-listing-desc').text();
+ expect(itemLabel).toBe(data[idx].label);
+ expect(itemTerm).toBe(data[idx].text);
+ expect(itemDesc).toBe(data[idx].description);
+ });
+ });
+
+ it("applies items config correctly", function() {
+ var config = {
+ 1: { label_color: 'rgb(0, 0, 255)', center: true },
+ 2: { label_color: 'rgb(0, 0, 255)' },
+ 3: { center: true }
+ };
+ var fancyTile = getFancyTile();
+ var itemContainer = $(fancyTile).find('.tile-content');
+ var nodeToClone = $(itemContainer).children()[0];
+ FancyListing.populateItems(fancyTile, nodeToClone, data);
+ FancyListing.applyConfig(fancyTile, config);
+
+ var items = $(itemContainer).children().slice(1);
+ $.each(items, function(idx, item) {
+ var expectedColor, expectedAlign;
+ var configColor = config[idx + 1].label_color;
+ if (typeof configColor !== "undefined") {
+ expectedColor = configColor;
+ } else {
+ expectedColor = $(nodeToClone).css('background-color');
+ }
+ var itemColor = $(item).find('.fancy-listing-label').css(
+ 'background-color'
+ );
+ expect(itemColor).toBe(expectedColor);
+
+ var configAlign = config[idx + 1].center;
+ if (typeof configAlign !== "undefined") {
+ expectedAlign = configAlign;
+ } else {
+ expectedAlign = false;
+ }
+ var itemAlign = $(item).find('.fancy-listing-def').css(
+ 'text-align'
+ );
+ itemAlign = itemAlign === 'center' ? true : false;
+ expect(itemAlign).toBe(expectedAlign);
+ });
+ });
+
+ });
+
+});
+
+describe("Flipboard", function() {
+
+ it("path cycling works correctly", function() {
+ var paths = ['a', 'b', 'c'];
+ var okPaths = paths.slice();
+ okPaths.push(paths[0]);
+ Flipboard.init(paths);
+ $.each(okPaths, function(idx, okPath) {
+ var returnedPath = Flipboard.getNextDashboardPath();
+ expect(returnedPath).toBe(okPath);
+ });
+ });
+});
+
diff --git a/tipboard/tests/test_config_parser.py b/tipboard/tests/test_config_parser.py
new file mode 100644
index 0000000..d2968fb
--- /dev/null
+++ b/tipboard/tests/test_config_parser.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import random
+import shutil
+import string
+import sys
+import unittest
+
+from mock import patch
+
+from tipboard import console, parser, settings
+
+DEFAULT_CONFIG_NAME = 'layout_config'
+SECOND_CONFIG_NAME = '2nd-' + DEFAULT_CONFIG_NAME
+
+
+@patch.object(sys, 'argv', [None, 'create_project', 'test_project'])
+def _create_test_project():
+ console.main()
+
+
+class TestConfigParser(unittest.TestCase):
+
+ def setUp(self):
+ # backup userspace .tipboard
+ _temp = __import__('tipboard', fromlist=[str('settings')])
+ self.settings = _temp.settings
+ # we need to make sure that already existing '~/.tipboard' dir won't be
+ # affected by tests, so we back it up (and restore it in tearDown)
+ if os.path.exists(self.settings._user_config_dir):
+ self.orig_conf_dir = self.settings._user_config_dir
+ rnd_str = ''.join(
+ random.choice(string.ascii_uppercase + string.digits)
+ for x in range(8)
+ )
+ self.bkp_conf_dir = '_'.join((self.orig_conf_dir, rnd_str))
+ os.rename(self.orig_conf_dir, self.bkp_conf_dir)
+ _create_test_project()
+
+ def test_get_rows(self):
+ """simple call of get_rows method and check rows count"""
+ EXPECTED = 2
+ config_file = parser.process_layout_config(DEFAULT_CONFIG_NAME)
+ found_rows = parser._get_rows(config_file['layout'])
+ rows_count = len(found_rows)
+ msg = u'Found {} rows instead of expected {}'.format(
+ rows_count, EXPECTED
+ )
+ self.assertEqual(rows_count, EXPECTED, msg)
+
+ def test_get_rows_cols_validator(self):
+ """
+ negative test: checks if parser's get_rows method find wrong rows count
+ """
+ config_file = parser.process_layout_config(DEFAULT_CONFIG_NAME)
+ broken_layout = config_file['layout'][:]
+ popped_row = broken_layout.pop()
+ row_class, cols_data = popped_row.items()[0]
+ broken_key = row_class.replace('1', '5')
+ broken_row = {
+ broken_key: cols_data,
+ }
+ broken_layout.append(broken_row)
+ try:
+ parser._get_rows(broken_layout)
+ except parser.WrongSumOfRows:
+ # test passed
+ pass
+ else:
+ raise Exception("Parser's get_rows method skipped layout error")
+
+ def tearDown(self):
+ try:
+ # delete '~/.tipboard' dir that we created for tests
+ shutil.rmtree(self.settings._user_config_dir)
+ except (OSError):
+ pass
+ try:
+ if os.path.exists(self.bkp_conf_dir):
+ # restore conf dir backed up by setUp
+ os.rename(self.bkp_conf_dir, self.orig_conf_dir)
+ except AttributeError:
+ pass
+ reload(self.settings)
+
+
+class TestConfigFiles(unittest.TestCase):
+
+ def setUp(self):
+ # backup userspace .tipboard
+ _temp = __import__('tipboard', fromlist=[str('settings')])
+ self.settings = _temp.settings
+ # we need to make sure that already existing '~/.tipboard' dir won't be
+ # affected by tests, so we back it up (and restore it in tearDown)
+ if os.path.exists(self.settings._user_config_dir):
+ self.orig_conf_dir = self.settings._user_config_dir
+ rnd_str = ''.join(
+ random.choice(string.ascii_uppercase + string.digits)
+ for x in range(8)
+ )
+ self.bkp_conf_dir = '_'.join((self.orig_conf_dir, rnd_str))
+ os.rename(self.orig_conf_dir, self.bkp_conf_dir)
+ _create_test_project()
+ self._create_second_config()
+
+ def test_finding_configs_files(self):
+ """
+ Check if function get_config_files_names finds all configs files in
+ user space
+ """
+ expected_configs_names = [DEFAULT_CONFIG_NAME, SECOND_CONFIG_NAME]
+ found_configs_names = parser.get_config_files_names()
+ self.assertEqual(
+ len(expected_configs_names), len(found_configs_names),
+ "Expected config number: {}, found: {}".format(
+ len(expected_configs_names), len(found_configs_names)
+ )
+ )
+ for config_name in found_configs_names:
+ config_file_path = parser.config_file_name2path(config_name)
+ self.assertTrue(
+ os.path.exists(config_file_path),
+ 'File does not exists: {}'.format(config_file_path),
+ )
+
+ def test_collecting_tiles_data(self):
+ """
+ Checks if function get_tiles_configs returns:
+ - correct number of tile ids
+ - correct number of tile types
+ - all required tiles_types and tiles_keys
+ """
+ tile_types_number = 2 # from data_to_check
+ tile_ids_number = 3 # from data_to_check
+ data_to_check = (
+ ('empty', 'empty'),
+ ('text', 'id1'),
+ ('text', 'id2'),
+ )
+ read_data = parser.get_tiles_configs()
+
+ self.assertEqual(
+ len(read_data['tiles_names']), tile_types_number,
+ 'Expected tile_types number: {} found {}'.format(
+ tile_types_number, len(read_data['tiles_names'])
+ )
+ )
+ self.assertEqual(
+ len(read_data['tiles_keys']), tile_ids_number,
+ 'Expected tile_ids number: {} found {}'.format(
+ tile_types_number, len(read_data['tiles_names'])
+ )
+ )
+ for tile_name, tile_key in data_to_check:
+ self.assertIn(tile_name, read_data['tiles_names'])
+ self.assertIn(tile_key, read_data['tiles_keys'])
+
+ def _create_second_config(self):
+ file_content = """
+details:
+ page_title: Empty Dashboard
+layout:
+ - row_1_of_2:
+ - col_1_of_1:
+ - tile_template: text
+ tile_id: id1
+ title: Empty Tile
+ classes:
+
+ - row_1_of_2:
+ - col_1_of_1:
+ - tile_template: text
+ tile_id: id2
+ title: Empty Tile
+ classes: """
+ dst = os.path.join(
+ settings._user_config_dir, '.'.join([SECOND_CONFIG_NAME, 'yaml'])
+ )
+ with open(dst, 'wb') as config_file:
+ config_file.write(str(file_content))
+
+ def tearDown(self):
+ try:
+ # delete '~/.tipboard' dir that we created for tests
+ shutil.rmtree(self.settings._user_config_dir)
+ except (OSError):
+ pass
+ try:
+ if os.path.exists(self.bkp_conf_dir):
+ # restore conf dir backed up by setUp
+ os.rename(self.bkp_conf_dir, self.orig_conf_dir)
+ except AttributeError:
+ pass
+ reload(self.settings)
diff --git a/tipboard/tests/test_console_commands.py b/tipboard/tests/test_console_commands.py
new file mode 100644
index 0000000..d6b7410
--- /dev/null
+++ b/tipboard/tests/test_console_commands.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import random
+import re
+import shutil
+import string
+import sys
+import time
+import unittest
+
+from mock import patch
+
+from tipboard import console, settings
+
+
+#TODO: mute command's output that goes to stdout
+class TestConfigFiles(unittest.TestCase):
+
+ def setUp(self):
+ _temp = __import__('tipboard', fromlist=[str('settings')])
+ self.settings = _temp.settings
+ # we need to make sure that already existing '~/.tipboard' dir won't be
+ # affected by tests, so we back it up (and restore it in tearDown)
+ if os.path.exists(self.settings._user_config_dir):
+ self.orig_conf_dir = self.settings._user_config_dir
+ rnd_str = ''.join(
+ random.choice(string.ascii_uppercase + string.digits)
+ for x in range(8)
+ )
+ self.bkp_conf_dir = '_'.join((self.orig_conf_dir, rnd_str))
+ os.rename(self.orig_conf_dir, self.bkp_conf_dir)
+
+ @patch.object(sys, 'argv', [None, 'create_project', 'test_project'])
+ def test_all_files_created(self):
+ """
+ check if command 'create_project test_project' creates required
+ project structure.
+
+ project structure contains:
+ ~/.tipboard
+ custom_tiles
+ layout_config.yaml
+ settings-local.py
+ """
+ console.main()
+ config_paths = self.project_config_paths()
+ for path_to_check in config_paths:
+ self.assertTrue(os.path.exists(path_to_check))
+
+ @patch.object(sys, 'argv', [None, 'create_project', 'test_project'])
+ def test_recreate_project(self):
+ """
+ check if command 'create_project test_project' called twice
+ doesn't affect already existing '~/.tipboard' dir
+ """
+ console.main()
+ config_paths = self.project_config_paths()
+ first_times, second_times = [], []
+ for path in config_paths:
+ mod_time = time.ctime(os.path.getmtime(path))
+ first_times.append(mod_time)
+ time.sleep(0.1)
+ console.main()
+ for path in config_paths:
+ mod_time = time.ctime(os.path.getmtime(path))
+ second_times.append(mod_time)
+ for first_time, second_time in zip(first_times, second_times):
+ item_idx = first_times.index(first_time)
+ msg = u"file has changed: {}".format(config_paths[item_idx])
+ self.assertEqual(first_time, second_time, msg)
+
+ @patch.object(sys, 'argv', [None, 'create_project', 'test_project'])
+ def test_config_contains_project_name(self):
+ """check if 'settings-local.py' contains project's name"""
+ console.main()
+ project_name_line = "PROJECT_NAME = 'test_project'"
+ _settings_local = os.path.join(
+ settings._user_config_dir, 'settings-local.py'
+ )
+ msg = 'settings-local.py does not include {}'.format(
+ repr(project_name_line)
+ )
+ with open(_settings_local) as settings_file:
+ self.assertIn(project_name_line, settings_file.read(), msg)
+
+ @patch.object(sys, 'argv', [None, 'create_project', 'test_project'])
+ def test_config_contains_api_key(self):
+ """check if 'settings-local.py' contains API key"""
+ console.main()
+ api_key_line = re.compile('API_KEY = \'[0-9a-z]{32}\'')
+ _settings_local = os.path.join(
+ settings._user_config_dir, 'settings-local.py'
+ )
+ msg = 'settings-local.py does not include API_KEY string'
+ with open(_settings_local) as settings_file:
+ self.assertRegexpMatches(settings_file.read(), api_key_line, msg)
+
+ def tearDown(self):
+ try:
+ # delete '~/.tipboard' dir that we created for tests
+ shutil.rmtree(self.settings._user_config_dir)
+ except (OSError):
+ pass
+ try:
+ if os.path.exists(self.bkp_conf_dir):
+ # restore conf dir backed up by setUp
+ os.rename(self.bkp_conf_dir, self.orig_conf_dir)
+ except AttributeError:
+ pass
+ self.reset_settings()
+
+ def reset_settings(self):
+ reload(self.settings)
+
+ def project_config_paths(self):
+ config_paths = [
+ self.settings._user_config_dir,
+ os.path.join(self.settings._user_config_dir, 'custom_tiles'),
+ os.path.join(self.settings._user_config_dir, 'layout_config.yaml'),
+ os.path.join(self.settings._user_config_dir, 'settings-local.py'),
+ ]
+ return config_paths
diff --git a/tipboard/tests/test_rest_api.py b/tipboard/tests/test_rest_api.py
new file mode 100644
index 0000000..75d22c5
--- /dev/null
+++ b/tipboard/tests/test_rest_api.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from datetime import datetime, timedelta
+import binascii
+import json
+import os
+import urllib
+
+from mock import patch
+from tornado.testing import AsyncHTTPTestCase
+
+from tipboard import settings, __version__
+from tipboard.api import api_version
+from tipboard.app import app
+
+TILE_ID = '_'.join(('test_tile', binascii.b2a_hex(os.urandom(5))))
+
+
+# TODO: mock fake project id
+class TestRestApi(AsyncHTTPTestCase):
+ """
+ Basic tests for API resources provided by Tipboard, i.e.:
+ - push (POST)
+ - tileconfig (POST, DELETE)
+ - tiledata (GET, DELETE)
+ - info (GET)
+ """
+
+ def shortDescription(self):
+ # suppress printing tests' docstrings when verbosity level is >= 2
+ return None
+
+ def get_app(self):
+ return app
+
+ # in case of strange timeouts uncommenting this method may help
+ # (see: https://github.com/facebook/tornado/issues/663)
+ #def get_new_ioloop(self):
+ # return tornado.ioloop.IOLoop.instance()
+
+ # I know that tests should be independent, but in this case it is
+ # much better to run them in particular order.
+ def test_01_api_key(self):
+ """Test API key validation."""
+ valid_key = settings.API_KEY
+ invalid_key = 'some_invalid_key'
+ url = '/'.join(('', 'api', api_version, valid_key, 'info'))
+ self.http_client.fetch(self.get_url(url), self.stop)
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ url = '/'.join(('', 'api', api_version, invalid_key, 'info'))
+ self.http_client.fetch(self.get_url(url), self.stop)
+ response = self.wait()
+ self.assertEqual(response.code, 404)
+
+ def test_02_info_resource(self):
+ """Test if API resource 'info' works."""
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'info'))
+ self.http_client.fetch(self.get_url(url), self.stop)
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ info_expected = {
+ 'tipboard_version': __version__,
+ 'project_name': settings.PROJECT_NAME,
+ 'project_layout_config': settings.LAYOUT_CONFIG,
+ 'redis_db': settings.REDIS,
+ }
+ info_expected = json.dumps(info_expected)
+ self.assertEqual(response.body, info_expected)
+
+ def test_03_push_resource(self):
+ """Test if posting tile's data works."""
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'push'))
+ data_to_push = {
+ 'tile': 'text',
+ 'key': TILE_ID,
+ 'data': json.dumps('test string'),
+ }
+ body = urllib.urlencode(data_to_push)
+ fake_config = {
+ 'tiles_keys': [TILE_ID],
+ 'tiles_names': ['text'],
+ }
+ with patch('tipboard.api.get_tiles_configs') as m:
+ m.return_value = fake_config
+ self.http_client.fetch(
+ self.get_url(url), self.stop, method='POST', body=body
+ )
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+
+ def test_04_tileconfig_resource_post(self):
+ """Test if posting tile's config works."""
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tileconfig',
+ TILE_ID))
+ config_to_push = {
+ 'value': json.dumps("{'font_color': '#00FF00'}"),
+ }
+ body = urllib.urlencode(config_to_push)
+ fake_config = {
+ 'tiles_keys': [TILE_ID],
+ 'tiles_names': ['text'],
+ }
+ with patch('tipboard.api.process_layout_config') as m:
+ m.return_value = fake_config
+ self.http_client.fetch(
+ self.get_url(url), self.stop, method='POST', body=body
+ )
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.body, "Tile's config updated.\n")
+
+ def test_05_tiledata_resource_get(self):
+ """Test if we can get back tile's data and config."""
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tiledata',
+ TILE_ID))
+ fake_config = {
+ 'tiles_keys': [TILE_ID],
+ 'tiles_names': ['text'],
+ }
+ with patch('tipboard.api.process_layout_config') as m:
+ m.return_value = fake_config
+ self.http_client.fetch(self.get_url(url), self.stop)
+ mod_time_expected = datetime.now()
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ data_received = json.loads(response.body)
+ mod_time_received = datetime.strptime(data_received.pop('modified'),
+ '%Y-%m-%d %H:%M:%S')
+ data_expected = {
+ 'tile_template': 'text',
+ 'meta': "{'font_color': '#00FF00'}",
+ 'data': 'test string',
+ 'id': TILE_ID,
+ }
+ self.assertEqual(data_received, data_expected)
+ # 'sanity_diff' is just a safety margin
+ sanity_diff = timedelta(seconds=5)
+ self.assertLessEqual(mod_time_expected - mod_time_received,
+ sanity_diff)
+
+ def test_06_tileconfig_resource_delete(self):
+ """Test if we can delete tile's config."""
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tileconfig',
+ TILE_ID))
+ fake_config = {
+ 'tiles_keys': [TILE_ID],
+ 'tiles_names': ['text'],
+ }
+ with patch('tipboard.api.process_layout_config') as m:
+ m.return_value = fake_config
+ self.http_client.fetch(self.get_url(url), self.stop,
+ method='DELETE')
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ # now let's check if 'meta' field is empty (it should be)
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tiledata',
+ TILE_ID))
+ with patch('tipboard.api.process_layout_config') as m:
+ m.return_value = fake_config
+ self.http_client.fetch(self.get_url(url), self.stop)
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ data_received = json.loads(response.body)
+ self.assertEqual(data_received['meta'], {})
+
+ def test_07_tiledata_resource_delete(self):
+ """Test if deleting tile's key works."""
+ url = '/'.join(('', 'api', api_version, settings.API_KEY, 'tiledata',
+ TILE_ID))
+ fake_config = {
+ 'tiles_keys': [TILE_ID],
+ 'tiles_names': ['text'],
+ }
+ with patch('tipboard.api.process_layout_config') as m:
+ m.return_value = fake_config
+ self.http_client.fetch(self.get_url(url), self.stop,
+ method='DELETE')
+ response = self.wait()
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.body, "Tile's data deleted.\n")
diff --git a/tipboard/tiles/advanced_plot.html b/tipboard/tiles/advanced_plot.html
new file mode 100644
index 0000000..ef31356
--- /dev/null
+++ b/tipboard/tiles/advanced_plot.html
@@ -0,0 +1,14 @@
+<div class="tile advanced-plot-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3 >{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{ tile_id + '-title' }}" class="title"></h2>
+ <div
+ id="{{ tile_id + '-description' }}"
+ class="result label description"
+ ></div>
+
+ <div id="{{ tile_id + '-chart' }}" class="chart"> </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/advanced_plot.js b/tipboard/tiles/advanced_plot.js
new file mode 100644
index 0000000..04aae13
--- /dev/null
+++ b/tipboard/tiles/advanced_plot.js
@@ -0,0 +1,35 @@
+(function($) {
+ 'use strict';
+
+ var AdvancedPlotTile;
+
+ AdvancedPlotTile = {
+ buildChart: function (tileId, plotData, config) {
+ // DEPRECATED fn, use Tipboard.Dashboard.createGraph
+ var containerId, plot;
+ containerId = tileId + '-chart';
+ plot = $.jqplot(containerId, plotData, config);
+ Tipboard.Dashboard.chartsIds[tileId] = plot;
+ },
+ setDataByKeys: function (tileId, data, keys) {
+ Tipboard.Dashboard.setDataByKeys(tileId, data, keys);
+ },
+ rescaleContainers: function (tile) {
+ Tipboard.DisplayUtils.expandLastChild(tile);
+ Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]);
+ }
+ };
+
+ function updateTileAdvancedPlot(tileId, data, meta) {
+ var tile, newMeta;
+ tile = Tipboard.Dashboard.id2node(tileId);
+ AdvancedPlotTile.setDataByKeys(tileId, data, ['title', 'description']);
+ newMeta = $.extend(true, {}, meta);
+ var renderersSwapper = new RenderersSwapper();
+ renderersSwapper.swap(newMeta);
+ AdvancedPlotTile.rescaleContainers(tile);
+ AdvancedPlotTile.buildChart(tileId, data.plotData, newMeta);
+ }
+
+ Tipboard.Dashboard.registerUpdateFunction('advanced_plot', updateTileAdvancedPlot);
+}($));
diff --git a/tipboard/tiles/bar_chart.css b/tipboard/tiles/bar_chart.css
new file mode 100644
index 0000000..0131ba2
--- /dev/null
+++ b/tipboard/tiles/bar_chart.css
@@ -0,0 +1 @@
+.bar-chart .jqplot-point-label {color: #fff;}
diff --git a/tipboard/tiles/bar_chart.html b/tipboard/tiles/bar_chart.html
new file mode 100644
index 0000000..65df316
--- /dev/null
+++ b/tipboard/tiles/bar_chart.html
@@ -0,0 +1,10 @@
+<div class="tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{tile_id}}-title" class="result big-result"></h2>
+ <h3 id="{{tile_id}}-subtitle" class="result label" ></h3>
+ <div id="{{ tile_id + '-chart' }}" class="chart bar-chart" > </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/bar_chart.js b/tipboard/tiles/bar_chart.js
new file mode 100644
index 0000000..3b36b33
--- /dev/null
+++ b/tipboard/tiles/bar_chart.js
@@ -0,0 +1,69 @@
+function updateTileBarChart(tileId, data, meta, tipboard) {
+ Tipboard.Dashboard.setDataByKeys(tileId, data, ['title', 'subtitle']);
+ renderersSwapper = new RenderersSwapper();
+ renderersSwapper.swap(meta);
+ var config = jQuery.extend(true, {}, DEFAULT_BAR_CHART_CONFIG, {
+ 'grid': {
+ 'background': tipboard.color.tile_background,
+ 'gridLineColor': tipboard.color.tile_background
+ },
+ 'series': Tipboard.DisplayUtils.paletteAsSeriesColors()
+ }, meta);
+ if (Boolean(data.ticks)) {
+ // ticks from *data* are more important than ticks from *config*
+ config.axes.yaxis.ticks = data.ticks;
+ }
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ // TODO replace it with Tipboard.Dashboard.rescaleTile
+ Tipboard.DisplayUtils.expandLastChild(tile);
+ Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]);
+ Tipboard.Dashboard.chartsIds[tileId] = $.jqplot(
+ tileId + '-chart', data.series_list, config
+ );
+}
+
+Tipboard.Dashboard.registerUpdateFunction('bar_chart', updateTileBarChart);
+
+var DEFAULT_BAR_CHART_CONFIG = {
+ title: {
+ show: false
+ },
+ grid: {
+ borderWidth: 0,
+ shadow: false
+ },
+ showMarker: false,
+ axes: {
+ xaxis: {
+ tickOptions: {
+ showLabel: false,
+ showMark: false,
+ shadow: false
+ }
+ },
+ yaxis: {
+ renderer: $.jqplot.CategoryAxisRenderer,
+ tickRenderer: $.jqplot.CanvasAxisTickRenderer,
+ tickOptions: {
+ shadow: false,
+ textColor: '#a4a4a4',
+ markSize: 0
+ }
+ }
+ },
+ seriesDefaults: {
+ pointLabels: {
+ show: true
+ },
+ renderer: $.jqplot.BarRenderer,
+ rendererOptions: {
+ barWidth: 25,
+ barPadding: 3,
+ shadowAlpha: 0,
+ barDirection: 'horizontal'
+ },
+ trendline: {
+ show: false
+ }
+ }
+};
diff --git a/tipboard/tiles/big_value.css b/tipboard/tiles/big_value.css
new file mode 100644
index 0000000..22fe684
--- /dev/null
+++ b/tipboard/tiles/big_value.css
@@ -0,0 +1,40 @@
+.tile.big_value_tile > .tile-content > .highlight-footer {
+ clear: both;
+ height: 30%;
+ overflow: hidden;
+ padding-top: 10%;
+ width: 100%;
+}
+
+.row_1_of_3 .tile.big_value_tile > .tile-content .highlight-footer {
+ height: 30%;
+ overflow: hidden;
+ padding-top: 3%;
+ width: 100%;
+}
+
+.tile.big_value_tile > .tile-content > .result-row > .result-col {
+ float: left;
+ width: 45%;
+ height: 15%;
+}
+
+.tile.big_value_tile > .tile-content > .result-row > .result-half-col {
+ float: left;
+ width: 45%;
+}
+
+.tile.big_value_tile > .tile-content > .result-row > .result-half-col {
+ margin-left: 5%;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.tile.big_value_tile > .tile-content > .result-row > .result-half-col > .result {
+ font-size: 2.0rem;
+ height: 15%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+}
diff --git a/tipboard/tiles/big_value.html b/tipboard/tiles/big_value.html
new file mode 100644
index 0000000..eb9b2d7
--- /dev/null
+++ b/tipboard/tiles/big_value.html
@@ -0,0 +1,35 @@
+<div class="tile highlighter-tile big_value_tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3 >{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{ tile_id + '-title' }}" class="result big-result fixed-height"></h2>
+ <h3 id="{{ tile_id + '-description' }}" class="result label fixed-height"
+ ></h3>
+ <div class="result big-value-container">
+ <span id="{{ tile_id + '-big-value' }}" class="big-value"></span>
+ </div>
+ <div class="result-row highlight-footer">
+ <div class="result-half-col">
+ <div class="result">
+ <span id="{{ tile_id + '-upper-left-label' }}" class="label"></span>
+ <span id="{{ tile_id + '-upper-left-value' }}"></span>
+ </div>
+ <div class="result">
+ <span id="{{ tile_id + '-lower-left-label' }}" class="label"></span>
+ <span id="{{ tile_id + '-lower-left-value' }}"></span>
+ </div>
+ </div>
+ <div class="result-half-col">
+ <div class="result">
+ <span id="{{ tile_id + '-upper-right-label' }}" class="label"></span>
+ <span id="{{ tile_id + '-upper-right-value' }}"></span>
+ </div>
+ <div class="result">
+ <span id="{{ tile_id + '-lower-right-label' }}" class="label"></span>
+ <span id="{{ tile_id + '-lower-right-value' }}"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/big_value.js b/tipboard/tiles/big_value.js
new file mode 100644
index 0000000..6304624
--- /dev/null
+++ b/tipboard/tiles/big_value.js
@@ -0,0 +1,33 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+
+function updateTileBigValue(tileId, data, config) {
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ BigValueTile.setDataByKeys(tileId, data, 'all');
+ var highlighterNode = $('#' + tileId + '-big-value').parent();
+ Tipboard.DisplayUtils.applyHighlighterConfig(
+ highlighterNode, config.big_value_color, config.fading_background
+ );
+ Tipboard.TileDisplayDecorator.runAllDecorators(tile);
+}
+
+Tipboard.Dashboard.registerUpdateFunction('big_value', updateTileBigValue);
+
+BigValueTile = {
+ setDataByKeys: function(tileId, data, keys) {
+ Tipboard.Dashboard.setDataByKeys(tileId, data, keys);
+ },
+ setBigValueColor: function(tileId, meta) {
+ // DEPRECATED function, Tipboard.DisplayUtils.applyHighlighterConfig
+ var color = meta['big_value_color'];
+ if (color !== void 0) {
+ color = Tipboard.DisplayUtils.replaceFromPalette(color)
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ var bigValue = $(tile).find('#' + tileId + '-big-value')[0]
+ var dst = $(bigValue).parent();
+ $(dst).css('background-color', color);
+ }
+ }
+};
+
diff --git a/tipboard/tiles/cumulative_flow.css b/tipboard/tiles/cumulative_flow.css
new file mode 100644
index 0000000..aa84d6e
--- /dev/null
+++ b/tipboard/tiles/cumulative_flow.css
@@ -0,0 +1,18 @@
+.tile > .tile-content > .cumulative-flow-chart {
+ width: 70%;
+ float: left;
+}
+
+.tile > .tile-content > .plot-labels {
+ padding-left: 10px;
+ margin-left: 70%;
+ overflow: hidden;
+}
+
+.tile > .tile-content > .plot-labels h2 {
+ font-size: 2.8rem;
+}
+
+.tile > .tile-content > .plot-labels .medium-result {
+ font-size: 2rem;
+}
diff --git a/tipboard/tiles/cumulative_flow.html b/tipboard/tiles/cumulative_flow.html
new file mode 100644
index 0000000..69f7854
--- /dev/null
+++ b/tipboard/tiles/cumulative_flow.html
@@ -0,0 +1,16 @@
+<div class="tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <div id="{{ tile_id + '-chart' }}" class="chart cumulative-flow-chart chart-only"> </div>
+
+ <div class="plot-labels">
+ <h2 id="{{ tile_id + '-title' }}" class="labels-title"></h2>
+ <!--
+ js appends labels dynamically here
+ -->
+ </div>
+ </div>
+</div>
+
diff --git a/tipboard/tiles/cumulative_flow.js b/tipboard/tiles/cumulative_flow.js
new file mode 100644
index 0000000..2301b0a
--- /dev/null
+++ b/tipboard/tiles/cumulative_flow.js
@@ -0,0 +1,103 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+function updateTileCumulativeFlow(id, data, meta, tipboard) {
+ if (data.series_list.length > 7) {
+ console.log('ERROR: more then 7 series passed - RENDERING STOPPED')
+ return;
+ }
+ var tile = $('#' + id)[0];
+ $('#' + id + '-title').html(data['title']);
+ var config = jQuery.extend(true, {}, DEFAULT_CUMULATIVE_FLOW_CHART_CONFIG);
+ config['axes']['xaxis']['ticks'] = Boolean(meta.ticks) ? meta.ticks : [];
+ config['grid']['background'] = tipboard.color.tile_background;
+ config['grid']['gridLineColor'] = tipboard.color.tile_background;
+ var palette = Tipboard.DisplayUtils.getPalette(tipboard.color);
+ var seriesForJqplot = [];
+ // clear html labels before inserting
+ $($(tile).find('.plot-labels')[0]).find('div').remove()
+ for (idx in data.series_list) {
+ // unpack series for jqplot
+ var seriesData = data.series_list[idx];
+ var series = seriesData['series'];
+ seriesForJqplot.push(series);
+ // update plots labels and colors
+ config['series'][idx] = {
+ 'label': data.series_list[idx]['label'],
+ 'color': palette[idx]
+ };
+ // insert labels to html
+ var lastValue = series[series.length - 1];
+ appendHtmlLabels(
+ $(tile).find('.plot-labels')[0],
+ config['series'][idx]['label'],
+ lastValue,
+ config['series'][idx]['color']
+ );
+ }
+ var plot = $.jqplot(id + '-chart', seriesForJqplot, config);
+ Tipboard.Dashboard.chartsIds[id] = plot;
+ Tipboard.TileDisplayDecorator.runAllDecorators($("#" + id)[0]);
+}
+
+Tipboard.Dashboard.registerUpdateFunction('cumulative_flow', updateTileCumulativeFlow);
+
+var DEFAULT_CUMULATIVE_FLOW_CHART_CONFIG = {
+ grid: {
+ drawGridLines: false,
+ background: '#232526',
+ gridLineColor: '#232526',
+ borderWidth: 0,
+ shadow: false
+ },
+ stackSeries: true,
+ seriesDefaults: {
+ fill: true,
+ pointLabels: {
+ show: false
+ },
+ trendline: {
+ show: false
+ },
+ showMarker: false
+ },
+ series: [],
+ legend: {
+ show: false
+ },
+ axes: {
+ xaxis: {
+ ticks: [],
+ tickRenderer: $.jqplot.CanvasAxisTickRenderer,
+ tickOptions: {
+ angle: - 90
+ },
+ drawMajorGridlines: false
+ },
+ yaxis: {
+ min: 0,
+ tickOptions: {
+ showLabel: true,
+ showMark: false,
+ shadow: false
+ }
+ }
+ }
+}
+
+function appendHtmlLabels(container, label, value, color) {
+ var htmlLabel = [
+ '<div class="result bugs-count-result medium-result">',
+ '<span style="float:left" class="issue-stats-bugs-number">',
+ label,
+ '</span>',
+ '<span style="float:right" class="issue-stats-bugs-number float-right">',
+ value,
+ '</span>',
+ '<div style="clear:both"></div>',
+ '</div>'
+ ].join('\n');
+ $(container).append(htmlLabel);
+ $(container).find('span').last().css('color', color);
+}
+
diff --git a/tipboard/tiles/empty.css b/tipboard/tiles/empty.css
new file mode 100644
index 0000000..5229560
--- /dev/null
+++ b/tipboard/tiles/empty.css
@@ -0,0 +1,3 @@
+.tile-empty h2 {
+ color: red;
+}
diff --git a/tipboard/tiles/empty.html b/tipboard/tiles/empty.html
new file mode 100644
index 0000000..a7609d5
--- /dev/null
+++ b/tipboard/tiles/empty.html
@@ -0,0 +1,8 @@
+<div class="tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content tile-empty">
+ <h2>This is an empty tile.</h2>
+ </div>
+</div>
diff --git a/tipboard/tiles/fancy_listing.css b/tipboard/tiles/fancy_listing.css
new file mode 100644
index 0000000..0010171
--- /dev/null
+++ b/tipboard/tiles/fancy_listing.css
@@ -0,0 +1,46 @@
+.overflow-proof {
+ /*
+ testing class, it's a candidate to be used in each tile type
+ */
+ overflow: hidden;
+}
+.tile.fancy-listing .fancy-listing-item {
+ height: 10%;
+ margin: 0 0 1%;
+ overflow: hidden;
+ width: 100%;
+}
+
+.tile.fancy-listing .fancy-listing-item:first-child {
+ /*
+ this element is cloned by js
+ */
+ display: none;
+}
+
+.tile.fancy-listing .fancy-listing-item .float-wrapper{
+ float: left;
+ height: 100%;
+ overflow: hidden;
+ width: 30%;
+}
+
+.tile.fancy-listing .fancy-listing-label {
+ font-weight: bold;
+ color: white;
+}
+
+.tile.fancy-listing .fancy-listing-label-inside {
+ font-weight: 900;
+ font-size: 1.5em;
+}
+
+.tile.fancy-listing .fancy-listing-def {
+ float: right;
+ width: 68%;
+}
+
+.tile.fancy-listing .fancy-listing-term {
+ font-size: 1.5em;
+ white-space: nowrap;
+}
diff --git a/tipboard/tiles/fancy_listing.html b/tipboard/tiles/fancy_listing.html
new file mode 100644
index 0000000..7ac7ccf
--- /dev/null
+++ b/tipboard/tiles/fancy_listing.html
@@ -0,0 +1,19 @@
+<div class="tile fancy-listing" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content overflow-proof">
+ <div id="{{tile_id}}-fancy-listing-item" class="fancy-listing-item">
+ <div class="float-wrapper">
+ <div class="fancy-listing-label centerbox">
+ <span class="fancy-listing-label-inside"></span>
+ </div>
+ </div>
+ <div class="fancy-listing-def">
+ <h3 class="fancy-listing-term"></h3>
+ <p class="fancy-listing-desc"></p>
+ </div>
+ </div>
+ </div>
+</div>
+
diff --git a/tipboard/tiles/fancy_listing.js b/tipboard/tiles/fancy_listing.js
new file mode 100644
index 0000000..4217496
--- /dev/null
+++ b/tipboard/tiles/fancy_listing.js
@@ -0,0 +1,78 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+function updateTileFancyListing(tileId, data, config, tipboard) {
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ var nodeToClone = FancyListing.initContainer(tile);
+ if (nodeToClone === void 0) {
+ return false;
+ }
+ FancyListing.populateItems(tile, nodeToClone, data);
+ FancyListing.applyConfig(tile, config);
+ Tipboard.TileDisplayDecorator.runAllDecorators(tile);
+ if (config['vertical_center'] === true) {
+ FancyListing.verticalCenter(tile);
+ }
+}
+
+Tipboard.Dashboard.registerUpdateFunction('fancy_listing', updateTileFancyListing);
+
+FancyListing = {
+ initContainer: function(tile) {
+ var nodeToClone = $(tile).find('.fancy-listing-item')[0];
+ if (nodeToClone === void 0) {
+ console.log('ABORTING - no node to clone');
+ return false;
+ }
+ $(tile).find('.fancy-listing-item').slice(1).remove();
+ return nodeToClone;
+ },
+ appendCloned: function(tile, nodeToClone) {
+ var container = $(tile).find('.tile-content')[0];
+ $(nodeToClone).clone().appendTo(container);
+ },
+ populateItems: function(tile, clonedNode, data) {
+ $.each(data, function(idx, tileData) {
+ FancyListing.appendCloned(tile, clonedNode);
+ FancyListing.replaceData(tile, tileData);
+ });
+ },
+ applyConfig: function(tile, config) {
+ $.each(config, function(idx, tileConfig) {
+ if (/\d+/.test(idx)) {
+ var item = $(tile).find('.fancy-listing-item')[idx];
+ // set color
+ var color = Tipboard.DisplayUtils.replaceFromPalette(
+ tileConfig['label_color']
+ );
+ $(item).find('.fancy-listing-label').css('background-color', color);
+ // set centering
+ if (tileConfig['center'] === true) {
+ $(item).find('.fancy-listing-def').css(
+ 'text-align', 'center'
+ );
+ }
+ }
+ });
+ },
+ verticalCenter: function(tile) {
+ // TODO: replace it with css class and toggle the class
+ containerHeight = $(tile).find('.tile-content').height();
+ children = $(tile).find('.tile-content').children().slice(1);
+ var childrensHeight = 0;
+ $.each(children, function(idx, child) {
+ childrensHeight += $(child).outerHeight(true);
+ });
+ positionToSet = (containerHeight - childrensHeight) / 2;
+ if (positionToSet > 0) {
+ $(children[0]).css('padding-top', positionToSet);
+ }
+ },
+ replaceData: function(tile, tileData) {
+ var lastItem = $(tile).find('.fancy-listing-item:last-child')[0];
+ $(lastItem).find('.fancy-listing-label-inside').html(tileData['label']);
+ $(lastItem).find('.fancy-listing-term').html(tileData['text']);
+ $(lastItem).find('.fancy-listing-desc').html(tileData['description']);
+ }
+};
+
diff --git a/tipboard/tiles/just_value.css b/tipboard/tiles/just_value.css
new file mode 100644
index 0000000..856bfc1
--- /dev/null
+++ b/tipboard/tiles/just_value.css
@@ -0,0 +1,39 @@
+.tile.just-value-tile > .tile-content {
+ overflow: hidden;
+ white-space: nowrap;
+}
+.tile.just-value-tile > .tile-content > .just-value-div {
+ height: 60%;
+ width: 100%;
+
+ /* Internet Explorer 10 */
+ display:-ms-flexbox;
+ -ms-flex-pack:center;
+ -ms-flex-align:center;
+ /* Firefox */
+ display:-moz-box;
+ -moz-box-pack:center;
+ -moz-box-align:center;
+ /* Safari, Opera, and Chrome */
+ display:-webkit-box;
+ -webkit-box-pack:center;
+ -webkit-box-align:center;
+ /* W3C */
+ display:box;
+ box-pack:center;
+ box-align:center;
+}
+
+.tile.just-value-tile > .tile-content > .just-value-div > .just-value {
+ font-size: 2.0em;
+ overflow: hidden;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.row_1_of_3 .tile.just-value-tile > .tile-content > .just-value-div > .just-value {
+ font-size: 12.0rem;
+ overflow: hidden;
+ text-align: center;
+ white-space: nowrap;
+}
diff --git a/tipboard/tiles/just_value.html b/tipboard/tiles/just_value.html
new file mode 100644
index 0000000..5f3bba9
--- /dev/null
+++ b/tipboard/tiles/just_value.html
@@ -0,0 +1,16 @@
+<div class="tile highlighter-tile just-value-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{ tile_id + '-title' }}" class="result big-result fixed-height"></h2>
+ <h3
+ id="{{ tile_id + '-description' }}"
+ class="result label fixed-height"
+ ></h3>
+ <div class="result big-result highlighted-result just-value-div" >
+ <span id="{{ tile_id + '-just-value' }}" class="just-value" > </span>
+ </div>
+
+ </div>
+</div>
diff --git a/tipboard/tiles/just_value.js b/tipboard/tiles/just_value.js
new file mode 100644
index 0000000..a753388
--- /dev/null
+++ b/tipboard/tiles/just_value.js
@@ -0,0 +1,29 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+function updateTileJustValue(tileId, data, config) {
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ JustValue.setDataByKeys(tileId, data, 'all');
+ var highlighterNode = $('#' + tileId + '-just-value').parent();
+ Tipboard.DisplayUtils.applyHighlighterConfig(
+ highlighterNode, config['just-value-color'], config.fading_background
+ );
+ Tipboard.TileDisplayDecorator.runAllDecorators(tile);
+}
+
+Tipboard.Dashboard.registerUpdateFunction('just_value', updateTileJustValue);
+
+JustValue = {
+ setDataByKeys: function(tileId, data, keys) {
+ Tipboard.Dashboard.setDataByKeys(tileId, data, keys);
+ },
+ setJustValueColor: function(tileId, meta) {
+ // DEPRECATED function, Tipboard.DisplayUtils.applyHighlighterConfig
+ var color = meta['just-value-color'];
+ if (color !== void 0) {
+ color = Tipboard.DisplayUtils.replaceFromPalette(color);
+ var dst = $('#' + tileId + '-just-value').parent();
+ $(dst).css('background-color', color);
+ }
+ }
+};
diff --git a/tipboard/tiles/line_chart.html b/tipboard/tiles/line_chart.html
new file mode 100644
index 0000000..7e1352f
--- /dev/null
+++ b/tipboard/tiles/line_chart.html
@@ -0,0 +1,10 @@
+<div class="tile line-chart-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{tile_id + '-subtitle' }}" class="result big-result"> </h2>
+ <h3 id="{{tile_id + '-description'}}" class="result label"> </h3>
+ <div id="{{tile_id + '-chart'}}" class='chart'> </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/line_chart.js b/tipboard/tiles/line_chart.js
new file mode 100644
index 0000000..0d91fbe
--- /dev/null
+++ b/tipboard/tiles/line_chart.js
@@ -0,0 +1,92 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+
+function updateTileLine(tileId, data, meta, tipboard) {
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ Tipboard.Dashboard.setDataByKeys(tileId, data, ['subtitle', 'description']);
+ // config creation
+ renderersSwapper = new RenderersSwapper();
+ renderersSwapper.swap(meta);
+ var graphColors = {
+ grid: {
+ background: tipboard.color.tile_background,
+ gridLineColor: tipboard.color.tile_background
+ },
+ series: Tipboard.DisplayUtils.paletteAsSeriesColors()
+ };
+ var config = $.extend(true, {}, DEFAULT_LINE_CHART_CONFIG, graphColors, meta);
+
+ // autoscale required nodes and plot
+ // TODO use Tipboard.Dashboard.buildChart
+ Tipboard.DisplayUtils.expandLastChild(tile);
+ Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]);
+ Tipboard.Dashboard.chartsIds[tileId] = $.jqplot(
+ tileId + '-chart', data.series_list, config
+ );
+}
+
+Tipboard.Dashboard.registerUpdateFunction('line_chart', updateTileLine);
+
+var DEFAULT_LINE_CHART_CONFIG = {
+ legend: {
+ show: false,
+ location: 's',
+ border: '#232526',
+ background: '#232526'
+ },
+ title: {
+ show: false
+ },
+ grid: {
+ drawGridLines: false,
+ borderWidth: 0,
+ shadow: false
+ },
+ axes: {
+ xaxis: {
+ renderer: $.jqplot.CategoryAxisRenderer,
+ show: true,
+ tickOptions: {
+ showLabel: true,
+ showMark: false,
+ shadow: false
+ }
+ },
+ yaxis: {
+ show: true,
+ autoscale: true,
+ tickOptions: {
+ showLabel: false,
+ showMark: false,
+ shadow: false
+ }
+ }
+ },
+ seriesDefaults: {
+ showMarker: true,
+ lineWidth: 3,
+ shadow: false,
+ trendline: {
+ lineWidth: 3
+ },
+ pointLabels: {
+ color: '#ffffff',
+ xpadding: 10,
+ ypadding: 10,
+ location: 'sw'
+ },
+ markerRenderer: $.jqplot.MarkerRenderer,
+ markerOptions: {
+ show: true,
+ style: 'filledCircle',
+ lineWidth: 2,
+ size: 9,
+ shadow: true,
+ shadowAngle: 45,
+ shadowOffset: 1,
+ shadowDepth: 3,
+ shadowAlpha: 0.07
+ }
+ }
+};
diff --git a/tipboard/tiles/listing.css b/tipboard/tiles/listing.css
new file mode 100644
index 0000000..337fa40
--- /dev/null
+++ b/tipboard/tiles/listing.css
@@ -0,0 +1,14 @@
+.listing {
+ padding-top: 0.5em;
+ padding-left: 0;
+}
+
+.listing li {
+ font-size: 4rem;
+ margin-bottom: 0.2em;
+}
+
+.listing li span {
+ padding-left: 0.2em;
+}
+
diff --git a/tipboard/tiles/listing.html b/tipboard/tiles/listing.html
new file mode 100644
index 0000000..52719e6
--- /dev/null
+++ b/tipboard/tiles/listing.html
@@ -0,0 +1,13 @@
+<div class="tile listing-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <ul class="listing">
+ <!--
+ listing items added here by js
+ -->
+ </ul>
+ </div>
+</div>
+
diff --git a/tipboard/tiles/listing.js b/tipboard/tiles/listing.js
new file mode 100644
index 0000000..646ba8d
--- /dev/null
+++ b/tipboard/tiles/listing.js
@@ -0,0 +1,35 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+function updateTileListing(id, data, meta, tipboard) {
+ var MAX_ITEMS = 7;
+ var tile = $('#' + id)[0];
+ var container = $(tile).find('.listing')[0];
+ $(container).children().remove();
+ for (idx in data.items) {
+ if (idx > MAX_ITEMS) {
+ console.log([
+ 'ERROR: more then',
+ MAX_ITEMS,
+ 'items passed - RENDERING STOPPED'
+ ].join(' '))
+ break;
+ }
+ var textContent = data.items[idx];
+ appendListingItem(container, textContent);
+ }
+ Tipboard.TileDisplayDecorator.runAllDecorators(tile);
+}
+
+Tipboard.Dashboard.registerUpdateFunction('listing', updateTileListing);
+
+
+function appendListingItem(container, itemText) {
+ var htmlLabel = [
+ '<li class="listing-item">',
+ itemText,
+ '</li>'
+ ].join('\n');
+ $(container).append(htmlLabel);
+}
+
diff --git a/tipboard/tiles/norm_chart.html b/tipboard/tiles/norm_chart.html
new file mode 100644
index 0000000..4db929d
--- /dev/null
+++ b/tipboard/tiles/norm_chart.html
@@ -0,0 +1,10 @@
+<div class="tile norm-chart-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3 >{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{ tile_id + '-title' }}" class="result big-result"></h2>
+ <h3 id="{{tile_id + '-description'}}" class="result label"> </h3>
+ <div id="{{ tile_id + '-chart' }}" class="chart round-legend"> </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/norm_chart.js b/tipboard/tiles/norm_chart.js
new file mode 100644
index 0000000..dff6872
--- /dev/null
+++ b/tipboard/tiles/norm_chart.js
@@ -0,0 +1,142 @@
+(function ($) {
+ 'use strict';
+
+ var NormChartTile;
+
+ NormChartTile = {
+ defaultConfig: {
+ legend: {
+ location: 's',
+ renderer: $.jqplot.EnhancedLegendRenderer,
+ show: true,
+ rendererOptions: {
+ numberColumns: 3
+ }
+ },
+ grid: {
+ gridLineWidth: 0,
+ gridLineColor: '#4a4d52',
+ borderWidth: 0,
+ shadow: false
+ },
+ axes: {
+ yaxis: {
+ tickOptions: {
+ textColor: 'white',
+ showMark: false
+ }
+ },
+ xaxis: {
+ drawMajorGridlines: false,
+ show: false,
+ autoscale: true,
+ tickOptions: {
+ showLabel: false,
+ showMark: false,
+ shadow: false
+ }
+ }
+ },
+ seriesDefaults: {
+ trendline: {
+ show: false
+ },
+ pointLabels: {
+ show: false
+ },
+ rendererOptions: {
+ smooth: true
+ },
+ showMarker: false
+ },
+ },
+
+ isRawNormsSet: function (config) {
+ var isSet;
+ try {
+ isSet = config.canvasOverlay.objects;
+ isSet = typeof isSet === "object" ? true : false;
+ } catch (e) {
+ if (e.name === "TypeError") {
+ isSet = false;
+ } else {
+ throw e;
+ }
+ }
+ return isSet;
+ },
+
+ hasEasyNorms: function (config) {
+ var hasIt = typeof config.easyNorms === 'object' ? true : false;
+ return hasIt;
+ },
+
+ useEasyNorms: function (config) {
+ // config.easyNorms is [[color, y, width], [..]]
+ config.canvasOverlay = {
+ show: true,
+ objects: []
+ };
+
+ $.each(config.easyNorms, function (idx, val) {
+ var color = (
+ Tipboard.DisplayUtils.replaceFromPalette(val[0]) ||
+ Tipboard.DisplayUtils.paletteAsList()[
+ // wrap colors when out of list
+ idx % Tipboard.DisplayUtils.paletteAsList.length
+ ]);
+ config.canvasOverlay.objects.push({
+ horizontalLine: {
+ color: color,
+ y: val[1] || 0,
+ lineWidth: val[2] || 2,
+ shadow: false
+ //lineCap: 'butt',
+ //xOffset: 0
+ }
+ });
+ });
+ delete config.easyNorms;
+ },
+
+ setNorms: function (config) {
+ if (NormChartTile.isRawNormsSet(config)) {
+ delete config.easyNorms;
+ } else {
+ if (NormChartTile.hasEasyNorms(config)) {
+ NormChartTile.useEasyNorms(config);
+ }
+ }
+ },
+
+ updateTile: function (tileId, data, config) {
+ Tipboard.Dashboard.setDataByKeys(
+ tileId, data, ['title', 'description']
+ );
+ var newConfig = $.extend(true, {
+ // XXX: it could be set in defaultConfig, but initially palette
+ // is undefined
+ legend: {
+ border: Tipboard.DisplayUtils.palette.tile_background,
+ background: Tipboard.DisplayUtils.palette.tile_background
+ },
+ grid: {
+ background: Tipboard.DisplayUtils.palette.tile_background
+ }
+ }, NormChartTile.defaultConfig, config);
+ NormChartTile.setNorms(newConfig);
+ renderersSwapper.swap(newConfig);
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ Tipboard.DisplayUtils.rescaleTile(tile);
+ Tipboard.DisplayUtils.createGraph(
+ tileId, data.plot_data, newConfig
+ );
+ }
+ };
+
+ window.NormChartTile = NormChartTile;
+ Tipboard.Dashboard.registerUpdateFunction(
+ 'norm_chart', NormChartTile.updateTile
+ );
+
+}($));
diff --git a/tipboard/tiles/pie_chart.html b/tipboard/tiles/pie_chart.html
new file mode 100644
index 0000000..fa65a45
--- /dev/null
+++ b/tipboard/tiles/pie_chart.html
@@ -0,0 +1,10 @@
+<div class="tile pie-chart-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{tile_id + '-title' }}" class="result big-result"> </h2>
+ <h3 id="{{tile_id + '-description'}}" class="result label"> </h3>
+ <div id="{{tile_id + '-chart'}}" class='chart round-legend'> </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/pie_chart.js b/tipboard/tiles/pie_chart.js
new file mode 100644
index 0000000..27ab76e
--- /dev/null
+++ b/tipboard/tiles/pie_chart.js
@@ -0,0 +1,64 @@
+function updateTilePie(tileId, data, config, tipboard) {
+ Tipboard.Dashboard.setDataByKeys(tileId, data, ['title', 'description']);
+ renderersSwapper = new RenderersSwapper();
+ renderersSwapper.swap(config);
+ var config = jQuery.extend(true, DEFAULT_PIE_CHART_CONFIG, {
+ legend: {
+ border: Tipboard.DisplayUtils.palette.tile_background,
+ background: Tipboard.DisplayUtils.palette.tile_background
+ },
+ grid: {
+ background: Tipboard.DisplayUtils.palette.tile_background
+ }
+ }, config);
+ // colors from palette
+ if (config.seriesColors) {
+ $.each(config.seriesColors, function (idx, color) {
+ config.seriesColors[idx] = Tipboard.DisplayUtils.replaceFromPalette(color);
+ });
+ } else {
+ config.seriesColors = Tipboard.DisplayUtils.getPalette(
+ tipboard.color
+ );
+ }
+ // autoscale required nodes and plot
+ // TODO: use buildFittedChart
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ Tipboard.DisplayUtils.expandLastChild(tile);
+ Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]);
+ Tipboard.Dashboard.chartsIds[tileId] = $.jqplot(
+ tileId + '-chart', [data.pie_data], config
+ );
+}
+Tipboard.Dashboard.registerUpdateFunction('pie_chart', updateTilePie);
+
+var DEFAULT_PIE_CHART_CONFIG = {
+ title: false,
+ legend: {
+ textColor: 'white',
+ renderer: $.jqplot.DonutLegendRenderer,
+ location: 's',
+ show: true,
+ border: '0',
+ //placement: "outside",
+ rendererOptions: {
+ numberColumns: 3
+ }
+ },
+ grid: {
+ drawGridLines: false,
+ borderWidth: 0,
+ shadow: false
+ },
+ seriesDefaults: {
+ renderer: $.jqplot.DonutRenderer,
+ rendererOptions: {
+ padding: 0,
+ shadowAlpha: 0,
+ sliceMargin: 0,
+ innerDiameter: 60,
+ showDataLabels: true,
+ startAngle: -90
+ }
+ }
+};
diff --git a/tipboard/tiles/simple_percentage.css b/tipboard/tiles/simple_percentage.css
new file mode 100644
index 0000000..1140aba
--- /dev/null
+++ b/tipboard/tiles/simple_percentage.css
@@ -0,0 +1,31 @@
+.tile > .tile-content .highlight-footer {
+ height: 30%;
+ overflow: hidden;
+ padding-top: 10%;
+ position: relative;
+ width: 100%;
+}
+
+.row_1_of_3 .tile > .tile-content .highlight-footer {
+ height: 30%;
+ overflow: hidden;
+ padding-top: 2%;
+ width: 100%;
+}
+
+.tile > .tile-content .result-row > .result-col {
+ float: left;
+ width: 40%;
+ margin-left: 5%;
+ margin-right: 0;
+}
+
+.tile > .tile-content .result-row > .with-right-border {
+}
+
+.tile.simple-percentage-tile .result-col * {
+ white-space: nowrap;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/tipboard/tiles/simple_percentage.html b/tipboard/tiles/simple_percentage.html
new file mode 100644
index 0000000..7edb9af
--- /dev/null
+++ b/tipboard/tiles/simple_percentage.html
@@ -0,0 +1,22 @@
+<div class="tile highlighter-tile simple-percentage-tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <h2 id="{{tile_id + '-title'}}" class="result big-result fixed-height"></h2>
+ <h3 id="{{tile_id + '-subtitle'}}" class="result subtitle label fixed-height"></h3>
+ <div class="result big-value-container">
+ <span id="{{tile_id + '-big_value'}}"></span>
+ </div>
+ <div class="result-row highlight-footer">
+ <div class="result-col with-right-border">
+ <div id="{{tile_id + '-left_label'}}" class="result label"></div>
+ <div id="{{tile_id + '-left_value'}}" class="result medium-result"><span></span></div>
+ </div>
+ <div class="result-col">
+ <div id="{{tile_id + '-right_label'}}" class="result label"></div>
+ <div id="{{tile_id + '-right_value'}}" class="result medium-result"><span></span></div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/tipboard/tiles/simple_percentage.js b/tipboard/tiles/simple_percentage.js
new file mode 100644
index 0000000..a1c4a6c
--- /dev/null
+++ b/tipboard/tiles/simple_percentage.js
@@ -0,0 +1,18 @@
+/*jslint browser: true, devel: true*/
+/*global $, WebSocket: false, Tipboard: false*/
+
+"use strict";
+
+function updateTileSimplePercentage(tileId, data, config) {
+ Tipboard.Dashboard.setDataByKeys(tileId, data, 'all');
+ var highlighterNode = $('#' + tileId + '-big_value').parent();
+ Tipboard.DisplayUtils.applyHighlighterConfig(
+ highlighterNode, config.bigValueColor, config.fadingBackground
+ );
+ var tile = Tipboard.Dashboard.id2node(tileId);
+ // TODO: use rescaleTile instead
+ Tipboard.TileDisplayDecorator.runAllDecorators(tile);
+ Tipboard.DisplayUtils.expandLastChild($(tile).find('.tile-content')[0]);
+}
+
+Tipboard.Dashboard.registerUpdateFunction('simple_percentage', updateTileSimplePercentage);
diff --git a/tipboard/tiles/text.html b/tipboard/tiles/text.html
new file mode 100644
index 0000000..d0fd9a3
--- /dev/null
+++ b/tipboard/tiles/text.html
@@ -0,0 +1,9 @@
+<div class="tile" id="{{ tile_id }}">
+ <div class="tile-header">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="tile-content">
+ <span class="text-container"></span>
+ </div>
+</div>
+
diff --git a/tipboard/tiles/text.js b/tipboard/tiles/text.js
new file mode 100644
index 0000000..457cc02
--- /dev/null
+++ b/tipboard/tiles/text.js
@@ -0,0 +1,28 @@
+/*jslint browser: true, devel: true*/
+/*global WebSocket: false, Tipboard: false*/
+
+function updateTileText(id, data, meta, tipboard) {
+ var tile = $("#" + id)[0];
+ var textSelector = 'span.text-container';
+ containers = $(tile).find(textSelector);
+ if (containers.length != 1) {
+ console.log('tile ' + tile + 'does not include ONE: ' + textSelector);
+ }
+ var nodeWithText = containers[0];
+ $(nodeWithText).html(data['text']);
+
+ if (meta.font_size) {
+ $('.text-container').css("font-size", meta.font_size);
+ }
+ if (meta.font_color) {
+ $('.text-container').css(
+ "color", Tipboard.DisplayUtils.replaceFromPalette(meta.font_color)
+ );
+ }
+ if (meta.font_weight) {
+ $('.text-container').css("font-weight", meta.font_weight);
+ }
+ Tipboard.TileDisplayDecorator.runAllDecorators(tile);
+}
+Tipboard.Dashboard.updateFunctions['text'] = updateTileText;
+