Bug 679513 - Add Test Pilot to Thunderbird. r=Standard8
authorJim Porter <squibblyflabbetydoo@gmail.com>
Tue, 27 Sep 2011 18:37:10 +0100
changeset 9258 bfdcd57f0ad1d0ddaac5cde01d73c2c8ca8c4dd9
parent 9257 79ed2d94ef61667fb9eb70cd4b26fd04bc888a60
child 9259 8ccfe1db44231962af053373c5da4b2909b5b9cd
push id230
push userbugzilla@standard8.plus.com
push dateTue, 08 Nov 2011 22:55:24 +0000
treeherdercomm-beta@63dad5648415 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs679513
Bug 679513 - Add Test Pilot to Thunderbird. r=Standard8
mail/app/profile/Makefile.in
mail/app/profile/extensions/Makefile.in
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/bootstrap.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/chrome.manifest
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/components/TestPilot.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies-window.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies-window.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/browser.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/browser.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/browser.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/debug.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/experiment-page.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/feedback-browser.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/fennec-options.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.image.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.image.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.selection.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.selection.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.stack.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.stack.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.min.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/notificationBindings.xml
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/raw-data-dialog.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/raw-data-dialog.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/raw-data.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/screen.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/status-quit.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/status.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/status_mobile.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/survey-generator.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/take-survey.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/tp-browser-customNotifications.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/tp-browser-popupNotifications.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/welcome-page.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/welcome.html
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/window-utils.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/defaults/preferences/preferences.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/install.rdf.in
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/instrument/chrome.manifest
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/instrument/install.rdf
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/instrument/instrument.jsm
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/instrument/instrument.xul
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/Observers.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/dbutils.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/experiment_data_store.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/feedback.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/interface.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/interface.js.orig
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/jar-code-store.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/cuddlefish.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/file.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/memory.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/observer-service.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/plain-text-console.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/preferences-service.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/securable-module.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/timer.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/traceback.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/unit-test.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/unload.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/url.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/lib/xhr.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/log4moz.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/metadata.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/notifications.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/remote-experiment-loader.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/setup.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/string_sanitizer.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/modules/tasks.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/badge-default.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/bg.jpg
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/css/screen-standalone-mobile.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/css/screen-standalone.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/dino_32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/bg-status.jpg
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/callout.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/callout_continue.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/data1.jpg
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/data2.jpg
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_comments.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_computer.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_continue.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_quit.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_results.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_twitter.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/images/home_upcoming.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/logo.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/mozilla-logo.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/notification-tail-down.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/status-completed.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/status-ejected.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/status-missed.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/testPilot_200x200.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/testpilot_16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/testpilot_32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-currentstudies-32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-learned-32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-results-48x48.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-settings-32x32.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-study-48x48.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/all/tp-submit-48x48.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/close_button.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/feedback-broken-website.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/feedback-frown-16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/feedback-idea.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/feedback-rate.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/feedback-smile-16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/linux/feedback.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/close_button.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/feedback-broken-website.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/feedback-frown-16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/feedback-idea.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/feedback-rate.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/feedback-smile-16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/feedback.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/notification-tail-down.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/mac/notification-tail-up.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/close_button.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/feedback-broken-website.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/feedback-frown-16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/feedback-idea.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/feedback-rate.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/feedback-smile-16x16.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/feedback.css
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/notification-tail-down.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/skin/win/notification-tail-up.png
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/tests/foo.jar
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/tests/foo.js
mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/tests/test_data_store.js
mail/locales/en-US/feedback/main.dtd
mail/locales/en-US/feedback/main.properties
mail/locales/jar.mn
--- a/mail/app/profile/Makefile.in
+++ b/mail/app/profile/Makefile.in
@@ -37,16 +37,18 @@
 
 DEPTH		= ../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
+DIRS		= extensions
+
 include $(topsrcdir)/config/rules.mk
 
 FILES := \
 	mimeTypes.rdf \
 	localstore.rdf \
 	$(NULL)
 
 libs:: $(FILES)
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/Makefile.in
@@ -0,0 +1,69 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (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.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 2001
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+DISTROEXT = $(call core_abspath,$(DIST))/bin/distribution/extensions
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+ifneq (,$(filter nightly aurora beta,$(MOZ_UPDATE_CHANNEL)))
+EXTENSIONS = \
+  tbtestpilot@labs.mozilla.com \
+  $(NULL)
+
+DEFINES += -DTHUNDERBIRD_VERSION=$(THUNDERBIRD_VERSION)
+
+define _INSTALL_EXTENSION
+$(NSINSTALL) -D $(dir) && \
+  $(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $(srcdir)/$(dir)/install.rdf.in > $(dir)/install.rdf && \
+  cd $(dir) && \
+  $(ZIP) -r9XD $(DISTROEXT)/$(dir).xpi install.rdf && \
+  cd $(call core_abspath,$(srcdir)/$(dir)) && \
+  $(ZIP) -r9XD $(DISTROEXT)/$(dir).xpi * -x install.rdf.in
+
+endef # do not remove the blank line!
+
+libs::
+	$(NSINSTALL) -D $(DISTROEXT)
+	$(foreach dir,$(EXTENSIONS),$(_INSTALL_EXTENSION))
+endif
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/bootstrap.js
@@ -0,0 +1,76 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Test Pilot.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jono X <jono@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const APP_STARTUP = 1; //The application is starting up.
+const APP_SHUTDOWN = 2; //The application is shutting down.
+const ADDON_ENABLE = 3;	//The add-on is being enabled.
+const ADDON_DISABLE = 4; //The add-on is being disabled.
+const ADDON_INSTALL = 5; //The add-on is being installed.
+const ADDON_UNINSTALL = 6; //The add-on is being uninstalled.
+const ADDON_UPGRADE = 7; //The add-on is being upgraded.
+const ADDON_DOWNGRADE = 8; //The add-on is being downgraded.
+
+
+function startup(data, reason) {
+   // called when the extension needs to start itself up -
+   // data tells us extension id, version, and installPath.
+   // reason is one of APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL,
+   // ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+  /* TODO this will need to register a listener for new window opens,
+   * so tht it can apply the TestPilotWindowHandlers.onWindowLoad()
+   * currently defined in browser.js.  (Without an overlay, we have no
+   * other way of ensuring that the window load handler gets called for
+   * each window.)
+   *
+   * This will also need to manually insert CSS styles (which are otherwise
+   * included by the overlay.)   Look at the document.loadOverlay function.
+   * https://developer.mozilla.org/En/DOM/Document.loadOverlay
+   */
+}
+
+function shutdown(data, reason) {
+   // reason is one of APP_SHUTDOWN, ADDON_DISABLE, ADDON_UNINSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+}
+
+function install(data, reason) {
+  // Optional.  Called before first call to startup() when
+  // extension first installed.
+}
+
+function uninstall(data, reason) {
+  // Optional.  Called after last call to shutdown() when uninstalled.
+}
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/chrome.manifest
@@ -0,0 +1,25 @@
+resource testpilot ./
+content testpilot content/
+skin testpilot skin skin/all/
+skin testpilot-os skin skin/linux/ os=Linux
+skin testpilot-os skin skin/linux/ os=SunOS
+skin testpilot-os skin skin/mac/ os=Darwin
+skin testpilot-os skin skin/win/ os=WINNT
+
+overlay chrome://browser/content/macBrowserOverlay.xul chrome://testpilot/content/browser.xul
+
+overlay chrome://browser/content/browser.xul chrome://testpilot/content/browser.xul
+overlay chrome://messenger/content/mailWindowOverlay.xul chrome://testpilot/content/browser.xul
+
+style	chrome://global/content/customizeToolbar.xul	chrome://testpilot/content/browser.css
+# For the menubar on Mac
+overlay chrome://testpilot/content/all-studies-window.xul chrome://browser/content/macBrowserOverlay.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} os=Darwin
+
+
+# Remove the component for the Fennec version - don't think I need it:
+# component {e6e5e58f-7977-485a-b076-2f74bee2677b} components/TestPilot.js
+# contract @mozilla.org/testpilot/service;1 {e6e5e58f-7977-485a-b076-2f74bee2677b}
+# category profile-after-change testpilot @mozilla.org/testpilot/service;1
+
+# For the options on Fennec
+override chrome://testpilot/content/options.xul chrome://testpilot/content/fennec-options.xul application={a23983c0-fd0e-11dc-95ff-0800200c9a66}
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/components/TestPilot.js
@@ -0,0 +1,88 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Weave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Dan Mills <thunder@mozilla.com>
+ *  Jono X <jono@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function TestPilotComponent() {}
+TestPilotComponent.prototype = {
+  classDescription: "Test Pilot Component",
+  contractID: "@mozilla.org/testpilot/service;1",
+  classID: Components.ID("{e6e5e58f-7977-485a-b076-2f74bee2677b}"),
+  _xpcom_categories: [{ category: "profile-after-change" }],
+  _startupTimer: null,
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
+  observe: function TPC__observe(subject, topic, data) {
+    let os = Cc["@mozilla.org/observer-service;1"].
+        getService(Ci.nsIObserverService);
+    switch (topic) {
+    case "profile-after-change":
+      Services.console.logStringMessage("Test Pilot Component Sessionstore\n");
+      os.addObserver(this, "sessionstore-windows-restored", true);
+      break;
+    case "sessionstore-windows-restored":
+      Services.console.logStringMessage("Test Pilot Component Restored\n");
+      /* Stop oberver, to ensure that globalStartup doesn't get
+       * called more than once. */
+      os.removeObserver(this, "sessionstore-windows-restored", false);
+      /* Call global startup on a timer so that it's off of the main
+       * thread... delay a few seconds to give firefox time to finish
+       * starting up.
+       */
+      this._startupTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this._startupTimer.initWithCallback(
+        {notify: function(timer) {
+           Cu.import("resource://testpilot/modules/setup.js");
+           TestPilotSetup.globalStartup();
+         }}, 10000, Ci.nsITimer.TYPE_ONE_SHOT);
+      break;
+    }
+  }
+};
+
+const components = [TestPilotComponent];
+var NSGetFactory, NSGetModule;
+if (XPCOMUtils.generateNSGetFactory)
+  NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
+else
+  NSGetModule = XPCOMUtils.generateNSGetModule(components);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies-window.js
@@ -0,0 +1,478 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Test Pilot.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jono X <jono@mozilla.com>
+ *   Raymond Lee <raymond@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// TODO Show individual status page in new chromeless window as html with
+// background color set to "moz-dialog".
+
+const NO_STUDIES_IMG = "chrome://testpilot/skin/testPilot_200x200.png";
+const PROPOSE_STUDY_URL =
+  "https://wiki.mozilla.org/Labs/Test_Pilot#For_researchers";
+
+var TestPilotXulWindow = {
+  _stringBundle : null,
+
+  onSubmitButton: function(experimentId) {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    let task = TestPilotSetup.getTaskById(experimentId);
+    let button = document.getElementById("submit-button-" + task.id);
+
+    // Hide the upload button so it doesn't get clicked again...
+    let parent = button.parentNode;
+    while (parent.firstChild) {
+      parent.removeChild(parent.firstChild);
+    }
+    // Replace it with a message:
+    this.addLabel(
+      parent,
+      this._stringBundle.getString("testpilot.studiesWindow.uploading"));
+    let self = this;
+
+    task.upload( function(success) {
+      while (parent.firstChild) {
+        parent.removeChild(parent.firstChild);
+      }
+      if (success) {
+        self.addThanksMessage(parent);
+        // TODO or should we move it to 'finished studies' immediately?
+      } else {
+        // TODO a better error message?
+        self.addLabel(
+          parent,
+          self._stringBundle.getString(
+            "testpilot.studiesWindow.unableToReachServer"));
+      }
+    });
+
+  },
+
+  addThanksMessage: function(container) {
+    // Fill in status box with icon and message to show success
+    let hbox = document.createElement("hbox");
+    container.appendChild(this.makeSpacer());
+    container.appendChild(hbox);
+    this.addLabel(
+      container,
+      this._stringBundle.getString(
+        "testpilot.studiesWindow.thanksForContributing"));
+    container.appendChild(this.makeSpacer());
+    hbox.appendChild(this.makeSpacer());
+    this.addImg(hbox, "study-submitted");
+    hbox.appendChild(this.makeSpacer());
+  },
+
+  addXulLink: function (container, text, url, openInTab) {
+    let linkContainer = document.createElement("hbox");
+    let link = document.createElement("label");
+    let spacer = document.createElement("spacer");
+    link.setAttribute("value", text);
+    link.setAttribute("class", "text-link");
+    if (openInTab) {
+      link.setAttribute(
+        "onclick",
+        "if (event.button==0) { " +
+        "TestPilotWindowUtils.openInTab('" + url + "'); }");
+    } else {
+      link.setAttribute(
+        "onclick",
+        "if (event.button==0) { " +
+        "TestPilotWindowUtils.openChromeless('" + url + "'); }");
+    }
+    linkContainer.appendChild(link);
+    spacer.setAttribute("flex", "1");
+    linkContainer.appendChild(spacer);
+    container.appendChild(linkContainer);
+  },
+
+  addLabel: function(container, text, styleClass) {
+    let label = document.createElement("label");
+    label.setAttribute("value", text);
+    if (styleClass) {
+      label.setAttribute("class", styleClass);
+    }
+    container.appendChild(label);
+  },
+
+  addImg: function(container, iconClass) {
+    let newImg = document.createElement("image");
+    newImg.setAttribute("class", iconClass);
+    container.appendChild(newImg);
+  },
+
+  makeSpacer: function() {
+    let spacer = document.createElement("spacer");
+    spacer.setAttribute("flex", "1");
+    return spacer;
+  },
+
+  addThumbnail: function(container, imgUrl) {
+    let boundingBox = document.createElement("vbox");
+    boundingBox.setAttribute("class", "results-thumbnail");
+    let bBox2 = document.createElement("hbox");
+
+    boundingBox.appendChild(this.makeSpacer());
+    boundingBox.appendChild(bBox2);
+    boundingBox.appendChild(this.makeSpacer());
+
+    bBox2.appendChild(this.makeSpacer());
+    let newImg = document.createElement("image");
+    newImg.setAttribute("src", imgUrl);
+    newImg.setAttribute("class", "results-thumbnail");
+    bBox2.appendChild(newImg);
+    bBox2.appendChild(this.makeSpacer());
+
+    container.appendChild(boundingBox);
+  },
+
+  addProgressBar: function(container, percent) {
+    let progBar = document.createElement("progressmeter");
+    progBar.setAttribute("mode", "determined");
+    progBar.setAttribute("value", Math.ceil(percent).toString());
+    container.appendChild(progBar);
+  },
+
+  addDescription: function(container, title, paragraph) {
+    let desc = document.createElement("description");
+    desc.setAttribute("class", "study-title");
+    let txtNode = document.createTextNode(title);
+    desc.appendChild(txtNode);
+    container.appendChild(desc);
+
+    desc = document.createElement("description");
+    desc.setAttribute("class", "study-description");
+    desc.setAttribute("crop", "none");
+    txtNode = document.createTextNode(paragraph);
+    desc.appendChild(txtNode);
+    container.appendChild(desc);
+  },
+
+  addButton: function(container, label, id, onClickHandler) {
+    let button = document.createElement("button");
+    button.setAttribute("label", label);
+    button.setAttribute("id", id);
+    button.setAttribute("oncommand", onClickHandler);
+    container.appendChild(button);
+  },
+
+  _sortNewestFirst: function(experiments) {
+    experiments.sort(
+      function sortFunc(a, b) {
+        if (a.endDate && b.endDate) {
+          return b.endDate - a.endDate;
+        }
+        if (a.publishDate && b.publishDate) {
+          if (isNaN(a.publishDate) || isNaN(b.publishDate)) {
+            return 0;
+          }
+          return b.publishDate - a.publishDate;
+        }
+        return 0;
+      });
+    return experiments;
+  },
+
+  onLoad: function () {
+    Components.utils.import("resource://testpilot/modules/Observers.js");
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    Components.utils.import("resource://testpilot/modules/tasks.js");
+
+    this._stringBundle = document.getElementById("testpilot-stringbundle");
+    this.sizeWindow();
+    this._init(false);
+    Observers.add("testpilot:task:changed", this._onTaskStatusChanged, this);
+  },
+
+  onUnload: function() {
+    document.getElementById("settings-pane").writePreferences(true);
+    Observers.remove("testpilot:task:changed", this._onTaskStatusChanged, this);
+  },
+
+  _onTaskStatusChanged : function() {
+    this._init(true);
+  },
+
+  onTakeSurveyButton: function(taskId) {
+    let task = TestPilotSetup.getTaskById(taskId);
+    TestPilotWindowUtils.openChromeless(task.defaultUrl);
+    task.onDetailPageOpened();
+  },
+
+  _init: function(aReload) {
+    let experiments;
+    let ready = false;
+
+    // Are we done loading tasks?
+    if (TestPilotSetup.startupComplete) {
+      experiments = TestPilotSetup.getAllTasks();
+      if (experiments.length > 0 ) {
+        ready = true;
+      }
+    }
+
+    if (!ready) {
+      // If you opened the window before tasks are done loading, exit now
+      // but try again in a few seconds.
+      window.setTimeout(
+        function() { TestPilotXulWindow._init(aReload); }, 2000);
+      return;
+    }
+
+    let numFinishedStudies = 0;
+    let numCurrentStudies = 0;
+
+    /* Remove 'loading' message */
+    let msg = window.document.getElementById("still-loading-msg");
+    msg.setAttribute("hidden", "true");
+
+    if (aReload) {
+      /* If we're reloading, start by clearing out any old stuff already
+       * present in the listboxes. */
+      let listboxIds =
+        ["current-studies-listbox", "finished-studies-listbox",
+         "study-results-listbox"];
+      for (let i = 0; i < listboxIds.length; i++) {
+        let listbox = document.getElementById(listboxIds[i]);
+
+        while (listbox.lastChild) {
+          listbox.removeChild(listbox.lastChild);
+        }
+      }
+    }
+
+    experiments = this._sortNewestFirst(experiments);
+
+    for (let i = 0; i < experiments.length; i++) {
+      let task = experiments[i];
+      let newRow = document.createElement("richlistitem");
+      newRow.setAttribute("class", "tp-study-list");
+
+      this.addThumbnail(newRow, task.thumbnail);
+
+      let textVbox = document.createElement("vbox");
+      newRow.appendChild(textVbox);
+
+      let openInTab = (task.taskType == TaskConstants.TYPE_LEGACY);
+
+      this.addDescription(textVbox, task.title, task.summary);
+      this.addXulLink(
+        textVbox, this._stringBundle.getString("testpilot.moreInfo"),
+        task.defaultUrl, openInTab);
+
+      // Create the rightmost status area, depending on status:
+      let statusVbox = document.createElement("vbox");
+      if (task.status == TaskConstants.STATUS_FINISHED) {
+        this.addLabel(
+          statusVbox,
+          this._stringBundle.getFormattedString(
+            "testpilot.studiesWindow.finishedOn",
+            [(new Date(task.endDate)).toLocaleDateString()]));
+        this.addButton(statusVbox,
+          this._stringBundle.getString("testpilot.submit"),
+          "submit-button-" + task.id,
+          "TestPilotXulWindow.onSubmitButton(" + task.id + ");");
+      }
+      if (task.status == TaskConstants.STATUS_CANCELLED) {
+        let hbox = document.createElement("hbox");
+        newRow.setAttribute("class", "tp-opted-out");
+        statusVbox.appendChild(this.makeSpacer());
+        statusVbox.appendChild(hbox);
+        this.addLabel(
+          statusVbox,
+          this._stringBundle.getString("testpilot.studiesWindow.canceledStudy"));
+        statusVbox.appendChild(this.makeSpacer());
+        hbox.appendChild(this.makeSpacer());
+        this.addImg(hbox, "study-canceled");
+        hbox.appendChild(this.makeSpacer());
+      }
+      if (task.status == TaskConstants.STATUS_NEW ||
+          task.status == TaskConstants.STATUS_PENDING ) {
+        newRow.setAttribute("class", "tp-new-results");
+
+        if (task.taskType == TaskConstants.TYPE_SURVEY) {
+          this.addButton(
+            statusVbox,
+            this._stringBundle.getString("testpilot.takeSurvey"),
+            "survey-button",
+            "TestPilotXulWindow.onTakeSurveyButton('" + task.id + "');");
+        } else if (task.taskType == TaskConstants.TYPE_EXPERIMENT) {
+          if (task.startDate) {
+            this.addLabel(
+              statusVbox,
+              this._stringBundle.getFormattedString(
+                "testpilot.studiesWindow.willStart",
+                [(new Date(task.startDate)).toLocaleDateString()]));
+          }
+        }
+      }
+      if (task.status == TaskConstants.STATUS_IN_PROGRESS ||
+          task.status == TaskConstants.STATUS_STARTING) {
+
+        if (task.taskType == TaskConstants.TYPE_SURVEY) {
+          this.addButton(
+            statusVbox,
+            this._stringBundle.getString("testpilot.takeSurvey"),
+            "survey-button",
+            "TestPilotXulWindow.onTakeSurveyButton('" + task.id + "');");
+        } else if (task.taskType == TaskConstants.TYPE_EXPERIMENT) {
+          this.addLabel(
+            statusVbox,
+            this._stringBundle.getString(
+             "testpilot.studiesWindow.gatheringData"));
+             let now = (new Date()).getTime();
+          let progress =
+            100 * (now - task.startDate) / (task.endDate - task.startDate);
+          this.addProgressBar(statusVbox, progress);
+          this.addLabel(
+            statusVbox,
+            this._stringBundle.getFormattedString(
+              "testpilot.studiesWindow.willFinish",
+              [(new Date(task.endDate)).toLocaleDateString()]));
+        }
+      }
+      if (task.status >= TaskConstants.STATUS_SUBMITTED) {
+        if (task.taskType == TaskConstants.TYPE_RESULTS) {
+          let maintask = TestPilotSetup.getTaskById(task.relatedStudyId);
+          if (maintask && maintask.status >= TaskConstants.STATUS_SUBMITTED) {
+            this.addThanksMessage(statusVbox);
+          }
+        } else {
+          if (task.status == TaskConstants.STATUS_MISSED) {
+            // Icon for missed studies
+            let hbox = document.createElement("hbox");
+            newRow.setAttribute("class", "tp-opted-out");
+            statusVbox.appendChild(this.makeSpacer());
+            statusVbox.appendChild(hbox);
+            this.addLabel(
+              statusVbox,
+              this._stringBundle.getString("testpilot.studiesWindow.missedStudy"));
+            statusVbox.appendChild(this.makeSpacer());
+            hbox.appendChild(this.makeSpacer());
+            this.addImg(hbox, "study-missed");
+            hbox.appendChild(this.makeSpacer());
+          } else {
+            this.addThanksMessage(statusVbox);
+            numFinishedStudies ++;
+          }
+        }
+      }
+      let spacer = document.createElement("spacer");
+      spacer.setAttribute("flex", "1");
+      newRow.appendChild(spacer);
+      newRow.appendChild(statusVbox);
+
+      // Use status to decide which panel to add this to:
+      let rowset;
+      if (task.taskType == TaskConstants.TYPE_RESULTS) {
+        rowset = document.getElementById("study-results-listbox");
+      } else if (task.status > TaskConstants.STATUS_FINISHED) {
+        rowset = document.getElementById("finished-studies-listbox");
+      } else {
+        rowset = document.getElementById("current-studies-listbox");
+        numCurrentStudies++;
+      }
+
+      // TODO further distinguish by background colors.
+      rowset.appendChild(newRow);
+    }
+
+    // If there are no current studies, show a message about upcoming
+    // studies:
+    if (numCurrentStudies == 0) {
+      let newRow = document.createElement("richlistitem");
+      newRow.setAttribute("class", "tp-study-list");
+      this.addThumbnail(newRow, NO_STUDIES_IMG);
+      let textVbox = document.createElement("vbox");
+      textVbox.setAttribute("class", "pilot-largetext");
+      newRow.appendChild(textVbox);
+      this.addDescription(
+        textVbox, "",
+        this._stringBundle.getString("testpilot.studiesWindow.noStudies"));
+      this.addXulLink(
+        textVbox,
+        this._stringBundle.getString("testpilot.studiesWindow.proposeStudy"),
+        PROPOSE_STUDY_URL, true);
+      document.getElementById("current-studies-listbox").appendChild(newRow);
+    }
+
+    // Show number of studies the user finished on badge:
+    document.getElementById("num-finished-badge").setAttribute(
+      "value", numFinishedStudies);
+  },
+
+  sizeWindow: function() {
+    // Size listboxes based on available screen size, then size window to fit
+    // list boxes.
+    let currList = document.getElementById("current-studies-listbox");
+    let finList = document.getElementById("finished-studies-listbox");
+    let resultsList = document.getElementById("study-results-listbox");
+
+    let screenWidth = window.screen.availWidth;
+    let screenHeight = window.screen.availHeight;
+    let width = screenWidth >= 800 ? 700 : screenWidth - 100;
+    let height = screenHeight >= 800 ? 700 : screenHeight - 100;
+
+    height -= 130; // Or whatever is height of title bar plus windowdragbox
+
+    currList.width = width;
+    currList.height = height;
+    finList.width = width;
+    finList.height = height;
+    resultsList.width = width;
+    resultsList.height = height;
+    window.sizeToContent();
+  },
+
+  focusPane: function(paneIndex) {
+    document.getElementById("tp-xulwindow-deck").selectedIndex = paneIndex;
+
+    // When you focus the 'study findings' tab, any results there which
+    // are still marked "new" should have their status changed as the user
+    // is considered to have seen them.
+    if (paneIndex == 2) {
+      Components.utils.import("resource://testpilot/modules/setup.js");
+      Components.utils.import("resource://testpilot/modules/tasks.js");
+
+      let experiments = TestPilotSetup.getAllTasks();
+      for each (let experiment in experiments) {
+        if (experiment.taskType == TaskConstants.TYPE_RESULTS) {
+          if (experiment.status == TaskConstants.STATUS_NEW) {
+            experiment.changeStatus(TaskConstants.STATUS_ARCHIVED, true);
+          }
+        }
+      }
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies-window.xul
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/content/extensions/extensions.css"
+  type="text/css"?>
+<?xml-stylesheet href="chrome://testpilot/content/browser.css" type="text/css"?>
+
+
+<!DOCTYPE prefwindow [
+  <!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
+    %testpilotDTD;
+]>
+
+<prefwindow id="test-pilot-all-studies-window"
+  title="&testpilot.studiesWindow.title;"
+  windowtype="extensions:testpilot:all_studies_window"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+  onload="TestPilotXulWindow.onLoad();"
+  onunload="TestPilotXulWindow.onUnload();">
+
+  <script src="chrome://testpilot/content/window-utils.js"
+    type="application/javascript;version=1.8"/>
+  <script src="chrome://testpilot/content/all-studies-window.js"
+    type="application/javascript;version=1.8"/>
+  <script src="chrome://communicator/content/utilityOverlay.js"
+    type="application/javascript;version=1.8"/>
+
+  <stringbundleset id="stringbundleset">
+    <stringbundle id="testpilot-stringbundle"
+      src="chrome://testpilot/locale/main.properties" />
+  </stringbundleset>
+
+  <vbox flex="1">
+    <windowdragbox orient="vertical">
+      <radiogroup id="tp-radiogroup" orient="horizontal" role="listbox"
+        class="paneSelector"
+        onselect="TestPilotXulWindow.focusPane(this.selectedIndex);">
+        <radio pane="current-studies-pane-button" orient="vertical">
+          <image class="paneButtonIcon" />
+          <label class="paneButtonLabel"
+            value="&testpilot.studiesWindow.currentStudies.label;"/>
+        </radio>
+        <radio pane="finished-studies-pane-button" orient="vertical">
+          <stack align="center" pack="center">
+            <hbox align="center" pack="center">
+             <image class="paneButtonIcon" />
+            </hbox>
+            <label id="num-finished-badge" class="pane-button-badge"/>
+          </stack>
+          <label class="paneButtonLabel"
+            value="&testpilot.studiesWindow.finishedStudies.label;"/>
+        </radio>
+        <radio pane="study-results-pane-button" orient="vertical">
+          <image class="paneButtonIcon" />
+          <label class="paneButtonLabel"
+            value="&testpilot.studiesWindow.studyFindings.label;"/>
+        </radio>
+        <radio pane="settings-pane-button" orient="vertical">
+          <image class="paneButtonIcon" />
+          <label class="paneButtonLabel"
+            value="&testpilot.studiesWindow.settings.label;"/>
+        </radio>
+      </radiogroup>
+    </windowdragbox>
+
+    <deck id="tp-xulwindow-deck" flex="1">
+      <prefpane id="current-studies-pane" class="tp-tab-panel">
+        <richlistbox id="current-studies-listbox" class="tp-study-list"
+          disabled="true">
+          <richlistitem id="still-loading-msg" class="tp-study-list">
+            <description class="pilot-largetext">
+              &testpilot.studiesWindow.stillLoadingMessage;
+            </description>
+          </richlistitem>
+        </richlistbox>
+      </prefpane>
+
+      <prefpane id="finished-studies-pane" class="tp-tab-panel">
+        <richlistbox id="finished-studies-listbox" class="tp-study-list"
+          disabled="true"/>
+      </prefpane>
+
+      <prefpane id="study-results-pane" class="tp-tab-panel">
+        <richlistbox id="study-results-listbox" class="tp-study-list"
+          disabled="true"/>
+      </prefpane>
+
+      <prefpane id="settings-pane" class="tp-tab-panel">
+        <preferences>
+          <preference id="notify-finished" type="bool"
+                      name="extensions.testpilot.popup.showOnStudyFinished"/>
+          <preference id="notify-new" type="bool"
+                      name="extensions.testpilot.popup.showOnNewStudy"/>
+          <preference id="notify-results" type="bool"
+                      name="extensions.testpilot.popup.showOnNewResults"/>
+          <preference id="always-submit-data" type="bool"
+                      name="extensions.testpilot.alwaysSubmitData"/>
+        </preferences>
+        <vbox style="padding: 12px;">
+          <groupbox>
+            <caption label="&testpilot.settings.dataSubmission.label;" />
+            <checkbox label="&testpilot.settings.alwaysSubmitData.shortLabel;"
+                      preference="always-submit-data"/>
+          </groupbox>
+          <groupbox>
+            <caption label="&testpilot.settings.notifications.label;" />
+            <label value="&testpilot.settings.notifyWhen.label;"/>
+            <hbox>
+              <separator orient="vertical" />
+              <vbox>
+                <checkbox label="&testpilot.settings.readyToSubmit.label;"
+                          preference="notify-finished"/>
+                <checkbox label="&testpilot.settings.newStudy.label;"
+                          preference="notify-new"/>
+                <checkbox label="&testpilot.settings.hasNewResults.label;"
+                          preference="notify-results"/>
+              </vbox>
+            </hbox>
+          </groupbox>
+        </vbox>
+      </prefpane>
+    </deck>
+  </vbox>
+
+  <!-- For the menubar on mac, the below elements are needed for
+       macBrowserOverlay.xul to overlay this window. -->
+  <stringbundleset id="stringbundleset"/>
+
+  <commandset id="mainCommandSet"/>
+  <commandset id="baseMenuCommandSet"/>
+  <commandset id="placesCommands"/>
+
+  <broadcasterset id="mainBroadcasterSet"/>
+
+  <keyset id="mainKeyset"/>
+  <keyset id="baseMenuKeyset"/>
+
+  <menubar id="main-menubar" style="border:none !important;margin:0;padding:0;"/>
+
+</prefwindow>
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+  <head>
+    <title>All Test Pilot Studies</title>
+    <script src="chrome://testpilot/content/all-studies.js"
+             type="application/javascript;version=1.8">
+    </script>
+  </head>
+  <body onload="fillAllStudiesPage();">
+    <h1>All Test Pilot Studies</h1>
+    <p id="still-loading-msg">Loading, please wait...</p>
+    <table id="studies-list"></table>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/all-studies.js
@@ -0,0 +1,102 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Test Pilot.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jono X <jono@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// for the HTML version
+
+function _sortNewestFirst(experiments) {
+    experiments.sort(
+      function sortFunc(a, b) {
+        if (a.endDate && b.endDate) {
+          return b.endDate - a.endDate;
+        }
+        if (a.publishDate && b.publishDate) {
+          if (isNaN(a.publishDate) || isNaN(b.publishDate)) {
+            return 0;
+          }
+          return b.publishDate - a.publishDate;
+        }
+        return 0;
+      });
+    return experiments;
+}
+
+
+function fillAllStudiesPage() {
+  Components.utils.import("resource://testpilot/modules/Observers.js");
+  Components.utils.import("resource://testpilot/modules/setup.js");
+  Components.utils.import("resource://testpilot/modules/tasks.js");
+  //this._stringBundle = document.getElementById("testpilot-stringbundle");
+
+
+  // Are we done loading tasks?
+  if (!TestPilotSetup.startupComplete || TestPilotSetup.getAllTasks().length == 0) {
+    // If you opened the window before tasks are done loading, exit now
+    // but try again in a few seconds.
+    window.setTimeout(fillAllStudiesPage, 2000);
+    return;
+  }
+
+  // hide the 'loading' msg
+  window.document.getElementById("still-loading-msg").innerHTML = "";
+
+  // clear the table
+  let table = window.document.getElementById("studies-list");
+  table.innerHTML = "";
+
+  let experiments = _sortNewestFirst(TestPilotSetup.getAllTasks());
+
+  for (let i = 0; i < experiments.length; i++) {
+    let task = experiments[i];
+    let newRow = document.createElement("tr");
+
+    let newCell = document.createElement("td");
+    newCell.innerHTML = task.title;
+    newRow.appendChild(newCell);
+    newCell = document.createElement("td");
+    newCell.innerHTML = task.summary;
+    newRow.appendChild(newCell);
+
+    let link = document.createElement("a");
+    link.setAttribute("href", task.defaultUrl);
+    link.innerHTML = "More Info";
+
+    newCell = document.createElement("td");
+    newCell.appendChild(link);
+    newRow.appendChild(newCell);
+
+    table.appendChild(newRow);
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/browser.css
@@ -0,0 +1,285 @@
+/* Toolbar Button */
+
+#feedback-menu-button {
+  -moz-box-orient: horizontal;
+}
+
+#feedback-menu-button .toolbarbutton-icon {
+  display: none;
+}
+
+#feedback-menu-button .toolbarbutton-menu-dropmarker {
+  -moz-padding-start: 5px;
+}
+
+#pilot-notifications-button {
+  margin-right: 10px;
+}
+
+/* For Firefox 4 built-in popup notification system */
+#tp-notification-popup-icon {
+  list-style-image: url("chrome://testpilot/skin/testpilot_16x16.png");
+}
+
+#tp-notification-popup-box[anchorid="tp-notification-popup-icon"] > #tp-notification-popup-icon {
+  display: -moz-box;
+}
+
+#testpilot-notification {
+  -moz-binding: url("chrome://testpilot/content/notificationBindings.xml#testpilot-notification");
+}
+
+/* hide menu separator and "not now" item: */
+#testpilot-notification menuseparator { display: none }
+#testpilot-notification .popup-notification-closeitem { display: none }
+
+.testpilot-notification-title {
+    text-align: center;
+    font-size: large;
+}
+
+/* .popup-notification-icon[popupid="study-finished"] {
+  list-style-image: url("chrome://testpilot/skin/tp-submit-48x48.png");
+  height: 48px;
+  width: 48px;
+}
+.popup-notification-icon[popupid="new-study"] {
+  list-style-image: url("chrome://testpilot/skin/tp-study-48x48.png");
+  height: 48px;
+  width: 48px;
+}
+.popup-notification-icon[popupid="new-results"] {
+  list-style-image: url("chrome://testpilot/skin/tp-results-48x48.png");
+  height: 48px;
+  width: 48px;
+}
+.popup-notification-icon[popupid="study-submitted"] {
+  list-style-image: url("chrome://testpilot/skin/status-completed.png");
+  height: 32px;
+  width: 64px;
+}*/
+
+/* For older notification system */
+
+/* Popup Bounding Box */
+#pilot-notification-popup {
+  -moz-appearance: none;
+  -moz-window-shadow: none;
+  background-color: transparent;
+  margin-top: -6px;
+  margin-right: -3px;
+  width: 480px;
+}
+
+.tail-up {
+ -moz-border-image: url(chrome://testpilot-os/skin/notification-tail-up.png) 26 56 22 18 / 26px 56px 22px 18px round stretch;
+}
+
+/* tail-down uses the old styling; it doesn't look as good as the new styling,
+   but the new styling doesn't work on 3.6.
+   TODO: If someone is using 3.7.* or 4.* but is NOT on the beta channel and
+   installed Test Pilot from AMO, they should get the new styling, similar
+   to .tail-up! */
+.tail-down {
+ -moz-border-image: url(chrome://testpilot/skin/notification-tail-down.png) 26 50 22 18 / 26px 50px 22px 18px repeat;
+ color: white;
+}
+
+.pilot-notification-popup-container {
+  -moz-appearance: none;
+  margin-right: -42px;
+  padding: 0px 5px 5px 5px;
+  font-size: 14px;
+}
+
+.pilot-notification-toprow {
+  margin-bottom: 12px;
+}
+
+#pilot-notification-text,
+#pilot-notification-link {
+  margin-bottom: 5px;
+}
+
+#pilot-notification-close {
+  list-style-image: url("chrome://testpilot-os/skin/close_button.png");
+  -moz-image-region: rect(0px, 14px, 14px, 0px);
+  width: 14px;
+  height: 14px;
+}
+
+#pilot-notification-close:hover {
+  -moz-image-region: rect(0px, 28px, 14px, 14px);
+}
+
+#pilot-notification-close:hover:active {
+  -moz-image-region: rect(0px, 42px, 14px, 28px);
+}
+
+.pilot-notify-me-when[disabled="true"] {
+  color: MenuText;
+}
+.pilot-title {
+  font-size: 25px;
+}
+
+image.study-finished {
+  list-style-image: url("chrome://testpilot/skin/tp-submit-48x48.png");
+  height: 48px;
+  width: 48px;
+  margin-right: 8px;
+}
+
+image.study-submitted {
+  list-style-image: url("chrome://testpilot/skin/status-completed.png");
+  height: 32px;
+  width: 64px;
+  margin-right: 8px;
+}
+
+image.study-canceled {
+  list-style-image: url("chrome://testpilot/skin/status-ejected.png");
+  height: 32px;
+  width: 64px;
+  margin-right: 8px;
+}
+
+image.study-missed {
+  list-style-image: url("chrome://testpilot/skin/status-missed.png");
+  height: 32px;
+  width: 64px;
+  margin-right: 8px;
+}
+
+image.new-study {
+  list-style-image: url("chrome://testpilot/skin/tp-study-48x48.png");
+  height: 48px;
+  width: 48px;
+  margin-right: 8px;
+}
+
+image.new-results {
+  list-style-image: url("chrome://testpilot/skin/tp-results-48x48.png");
+  height: 48px;
+  width: 48px;
+  margin-right: 8px;
+}
+
+image.update-extension {
+  list-style-image: url("chrome://testpilot/skin/testpilot_32x32.png");
+  height: 48px;
+  width: 48px;
+  margin-right: 8px;
+}
+
+image.study-result {
+  list-style-image: url("chrome://testpilot/skin/badge-default.png");
+  height: 96px;
+  width: 96px;
+  margin-right: 8px;
+}
+
+/* All studies window */
+.pilot-largetext {
+  font-size: 16px;
+}
+
+#test-pilot-all-studies-window > .prefWindow-dlgbuttons {
+    display: none;
+}
+
+.paneSelector {
+    margin: 0 !important;
+}
+
+.paneSelector radio[pane="current-studies-pane-button"] .paneButtonIcon {
+    list-style-image: url("chrome://testpilot/skin/tp-currentstudies-32x32.png");
+    padding-top: 3px;
+}
+.paneSelector radio[pane="finished-studies-pane-button"] .paneButtonIcon {
+    list-style-image: url("chrome://testpilot/skin/tp-completedstudies-32x32.png");
+    padding-top: 3px;
+}
+.paneSelector radio[pane="study-results-pane-button"] .paneButtonIcon {
+    list-style-image: url("chrome://testpilot/skin/tp-learned-32x32.png");
+    padding-top: 3px;
+}
+.paneSelector radio[pane="settings-pane-button"] .paneButtonIcon {
+    list-style-image: url("chrome://testpilot/skin/tp-settings-32x32.png");
+    padding-top: 3px;
+}
+
+.pane-button-badge {
+    background-color: green;
+    color: white;
+    font-weight: bold;
+    padding: 2px;
+    -moz-border-radius: 100%;
+    margin-right: 25px;
+    margin-bottom: 13px;
+}
+
+richlistbox.tp-study-list {
+    overflow: auto;
+    margin: 0px;
+}
+
+.tp-tab-panel {
+    background-color: -moz-dialog;
+    padding: 0px;
+}
+
+description.study-description {
+    width: 350px;
+}
+
+description.study-title {
+    width: 350px;
+    font-size: 20px;
+    text-align: left;
+    margin-top: 10px;
+}
+
+richlistitem.tp-study-list {
+    min-height: 120px;
+    color: black;
+    background-color: -moz-dialog;
+}
+
+richlistitem.tp-new-results {
+    min-height: 120px;
+    color: black;
+    background-color: LemonChiffon;
+}
+
+richlistitem.tp-opted-out {
+    min-height: 120px;
+    color: grey;
+    background-color: -moz-dialog;
+}
+
+vbox.results-thumbnail {
+    height: 120px;
+    width: 120px;
+}
+
+image.results-thumbnail {
+    max-height: 90px;
+    max-width:  90px;
+    margin: 10px;
+}
+
+.notification-link {
+    text-decoration: underline;
+    cursor: pointer;
+}
+
+prefpane .groupbox-body {
+  -moz-appearance: none;
+  padding: 8px 4px 4px 4px;
+}
+
+prefpane .groupbox-title {
+  background: url("chrome://global/skin/50pct_transparent_grey.png") repeat-x bottom left;
+  margin-bottom: 4px;
+}
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/browser.js
@@ -0,0 +1,212 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Test Pilot.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Atul Varma <atul@mozilla.com>
+ *   Jono X <jono@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var TestPilotMenuUtils;
+
+(function() {
+  var Cc = Components.classes;
+  var Cu = Components.utils;
+  var Ci = Components.interfaces;
+
+  Cu.import("resource://testpilot/modules/setup.js");
+
+  TestPilotMenuUtils = {
+    __prefs: null,
+    get _prefs() {
+      this.__prefs = Cc["@mozilla.org/preferences-service;1"]
+        .getService(Ci.nsIPrefBranch);
+      return this.__prefs;
+    },
+
+    updateSubmenu: function() {
+      let ntfyMenuFinished =
+        document.getElementById("pilot-menu-notify-finished");
+      let ntfyMenuNew = document.getElementById("pilot-menu-notify-new");
+      let ntfyMenuResults = document.getElementById("pilot-menu-notify-results");
+      let alwaysSubmitData =
+        document.getElementById("pilot-menu-always-submit-data");
+      ntfyMenuFinished.setAttribute("checked",
+                                    this._prefs.getBoolPref(POPUP_SHOW_ON_FINISH));
+      ntfyMenuNew.setAttribute("checked",
+                                 this._prefs.getBoolPref(POPUP_SHOW_ON_NEW));
+      ntfyMenuResults.setAttribute("checked",
+                                   this._prefs.getBoolPref(POPUP_SHOW_ON_RESULTS));
+      alwaysSubmitData.setAttribute("checked",
+                                    this._prefs.getBoolPref(ALWAYS_SUBMIT_DATA));
+    },
+
+    togglePref: function(id) {
+      let prefName = "extensions.testpilot." + id;
+      let oldVal = false;
+      if (this._prefs.prefHasUserValue(prefName)) {
+        oldVal = this._prefs.getBoolPref(prefName);
+      }
+      this._prefs.setBoolPref(prefName, !oldVal);
+
+      // If you turn on or off the global pref, startup or shutdown test pilot
+      // accordingly:
+      if (prefName == RUN_AT_ALL_PREF) {
+        if (oldVal == true) {
+          TestPilotSetup.globalShutdown();
+        }
+        if (oldVal == false) {
+          TestPilotSetup.globalStartup();
+        }
+      }
+    },
+
+    onPopupShowing: function(event) {
+      this._setMenuLabels();
+    },
+
+    onPopupHiding: function(event) {
+      let target = event.target;
+      if (target.id == "pilot-menu-popup") {
+        let menu = document.getElementById("pilot-menu");
+        if (target.parentNode != menu) {
+          menu.appendChild(target);
+        }
+      }
+    },
+
+    _setMenuLabels: function() {
+      // Make the enable/disable User Studies menu item show the right label
+      // for the current status...
+      let runStudiesToggle = document.getElementById("feedback-menu-enable-studies");
+      if (runStudiesToggle) {
+        let currSetting = this._prefs.getBoolPref(RUN_AT_ALL_PREF);
+
+        let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
+          getService(Ci.nsIStringBundleService).
+          createBundle("chrome://testpilot/locale/main.properties");
+
+        if (currSetting) {
+          runStudiesToggle.setAttribute("label",
+            stringBundle.GetStringFromName("testpilot.turnOff"));
+        } else {
+          runStudiesToggle.setAttribute("label",
+            stringBundle.GetStringFromName("testpilot.turnOn"));
+        }
+      }
+
+      let studiesMenuItem = document.getElementById("feedback-menu-show-studies");
+      studiesMenuItem.setAttribute("disabled",
+                                   !this._prefs.getBoolPref(RUN_AT_ALL_PREF));
+    },
+
+    onMenuButtonMouseDown: function(attachPointId) {
+      if (!attachPointId) {
+        attachPointId = "pilot-notifications-button";
+      }
+      let menuPopup = document.getElementById("pilot-menu-popup");
+      let menuButton = document.getElementById(attachPointId);
+
+      // TODO failing here with "menuPopup is null" for Tracy
+      if (menuPopup.parentNode != menuButton)
+        menuButton.appendChild(menuPopup);
+
+      let alignment;
+      // Menu should appear above status bar icon, but below Feedback button
+      if (attachPointId == "pilot-notifications-button") {
+        alignment = "before_start";
+      } else {
+        alignment = "after_end";
+      }
+
+      menuPopup.openPopup(menuButton, alignment, 0, 0, true);
+    }
+  };
+
+
+  var TestPilotWindowHandlers = {
+    initialized: false,
+    onWindowLoad: function() {
+      try {
+      // Customize the interface of the newly opened window.
+      Cu.import("resource://testpilot/modules/interface.js");
+      Services.console.logStringMessage("Interface module loaded.\n");
+      TestPilotUIBuilder.buildCorrectInterface(window);
+
+      /* "Hold" window load events for TestPilotSetup, passing them along only
+       * after startup is complete.  It's hacky, but the benefit is that
+       * TestPilotSetup.onWindowLoad can treat all windows the same no matter
+       * whether they opened with Firefox on startup or were opened later. */
+
+      if (("TestPilotSetup" in window) && TestPilotSetup.startupComplete) {
+        Services.console.logStringMessage("Startup complete, that's funny.\n");
+        TestPilotSetup.onWindowLoad(window);
+      } else {
+        Services.console.logStringMessage("Initializing timer.\n");
+        // TODO only want to start this timer ONCE so we need some global state to
+        // remember whether we already started it (only a problem on multi-window systems)
+        // (that's essentially the problem the component solved) deal with this later.
+        window.setTimeout(function() {
+           Services.console.logStringMessage("Timer got called back!.\n");
+             Services.console.logStringMessage("Impoting setup");
+             Cu.import("resource://testpilot/modules/setup.js");
+             Services.console.logStringMessage("globally globalStartuping");
+             TestPilotSetup.globalStartup();
+             Services.console.logStringMessage("Did it.");
+        }, 10000);
+        Services.console.logStringMessage("Timer made.\n");
+
+        let observerSvc = Cc["@mozilla.org/observer-service;1"]
+                             .getService(Ci.nsIObserverService);
+        let observer = {
+          observe: function(subject, topic, data) {
+            observerSvc.removeObserver(this, "testpilot:startup:complete");
+            TestPilotSetup.onWindowLoad(window);
+          }
+        };
+        Services.console.logStringMessage("Registering observer for startup completion.\n");
+        observerSvc.addObserver(observer, "testpilot:startup:complete", false);
+      }
+
+      } catch (e) {
+        Services.console.logStringMessage(e.toString());
+      }
+    },
+
+    onWindowUnload: function() {
+      TestPilotSetup.onWindowUnload(window);
+    }
+  };
+
+  window.addEventListener("load", TestPilotWindowHandlers.onWindowLoad, false);
+  window.addEventListener("unload", TestPilotWindowHandlers.onWindowUnload, false);
+}());
+
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/browser.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://testpilot/content/browser.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+  <!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
+    %testpilotDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script src="chrome://testpilot/content/browser.js"
+  type="application/x-javascript" />
+<script src="chrome://testpilot/content/window-utils.js"
+  type="application/x-javascript" />
+
+<toolbarpalette id="MailToolbarPalette">
+  <toolbarbutton id="feedback-menu-button"
+    type="menu" class="toolbarbutton-1" label="&testpilot.feedbackbutton.label;"
+    onmousedown="event.preventDefault();
+    TestPilotMenuUtils.onMenuButtonMouseDown('feedback-menu-button');"/>
+
+</toolbarpalette>
+
+</overlay>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/debug.html
@@ -0,0 +1,299 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html> <head>
+<title>Test Pilot Debug Page</title>
+<script src="experiment-page.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+
+  function getEid() {
+    var selector = document.getElementById("task-selector");
+    var i = selector.selectedIndex;
+    return selector.options[i].getAttribute("value");
+  }
+
+  function setTaskStatus() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var newStatus = document.getElementById("status-code").value;
+    newStatus = parseInt(newStatus);
+    var task = TestPilotSetup.getTaskById(getEid());
+    task.changeStatus(newStatus, false);
+  }
+
+  function reloadAllExperiments() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    TestPilotSetup.reloadRemoteExperiments(function(success) {
+      let errors = TestPilotSetup._remoteExperimentLoader.getLoadErrors();
+      let str;
+      if (errors.length > 0) {
+        str = "<ul>";
+        for each (let errStr in errors) {
+          str += "<li>" + errStr + "</li>";          
+        }
+        str += "</ul>";
+      } else {
+        str = "All experiments reloaded, no errors.";
+      }
+      document.getElementById("debug").innerHTML = str;
+    });
+  }
+
+  function runUnitTests() {
+   Components.utils.import("resource://testpilot/tests/test_data_store.js");
+   runAllTests();
+  }
+
+  function testJarStore() {
+    var Cuddlefish = {};
+    Components.utils.import("resource://testpilot/modules/lib/cuddlefish.js",
+                        Cuddlefish);
+    var loader = new Cuddlefish.Loader(
+      {rootPaths: ["resource://testpilot/modules/",
+                   "resource://testpilot/modules/lib/"]});
+    var jarStoreModule = loader.require("jar-code-store");
+    var SecurableModule = loader.require("securable-module");
+    var jarStore = new jarStoreModule.JarStore();
+
+    // OK now watch this!  It's gonna be awesome!
+    var clientLoader = Cuddlefish.Loader(
+      {fs: new SecurableModule.CompositeFileSystem(
+         [jarStore, loader.fs])});
+    dump("Debug page Requiring toolbar study.\n");
+    var toolbarStudy = clientLoader.require("toolbar-study");
+    
+  }
+
+  function remindMe() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    TestPilotSetup._notifyUserOfTasks();
+    //TestPilotSetup._doHousekeeping();
+  }
+
+  function getCodeStorage() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var loader = TestPilotSetup._remoteExperimentLoader;
+    return loader._jarStore;
+  }
+
+  function getSelectedFilename() {
+    var selector = document.getElementById("file-selector");
+    var i = selector.selectedIndex;
+    return selector.options[i].text;
+  }
+
+  function loadExperimentCode() {
+    var filename = getSelectedFilename();
+    var codeStore = getCodeStorage();
+    var textArea = document.getElementById("experiment-code-area");
+    var path = codeStore.resolveModule(null, filename);
+    code = codeStore.getFile(path).contents;
+    textArea.value = code;
+  }
+
+  function saveAndRun() {
+    var filename = getSelectedFilename();
+    var codeStore = getCodeStorage();
+    var path = codeStore.resolveModule(null, filename);
+    var textArea = document.getElementById("experiment-code-area");
+    codeStore.setLocalOverride(path, textArea.value);
+    reloadAllExperiments();
+  }
+
+  function showMetaData() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    let task = TestPilotSetup.getTaskById(getEid());
+    let json = task._prependMetadataToJSON(function(json) {
+      document.getElementById("debug").innerHTML = json;
+    });
+  }
+
+  function makeThereBeAPopup() {
+    document.getElementById("debug").innerHTML = "Making popup...";
+    Components.utils.import("resource://testpilot/modules/interface.js");
+    var notifier = TestPilotUIBuilder.getNotificationManager();
+    document.getElementById("debug").innerHTML = "Got Notfn Manager";
+    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
+               getService(Components.interfaces.nsIWindowMediator);
+    var window = wm.getMostRecentWindow("navigator:browser") ||
+                 wm.getMostRecentWindow("mail:3pane");
+    document.getElementById("debug").innerHTML = "Got Window.";
+    try {
+    notifier.showNotification(window, { title: "This is title",
+                                        text: "This is text",
+                                        iconClass: "study-submitted",
+                                        moreInfoLabel: "This is a link",
+                                        moreInfoCallback: function() {dump("You picked More Info\n");},
+                                        closeCallback: function() {dump("You closed the notfn\n");},
+                                        fragile: true
+     });
+    } catch(e) {
+      document.getElementById("debug").innerHTML = "Error: " + e;
+    }
+  }
+
+  function wipeDb() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var task = TestPilotSetup.getTaskById(getEid());
+    task.dataStore.wipeAllData();
+    var debug = document.getElementById("debug");
+    debug.innerHTML = "Wiped!";
+  }
+
+  function nukeDb() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var task = TestPilotSetup.getTaskById(getEid());
+    task.dataStore.nukeTable();
+    var debug = document.getElementById("debug");
+    debug.innerHTML = "Nuked!";
+  }
+
+  function populateFileDropdown() {
+    var codeStore = getCodeStorage();
+    var files = codeStore.listAllFiles();
+    var selector = document.getElementById("file-selector");
+    var opt, i;
+    for (var i = 0; i < files.length; i++) {
+      opt = document.createElement("option");
+      opt.innerHTML = files[i];
+      selector.appendChild(opt);
+    }
+
+    selector = document.getElementById("task-selector");
+    var tasks = TestPilotSetup.getAllTasks();
+    var title;
+    for (i = 0; i < tasks.length; i++) {
+      opt = document.createElement("option");
+      title = tasks[i].title;
+      opt.innerHTML = title;
+      opt.setAttribute("value", tasks[i].id);
+      selector.appendChild(opt);
+    }
+  }
+
+  function showSelectedTaskStatus() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var task = TestPilotSetup.getTaskById(getEid());
+    document.getElementById("show-status-span").innerHTML = task.status;
+    var selector = document.getElementById("status-selector");
+    selector.selectedIndex = task.status;
+  }
+
+  function resetSelectedTask() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var task = TestPilotSetup.getTaskById(getEid());
+    task.changeStatus(0, true);
+    var prefService  = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+    var prefName= "extensions.testpilot.startDate." + task.id;
+    if (prefService.prefHasUserValue(prefName)) {
+      prefService.clearUserPref(prefName);
+    }
+  }
+
+  function setSelectedTaskStatus() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var task = TestPilotSetup.getTaskById(getEid());
+
+    var selector = document.getElementById("status-selector");
+    var i = selector.selectedIndex;
+    var newStatus = parseInt( selector.options[i].value );
+    task.changeStatus(newStatus, false);
+  }
+
+  function showIndexFileDropdown() {
+    var prefService  = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+    var prefName = "extensions.testpilot.indexFileName";
+    var selector = document.getElementById("index-file-selector");
+    switch (prefService.getCharPref(prefName)) {
+      case "index.json":
+        selector.selectedIndex = 0;
+      break;
+      case "index-dev.json":
+        selector.selectedIndex = 1;
+      break;
+      case "index-mobile.json":
+        selector.selectedIndex = 2;
+      break;
+      case "index-tb-dev.json":
+        selector.selectedIndex = 3;
+      break;
+    }
+  }
+
+  function setSelectedIndexFile() {
+    document.getElementById("debug").innerHTML = "Setting index file";
+    try {
+    var prefService  = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+    var prefName = "extensions.testpilot.indexFileName";
+    var selector = document.getElementById("index-file-selector");
+    var i = selector.selectedIndex;
+    document.getElementById("debug").innerHTML = "Setting index file to " + selector.options[i].value;
+    prefService.setCharPref( prefName, selector.options[i].value );
+
+    // DELETE CACHED INDEX FILE
+    var file = Components.classes["@mozilla.org/file/directory_service;1"].
+                     getService(Components.interfaces.nsIProperties).
+                     get("ProfD", Components.interfaces.nsIFile);
+    file.append("TestPilotExperimentFiles");
+    file.append("index.json");
+    if (file.exists()) {
+      file.remove(false);
+    }
+    } catch(e) {
+       document.getElementById("debug").innerHTML = "Error: " + e;
+    }
+  }
+
+</script>
+
+<style type="text/css">
+      canvas { border: 1px solid black; }
+    </style>
+
+</head>
+
+<body onload="populateFileDropdown();showSelectedTaskStatus();showIndexFileDropdown();">
+
+<fieldset>
+<p><select id="task-selector" onchange="showSelectedTaskStatus();"></select> Current Status = <span id="show-status-span"></span>.
+<button onclick="resetSelectedTask();showSelectedTaskStatus();">Reset Task</button>
+or set it to
+<select id="status-selector" onchange="setSelectedTaskStatus(); showSelectedTaskStatus();">
+  <option value="0">0 (New)</option>
+  <option value="1">1 (Pending)</option>
+  <option value="2">2 (Starting)</option>
+  <option value="3">3 (In Progress)</option>
+  <option value="4">4 (Finished)</option>
+  <option value="5">5 (Cancelled)</option>
+  <option value="6">6 (Submitted)</option>
+  <option value="7">7 (Results)</option>
+  <option value="8">8 (Archived)</option>
+</select>
+<button onclick="wipeDb();">Wipe My Data</button>
+<button onclick="nukeDb();">NUKE</button>
+<button onclick="uploadData();">Upload My Data</button>
+<button onclick="showMetaData();">Show Metadata</button>
+<button onclick="runUnitTests();">Run Tests</button>
+</fieldset>
+<fieldset>
+<p><button onclick="makeThereBeAPopup();">Show Dummy Popup</button>
+<button onclick="reloadAllExperiments();">Reload All Experiments</button>
+<button onclick="remindMe();">Notify Me</button>
+<button onclick="testJarStore();">Test Jar Store</button>
+Index file: 
+<select id="index-file-selector" onchange="setSelectedIndexFile();">
+  <option value="index.json">index.json</option>
+  <option value="index-dev.json">index-dev.json</option>
+  <option value="index-mobile.json">index-mobile.json</option>
+  <option value="index-tb-dev.json">index-tb-dev.json</option>
+</select>
+</p>
+</fieldset>
+<p><span id="debug"></span></p>
+
+
+<textarea id="experiment-code-area" rows="40" cols="80">
+</textarea>
+<select id="file-selector"></select>
+<button onclick="loadExperimentCode();">Load Ye Code</button>
+<button onclick="saveAndRun();">Save And Run Ye Code</button>
+
+
+</body> </html>
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/experiment-page.js
@@ -0,0 +1,515 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Test Pilot.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jono X <jono@mozilla.com>
+ *   Raymond Lee <raymond@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const PAGE_TYPE_STATUS = 0;
+const PAGE_TYPE_QUIT = 1;
+var stringBundle;
+
+  function showRawData(experimentId) {
+    window.openDialog(
+      "chrome://testpilot/content/raw-data-dialog.xul",
+      "TestPilotRawDataDialog", "chrome,centerscreen,resizable,scrollbars",
+      experimentId);
+  }
+
+  function getUrlParam(name) {
+    // from http://www.netlobo.com/url_query_string_javascript.html
+    name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
+    var regexS = "[\\?&]"+name+"=([^&#]*)";
+    var regex = new RegExp(regexS);
+    var results = regex.exec(window.location.href);
+    if( results == null )
+      return "";
+    else
+      return results[1];
+  }
+
+  function uploadData() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    let eid = getUrlParam("eid");
+    let task = TestPilotSetup.getTaskById(eid);
+
+    // If always-submit-checkbox is checked, set the pref
+    if (task._recursAutomatically) {
+      let checkBox = document.getElementById("always-submit-checkbox");
+      if (checkBox && checkBox.checked) {
+        task.setRecurPref(TaskConstants.ALWAYS_SUBMIT);
+      }
+    }
+
+    // Study web content must provide an element with id 'upload-status'.
+    // Fill it first with a message about data being uploaded; if there's
+    // an error, replace it with the error message.
+    let uploadStatus = document.getElementById("upload-status");
+    uploadStatus.innerHTML =
+      stringBundle.GetStringFromName("testpilot.statusPage.uploadingData");
+    task.upload( function(success) {
+      if (success) {
+        onStatusPageLoad();
+      } else {
+        // Replace 'now uploading' message
+        let errorParagraph = document.createElement("p");
+        errorParagraph.innerHTML = stringBundle.GetStringFromName("testpilot.statusPage.uploadErrorMsg");
+        let willRetryParagraph = document.createElement("p");
+        willRetryParagraph.innerHTML = stringBundle.GetStringFromName("testpilot.statusPage.willRetry");
+        uploadStatus.innerHTML = "";
+        uploadStatus.appendChild(errorParagraph);
+        uploadStatus.appendChild(willRetryParagraph);
+      }
+    });
+  }
+
+  function deleteData() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    Components.utils.import("resource://testpilot/modules/tasks.js");
+    let eid = getUrlParam("eid");
+    let task = TestPilotSetup.getTaskById(eid);
+    task.dataStore.wipeAllData();
+    // reload the URL after wiping all data.
+    window.location = "chrome://testpilot/content/status.html?eid=" + eid;
+  }
+
+  function saveCanvas(canvas) {
+    const nsIFilePicker = Components.interfaces.nsIFilePicker;
+    let filePicker = Components.classes["@mozilla.org/filepicker;1"].
+      createInstance(nsIFilePicker);
+    filePicker.init(window, null, nsIFilePicker.modeSave);
+    filePicker.appendFilters(
+	nsIFilePicker.filterImages | nsIFilePicker.filterAll);
+    filePicker.defaultString = "canvas.png";
+
+    let response = filePicker.show();
+    if (response == nsIFilePicker.returnOK ||
+	response == nsIFilePicker.returnReplace) {
+      const nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist;
+      let file = filePicker.file;
+
+      // create a data url from the canvas and then create URIs of the source
+      // and targets
+      let io = Components.classes["@mozilla.org/network/io-service;1"].
+	getService(Components.interfaces.nsIIOService);
+      let source = io.newURI(canvas.toDataURL("image/png", ""), "UTF8", null);
+      let target = io.newFileURI(file);
+
+      // prepare to save the canvas data
+      let persist = Components.classes[
+	"@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].
+	  createInstance(nsIWebBrowserPersist);
+      persist.persistFlags = nsIWebBrowserPersist.
+	PERSIST_FLAGS_REPLACE_EXISTING_FILES;
+      persist.persistFlags |= nsIWebBrowserPersist.
+        PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+      // displays a download dialog (remove these 3 lines for silent download)
+      let xfer = Components.classes["@mozilla.org/transfer;1"].
+	createInstance(Components.interfaces.nsITransfer);
+      xfer.init(source, target, "", null, null, null, persist);
+      persist.progressListener = xfer;
+
+      // save the canvas data to the file
+      persist.saveURI(source, null, null, null, null, file);
+    }
+  }
+
+  function exportData() {
+    const nsIFilePicker = Components.interfaces.nsIFilePicker;
+    let filePicker = Components.classes["@mozilla.org/filepicker;1"].
+      createInstance(nsIFilePicker);
+    let eid = getUrlParam("eid");
+    let task = TestPilotSetup.getTaskById(eid);
+
+    filePicker.init(window, null, nsIFilePicker.modeSave);
+    filePicker.appendFilters(
+	nsIFilePicker.filterImages | nsIFilePicker.filterAll);
+    filePicker.defaultString = task.title + ".csv";
+
+    let response = filePicker.show();
+    if (response == nsIFilePicker.returnOK ||
+	response == nsIFilePicker.returnReplace) {
+      const nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist;
+      let foStream =
+        Components.classes["@mozilla.org/network/file-output-stream;1"].
+	  createInstance(Components.interfaces.nsIFileOutputStream);
+      let converter =
+        Components.classes["@mozilla.org/intl/converter-output-stream;1"].
+	  createInstance(Components.interfaces.nsIConverterOutputStream);
+      let file = filePicker.file;
+      let dataStore = task.dataStore;
+      let columnNames = dataStore.getHumanReadableColumnNames();
+      let propertyNames = dataStore.getPropertyNames();
+      let csvString = "";
+
+      // titles
+      for (let i = 0; i < columnNames.length; i++) {
+	csvString += "\"" + columnNames[i] + "\",";
+      }
+      if (csvString.length > 0) {
+	csvString = csvString.substring(0, (csvString.length - 1));
+        csvString += "\n";
+      }
+
+      dataStore.getAllDataAsJSON(true, function(rawData) {
+        // data
+        for (let i = 0; i < rawData.length; i++) {
+          for (let j = 0; j < columnNames.length; j++) {
+	    csvString += "\"" + rawData[i][propertyNames[j]] + "\",";
+          }
+	  csvString = csvString.substring(0, (csvString.length - 1));
+          csvString += "\n";
+        }
+
+        // write, create, truncate
+        foStream.init(file, 0x02 | 0x08 | 0x20, 0664, 0);
+        converter.init(foStream, "UTF-8", 0, 0);
+        converter.writeString(csvString);
+        converter.close();
+      });
+    }
+  }
+
+  function openLink(url) {
+    // open the link in the chromeless window
+    let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+                       .getService(Components.interfaces.nsIWindowMediator);
+    let recentWindow = wm.getMostRecentWindow("navigator:browser");
+
+    if (recentWindow) {
+      recentWindow.TestPilotWindowUtils.openInTab(url);
+    } else {
+      window.open(url);
+    }
+  }
+
+  function getTestEndingDate(experimentId) {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    var task = TestPilotSetup.getTaskById(experimentId);
+    var endDate = new Date(task.endDate);
+    var diff = (endDate - Date.now());
+    var span = document.getElementById("test-end-time");
+    if (!span) {
+      return;
+    }
+    if (diff < 0) {
+      span.innerHTML =
+        stringBundle.GetStringFromName("testpilot.statusPage.endedAlready");
+      return;
+    }
+    var hours = diff / (60 * 60 * 1000);
+    if (hours < 24) {
+      span.innerHTML =
+        stringBundle.formatStringFromName(
+	  "testpilot.statusPage.todayAt", [endDate.toLocaleTimeString()], 1);
+    } else {
+      span.innerHTML =
+        stringBundle.formatStringFromName(
+	  "testpilot.statusPage.endOn", [endDate.toLocaleString()], 1);
+    }
+  }
+
+  function showMetaData() {
+    Components.utils.import("resource://testpilot/modules/metadata.js");
+    Components.utils.import("resource://gre/modules/PluralForm.jsm");
+    MetadataCollector.getMetadata(function(md) {
+      var mdLocale = document.getElementById("md-locale");
+      if (mdLocale)
+        mdLocale.innerHTML = md.location;
+      var mdVersion = document.getElementById("md-version");
+      if (mdVersion)
+        mdVersion.innerHTML = md.version;
+      var mdOs = document.getElementById("md-os");
+      if (mdOs)
+        mdOs.innerHTML = md.operatingSystem;
+      var mdNumExt = document.getElementById("md-num-ext");
+      if (mdNumExt) {
+        // This computes the correctly localized singular or plural string
+        // of the number of extensions, e.g. "1 extension", "2 extensions", etc.
+        let str = stringBundle.GetStringFromName("testpilot.statusPage.numExtensions");
+        var numExt = md.extensions.length;
+        mdNumExt.innerHTML = PluralForm.get(numExt, str).replace("#1", numExt);
+      }
+    });
+  }
+
+  function onQuitPageLoad() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    setStrings(PAGE_TYPE_QUIT);
+    let eid = getUrlParam("eid");
+    let task = TestPilotSetup.getTaskById(eid);
+    let header = document.getElementById("about-quit-title");
+    header.innerHTML =
+      stringBundle.formatStringFromName(
+	"testpilot.quitPage.aboutToQuit", [task.title], 1);
+
+    if (task._recursAutomatically) {
+      document.getElementById("recur-options").setAttribute("style", "");
+      document.getElementById("recur-checkbox-container").
+        setAttribute("style", "");
+    }
+  }
+
+  function quitExperiment() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    Components.utils.import("resource://testpilot/modules/tasks.js");
+    let eid = getUrlParam("eid");
+    let reason = document.getElementById("reason-for-quit").value;
+    let task = TestPilotSetup.getTaskById(eid);
+    task.optOut(reason, function(success) {
+      // load the you-are-canceleed page.
+      $("#quit-ui").slideUp();
+      $("#main-experiment-ui").slideDown();
+      onStatusPageLoad();
+    });
+
+    // If opt-out-forever checkbox is checked, opt out forever!
+    if (task._recursAutomatically) {
+      let checkBox = document.getElementById("opt-out-forever");
+      if (checkBox.checked) {
+        task.setRecurPref(TaskConstants.NEVER_SUBMIT);
+      }
+      // quit test so rescheduling
+      task._reschedule();
+    }
+  }
+
+  function updateRecurSettings() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    let eid = getUrlParam("eid");
+    let experiment = TestPilotSetup.getTaskById(eid);
+    let recurSelector = document.getElementById("recur-selector");
+    let newValue = recurSelector.options[recurSelector.selectedIndex].value;
+    experiment.setRecurPref(parseInt(newValue));
+  }
+
+  function showRecurControls(experiment) {
+    Components.utils.import("resource://testpilot/modules/tasks.js");
+    let recurPrefSpan = document.getElementById("recur-pref");
+    if (!recurPrefSpan) {
+      return;
+    }
+    let days = experiment._recurrenceInterval;
+    recurPrefSpan.innerHTML =
+      stringBundle.formatStringFromName(
+	"testpilot.statusPage.recursEveryNumberOfDays", [days], 1);
+
+    let controls = document.getElementById("recur-controls");
+    let selector = document.createElement("select");
+    controls.appendChild(selector);
+    selector.setAttribute("onchange", "updateRecurSettings();");
+    selector.setAttribute("id", "recur-selector");
+
+    let option = document.createElement("option");
+    option.setAttribute("value", TaskConstants.ASK_EACH_TIME);
+    if (experiment.recurPref == TaskConstants.ASK_EACH_TIME) {
+      option.setAttribute("selected", "true");
+    }
+    option.innerHTML =
+      stringBundle.GetStringFromName(
+	"testpilot.statusPage.askMeBeforeSubmitData");
+    selector.appendChild(option);
+
+    option = document.createElement("option");
+    option.setAttribute("value", TaskConstants.ALWAYS_SUBMIT);
+    if (experiment.recurPref == TaskConstants.ALWAYS_SUBMIT) {
+      option.setAttribute("selected", "true");
+    }
+    option.innerHTML =
+      stringBundle.GetStringFromName(
+	"testpilot.statusPage.alwaysSubmitData");
+    selector.appendChild(option);
+
+    option = document.createElement("option");
+    option.setAttribute("value", TaskConstants.NEVER_SUBMIT);
+    if (experiment.recurPref == TaskConstants.NEVER_SUBMIT) {
+      option.setAttribute("selected", "true");
+    }
+    option.innerHTML =
+      stringBundle.GetStringFromName(
+	"testpilot.statusPage.neverSubmitData");
+    selector.appendChild(option);
+  }
+
+  function loadExperimentPage() {
+    Components.utils.import("resource://testpilot/modules/setup.js");
+    Components.utils.import("resource://testpilot/modules/tasks.js");
+    var contentDiv = $("#experiment-specific-text");
+    var dataPrivacyDiv = $("#data-privacy-text");
+    // Get experimentID from the GET args of page
+    var eid = getUrlParam("eid");
+    var experiment = TestPilotSetup.getTaskById(eid);
+    if (!experiment) {
+      // Possible that experiments aren't done loading yet.  Try again in
+      // a few seconds.
+      contentDiv.html(stringBundle.GetStringFromName("testpilot.statusPage.loading"));
+      window.setTimeout(function() { loadExperimentPage(); }, 2000);
+      return;
+    }
+
+    // Fill in "opt out" and "raw data" links.
+    $("#raw-data-link").attr("href", "raw-data.html?eid=" + eid);
+
+    // Let the experiment fill in its web content (asynchronous)
+    experiment.getWebContent(function(webContent) {
+      contentDiv.html(webContent);
+
+      // Metadata and start/end date should be filled in for every experiment:
+      showMetaData();
+      getTestEndingDate(eid);
+      if (experiment._recursAutomatically &&
+        experiment.status != TaskConstants.STATUS_FINISHED) {
+        showRecurControls(experiment);
+      }
+
+      // Do whatever the experiment's web content wants done on load
+      // (Usually drawing a graph) - must be done after innerHTML is set.
+      experiment.webContent.onPageLoad(experiment, document, jQuery);
+    });
+
+    experiment.getDataPrivacyContent(function(dataPrivacyContent) {
+      if (dataPrivacyContent && dataPrivacyContent.length > 0) {
+        dataPrivacyDiv.html(dataPrivacyContent);
+        dataPrivacyDiv.removeAttr("hidden");
+      }
+    });
+  }
+
+  function onStatusPageLoad() {
+    setStrings(PAGE_TYPE_STATUS);
+    /* An experiment ID (eid) must be provided in the url params. Show status
+     * for that experiment.*/
+    loadExperimentPage();
+  }
+
+  function toggleSection(id) {
+    let div = $("#" + id + "-text");
+    div.slideToggle();
+    let button = $("#" + id + "-button");
+    if (button.html() == "Hide") {
+      button.html("Show");
+    } else {
+      button.html("Hide");
+    }
+  }
+
+  function setStrings(pageType) {
+    Components.utils.import("resource://gre/modules/Services.jsm");
+
+    stringBundle =
+      Components.classes["@mozilla.org/intl/stringbundle;1"].
+        getService(Components.interfaces.nsIStringBundleService).
+	  createBundle("chrome://testpilot/locale/main.properties");
+    let map;
+    let mapLength;
+
+    if (pageType == PAGE_TYPE_STATUS) {
+      map = [
+	{ id: "page-title", stringKey: "testpilot.fullBrandName" },
+	{ id: "comments-and-discussions-link",
+	  stringKey: "testpilot.page.commentsAndDiscussions" },
+	{ id: "propose-test-link",
+	  stringKey: "testpilot.page.proposeATest" },
+	{ id: "testpilot-twitter-link",
+	  stringKey: "testpilot.page.testpilotOnTwitter" }
+      ];
+    } else if (pageType == PAGE_TYPE_QUIT) {
+      map = [
+	{ id: "page-title", stringKey: "testpilot.fullBrandName" },
+	{ id: "comments-and-discussions-link",
+	  stringKey: "testpilot.page.commentsAndDiscussions" },
+	{ id: "propose-test-link",
+	  stringKey: "testpilot.page.proposeATest" },
+	{ id: "testpilot-twitter-link",
+	  stringKey: "testpilot.page.testpilotOnTwitter" },
+	{ id: "optional-message",
+	  stringKey: "testpilot.quitPage.optionalMessage" },
+	{ id: "reason-text",
+	  stringKey: "testpilot.quitPage.reason" },
+	{ id: "recur-options",
+	  stringKey: "testpilot.quitPage.recurringStudy" },
+	{ id: "quit-forever-text",
+	  stringKey: "testpilot.quitPage.quitForever" },
+	{ id: "quit-study-link",
+	  stringKey: "testpilot.quitPage.quitStudy" }
+      ];
+    }
+    mapLength = map.length;
+    for (let i = 0; i < mapLength; i++) {
+      let entry = map[i];
+      let elem = document.getElementById(entry.id);
+      if (!elem) {
+        Services.console.logStringMessage("No elem as " + entry.id +".\n");
+        continue;
+      }
+      elem.innerHTML = stringBundle.GetStringFromName(entry.stringKey);
+    }
+  }
+
+function showDbContentsHtml() {
+  Components.utils.import("resource://testpilot/modules/setup.js");
+  var experimentId = getUrlParam("eid");
+  var experiment = TestPilotSetup.getTaskById(experimentId);
+  var dataStore = experiment.dataStore;
+  var table = document.getElementById("raw-data-table");
+  var columnNames = dataStore.getHumanReadableColumnNames();
+  var propertyNames = dataStore.getPropertyNames();
+
+  $("title").html("Raw Data For " + experiment.title + " Study");
+
+  var headerRow = $("#raw-data-header-row");
+
+  var i, j;
+  for (j = 0; j < columnNames.length; j++) {
+    headerRow.append($("<th></th>").html(columnNames[j]));
+  }
+
+  dataStore.getAllDataAsJSON(true, function(rawData) {
+    // Convert each object in the JSON into a row of the table.
+    for (i = 0; i < rawData.length; i++) {
+      var row = $("<tr></tr>");
+      for (j = 0; j < columnNames.length; j++) {
+        row.append($("<td></td>").html(rawData[i][propertyNames[j]]));
+      }
+      $("#raw-data-table").append(row);
+    }
+  });
+
+}
+
+function showQuitUi() {
+  $('#quit-ui').slideDown();
+  $("#main-experiment-ui").slideUp();
+  onQuitPageLoad();
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/feedback-browser.xul
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://testpilot/content/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://testpilot-os/skin/feedback.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+  <!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
+    %testpilotDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<menupopup id="menu_ToolsPopup">
+  <menu id="pilot-menu" insertafter="menu_openAddons" />
+</menupopup>
+<menupopup id="taskPopup">
+  <menu id="pilot-menu" insertafter="addonsManager" />
+</menupopup>
+
+<toolbar id="nav-bar">
+  <panel id="pilot-notification-popup"/>
+</toolbar>
+<toolbar id="mail-bar3">
+  <panel id="pilot-notification-popup"/>
+</toolbar>
+
+
+<panel id="pilot-notification-popup" hidden="true" noautofocus="true"
+  level="parent" position="after_start">
+  <vbox class="pilot-notification-popup-container">
+    <hbox class="pilot-notification-toprow">
+      <image id="pilot-notification-icon" />
+      <vbox pack="center">
+        <label id="pilot-notification-title" class="pilot-title" />
+      </vbox>
+      <spacer flex="1" />
+      <vbox pack="start">
+        <image id="pilot-notification-close"
+          tooltiptext="&testpilot.notification.close.tooltip;" />
+      </vbox>
+    </hbox>
+    <description id="pilot-notification-text" />
+    <hbox align="right"><label id="pilot-notification-link" /></hbox>
+    <hbox>
+      <checkbox id="pilot-notification-always-submit-checkbox"
+        label="&testpilot.settings.alwaysSubmitData.shortLabel;" />
+      <spacer flex="1" />
+    </hbox>
+    <hbox align="right">
+      <button id="pilot-notification-submit" />
+    </hbox>
+  </vbox>
+</panel>
+
+<menu id="pilot-menu" class="menu-iconic"
+      label="&testpilot.feedbackbutton.label;"
+      insertafter="addonsManager">
+  <menupopup id="pilot-menu-popup"
+             onpopupshowing="TestPilotMenuUtils.onPopupShowing(event);"
+             onpopuphiding="TestPilotMenuUtils.onPopupHiding(event);">
+    <menuitem id="feedback-menu-happy-button"
+              class="menuitem-iconic"
+              image="chrome://testpilot-os/skin/feedback-smile-16x16.png"
+              label="&testpilot.happy.label;"
+              thunderbirdLabel="&testpilot.happy.thunderbirdLabel;"
+              oncommand="TestPilotWindowUtils.openFeedbackPage('happy');"/>
+    <menuitem id="feedback-menu-sad-button"
+              class="menuitem-iconic"
+              image="chrome://testpilot-os/skin/feedback-frown-16x16.png"
+              label="&testpilot.sad.label;"
+              thunderbirdLabel="&testpilot.sad.thunderbirdLabel;"
+              oncommand="TestPilotWindowUtils.openFeedbackPage('sad');"/>
+    <menuseparator/>
+    <menuitem id="feedback-menu-show-studies"
+              label="&testpilot.allYourStudies.label;"
+              oncommand="TestPilotWindowUtils.openAllStudiesWindow();"/>
+    <menuitem id="feedback-menu-enable-studies" 
+              label="&testpilot.enable.label;"
+              oncommand="TestPilotMenuUtils.togglePref('runStudies');"/>
+  </menupopup>
+</menu>
+</overlay>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/fennec-options.xul
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <setting pref="extensions.testpilot.runStudies" type="bool" title="Run User Studies"/>
+  <setting pref="extensions.testpilot.alwaysSubmitData" type="bool" title="Automatically Submit">
+    Data from finished studies will be submitted without asking you each time.
+  </setting>
+  <setting title="Test Pilot Studies" type="control">
+    See all Test Pilot studies currently running
+    <button id="see-all-studies-button" label="See All Studies"
+            oncommand="TestPilotWindowUtils.openAllStudies();"/>
+  </setting>
+</vbox>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.js
@@ -0,0 +1,174 @@
+/* Plugin for jQuery for working with colors.
+ * 
+ * Version 1.0.
+ * 
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ *   var c = $.color.extract($("#mydiv"), 'background-color');
+ *   console.log(c.r, c.g, c.b, c.a);
+ *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() work in-place instead of returning
+ * new objects.
+ */ 
+
+(function() {
+    jQuery.color = {};
+
+    // construct color object with some convenient chainable helpers
+    jQuery.color.make = function (r, g, b, a) {
+        var o = {};
+        o.r = r || 0;
+        o.g = g || 0;
+        o.b = b || 0;
+        o.a = a != null ? a : 1;
+
+        o.add = function (c, d) {
+            for (var i = 0; i < c.length; ++i)
+                o[c.charAt(i)] += d;
+            return o.normalize();
+        };
+        
+        o.scale = function (c, f) {
+            for (var i = 0; i < c.length; ++i)
+                o[c.charAt(i)] *= f;
+            return o.normalize();
+        };
+        
+        o.toString = function () {
+            if (o.a >= 1.0) {
+                return "rgb("+[o.r, o.g, o.b].join(",")+")";
+            } else {
+                return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")";
+            }
+        };
+
+        o.normalize = function () {
+            function clamp(min, value, max) {
+                return value < min ? min: (value > max ? max: value);
+            }
+            
+            o.r = clamp(0, parseInt(o.r), 255);
+            o.g = clamp(0, parseInt(o.g), 255);
+            o.b = clamp(0, parseInt(o.b), 255);
+            o.a = clamp(0, o.a, 1);
+            return o;
+        };
+
+        o.clone = function () {
+            return jQuery.color.make(o.r, o.b, o.g, o.a);
+        };
+
+        return o.normalize();
+    }
+
+    // extract CSS color property from element, going up in the DOM
+    // if it's "transparent"
+    jQuery.color.extract = function (elem, css) {
+        var c;
+        do {
+            c = elem.css(css).toLowerCase();
+            // keep going until we find an element that has color, or
+            // we hit the body
+            if (c != '' && c != 'transparent')
+                break;
+            elem = elem.parent();
+        } while (!jQuery.nodeName(elem.get(0), "body"));
+
+        // catch Safari's way of signalling transparent
+        if (c == "rgba(0, 0, 0, 0)")
+            c = "transparent";
+        
+        return jQuery.color.parse(c);
+    }
+    
+    // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
+    // returns color object
+    jQuery.color.parse = function (str) {
+        var res, m = jQuery.color.make;
+
+        // Look for rgb(num,num,num)
+        if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))
+            return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
+        
+        // Look for rgba(num,num,num,num)
+        if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
+            return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
+            
+        // Look for rgb(num%,num%,num%)
+        if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))
+            return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55);
+
+        // Look for rgba(num%,num%,num%,num)
+        if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
+            return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4]));
+        
+        // Look for #a0b1c2
+        if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))
+            return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
+
+        // Look for #fff
+        if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))
+            return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16));
+
+        // Otherwise, we're most likely dealing with a named color
+        var name = jQuery.trim(str).toLowerCase();
+        if (name == "transparent")
+            return m(255, 255, 255, 0);
+        else {
+            res = lookupColors[name];
+            return m(res[0], res[1], res[2]);
+        }
+    }
+    
+    var lookupColors = {
+        aqua:[0,255,255],
+        azure:[240,255,255],
+        beige:[245,245,220],
+        black:[0,0,0],
+        blue:[0,0,255],
+        brown:[165,42,42],
+        cyan:[0,255,255],
+        darkblue:[0,0,139],
+        darkcyan:[0,139,139],
+        darkgrey:[169,169,169],
+        darkgreen:[0,100,0],
+        darkkhaki:[189,183,107],
+        darkmagenta:[139,0,139],
+        darkolivegreen:[85,107,47],
+        darkorange:[255,140,0],
+        darkorchid:[153,50,204],
+        darkred:[139,0,0],
+        darksalmon:[233,150,122],
+        darkviolet:[148,0,211],
+        fuchsia:[255,0,255],
+        gold:[255,215,0],
+        green:[0,128,0],
+        indigo:[75,0,130],
+        khaki:[240,230,140],
+        lightblue:[173,216,230],
+        lightcyan:[224,255,255],
+        lightgreen:[144,238,144],
+        lightgrey:[211,211,211],
+        lightpink:[255,182,193],
+        lightyellow:[255,255,224],
+        lime:[0,255,0],
+        magenta:[255,0,255],
+        maroon:[128,0,0],
+        navy:[0,0,128],
+        olive:[128,128,0],
+        orange:[255,165,0],
+        pink:[255,192,203],
+        purple:[128,0,128],
+        violet:[128,0,128],
+        red:[255,0,0],
+        silver:[192,192,192],
+        white:[255,255,255],
+        yellow:[255,255,0]
+    };    
+})();
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.min.js
@@ -0,0 +1,1 @@
+(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.js
@@ -0,0 +1,156 @@
+/*
+Flot plugin for showing a crosshair, thin lines, when the mouse hovers
+over the plot.
+
+  crosshair: {
+    mode: null or "x" or "y" or "xy"
+    color: color
+    lineWidth: number
+  }
+
+Set the mode to one of "x", "y" or "xy". The "x" mode enables a
+vertical crosshair that lets you trace the values on the x axis, "y"
+enables a horizontal crosshair and "xy" enables them both. "color" is
+the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
+"lineWidth" is the width of the drawn lines (default is 1).
+
+The plugin also adds four public methods:
+
+  - setCrosshair(pos)
+
+    Set the position of the crosshair. Note that this is cleared if
+    the user moves the mouse. "pos" should be on the form { x: xpos,
+    y: ypos } (or x2 and y2 if you're using the secondary axes), which
+    is coincidentally the same format as what you get from a "plothover"
+    event. If "pos" is null, the crosshair is cleared.
+
+  - clearCrosshair()
+
+    Clear the crosshair.
+
+  - lockCrosshair(pos)
+
+    Cause the crosshair to lock to the current location, no longer
+    updating if the user moves the mouse. Optionally supply a position
+    (passed on to setCrosshair()) to move it to.
+
+    Example usage:
+      var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
+      $("#graph").bind("plothover", function (evt, position, item) {
+        if (item) {
+          // Lock the crosshair to the data point being hovered
+          myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
+        }
+        else {
+          // Return normal crosshair operation
+          myFlot.unlockCrosshair();
+        }
+      });
+
+  - unlockCrosshair()
+
+    Free the crosshair to move again after locking it.
+*/
+
+(function ($) {
+    var options = {
+        crosshair: {
+            mode: null, // one of null, "x", "y" or "xy",
+            color: "rgba(170, 0, 0, 0.80)",
+            lineWidth: 1
+        }
+    };
+    
+    function init(plot) {
+        // position of crosshair in pixels
+        var crosshair = { x: -1, y: -1, locked: false };
+
+        plot.setCrosshair = function setCrosshair(pos) {
+            if (!pos)
+                crosshair.x = -1;
+            else {
+                var axes = plot.getAxes();
+                
+                crosshair.x = Math.max(0, Math.min(pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plot.width()));
+                crosshair.y = Math.max(0, Math.min(pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plot.height()));
+            }
+            
+            plot.triggerRedrawOverlay();
+        };
+        
+        plot.clearCrosshair = plot.setCrosshair; // passes null for pos
+        
+        plot.lockCrosshair = function lockCrosshair(pos) {
+            if (pos)
+                plot.setCrosshair(pos);
+            crosshair.locked = true;
+        }
+
+        plot.unlockCrosshair = function unlockCrosshair() {
+            crosshair.locked = false;
+        }
+
+        plot.hooks.bindEvents.push(function (plot, eventHolder) {
+            if (!plot.getOptions().crosshair.mode)
+                return;
+
+            eventHolder.mouseout(function () {
+                if (crosshair.x != -1) {
+                    crosshair.x = -1;
+                    plot.triggerRedrawOverlay();
+                }
+            });
+            
+            eventHolder.mousemove(function (e) {
+                if (plot.getSelection && plot.getSelection()) {
+                    crosshair.x = -1; // hide the crosshair while selecting
+                    return;
+                }
+                
+                if (crosshair.locked)
+                    return;
+                
+                var offset = plot.offset();
+                crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
+                crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
+                plot.triggerRedrawOverlay();
+            });
+        });
+
+        plot.hooks.drawOverlay.push(function (plot, ctx) {
+            var c = plot.getOptions().crosshair;
+            if (!c.mode)
+                return;
+
+            var plotOffset = plot.getPlotOffset();
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            if (crosshair.x != -1) {
+                ctx.strokeStyle = c.color;
+                ctx.lineWidth = c.lineWidth;
+                ctx.lineJoin = "round";
+
+                ctx.beginPath();
+                if (c.mode.indexOf("x") != -1) {
+                    ctx.moveTo(crosshair.x, 0);
+                    ctx.lineTo(crosshair.x, plot.height());
+                }
+                if (c.mode.indexOf("y") != -1) {
+                    ctx.moveTo(0, crosshair.y);
+                    ctx.lineTo(plot.width(), crosshair.y);
+                }
+                ctx.stroke();
+            }
+            ctx.restore();
+        });
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'crosshair',
+        version: '1.0'
+    });
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.min.js
@@ -0,0 +1,1 @@
+(function(B){var A={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function C(G){var H={x:-1,y:-1,locked:false};G.setCrosshair=function D(J){if(!J){H.x=-1}else{var I=G.getAxes();H.x=Math.max(0,Math.min(J.x!=null?I.xaxis.p2c(J.x):I.x2axis.p2c(J.x2),G.width()));H.y=Math.max(0,Math.min(J.y!=null?I.yaxis.p2c(J.y):I.y2axis.p2c(J.y2),G.height()))}G.triggerRedrawOverlay()};G.clearCrosshair=G.setCrosshair;G.lockCrosshair=function E(I){if(I){G.setCrosshair(I)}H.locked=true};G.unlockCrosshair=function F(){H.locked=false};G.hooks.bindEvents.push(function(J,I){if(!J.getOptions().crosshair.mode){return }I.mouseout(function(){if(H.x!=-1){H.x=-1;J.triggerRedrawOverlay()}});I.mousemove(function(K){if(J.getSelection&&J.getSelection()){H.x=-1;return }if(H.locked){return }var L=J.offset();H.x=Math.max(0,Math.min(K.pageX-L.left,J.width()));H.y=Math.max(0,Math.min(K.pageY-L.top,J.height()));J.triggerRedrawOverlay()})});G.hooks.drawOverlay.push(function(K,I){var L=K.getOptions().crosshair;if(!L.mode){return }var J=K.getPlotOffset();I.save();I.translate(J.left,J.top);if(H.x!=-1){I.strokeStyle=L.color;I.lineWidth=L.lineWidth;I.lineJoin="round";I.beginPath();if(L.mode.indexOf("x")!=-1){I.moveTo(H.x,0);I.lineTo(H.x,K.height())}if(L.mode.indexOf("y")!=-1){I.moveTo(0,H.y);I.lineTo(K.width(),H.y)}I.stroke()}I.restore()})}B.plot.plugins.push({init:C,options:A,name:"crosshair",version:"1.0"})})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.image.js
@@ -0,0 +1,237 @@
+/*
+Flot plugin for plotting images, e.g. useful for putting ticks on a
+prerendered complex visualization.
+
+The data syntax is [[image, x1, y1, x2, y2], ...] where (x1, y1) and
+(x2, y2) are where you intend the two opposite corners of the image to
+end up in the plot. Image must be a fully loaded Javascript image (you
+can make one with new Image()). If the image is not complete, it's
+skipped when plotting.
+
+There are two helpers included for retrieving images. The easiest work
+the way that you put in URLs instead of images in the data (like
+["myimage.png", 0, 0, 10, 10]), then call $.plot.image.loadData(data,
+options, callback) where data and options are the same as you pass in
+to $.plot. This loads the images, replaces the URLs in the data with
+the corresponding images and calls "callback" when all images are
+loaded (or failed loading). In the callback, you can then call $.plot
+with the data set. See the included example.
+
+A more low-level helper, $.plot.image.load(urls, callback) is also
+included. Given a list of URLs, it calls callback with an object
+mapping from URL to Image object when all images are loaded or have
+failed loading.
+
+Options for the plugin are
+
+  series: {
+      images: {
+          show: boolean
+          anchor: "corner" or "center"
+          alpha: [0,1]
+      }
+  }
+
+which can be specified for a specific series
+
+  $.plot($("#placeholder"), [{ data: [ ... ], images: { ... } ])
+
+Note that because the data format is different from usual data points,
+you can't use images with anything else in a specific data series.
+
+Setting "anchor" to "center" causes the pixels in the image to be
+anchored at the corner pixel centers inside of at the pixel corners,
+effectively letting half a pixel stick out to each side in the plot.
+
+
+A possible future direction could be support for tiling for large
+images (like Google Maps).
+
+*/
+
+(function ($) {
+    var options = {
+        series: {
+            images: {
+                show: false,
+                alpha: 1,
+                anchor: "corner" // or "center"
+            }
+        }
+    };
+
+    $.plot.image = {};
+
+    $.plot.image.loadDataImages = function (series, options, callback) {
+        var urls = [], points = [];
+
+        var defaultShow = options.series.images.show;
+        
+        $.each(series, function (i, s) {
+            if (!(defaultShow || s.images.show))
+                return;
+            
+            if (s.data)
+                s = s.data;
+
+            $.each(s, function (i, p) {
+                if (typeof p[0] == "string") {
+                    urls.push(p[0]);
+                    points.push(p);
+                }
+            });
+        });
+
+        $.plot.image.load(urls, function (loadedImages) {
+            $.each(points, function (i, p) {
+                var url = p[0];
+                if (loadedImages[url])
+                    p[0] = loadedImages[url];
+            });
+
+            callback();
+        });
+    }
+    
+    $.plot.image.load = function (urls, callback) {
+        var missing = urls.length, loaded = {};
+        if (missing == 0)
+            callback({});
+
+        $.each(urls, function (i, url) {
+            var handler = function () {
+                --missing;
+                
+                loaded[url] = this;
+                
+                if (missing == 0)
+                    callback(loaded);
+            };
+
+            $('<img />').load(handler).error(handler).attr('src', url);
+        });
+    }
+    
+    function draw(plot, ctx) {
+        var plotOffset = plot.getPlotOffset();
+        
+        $.each(plot.getData(), function (i, series) {
+            var points = series.datapoints.points,
+                ps = series.datapoints.pointsize;
+            
+            for (var i = 0; i < points.length; i += ps) {
+                var img = points[i],
+                    x1 = points[i + 1], y1 = points[i + 2],
+                    x2 = points[i + 3], y2 = points[i + 4],
+                    xaxis = series.xaxis, yaxis = series.yaxis,
+                    tmp;
+
+                // actually we should check img.complete, but it
+                // appears to be a somewhat unreliable indicator in
+                // IE6 (false even after load event)
+                if (!img || img.width <= 0 || img.height <= 0)
+                    continue;
+
+                if (x1 > x2) {
+                    tmp = x2;
+                    x2 = x1;
+                    x1 = tmp;
+                }
+                if (y1 > y2) {
+                    tmp = y2;
+                    y2 = y1;
+                    y1 = tmp;
+                }
+                
+                // if the anchor is at the center of the pixel, expand the 
+                // image by 1/2 pixel in each direction
+                if (series.images.anchor == "center") {
+                    tmp = 0.5 * (x2-x1) / (img.width - 1);
+                    x1 -= tmp;
+                    x2 += tmp;
+                    tmp = 0.5 * (y2-y1) / (img.height - 1);
+                    y1 -= tmp;
+                    y2 += tmp;
+                }
+                
+                // clip
+                if (x1 == x2 || y1 == y2 ||
+                    x1 >= xaxis.max || x2 <= xaxis.min ||
+                    y1 >= yaxis.max || y2 <= yaxis.min)
+                    continue;
+
+                var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height;
+                if (x1 < xaxis.min) {
+                    sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1);
+                    x1 = xaxis.min;
+                }
+
+                if (x2 > xaxis.max) {
+                    sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1);
+                    x2 = xaxis.max;
+                }
+
+                if (y1 < yaxis.min) {
+                    sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1);
+                    y1 = yaxis.min;
+                }
+
+                if (y2 > yaxis.max) {
+                    sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1);
+                    y2 = yaxis.max;
+                }
+                
+                x1 = xaxis.p2c(x1);
+                x2 = xaxis.p2c(x2);
+                y1 = yaxis.p2c(y1);
+                y2 = yaxis.p2c(y2);
+                
+                // the transformation may have swapped us
+                if (x1 > x2) {
+                    tmp = x2;
+                    x2 = x1;
+                    x1 = tmp;
+                }
+                if (y1 > y2) {
+                    tmp = y2;
+                    y2 = y1;
+                    y1 = tmp;
+                }
+
+                tmp = ctx.globalAlpha;
+                ctx.globalAlpha *= series.images.alpha;
+                ctx.drawImage(img,
+                              sx1, sy1, sx2 - sx1, sy2 - sy1,
+                              x1 + plotOffset.left, y1 + plotOffset.top,
+                              x2 - x1, y2 - y1);
+                ctx.globalAlpha = tmp;
+            }
+        });
+    }
+
+    function processRawData(plot, series, data, datapoints) {
+        if (!series.images.show)
+            return;
+
+        // format is Image, x1, y1, x2, y2 (opposite corners)
+        datapoints.format = [
+            { required: true },
+            { x: true, number: true, required: true },
+            { y: true, number: true, required: true },
+            { x: true, number: true, required: true },
+            { y: true, number: true, required: true }
+        ];
+    }
+    
+    function init(plot) {
+        plot.hooks.processRawData.push(processRawData);
+        plot.hooks.draw.push(draw);
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'image',
+        version: '1.1'
+    });
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.image.min.js
@@ -0,0 +1,1 @@
+(function(D){var B={series:{images:{show:false,alpha:1,anchor:"corner"}}};D.plot.image={};D.plot.image.loadDataImages=function(G,F,K){var J=[],H=[];var I=F.series.images.show;D.each(G,function(L,M){if(!(I||M.images.show)){return }if(M.data){M=M.data}D.each(M,function(N,O){if(typeof O[0]=="string"){J.push(O[0]);H.push(O)}})});D.plot.image.load(J,function(L){D.each(H,function(N,O){var M=O[0];if(L[M]){O[0]=L[M]}});K()})};D.plot.image.load=function(H,I){var G=H.length,F={};if(G==0){I({})}D.each(H,function(K,J){var L=function(){--G;F[J]=this;if(G==0){I(F)}};D("<img />").load(L).error(L).attr("src",J)})};function A(H,F){var G=H.getPlotOffset();D.each(H.getData(),function(O,P){var X=P.datapoints.points,I=P.datapoints.pointsize;for(var O=0;O<X.length;O+=I){var Q=X[O],M=X[O+1],V=X[O+2],K=X[O+3],T=X[O+4],W=P.xaxis,S=P.yaxis,N;if(!Q||Q.width<=0||Q.height<=0){continue}if(M>K){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}if(P.images.anchor=="center"){N=0.5*(K-M)/(Q.width-1);M-=N;K+=N;N=0.5*(T-V)/(Q.height-1);V-=N;T+=N}if(M==K||V==T||M>=W.max||K<=W.min||V>=S.max||T<=S.min){continue}var L=0,U=0,J=Q.width,R=Q.height;if(M<W.min){L+=(J-L)*(W.min-M)/(K-M);M=W.min}if(K>W.max){J+=(J-L)*(W.max-K)/(K-M);K=W.max}if(V<S.min){R+=(U-R)*(S.min-V)/(T-V);V=S.min}if(T>S.max){U+=(U-R)*(S.max-T)/(T-V);T=S.max}M=W.p2c(M);K=W.p2c(K);V=S.p2c(V);T=S.p2c(T);if(M>K){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}N=F.globalAlpha;F.globalAlpha*=P.images.alpha;F.drawImage(Q,L,U,J-L,R-U,M+G.left,V+G.top,K-M,T-V);F.globalAlpha=N}})}function C(I,F,G,H){if(!F.images.show){return }H.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function E(F){F.hooks.processRawData.push(C);F.hooks.draw.push(A)}D.plot.plugins.push({init:E,options:B,name:"image",version:"1.1"})})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.js
@@ -0,0 +1,2119 @@
+/* Javascript plotting library for jQuery, v. 0.6.
+ *
+ * Released under the MIT license by IOLA, December 2007.
+ *
+ */
+
+// first an inline dependency, jquery.colorhelpers.js, we inline it here
+// for convenience
+
+/* Plugin for jQuery for working with colors.
+ * 
+ * Version 1.0.
+ * 
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ *   var c = $.color.extract($("#mydiv"), 'background-color');
+ *   console.log(c.r, c.g, c.b, c.a);
+ *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() work in-place instead of returning
+ * new objects.
+ */ 
+(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();
+
+// the actual Flot code
+(function($) {
+    function Plot(placeholder, data_, options_, plugins) {
+        // data is on the form:
+        //   [ series1, series2 ... ]
+        // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
+        // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
+        
+        var series = [],
+            options = {
+                // the color theme used for graphs
+                colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
+                legend: {
+                    show: true,
+                    noColumns: 1, // number of colums in legend table
+                    labelFormatter: null, // fn: string -> string
+                    labelBoxBorderColor: "#ccc", // border color for the little label boxes
+                    container: null, // container (as jQuery object) to put legend in, null means default on top of graph
+                    position: "ne", // position of default legend container within plot
+                    margin: 5, // distance from grid edge to default legend container within plot
+                    backgroundColor: null, // null means auto-detect
+                    backgroundOpacity: 0.85 // set to 0 to avoid background
+                },
+                xaxis: {
+                    mode: null, // null or "time"
+                    transform: null, // null or f: number -> number to transform axis
+                    inverseTransform: null, // if transform is set, this should be the inverse function
+                    min: null, // min. value to show, null means set automatically
+                    max: null, // max. value to show, null means set automatically
+                    autoscaleMargin: null, // margin in % to add if auto-setting min/max
+                    ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
+                    tickFormatter: null, // fn: number -> string
+                    labelWidth: null, // size of tick labels in pixels
+                    labelHeight: null,
+                    
+                    // mode specific options
+                    tickDecimals: null, // no. of decimals, null means auto
+                    tickSize: null, // number or [number, "unit"]
+                    minTickSize: null, // number or [number, "unit"]
+                    monthNames: null, // list of names of months
+                    timeformat: null, // format string to use
+                    twelveHourClock: false // 12 or 24 time in time mode
+                },
+                yaxis: {
+                    autoscaleMargin: 0.02
+                },
+                x2axis: {
+                    autoscaleMargin: null
+                },
+                y2axis: {
+                    autoscaleMargin: 0.02
+                },
+                series: {
+                    points: {
+                        show: false,
+                        radius: 3,
+                        lineWidth: 2, // in pixels
+                        fill: true,
+                        fillColor: "#ffffff"
+                    },
+                    lines: {
+                        // we don't put in show: false so we can see
+                        // whether lines were actively disabled 
+                        lineWidth: 2, // in pixels
+                        fill: false,
+                        fillColor: null,
+                        steps: false
+                    },
+                    bars: {
+                        show: false,
+                        lineWidth: 2, // in pixels
+                        barWidth: 1, // in units of the x axis
+                        fill: true,
+                        fillColor: null,
+                        align: "left", // or "center" 
+                        horizontal: false // when horizontal, left is now top
+                    },
+                    shadowSize: 3
+                },
+                grid: {
+                    show: true,
+                    aboveData: false,
+                    color: "#545454", // primary color used for outline and labels
+                    backgroundColor: null, // null for transparent, else color
+                    tickColor: "rgba(0,0,0,0.15)", // color used for the ticks
+                    labelMargin: 5, // in pixels
+                    borderWidth: 2, // in pixels
+                    borderColor: null, // set if different from the grid color
+                    markings: null, // array of ranges or fn: axes -> array of ranges
+                    markingsColor: "#f4f4f4",
+                    markingsLineWidth: 2,
+                    // interactive stuff
+                    clickable: false,
+                    hoverable: false,
+                    autoHighlight: true, // highlight in case mouse is near
+                    mouseActiveRadius: 10 // how far the mouse can be away to activate an item
+                },
+                hooks: {}
+            },
+        canvas = null,      // the canvas for the plot itself
+        overlay = null,     // canvas for interactive stuff on top of plot
+        eventHolder = null, // jQuery object that events should be bound to
+        ctx = null, octx = null,
+        axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} },
+        plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
+        canvasWidth = 0, canvasHeight = 0,
+        plotWidth = 0, plotHeight = 0,
+        hooks = {
+            processOptions: [],
+            processRawData: [],
+            processDatapoints: [],
+            draw: [],
+            bindEvents: [],
+            drawOverlay: []
+        },
+        plot = this;
+
+        // public functions
+        plot.setData = setData;
+        plot.setupGrid = setupGrid;
+        plot.draw = draw;
+        plot.getPlaceholder = function() { return placeholder; };
+        plot.getCanvas = function() { return canvas; };
+        plot.getPlotOffset = function() { return plotOffset; };
+        plot.width = function () { return plotWidth; };
+        plot.height = function () { return plotHeight; };
+        plot.offset = function () {
+            var o = eventHolder.offset();
+            o.left += plotOffset.left;
+            o.top += plotOffset.top;
+            return o;
+        };
+        plot.getData = function() { return series; };
+        plot.getAxes = function() { return axes; };
+        plot.getOptions = function() { return options; };
+        plot.highlight = highlight;
+        plot.unhighlight = unhighlight;
+        plot.triggerRedrawOverlay = triggerRedrawOverlay;
+        plot.pointOffset = function(point) {
+            return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left),
+                     top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) };
+        };
+        
+
+        // public attributes
+        plot.hooks = hooks;
+        
+        // initialize
+        initPlugins(plot);
+        parseOptions(options_);
+        constructCanvas();
+        setData(data_);
+        setupGrid();
+        draw();
+        bindEvents();
+
+
+        function executeHooks(hook, args) {
+            args = [plot].concat(args);
+            for (var i = 0; i < hook.length; ++i)
+                hook[i].apply(this, args);
+        }
+
+        function initPlugins() {
+            for (var i = 0; i < plugins.length; ++i) {
+                var p = plugins[i];
+                p.init(plot);
+                if (p.options)
+                    $.extend(true, options, p.options);
+            }
+        }
+        
+        function parseOptions(opts) {
+            $.extend(true, options, opts);
+            if (options.grid.borderColor == null)
+                options.grid.borderColor = options.grid.color;
+            // backwards compatibility, to be removed in future
+            if (options.xaxis.noTicks && options.xaxis.ticks == null)
+                options.xaxis.ticks = options.xaxis.noTicks;
+            if (options.yaxis.noTicks && options.yaxis.ticks == null)
+                options.yaxis.ticks = options.yaxis.noTicks;
+            if (options.grid.coloredAreas)
+                options.grid.markings = options.grid.coloredAreas;
+            if (options.grid.coloredAreasColor)
+                options.grid.markingsColor = options.grid.coloredAreasColor;
+            if (options.lines)
+                $.extend(true, options.series.lines, options.lines);
+            if (options.points)
+                $.extend(true, options.series.points, options.points);
+            if (options.bars)
+                $.extend(true, options.series.bars, options.bars);
+            if (options.shadowSize)
+                options.series.shadowSize = options.shadowSize;
+
+            for (var n in hooks)
+                if (options.hooks[n] && options.hooks[n].length)
+                    hooks[n] = hooks[n].concat(options.hooks[n]);
+
+            executeHooks(hooks.processOptions, [options]);
+        }
+
+        function setData(d) {
+            series = parseData(d);
+            fillInSeriesOptions();
+            processData();
+        }
+        
+        function parseData(d) {
+            var res = [];
+            for (var i = 0; i < d.length; ++i) {
+                var s = $.extend(true, {}, options.series);
+
+                if (d[i].data) {
+                    s.data = d[i].data; // move the data instead of deep-copy
+                    delete d[i].data;
+
+                    $.extend(true, s, d[i]);
+
+                    d[i].data = s.data;
+                }
+                else
+                    s.data = d[i];
+                res.push(s);
+            }
+
+            return res;
+        }
+        
+        function axisSpecToRealAxis(obj, attr) {
+            var a = obj[attr];
+            if (!a || a == 1)
+                return axes[attr];
+            if (typeof a == "number")
+                return axes[attr.charAt(0) + a + attr.slice(1)];
+            return a; // assume it's OK
+        }
+        
+        function fillInSeriesOptions() {
+            var i;
+            
+            // collect what we already got of colors
+            var neededColors = series.length,
+                usedColors = [],
+                assignedColors = [];
+            for (i = 0; i < series.length; ++i) {
+                var sc = series[i].color;
+                if (sc != null) {
+                    --neededColors;
+                    if (typeof sc == "number")
+                        assignedColors.push(sc);
+                    else
+                        usedColors.push($.color.parse(series[i].color));
+                }
+            }
+            
+            // we might need to generate more colors if higher indices
+            // are assigned
+            for (i = 0; i < assignedColors.length; ++i) {
+                neededColors = Math.max(neededColors, assignedColors[i] + 1);
+            }
+
+            // produce colors as needed
+            var colors = [], variation = 0;
+            i = 0;
+            while (colors.length < neededColors) {
+                var c;
+                if (options.colors.length == i) // check degenerate case
+                    c = $.color.make(100, 100, 100);
+                else
+                    c = $.color.parse(options.colors[i]);
+
+                // vary color if needed
+                var sign = variation % 2 == 1 ? -1 : 1;
+                c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
+
+                // FIXME: if we're getting to close to something else,
+                // we should probably skip this one
+                colors.push(c);
+                
+                ++i;
+                if (i >= options.colors.length) {
+                    i = 0;
+                    ++variation;
+                }
+            }
+
+            // fill in the options
+            var colori = 0, s;
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                // assign colors
+                if (s.color == null) {
+                    s.color = colors[colori].toString();
+                    ++colori;
+                }
+                else if (typeof s.color == "number")
+                    s.color = colors[s.color].toString();
+
+                // turn on lines automatically in case nothing is set
+                if (s.lines.show == null) {
+                    var v, show = true;
+                    for (v in s)
+                        if (s[v].show) {
+                            show = false;
+                            break;
+                        }
+                    if (show)
+                        s.lines.show = true;
+                }
+
+                // setup axes
+                s.xaxis = axisSpecToRealAxis(s, "xaxis");
+                s.yaxis = axisSpecToRealAxis(s, "yaxis");
+            }
+        }
+        
+        function processData() {
+            var topSentry = Number.POSITIVE_INFINITY,
+                bottomSentry = Number.NEGATIVE_INFINITY,
+                i, j, k, m, length,
+                s, points, ps, x, y, axis, val, f, p;
+
+            for (axis in axes) {
+                axes[axis].datamin = topSentry;
+                axes[axis].datamax = bottomSentry;
+                axes[axis].used = false;
+            }
+
+            function updateAxis(axis, min, max) {
+                if (min < axis.datamin)
+                    axis.datamin = min;
+                if (max > axis.datamax)
+                    axis.datamax = max;
+            }
+
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                s.datapoints = { points: [] };
+                
+                executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
+            }
+            
+            // first pass: clean and copy data
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+
+                var data = s.data, format = s.datapoints.format;
+
+                if (!format) {
+                    format = [];
+                    // find out how to copy
+                    format.push({ x: true, number: true, required: true });
+                    format.push({ y: true, number: true, required: true });
+
+                    if (s.bars.show)
+                        format.push({ y: true, number: true, required: false, defaultValue: 0 });
+                    
+                    s.datapoints.format = format;
+                }
+
+                if (s.datapoints.pointsize != null)
+                    continue; // already filled in
+
+                if (s.datapoints.pointsize == null)
+                    s.datapoints.pointsize = format.length;
+                
+                ps = s.datapoints.pointsize;
+                points = s.datapoints.points;
+
+                insertSteps = s.lines.show && s.lines.steps;
+                s.xaxis.used = s.yaxis.used = true;
+                
+                for (j = k = 0; j < data.length; ++j, k += ps) {
+                    p = data[j];
+
+                    var nullify = p == null;
+                    if (!nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = p[m];
+                            f = format[m];
+
+                            if (f) {
+                                if (f.number && val != null) {
+                                    val = +val; // convert to number
+                                    if (isNaN(val))
+                                        val = null;
+                                }
+
+                                if (val == null) {
+                                    if (f.required)
+                                        nullify = true;
+                                    
+                                    if (f.defaultValue != null)
+                                        val = f.defaultValue;
+                                }
+                            }
+                            
+                            points[k + m] = val;
+                        }
+                    }
+                    
+                    if (nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = points[k + m];
+                            if (val != null) {
+                                f = format[m];
+                                // extract min/max info
+                                if (f.x)
+                                    updateAxis(s.xaxis, val, val);
+                                if (f.y)
+                                    updateAxis(s.yaxis, val, val);
+                            }
+                            points[k + m] = null;
+                        }
+                    }
+                    else {
+                        // a little bit of line specific stuff that
+                        // perhaps shouldn't be here, but lacking
+                        // better means...
+                        if (insertSteps && k > 0
+                            && points[k - ps] != null
+                            && points[k - ps] != points[k]
+                            && points[k - ps + 1] != points[k + 1]) {
+                            // copy the point to make room for a middle point
+                            for (m = 0; m < ps; ++m)
+                                points[k + ps + m] = points[k + m];
+
+                            // middle point has same y
+                            points[k + 1] = points[k - ps + 1];
+
+                            // we've added a point, better reflect that
+                            k += ps;
+                        }
+                    }
+                }
+            }
+
+            // give the hooks a chance to run
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
+            }
+
+            // second pass: find datamax/datamin for auto-scaling
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                points = s.datapoints.points,
+                ps = s.datapoints.pointsize;
+
+                var xmin = topSentry, ymin = topSentry,
+                    xmax = bottomSentry, ymax = bottomSentry;
+                
+                for (j = 0; j < points.length; j += ps) {
+                    if (points[j] == null)
+                        continue;
+
+                    for (m = 0; m < ps; ++m) {
+                        val = points[j + m];
+                        f = format[m];
+                        if (!f)
+                            continue;
+                        
+                        if (f.x) {
+                            if (val < xmin)
+                                xmin = val;
+                            if (val > xmax)
+                                xmax = val;
+                        }
+                        if (f.y) {
+                            if (val < ymin)
+                                ymin = val;
+                            if (val > ymax)
+                                ymax = val;
+                        }
+                    }
+                }
+                
+                if (s.bars.show) {
+                    // make sure we got room for the bar on the dancing floor
+                    var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
+                    if (s.bars.horizontal) {
+                        ymin += delta;
+                        ymax += delta + s.bars.barWidth;
+                    }
+                    else {
+                        xmin += delta;
+                        xmax += delta + s.bars.barWidth;
+                    }
+                }
+                
+                updateAxis(s.xaxis, xmin, xmax);
+                updateAxis(s.yaxis, ymin, ymax);
+            }
+
+            for (axis in axes) {
+                if (axes[axis].datamin == topSentry)
+                    axes[axis].datamin = null;
+                if (axes[axis].datamax == bottomSentry)
+                    axes[axis].datamax = null;
+            }
+        }
+
+        function constructCanvas() {
+            function makeCanvas(width, height) {
+                var c = document.createElement('canvas');
+                c.width = width;
+                c.height = height;
+                if ($.browser.msie) // excanvas hack
+                    c = window.G_vmlCanvasManager.initElement(c);
+                return c;
+            }
+            
+            canvasWidth = placeholder.width();
+            canvasHeight = placeholder.height();
+            placeholder.html(""); // clear placeholder
+            if (placeholder.css("position") == 'static')
+                placeholder.css("position", "relative"); // for positioning labels and overlay
+
+            if (canvasWidth <= 0 || canvasHeight <= 0)
+                throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
+
+            if ($.browser.msie) // excanvas hack
+                window.G_vmlCanvasManager.init_(document); // make sure everything is setup
+            
+            // the canvas
+            canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0);
+            ctx = canvas.getContext("2d");
+
+            // overlay canvas for interactive features
+            overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0);
+            octx = overlay.getContext("2d");
+            octx.stroke();
+        }
+
+        function bindEvents() {
+            // we include the canvas in the event holder too, because IE 7
+            // sometimes has trouble with the stacking order
+            eventHolder = $([overlay, canvas]);
+
+            // bind events
+            if (options.grid.hoverable)
+                eventHolder.mousemove(onMouseMove);
+
+            if (options.grid.clickable)
+                eventHolder.click(onClick);
+
+            executeHooks(hooks.bindEvents, [eventHolder]);
+        }
+
+        function setupGrid() {
+            function setTransformationHelpers(axis, o) {
+                function identity(x) { return x; }
+                
+                var s, m, t = o.transform || identity,
+                    it = o.inverseTransform;
+                    
+                // add transformation helpers
+                if (axis == axes.xaxis || axis == axes.x2axis) {
+                    // precompute how much the axis is scaling a point
+                    // in canvas space
+                    s = axis.scale = plotWidth / (t(axis.max) - t(axis.min));
+                    m = t(axis.min);
+
+                    // data point to canvas coordinate
+                    if (t == identity) // slight optimization
+                        axis.p2c = function (p) { return (p - m) * s; };
+                    else
+                        axis.p2c = function (p) { return (t(p) - m) * s; };
+                    // canvas coordinate to data point
+                    if (!it)
+                        axis.c2p = function (c) { return m + c / s; };
+                    else
+                        axis.c2p = function (c) { return it(m + c / s); };
+                }
+                else {
+                    s = axis.scale = plotHeight / (t(axis.max) - t(axis.min));
+                    m = t(axis.max);
+                    
+                    if (t == identity)
+                        axis.p2c = function (p) { return (m - p) * s; };
+                    else
+                        axis.p2c = function (p) { return (m - t(p)) * s; };
+                    if (!it)
+                        axis.c2p = function (c) { return m - c / s; };
+                    else
+                        axis.c2p = function (c) { return it(m - c / s); };
+                }
+            }
+
+            function measureLabels(axis, axisOptions) {
+                var i, labels = [], l;
+                
+                axis.labelWidth = axisOptions.labelWidth;
+                axis.labelHeight = axisOptions.labelHeight;
+
+                if (axis == axes.xaxis || axis == axes.x2axis) {
+                    // to avoid measuring the widths of the labels, we
+                    // construct fixed-size boxes and put the labels inside
+                    // them, we don't need the exact figures and the
+                    // fixed-size box content is easy to center
+                    if (axis.labelWidth == null)
+                        axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1);
+
+                    // measure x label heights
+                    if (axis.labelHeight == null) {
+                        labels = [];
+                        for (i = 0; i < axis.ticks.length; ++i) {
+                            l = axis.ticks[i].label;
+                            if (l)
+                                labels.push('<div class="tickLabel" style="float:left;width:' + axis.labelWidth + 'px">' + l + '</div>');
+                        }
+                        
+                        if (labels.length > 0) {
+                            var dummyDiv = $('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">'
+                                             + labels.join("") + '<div style="clear:left"></div></div>').appendTo(placeholder);
+                            axis.labelHeight = dummyDiv.height();
+                            dummyDiv.remove();
+                        }
+                    }
+                }
+                else if (axis.labelWidth == null || axis.labelHeight == null) {
+                    // calculate y label dimensions
+                    for (i = 0; i < axis.ticks.length; ++i) {
+                        l = axis.ticks[i].label;
+                        if (l)
+                            labels.push('<div class="tickLabel">' + l + '</div>');
+                    }
+                    
+                    if (labels.length > 0) {
+                        var dummyDiv = $('<div style="position:absolute;top:-10000px;font-size:smaller">'
+                                         + labels.join("") + '</div>').appendTo(placeholder);
+                        if (axis.labelWidth == null)
+                            axis.labelWidth = dummyDiv.width();
+                        if (axis.labelHeight == null)
+                            axis.labelHeight = dummyDiv.find("div").height();
+                        dummyDiv.remove();
+                    }
+                    
+                }
+
+                if (axis.labelWidth == null)
+                    axis.labelWidth = 0;
+                if (axis.labelHeight == null)
+                    axis.labelHeight = 0;
+            }
+            
+            function setGridSpacing() {
+                // get the most space needed around the grid for things
+                // that may stick out
+                var maxOutset = options.grid.borderWidth;
+                for (i = 0; i < series.length; ++i)
+                    maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
+                
+                plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset;
+                
+                var margin = options.grid.labelMargin + options.grid.borderWidth;
+                
+                if (axes.xaxis.labelHeight > 0)
+                    plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin);
+                if (axes.yaxis.labelWidth > 0)
+                    plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin);
+                if (axes.x2axis.labelHeight > 0)
+                    plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin);
+                if (axes.y2axis.labelWidth > 0)
+                    plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin);
+            
+                plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
+                plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
+            }
+            
+            var axis;
+            for (axis in axes)
+                setRange(axes[axis], options[axis]);
+            
+            if (options.grid.show) {
+                for (axis in axes) {
+                    prepareTickGeneration(axes[axis], options[axis]);
+                    setTicks(axes[axis], options[axis]);
+                    measureLabels(axes[axis], options[axis]);
+                }
+
+                setGridSpacing();
+            }
+            else {
+                plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
+                plotWidth = canvasWidth;
+                plotHeight = canvasHeight;
+            }
+            
+            for (axis in axes)
+                setTransformationHelpers(axes[axis], options[axis]);
+
+            if (options.grid.show)
+                insertLabels();
+            
+            insertLegend();
+        }
+        
+        function setRange(axis, axisOptions) {
+            var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin),
+                max = +(axisOptions.max != null ? axisOptions.max : axis.datamax),
+                delta = max - min;
+
+            if (delta == 0.0) {
+                // degenerate case
+                var widen = max == 0 ? 1 : 0.01;
+
+                if (axisOptions.min == null)
+                    min -= widen;
+                // alway widen max if we couldn't widen min to ensure we
+                // don't fall into min == max which doesn't work
+                if (axisOptions.max == null || axisOptions.min != null)
+                    max += widen;
+            }
+            else {
+                // consider autoscaling
+                var margin = axisOptions.autoscaleMargin;
+                if (margin != null) {
+                    if (axisOptions.min == null) {
+                        min -= delta * margin;
+                        // make sure we don't go below zero if all values
+                        // are positive
+                        if (min < 0 && axis.datamin != null && axis.datamin >= 0)
+                            min = 0;
+                    }
+                    if (axisOptions.max == null) {
+                        max += delta * margin;
+                        if (max > 0 && axis.datamax != null && axis.datamax <= 0)
+                            max = 0;
+                    }
+                }
+            }
+            axis.min = min;
+            axis.max = max;
+        }
+
+        function prepareTickGeneration(axis, axisOptions) {
+            // estimate number of ticks
+            var noTicks;
+            if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0)
+                noTicks = axisOptions.ticks;
+            else if (axis == axes.xaxis || axis == axes.x2axis)
+                 // heuristic based on the model a*sqrt(x) fitted to
+                 // some reasonable data points
+                noTicks = 0.3 * Math.sqrt(canvasWidth);
+            else
+                noTicks = 0.3 * Math.sqrt(canvasHeight);
+            
+            var delta = (axis.max - axis.min) / noTicks,
+                size, generator, unit, formatter, i, magn, norm;
+
+            if (axisOptions.mode == "time") {
+                // pretty handling of time
+                
+                // map of app. size of time units in milliseconds
+                var timeUnitSize = {
+                    "second": 1000,
+                    "minute": 60 * 1000,
+                    "hour": 60 * 60 * 1000,
+                    "day": 24 * 60 * 60 * 1000,
+                    "month": 30 * 24 * 60 * 60 * 1000,
+                    "year": 365.2425 * 24 * 60 * 60 * 1000
+                };
+
+
+                // the allowed tick sizes, after 1 year we use
+                // an integer algorithm
+                var spec = [
+                    [1, "second"], [2, "second"], [5, "second"], [10, "second"],
+                    [30, "second"], 
+                    [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+                    [30, "minute"], 
+                    [1, "hour"], [2, "hour"], [4, "hour"],
+                    [8, "hour"], [12, "hour"],
+                    [1, "day"], [2, "day"], [3, "day"],
+                    [0.25, "month"], [0.5, "month"], [1, "month"],
+                    [2, "month"], [3, "month"], [6, "month"],
+                    [1, "year"]
+                ];
+
+                var minSize = 0;
+                if (axisOptions.minTickSize != null) {
+                    if (typeof axisOptions.tickSize == "number")
+                        minSize = axisOptions.tickSize;
+                    else
+                        minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
+                }
+
+                for (i = 0; i < spec.length - 1; ++i)
+                    if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+                                 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
+                       && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
+                        break;
+                size = spec[i][0];
+                unit = spec[i][1];
+                
+                // special-case the possibility of several years
+                if (unit == "year") {
+                    magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
+                    norm = (delta / timeUnitSize.year) / magn;
+                    if (norm < 1.5)
+                        size = 1;
+                    else if (norm < 3)
+                        size = 2;
+                    else if (norm < 7.5)
+                        size = 5;
+                    else
+                        size = 10;
+
+                    size *= magn;
+                }
+
+                if (axisOptions.tickSize) {
+                    size = axisOptions.tickSize[0];
+                    unit = axisOptions.tickSize[1];
+                }
+                
+                generator = function(axis) {
+                    var ticks = [],
+                        tickSize = axis.tickSize[0], unit = axis.tickSize[1],
+                        d = new Date(axis.min);
+                    
+                    var step = tickSize * timeUnitSize[unit];
+
+                    if (unit == "second")
+                        d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
+                    if (unit == "minute")
+                        d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
+                    if (unit == "hour")
+                        d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
+                    if (unit == "month")
+                        d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
+                    if (unit == "year")
+                        d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
+                    
+                    // reset smaller components
+                    d.setUTCMilliseconds(0);
+                    if (step >= timeUnitSize.minute)
+                        d.setUTCSeconds(0);
+                    if (step >= timeUnitSize.hour)
+                        d.setUTCMinutes(0);
+                    if (step >= timeUnitSize.day)
+                        d.setUTCHours(0);
+                    if (step >= timeUnitSize.day * 4)
+                        d.setUTCDate(1);
+                    if (step >= timeUnitSize.year)
+                        d.setUTCMonth(0);
+
+
+                    var carry = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = d.getTime();
+                        ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
+                        if (unit == "month") {
+                            if (tickSize < 1) {
+                                // a bit complicated - we'll divide the month
+                                // up but we need to take care of fractions
+                                // so we don't end up in the middle of a day
+                                d.setUTCDate(1);
+                                var start = d.getTime();
+                                d.setUTCMonth(d.getUTCMonth() + 1);
+                                var end = d.getTime();
+                                d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+                                carry = d.getUTCHours();
+                                d.setUTCHours(0);
+                            }
+                            else
+                                d.setUTCMonth(d.getUTCMonth() + tickSize);
+                        }
+                        else if (unit == "year") {
+                            d.setUTCFullYear(d.getUTCFullYear() + tickSize);
+                        }
+                        else
+                            d.setTime(v + step);
+                    } while (v < axis.max && v != prev);
+
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    var d = new Date(v);
+
+                    // first check global format
+                    if (axisOptions.timeformat != null)
+                        return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
+                    
+                    var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+                    var span = axis.max - axis.min;
+                    var suffix = (axisOptions.twelveHourClock) ? " %p" : "";
+                    
+                    if (t < timeUnitSize.minute)
+                        fmt = "%h:%M:%S" + suffix;
+                    else if (t < timeUnitSize.day) {
+                        if (span < 2 * timeUnitSize.day)
+                            fmt = "%h:%M" + suffix;
+                        else
+                            fmt = "%b %d %h:%M" + suffix;
+                    }
+                    else if (t < timeUnitSize.month)
+                        fmt = "%b %d";
+                    else if (t < timeUnitSize.year) {
+                        if (span < timeUnitSize.year)
+                            fmt = "%b";
+                        else
+                            fmt = "%b %y";
+                    }
+                    else
+                        fmt = "%y";
+                    
+                    return $.plot.formatDate(d, fmt, axisOptions.monthNames);
+                };
+            }
+            else {
+                // pretty rounding of base-10 numbers
+                var maxDec = axisOptions.tickDecimals;
+                var dec = -Math.floor(Math.log(delta) / Math.LN10);
+                if (maxDec != null && dec > maxDec)
+                    dec = maxDec;
+
+                magn = Math.pow(10, -dec);
+                norm = delta / magn; // norm is between 1.0 and 10.0
+                
+                if (norm < 1.5)
+                    size = 1;
+                else if (norm < 3) {
+                    size = 2;
+                    // special case for 2.5, requires an extra decimal
+                    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+                        size = 2.5;
+                        ++dec;
+                    }
+                }
+                else if (norm < 7.5)
+                    size = 5;
+                else
+                    size = 10;
+
+                size *= magn;
+                
+                if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
+                    size = axisOptions.minTickSize;
+
+                if (axisOptions.tickSize != null)
+                    size = axisOptions.tickSize;
+
+                axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
+
+                generator = function (axis) {
+                    var ticks = [];
+
+                    // spew out all possible ticks
+                    var start = floorInBase(axis.min, axis.tickSize),
+                        i = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = start + i * axis.tickSize;
+                        ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
+                        ++i;
+                    } while (v < axis.max && v != prev);
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    return v.toFixed(axis.tickDecimals);
+                };
+            }
+
+            axis.tickSize = unit ? [size, unit] : size;
+            axis.tickGenerator = generator;
+            if ($.isFunction(axisOptions.tickFormatter))
+                axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
+            else
+                axis.tickFormatter = formatter;
+        }
+        
+        function setTicks(axis, axisOptions) {
+            axis.ticks = [];
+
+            if (!axis.used)
+                return;
+            
+            if (axisOptions.ticks == null)
+                axis.ticks = axis.tickGenerator(axis);
+            else if (typeof axisOptions.ticks == "number") {
+                if (axisOptions.ticks > 0)
+                    axis.ticks = axis.tickGenerator(axis);
+            }
+            else if (axisOptions.ticks) {
+                var ticks = axisOptions.ticks;
+
+                if ($.isFunction(ticks))
+                    // generate the ticks
+                    ticks = ticks({ min: axis.min, max: axis.max });
+                
+                // clean up the user-supplied ticks, copy them over
+                var i, v;
+                for (i = 0; i < ticks.length; ++i) {
+                    var label = null;
+                    var t = ticks[i];
+                    if (typeof t == "object") {
+                        v = t[0];
+                        if (t.length > 1)
+                            label = t[1];
+                    }
+                    else
+                        v = t;
+                    if (label == null)
+                        label = axis.tickFormatter(v, axis);
+                    axis.ticks[i] = { v: v, label: label };
+                }
+            }
+
+            if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
+                // snap to ticks
+                if (axisOptions.min == null)
+                    axis.min = Math.min(axis.min, axis.ticks[0].v);
+                if (axisOptions.max == null && axis.ticks.length > 1)
+                    axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v);
+            }
+        }
+      
+        function draw() {
+            ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+            var grid = options.grid;
+            
+            if (grid.show && !grid.aboveData)
+                drawGrid();
+
+            for (var i = 0; i < series.length; ++i)
+                drawSeries(series[i]);
+
+            executeHooks(hooks.draw, [ctx]);
+            
+            if (grid.show && grid.aboveData)
+                drawGrid();
+        }
+
+        function extractRange(ranges, coord) {
+            var firstAxis = coord + "axis",
+                secondaryAxis = coord + "2axis",
+                axis, from, to, reverse;
+
+            if (ranges[firstAxis]) {
+                axis = axes[firstAxis];
+                from = ranges[firstAxis].from;
+                to = ranges[firstAxis].to;
+            }
+            else if (ranges[secondaryAxis]) {
+                axis = axes[secondaryAxis];
+                from = ranges[secondaryAxis].from;
+                to = ranges[secondaryAxis].to;
+            }
+            else {
+                // backwards-compat stuff - to be removed in future
+                axis = axes[firstAxis];
+                from = ranges[coord + "1"];
+                to = ranges[coord + "2"];
+            }
+
+            // auto-reverse as an added bonus
+            if (from != null && to != null && from > to)
+                return { from: to, to: from, axis: axis };
+            
+            return { from: from, to: to, axis: axis };
+        }
+        
+        function drawGrid() {
+            var i;
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // draw background, if any
+            if (options.grid.backgroundColor) {
+                ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
+                ctx.fillRect(0, 0, plotWidth, plotHeight);
+            }
+
+            // draw markings
+            var markings = options.grid.markings;
+            if (markings) {
+                if ($.isFunction(markings))
+                    // xmin etc. are backwards-compatible, to be removed in future
+                    markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis });
+
+                for (i = 0; i < markings.length; ++i) {
+                    var m = markings[i],
+                        xrange = extractRange(m, "x"),
+                        yrange = extractRange(m, "y");
+
+                    // fill in missing
+                    if (xrange.from == null)
+                        xrange.from = xrange.axis.min;
+                    if (xrange.to == null)
+                        xrange.to = xrange.axis.max;
+                    if (yrange.from == null)
+                        yrange.from = yrange.axis.min;
+                    if (yrange.to == null)
+                        yrange.to = yrange.axis.max;
+
+                    // clip
+                    if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
+                        yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
+                        continue;
+
+                    xrange.from = Math.max(xrange.from, xrange.axis.min);
+                    xrange.to = Math.min(xrange.to, xrange.axis.max);
+                    yrange.from = Math.max(yrange.from, yrange.axis.min);
+                    yrange.to = Math.min(yrange.to, yrange.axis.max);
+
+                    if (xrange.from == xrange.to && yrange.from == yrange.to)
+                        continue;
+
+                    // then draw
+                    xrange.from = xrange.axis.p2c(xrange.from);
+                    xrange.to = xrange.axis.p2c(xrange.to);
+                    yrange.from = yrange.axis.p2c(yrange.from);
+                    yrange.to = yrange.axis.p2c(yrange.to);
+                    
+                    if (xrange.from == xrange.to || yrange.from == yrange.to) {
+                        // draw line
+                        ctx.beginPath();
+                        ctx.strokeStyle = m.color || options.grid.markingsColor;
+                        ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
+                        //ctx.moveTo(Math.floor(xrange.from), yrange.from);
+                        //ctx.lineTo(Math.floor(xrange.to), yrange.to);
+                        ctx.moveTo(xrange.from, yrange.from);
+                        ctx.lineTo(xrange.to, yrange.to);
+                        ctx.stroke();
+                    }
+                    else {
+                        // fill area
+                        ctx.fillStyle = m.color || options.grid.markingsColor;
+                        ctx.fillRect(xrange.from, yrange.to,
+                                     xrange.to - xrange.from,
+                                     yrange.from - yrange.to);
+                    }
+                }
+            }
+            
+            // draw the inner grid
+            ctx.lineWidth = 1;
+            ctx.strokeStyle = options.grid.tickColor;
+            ctx.beginPath();
+            var v, axis = axes.xaxis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axes.xaxis.max)
+                    continue;   // skip those lying on the axes
+
+                ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0);
+                ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight);
+            }
+
+            axis = axes.yaxis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axis.max)
+                    continue;
+
+                ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+                ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+            }
+
+            axis = axes.x2axis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axis.max)
+                    continue;
+    
+                ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5);
+                ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5);
+            }
+
+            axis = axes.y2axis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axis.max)
+                    continue;
+
+                ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+                ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+            }
+            
+            ctx.stroke();
+            
+            if (options.grid.borderWidth) {
+                // draw border
+                var bw = options.grid.borderWidth;
+                ctx.lineWidth = bw;
+                ctx.strokeStyle = options.grid.borderColor;
+                ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
+            }
+
+            ctx.restore();
+        }
+
+        function insertLabels() {
+            placeholder.find(".tickLabels").remove();
+            
+            var html = ['<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">'];
+
+            function addLabels(axis, labelGenerator) {
+                for (var i = 0; i < axis.ticks.length; ++i) {
+                    var tick = axis.ticks[i];
+                    if (!tick.label || tick.v < axis.min || tick.v > axis.max)
+                        continue;
+                    html.push(labelGenerator(tick, axis));
+                }
+            }
+
+            var margin = options.grid.labelMargin + options.grid.borderWidth;
+            
+            addLabels(axes.xaxis, function (tick, axis) {
+                return '<div style="position:absolute;top:' + (plotOffset.top + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
+            });
+            
+            
+            addLabels(axes.yaxis, function (tick, axis) {
+                return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;right:' + (plotOffset.right + plotWidth + margin) + 'px;width:' + axis.labelWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
+            });
+            
+            addLabels(axes.x2axis, function (tick, axis) {
+                return '<div style="position:absolute;bottom:' + (plotOffset.bottom + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
+            });
+            
+            addLabels(axes.y2axis, function (tick, axis) {
+                return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;left:' + (plotOffset.left + plotWidth + margin) +'px;width:' + axis.labelWidth + 'px;text-align:left" class="tickLabel">' + tick.label + "</div>";
+            });
+
+            html.push('</div>');
+            
+            placeholder.append(html.join(""));
+        }
+
+        function drawSeries(series) {
+            if (series.lines.show)
+                drawSeriesLines(series);
+            if (series.bars.show)
+                drawSeriesBars(series);
+            if (series.points.show)
+                drawSeriesPoints(series);
+        }
+        
+        function drawSeriesLines(series) {
+            function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    prevx = null, prevy = null;
+                
+                ctx.beginPath();
+                for (var i = ps; i < points.length; i += ps) {
+                    var x1 = points[i - ps], y1 = points[i - ps + 1],
+                        x2 = points[i], y2 = points[i + 1];
+                    
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min) {
+                        if (y2 < axisy.min)
+                            continue;   // line segment is outside
+                        // compute new intersection point
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min) {
+                        if (y1 < axisy.min)
+                            continue;
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max) {
+                        if (y2 > axisy.max)
+                            continue;
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max) {
+                        if (y1 > axisy.max)
+                            continue;
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (x1 != prevx || y1 != prevy)
+                        ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
+                    
+                    prevx = x2;
+                    prevy = y2;
+                    ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
+                }
+                ctx.stroke();
+            }
+
+            function plotLineArea(datapoints, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    bottom = Math.min(Math.max(0, axisy.min), axisy.max),
+                    top, lastX = 0, areaOpen = false;
+                
+                for (var i = ps; i < points.length; i += ps) {
+                    var x1 = points[i - ps], y1 = points[i - ps + 1],
+                        x2 = points[i], y2 = points[i + 1];
+                    
+                    if (areaOpen && x1 != null && x2 == null) {
+                        // close area
+                        ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
+                        ctx.fill();
+                        areaOpen = false;
+                        continue;
+                    }
+
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip x values
+                    
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (!areaOpen) {
+                        // open area
+                        ctx.beginPath();
+                        ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
+                        areaOpen = true;
+                    }
+                    
+                    // now first check the case where both is outside
+                    if (y1 >= axisy.max && y2 >= axisy.max) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
+                        lastX = x2;
+                        continue;
+                    }
+                    else if (y1 <= axisy.min && y2 <= axisy.min) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
+                        lastX = x2;
+                        continue;
+                    }
+                    
+                    // else it's a bit more complicated, there might
+                    // be two rectangles and two triangles we need to fill
+                    // in; to find these keep track of the current x values
+                    var x1old = x1, x2old = x2;
+
+                    // and clip the y values, without shortcutting
+                    
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+
+                    // if the x value was changed we got a rectangle
+                    // to fill
+                    if (x1 != x1old) {
+                        if (y1 <= axisy.min)
+                            top = axisy.min;
+                        else
+                            top = axisy.max;
+                        
+                        ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top));
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(top));
+                    }
+                    
+                    // fill the triangles
+                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
+                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+
+                    // fill the other rectangle if it's there
+                    if (x2 != x2old) {
+                        if (y2 <= axisy.min)
+                            top = axisy.min;
+                        else
+                            top = axisy.max;
+                        
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(top));
+                        ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top));
+                    }
+
+                    lastX = Math.max(x2, x2old);
+                }
+
+                if (areaOpen) {
+                    ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
+                    ctx.fill();
+                }
+            }
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+            ctx.lineJoin = "round";
+
+            var lw = series.lines.lineWidth,
+                sw = series.shadowSize;
+            // FIXME: consider another form of shadow when filling is turned on
+            if (lw > 0 && sw > 0) {
+                // draw shadow as a thick and thin line with transparency
+                ctx.lineWidth = sw;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                // position shadow at angle from the mid of line
+                var angle = Math.PI/18;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
+                ctx.lineWidth = sw/2;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
+            if (fillStyle) {
+                ctx.fillStyle = fillStyle;
+                plotLineArea(series.datapoints, series.xaxis, series.yaxis);
+            }
+
+            if (lw > 0)
+                plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function drawSeriesPoints(series) {
+            function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+                
+                for (var i = 0; i < points.length; i += ps) {
+                    var x = points[i], y = points[i + 1];
+                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                        continue;
+                    
+                    ctx.beginPath();
+                    ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false);
+                    if (fillStyle) {
+                        ctx.fillStyle = fillStyle;
+                        ctx.fill();
+                    }
+                    ctx.stroke();
+                }
+            }
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            var lw = series.lines.lineWidth,
+                sw = series.shadowSize,
+                radius = series.points.radius;
+            if (lw > 0 && sw > 0) {
+                // draw shadow in two steps
+                var w = sw / 2;
+                ctx.lineWidth = w;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                plotPoints(series.datapoints, radius, null, w + w/2, Math.PI,
+                           series.xaxis, series.yaxis);
+
+                ctx.strokeStyle = "rgba(0,0,0,0.2)";
+                plotPoints(series.datapoints, radius, null, w/2, Math.PI,
+                           series.xaxis, series.yaxis);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            plotPoints(series.datapoints, radius,
+                       getFillStyle(series.points, series.color), 0, 2 * Math.PI,
+                       series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) {
+            var left, right, bottom, top,
+                drawLeft, drawRight, drawTop, drawBottom,
+                tmp;
+
+            if (horizontal) {
+                drawBottom = drawRight = drawTop = true;
+                drawLeft = false;
+                left = b;
+                right = x;
+                top = y + barLeft;
+                bottom = y + barRight;
+
+                // account for negative bars
+                if (right < left) {
+                    tmp = right;
+                    right = left;
+                    left = tmp;
+                    drawLeft = true;
+                    drawRight = false;
+                }
+            }
+            else {
+                drawLeft = drawRight = drawTop = true;
+                drawBottom = false;
+                left = x + barLeft;
+                right = x + barRight;
+                bottom = b;
+                top = y;
+
+                // account for negative bars
+                if (top < bottom) {
+                    tmp = top;
+                    top = bottom;
+                    bottom = tmp;
+                    drawBottom = true;
+                    drawTop = false;
+                }
+            }
+           
+            // clip
+            if (right < axisx.min || left > axisx.max ||
+                top < axisy.min || bottom > axisy.max)
+                return;
+            
+            if (left < axisx.min) {
+                left = axisx.min;
+                drawLeft = false;
+            }
+
+            if (right > axisx.max) {
+                right = axisx.max;
+                drawRight = false;
+            }
+
+            if (bottom < axisy.min) {
+                bottom = axisy.min;
+                drawBottom = false;
+            }
+            
+            if (top > axisy.max) {
+                top = axisy.max;
+                drawTop = false;
+            }
+
+            left = axisx.p2c(left);
+            bottom = axisy.p2c(bottom);
+            right = axisx.p2c(right);
+            top = axisy.p2c(top);
+            
+            // fill the bar
+            if (fillStyleCallback) {
+                c.beginPath();
+                c.moveTo(left, bottom);
+                c.lineTo(left, top);
+                c.lineTo(right, top);
+                c.lineTo(right, bottom);
+                c.fillStyle = fillStyleCallback(bottom, top);
+                c.fill();
+            }
+
+            // draw outline
+            if (drawLeft || drawRight || drawTop || drawBottom) {
+                c.beginPath();
+
+                // FIXME: inline moveTo is buggy with excanvas
+                c.moveTo(left, bottom + offset);
+                if (drawLeft)
+                    c.lineTo(left, top + offset);
+                else
+                    c.moveTo(left, top + offset);
+                if (drawTop)
+                    c.lineTo(right, top + offset);
+                else
+                    c.moveTo(right, top + offset);
+                if (drawRight)
+                    c.lineTo(right, bottom + offset);
+                else
+                    c.moveTo(right, bottom + offset);
+                if (drawBottom)
+                    c.lineTo(left, bottom + offset);
+                else
+                    c.moveTo(left, bottom + offset);
+                c.stroke();
+            }
+        }
+        
+        function drawSeriesBars(series) {
+            function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+                
+                for (var i = 0; i < points.length; i += ps) {
+                    if (points[i] == null)
+                        continue;
+                    drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal);
+                }
+            }
+
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // FIXME: figure out a way to add shadows (for instance along the right edge)
+            ctx.lineWidth = series.bars.lineWidth;
+            ctx.strokeStyle = series.color;
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
+            plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function getFillStyle(filloptions, seriesColor, bottom, top) {
+            var fill = filloptions.fill;
+            if (!fill)
+                return null;
+
+            if (filloptions.fillColor)
+                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
+            
+            var c = $.color.parse(seriesColor);
+            c.a = typeof fill == "number" ? fill : 0.4;
+            c.normalize();
+            return c.toString();
+        }
+        
+        function insertLegend() {
+            placeholder.find(".legend").remove();
+
+            if (!options.legend.show)
+                return;
+            
+            var fragments = [], rowStarted = false,
+                lf = options.legend.labelFormatter, s, label;
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                label = s.label;
+                if (!label)
+                    continue;
+                
+                if (i % options.legend.noColumns == 0) {
+                    if (rowStarted)
+                        fragments.push('</tr>');
+                    fragments.push('<tr>');
+                    rowStarted = true;
+                }
+
+                if (lf)
+                    label = lf(label, s);
+                
+                fragments.push(
+                    '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
+                    '<td class="legendLabel">' + label + '</td>');
+            }
+            if (rowStarted)
+                fragments.push('</tr>');
+            
+            if (fragments.length == 0)
+                return;
+
+            var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
+            if (options.legend.container != null)
+                $(options.legend.container).html(table);
+            else {
+                var pos = "",
+                    p = options.legend.position,
+                    m = options.legend.margin;
+                if (m[0] == null)
+                    m = [m, m];
+                if (p.charAt(0) == "n")
+                    pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
+                else if (p.charAt(0) == "s")
+                    pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
+                if (p.charAt(1) == "e")
+                    pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
+                else if (p.charAt(1) == "w")
+                    pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
+                var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
+                if (options.legend.backgroundOpacity != 0.0) {
+                    // put in the transparent background
+                    // separately to avoid blended labels and
+                    // label boxes
+                    var c = options.legend.backgroundColor;
+                    if (c == null) {
+                        c = options.grid.backgroundColor;
+                        if (c && typeof c == "string")
+                            c = $.color.parse(c);
+                        else
+                            c = $.color.extract(legend, 'background-color');
+                        c.a = 1;
+                        c = c.toString();
+                    }
+                    var div = legend.children();
+                    $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
+                }
+            }
+        }
+
+
+        // interactive features
+        
+        var highlights = [],
+            redrawTimeout = null;
+        
+        // returns the data item the mouse is over, or null if none is found
+        function findNearbyItem(mouseX, mouseY, seriesFilter) {
+            var maxDistance = options.grid.mouseActiveRadius,
+                smallestDistance = maxDistance * maxDistance + 1,
+                item = null, foundPoint = false, i, j;
+
+            for (i = 0; i < series.length; ++i) {
+                if (!seriesFilter(series[i]))
+                    continue;
+                
+                var s = series[i],
+                    axisx = s.xaxis,
+                    axisy = s.yaxis,
+                    points = s.datapoints.points,
+                    ps = s.datapoints.pointsize,
+                    mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
+                    my = axisy.c2p(mouseY),
+                    maxx = maxDistance / axisx.scale,
+                    maxy = maxDistance / axisy.scale;
+
+                if (s.lines.show || s.points.show) {
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1];
+                        if (x == null)
+                            continue;
+                        
+                        // For points and lines, the cursor must be within a
+                        // certain distance to the data point
+                        if (x - mx > maxx || x - mx < -maxx ||
+                            y - my > maxy || y - my < -maxy)
+                            continue;
+
+                        // We have to calculate distances in pixels, not in
+                        // data units, because the scales of the axes may be different
+                        var dx = Math.abs(axisx.p2c(x) - mouseX),
+                            dy = Math.abs(axisy.p2c(y) - mouseY),
+                            dist = dx * dx + dy * dy; // we save the sqrt
+
+                        // use <= to ensure last point takes precedence
+                        // (last generally means on top of)
+                        if (dist <= smallestDistance) {
+                            smallestDistance = dist;
+                            item = [i, j / ps];
+                        }
+                    }
+                }
+                    
+                if (s.bars.show && !item) { // no other point can be nearby
+                    var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
+                        barRight = barLeft + s.bars.barWidth;
+                    
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1], b = points[j + 2];
+                        if (x == null)
+                            continue;
+  
+                        // for a bar graph, the cursor must be inside the bar
+                        if (series[i].bars.horizontal ? 
+                            (mx <= Math.max(b, x) && mx >= Math.min(b, x) && 
+                             my >= y + barLeft && my <= y + barRight) :
+                            (mx >= x + barLeft && mx <= x + barRight &&
+                             my >= Math.min(b, y) && my <= Math.max(b, y)))
+                                item = [i, j / ps];
+                    }
+                }
+            }
+
+            if (item) {
+                i = item[0];
+                j = item[1];
+                ps = series[i].datapoints.pointsize;
+                
+                return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
+                         dataIndex: j,
+                         series: series[i],
+                         seriesIndex: i };
+            }
+            
+            return null;
+        }
+
+        function onMouseMove(e) {
+            if (options.grid.hoverable)
+                triggerClickHoverEvent("plothover", e,
+                                       function (s) { return s["hoverable"] != false; });
+        }
+        
+        function onClick(e) {
+            triggerClickHoverEvent("plotclick", e,
+                                   function (s) { return s["clickable"] != false; });
+        }
+
+        // trigger click or hover event (they send the same parameters
+        // so we share their code)
+        function triggerClickHoverEvent(eventname, event, seriesFilter) {
+            var offset = eventHolder.offset(),
+                pos = { pageX: event.pageX, pageY: event.pageY },
+                canvasX = event.pageX - offset.left - plotOffset.left,
+                canvasY = event.pageY - offset.top - plotOffset.top;
+
+            if (axes.xaxis.used)
+                pos.x = axes.xaxis.c2p(canvasX);
+            if (axes.yaxis.used)
+                pos.y = axes.yaxis.c2p(canvasY);
+            if (axes.x2axis.used)
+                pos.x2 = axes.x2axis.c2p(canvasX);
+            if (axes.y2axis.used)
+                pos.y2 = axes.y2axis.c2p(canvasY);
+
+            var item = findNearbyItem(canvasX, canvasY, seriesFilter);
+
+            if (item) {
+                // fill in mouse pos for any listeners out there
+                item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
+                item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
+            }
+
+            if (options.grid.autoHighlight) {
+                // clear auto-highlights
+                for (var i = 0; i < highlights.length; ++i) {
+                    var h = highlights[i];
+                    if (h.auto == eventname &&
+                        !(item && h.series == item.series && h.point == item.datapoint))
+                        unhighlight(h.series, h.point);
+                }
+                
+                if (item)
+                    highlight(item.series, item.datapoint, eventname);
+            }
+            
+            placeholder.trigger(eventname, [ pos, item ]);
+        }
+
+        function triggerRedrawOverlay() {
+            if (!redrawTimeout)
+                redrawTimeout = setTimeout(drawOverlay, 30);
+        }
+
+        function drawOverlay() {
+            redrawTimeout = null;
+
+            // draw highlights
+            octx.save();
+            octx.clearRect(0, 0, canvasWidth, canvasHeight);
+            octx.translate(plotOffset.left, plotOffset.top);
+            
+            var i, hi;
+            for (i = 0; i < highlights.length; ++i) {
+                hi = highlights[i];
+
+                if (hi.series.bars.show)
+                    drawBarHighlight(hi.series, hi.point);
+                else
+                    drawPointHighlight(hi.series, hi.point);
+            }
+            octx.restore();
+            
+            executeHooks(hooks.drawOverlay, [octx]);
+        }
+        
+        function highlight(s, point, auto) {
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number")
+                point = s.data[point];
+
+            var i = indexOfHighlight(s, point);
+            if (i == -1) {
+                highlights.push({ series: s, point: point, auto: auto });
+
+                triggerRedrawOverlay();
+            }
+            else if (!auto)
+                highlights[i].auto = false;
+        }
+            
+        function unhighlight(s, point) {
+            if (s == null && point == null) {
+                highlights = [];
+                triggerRedrawOverlay();
+            }
+            
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number")
+                point = s.data[point];
+
+            var i = indexOfHighlight(s, point);
+            if (i != -1) {
+                highlights.splice(i, 1);
+
+                triggerRedrawOverlay();
+            }
+        }
+        
+        function indexOfHighlight(s, p) {
+            for (var i = 0; i < highlights.length; ++i) {
+                var h = highlights[i];
+                if (h.series == s && h.point[0] == p[0]
+                    && h.point[1] == p[1])
+                    return i;
+            }
+            return -1;
+        }
+        
+        function drawPointHighlight(series, point) {
+            var x = point[0], y = point[1],
+                axisx = series.xaxis, axisy = series.yaxis;
+            
+            if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                return;
+            
+            var pointRadius = series.points.radius + series.points.lineWidth / 2;
+            octx.lineWidth = pointRadius;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var radius = 1.5 * pointRadius;
+            octx.beginPath();
+            octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false);
+            octx.stroke();
+        }
+
+        function drawBarHighlight(series, point) {
+            octx.lineWidth = series.bars.lineWidth;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
+                    0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal);
+        }
+
+        function getColorOrGradient(spec, bottom, top, defaultColor) {
+            if (typeof spec == "string")
+                return spec;
+            else {
+                // assume this is a gradient spec; IE currently only
+                // supports a simple vertical gradient properly, so that's
+                // what we support too
+                var gradient = ctx.createLinearGradient(0, top, 0, bottom);
+                
+                for (var i = 0, l = spec.colors.length; i < l; ++i) {
+                    var c = spec.colors[i];
+                    if (typeof c != "string") {
+                        c = $.color.parse(defaultColor).scale('rgb', c.brightness);
+                        c.a *= c.opacity;
+                        c = c.toString();
+                    }
+                    gradient.addColorStop(i / (l - 1), c);
+                }
+                
+                return gradient;
+            }
+        }
+    }
+
+    $.plot = function(placeholder, data, options) {
+        var plot = new Plot($(placeholder), data, options, $.plot.plugins);
+        /*var t0 = new Date();
+        var t1 = new Date();
+        var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime())
+        if (window.console)
+            console.log(tstr);
+        else
+            alert(tstr);*/
+        return plot;
+    };
+
+    $.plot.plugins = [];
+
+    // returns a string with the date d formatted according to fmt
+    $.plot.formatDate = function(d, fmt, monthNames) {
+        var leftPad = function(n) {
+            n = "" + n;
+            return n.length == 1 ? "0" + n : n;
+        };
+        
+        var r = [];
+        var escape = false;
+        var hours = d.getUTCHours();
+        var isAM = hours < 12;
+        if (monthNames == null)
+            monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+        if (fmt.search(/%p|%P/) != -1) {
+            if (hours > 12) {
+                hours = hours - 12;
+            } else if (hours == 0) {
+                hours = 12;
+            }
+        }
+        for (var i = 0; i < fmt.length; ++i) {
+            var c = fmt.charAt(i);
+            
+            if (escape) {
+                switch (c) {
+                case 'h': c = "" + hours; break;
+                case 'H': c = leftPad(hours); break;
+                case 'M': c = leftPad(d.getUTCMinutes()); break;
+                case 'S': c = leftPad(d.getUTCSeconds()); break;
+                case 'd': c = "" + d.getUTCDate(); break;
+                case 'm': c = "" + (d.getUTCMonth() + 1); break;
+                case 'y': c = "" + d.getUTCFullYear(); break;
+                case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
+                case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
+                case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
+                }
+                r.push(c);
+                escape = false;
+            }
+            else {
+                if (c == "%")
+                    escape = true;
+                else
+                    r.push(c);
+            }
+        }
+        return r.join("");
+    };
+    
+    // round to nearby lower multiple of base
+    function floorInBase(n, base) {
+        return base * Math.floor(n / base);
+    }
+    
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.min.js
@@ -0,0 +1,1 @@
+(function(){jQuery.color={};jQuery.color.make=function(G,H,J,I){var A={};A.r=G||0;A.g=H||0;A.b=J||0;A.a=I!=null?I:1;A.add=function(C,D){for(var E=0;E<C.length;++E){A[C.charAt(E)]+=D}return A.normalize()};A.scale=function(C,D){for(var E=0;E<C.length;++E){A[C.charAt(E)]*=D}return A.normalize()};A.toString=function(){if(A.a>=1){return"rgb("+[A.r,A.g,A.b].join(",")+")"}else{return"rgba("+[A.r,A.g,A.b,A.a].join(",")+")"}};A.normalize=function(){function C(E,D,F){return D<E?E:(D>F?F:D)}A.r=C(0,parseInt(A.r),255);A.g=C(0,parseInt(A.g),255);A.b=C(0,parseInt(A.b),255);A.a=C(0,A.a,1);return A};A.clone=function(){return jQuery.color.make(A.r,A.b,A.g,A.a)};return A.normalize()};jQuery.color.extract=function(E,F){var A;do{A=E.css(F).toLowerCase();if(A!=""&&A!="transparent"){break}E=E.parent()}while(!jQuery.nodeName(E.get(0),"body"));if(A=="rgba(0, 0, 0, 0)"){A="transparent"}return jQuery.color.parse(A)};jQuery.color.parse=function(A){var F,H=jQuery.color.make;if(F=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(A)){return H(parseInt(F[1],10),parseInt(F[2],10),parseInt(F[3],10))}if(F=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(A)){return H(parseInt(F[1],10),parseInt(F[2],10),parseInt(F[3],10),parseFloat(F[4]))}if(F=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(A)){return H(parseFloat(F[1])*2.55,parseFloat(F[2])*2.55,parseFloat(F[3])*2.55)}if(F=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(A)){return H(parseFloat(F[1])*2.55,parseFloat(F[2])*2.55,parseFloat(F[3])*2.55,parseFloat(F[4]))}if(F=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(A)){return H(parseInt(F[1],16),parseInt(F[2],16),parseInt(F[3],16))}if(F=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(A)){return H(parseInt(F[1]+F[1],16),parseInt(F[2]+F[2],16),parseInt(F[3]+F[3],16))}var G=jQuery.trim(A).toLowerCase();if(G=="transparent"){return H(255,255,255,0)}else{F=B[G];return H(F[0],F[1],F[2])}};var B={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();(function(C){function B(l,W,X,E){var O=[],g={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{mode:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02},x2axis:{autoscaleMargin:null},y2axis:{autoscaleMargin:0.02},series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,tickColor:"rgba(0,0,0,0.15)",labelMargin:5,borderWidth:2,borderColor:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},P=null,AC=null,AD=null,Y=null,AJ=null,s={xaxis:{},yaxis:{},x2axis:{},y2axis:{}},e={left:0,right:0,top:0,bottom:0},y=0,Q=0,I=0,t=0,L={processOptions:[],processRawData:[],processDatapoints:[],draw:[],bindEvents:[],drawOverlay:[]},G=this;G.setData=f;G.setupGrid=k;G.draw=AH;G.getPlaceholder=function(){return l};G.getCanvas=function(){return P};G.getPlotOffset=function(){return e};G.width=function(){return I};G.height=function(){return t};G.offset=function(){var AK=AD.offset();AK.left+=e.left;AK.top+=e.top;return AK};G.getData=function(){return O};G.getAxes=function(){return s};G.getOptions=function(){return g};G.highlight=AE;G.unhighlight=x;G.triggerRedrawOverlay=q;G.pointOffset=function(AK){return{left:parseInt(T(AK,"xaxis").p2c(+AK.x)+e.left),top:parseInt(T(AK,"yaxis").p2c(+AK.y)+e.top)}};G.hooks=L;b(G);r(X);c();f(W);k();AH();AG();function Z(AM,AK){AK=[G].concat(AK);for(var AL=0;AL<AM.length;++AL){AM[AL].apply(this,AK)}}function b(){for(var AK=0;AK<E.length;++AK){var AL=E[AK];AL.init(G);if(AL.options){C.extend(true,g,AL.options)}}}function r(AK){C.extend(true,g,AK);if(g.grid.borderColor==null){g.grid.borderColor=g.grid.color}if(g.xaxis.noTicks&&g.xaxis.ticks==null){g.xaxis.ticks=g.xaxis.noTicks}if(g.yaxis.noTicks&&g.yaxis.ticks==null){g.yaxis.ticks=g.yaxis.noTicks}if(g.grid.coloredAreas){g.grid.markings=g.grid.coloredAreas}if(g.grid.coloredAreasColor){g.grid.markingsColor=g.grid.coloredAreasColor}if(g.lines){C.extend(true,g.series.lines,g.lines)}if(g.points){C.extend(true,g.series.points,g.points)}if(g.bars){C.extend(true,g.series.bars,g.bars)}if(g.shadowSize){g.series.shadowSize=g.shadowSize}for(var AL in L){if(g.hooks[AL]&&g.hooks[AL].length){L[AL]=L[AL].concat(g.hooks[AL])}}Z(L.processOptions,[g])}function f(AK){O=M(AK);U();m()}function M(AN){var AL=[];for(var AK=0;AK<AN.length;++AK){var AM=C.extend(true,{},g.series);if(AN[AK].data){AM.data=AN[AK].data;delete AN[AK].data;C.extend(true,AM,AN[AK]);AN[AK].data=AM.data}else{AM.data=AN[AK]}AL.push(AM)}return AL}function T(AM,AK){var AL=AM[AK];if(!AL||AL==1){return s[AK]}if(typeof AL=="number"){return s[AK.charAt(0)+AL+AK.slice(1)]}return AL}function U(){var AP;var AV=O.length,AK=[],AN=[];for(AP=0;AP<O.length;++AP){var AS=O[AP].color;if(AS!=null){--AV;if(typeof AS=="number"){AN.push(AS)}else{AK.push(C.color.parse(O[AP].color))}}}for(AP=0;AP<AN.length;++AP){AV=Math.max(AV,AN[AP]+1)}var AL=[],AO=0;AP=0;while(AL.length<AV){var AR;if(g.colors.length==AP){AR=C.color.make(100,100,100)}else{AR=C.color.parse(g.colors[AP])}var AM=AO%2==1?-1:1;AR.scale("rgb",1+AM*Math.ceil(AO/2)*0.2);AL.push(AR);++AP;if(AP>=g.colors.length){AP=0;++AO}}var AQ=0,AW;for(AP=0;AP<O.length;++AP){AW=O[AP];if(AW.color==null){AW.color=AL[AQ].toString();++AQ}else{if(typeof AW.color=="number"){AW.color=AL[AW.color].toString()}}if(AW.lines.show==null){var AU,AT=true;for(AU in AW){if(AW[AU].show){AT=false;break}}if(AT){AW.lines.show=true}}AW.xaxis=T(AW,"xaxis");AW.yaxis=T(AW,"yaxis")}}function m(){var AW=Number.POSITIVE_INFINITY,AQ=Number.NEGATIVE_INFINITY,Ac,Aa,AZ,AV,AL,AR,Ab,AX,AP,AO,AK,Ai,Af,AT;for(AK in s){s[AK].datamin=AW;s[AK].datamax=AQ;s[AK].used=false}function AN(Al,Ak,Aj){if(Ak<Al.datamin){Al.datamin=Ak}if(Aj>Al.datamax){Al.datamax=Aj}}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];AR.datapoints={points:[]};Z(L.processRawData,[AR,AR.data,AR.datapoints])}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];var Ah=AR.data,Ae=AR.datapoints.format;if(!Ae){Ae=[];Ae.push({x:true,number:true,required:true});Ae.push({y:true,number:true,required:true});if(AR.bars.show){Ae.push({y:true,number:true,required:false,defaultValue:0})}AR.datapoints.format=Ae}if(AR.datapoints.pointsize!=null){continue}if(AR.datapoints.pointsize==null){AR.datapoints.pointsize=Ae.length}AX=AR.datapoints.pointsize;Ab=AR.datapoints.points;insertSteps=AR.lines.show&&AR.lines.steps;AR.xaxis.used=AR.yaxis.used=true;for(Aa=AZ=0;Aa<Ah.length;++Aa,AZ+=AX){AT=Ah[Aa];var AM=AT==null;if(!AM){for(AV=0;AV<AX;++AV){Ai=AT[AV];Af=Ae[AV];if(Af){if(Af.number&&Ai!=null){Ai=+Ai;if(isNaN(Ai)){Ai=null}}if(Ai==null){if(Af.required){AM=true}if(Af.defaultValue!=null){Ai=Af.defaultValue}}}Ab[AZ+AV]=Ai}}if(AM){for(AV=0;AV<AX;++AV){Ai=Ab[AZ+AV];if(Ai!=null){Af=Ae[AV];if(Af.x){AN(AR.xaxis,Ai,Ai)}if(Af.y){AN(AR.yaxis,Ai,Ai)}}Ab[AZ+AV]=null}}else{if(insertSteps&&AZ>0&&Ab[AZ-AX]!=null&&Ab[AZ-AX]!=Ab[AZ]&&Ab[AZ-AX+1]!=Ab[AZ+1]){for(AV=0;AV<AX;++AV){Ab[AZ+AX+AV]=Ab[AZ+AV]}Ab[AZ+1]=Ab[AZ-AX+1];AZ+=AX}}}}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];Z(L.processDatapoints,[AR,AR.datapoints])}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];Ab=AR.datapoints.points,AX=AR.datapoints.pointsize;var AS=AW,AY=AW,AU=AQ,Ad=AQ;for(Aa=0;Aa<Ab.length;Aa+=AX){if(Ab[Aa]==null){continue}for(AV=0;AV<AX;++AV){Ai=Ab[Aa+AV];Af=Ae[AV];if(!Af){continue}if(Af.x){if(Ai<AS){AS=Ai}if(Ai>AU){AU=Ai}}if(Af.y){if(Ai<AY){AY=Ai}if(Ai>Ad){Ad=Ai}}}}if(AR.bars.show){var Ag=AR.bars.align=="left"?0:-AR.bars.barWidth/2;if(AR.bars.horizontal){AY+=Ag;Ad+=Ag+AR.bars.barWidth}else{AS+=Ag;AU+=Ag+AR.bars.barWidth}}AN(AR.xaxis,AS,AU);AN(AR.yaxis,AY,Ad)}for(AK in s){if(s[AK].datamin==AW){s[AK].datamin=null}if(s[AK].datamax==AQ){s[AK].datamax=null}}}function c(){function AK(AM,AL){var AN=document.createElement("canvas");AN.width=AM;AN.height=AL;if(C.browser.msie){AN=window.G_vmlCanvasManager.initElement(AN)}return AN}y=l.width();Q=l.height();l.html("");if(l.css("position")=="static"){l.css("position","relative")}if(y<=0||Q<=0){throw"Invalid dimensions for plot, width = "+y+", height = "+Q}if(C.browser.msie){window.G_vmlCanvasManager.init_(document)}P=C(AK(y,Q)).appendTo(l).get(0);Y=P.getContext("2d");AC=C(AK(y,Q)).css({position:"absolute",left:0,top:0}).appendTo(l).get(0);AJ=AC.getContext("2d");AJ.stroke()}function AG(){AD=C([AC,P]);if(g.grid.hoverable){AD.mousemove(D)}if(g.grid.clickable){AD.click(d)}Z(L.bindEvents,[AD])}function k(){function AL(AT,AU){function AP(AV){return AV}var AS,AO,AQ=AU.transform||AP,AR=AU.inverseTransform;if(AT==s.xaxis||AT==s.x2axis){AS=AT.scale=I/(AQ(AT.max)-AQ(AT.min));AO=AQ(AT.min);if(AQ==AP){AT.p2c=function(AV){return(AV-AO)*AS}}else{AT.p2c=function(AV){return(AQ(AV)-AO)*AS}}if(!AR){AT.c2p=function(AV){return AO+AV/AS}}else{AT.c2p=function(AV){return AR(AO+AV/AS)}}}else{AS=AT.scale=t/(AQ(AT.max)-AQ(AT.min));AO=AQ(AT.max);if(AQ==AP){AT.p2c=function(AV){return(AO-AV)*AS}}else{AT.p2c=function(AV){return(AO-AQ(AV))*AS}}if(!AR){AT.c2p=function(AV){return AO-AV/AS}}else{AT.c2p=function(AV){return AR(AO-AV/AS)}}}}function AN(AR,AT){var AQ,AS=[],AP;AR.labelWidth=AT.labelWidth;AR.labelHeight=AT.labelHeight;if(AR==s.xaxis||AR==s.x2axis){if(AR.labelWidth==null){AR.labelWidth=y/(AR.ticks.length>0?AR.ticks.length:1)}if(AR.labelHeight==null){AS=[];for(AQ=0;AQ<AR.ticks.length;++AQ){AP=AR.ticks[AQ].label;if(AP){AS.push('<div class="tickLabel" style="float:left;width:'+AR.labelWidth+'px">'+AP+"</div>")}}if(AS.length>0){var AO=C('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">'+AS.join("")+'<div style="clear:left"></div></div>').appendTo(l);AR.labelHeight=AO.height();AO.remove()}}}else{if(AR.labelWidth==null||AR.labelHeight==null){for(AQ=0;AQ<AR.ticks.length;++AQ){AP=AR.ticks[AQ].label;if(AP){AS.push('<div class="tickLabel">'+AP+"</div>")}}if(AS.length>0){var AO=C('<div style="position:absolute;top:-10000px;font-size:smaller">'+AS.join("")+"</div>").appendTo(l);if(AR.labelWidth==null){AR.labelWidth=AO.width()}if(AR.labelHeight==null){AR.labelHeight=AO.find("div").height()}AO.remove()}}}if(AR.labelWidth==null){AR.labelWidth=0}if(AR.labelHeight==null){AR.labelHeight=0}}function AM(){var AP=g.grid.borderWidth;for(i=0;i<O.length;++i){AP=Math.max(AP,2*(O[i].points.radius+O[i].points.lineWidth/2))}e.left=e.right=e.top=e.bottom=AP;var AO=g.grid.labelMargin+g.grid.borderWidth;if(s.xaxis.labelHeight>0){e.bottom=Math.max(AP,s.xaxis.labelHeight+AO)}if(s.yaxis.labelWidth>0){e.left=Math.max(AP,s.yaxis.labelWidth+AO)}if(s.x2axis.labelHeight>0){e.top=Math.max(AP,s.x2axis.labelHeight+AO)}if(s.y2axis.labelWidth>0){e.right=Math.max(AP,s.y2axis.labelWidth+AO)}I=y-e.left-e.right;t=Q-e.bottom-e.top}var AK;for(AK in s){K(s[AK],g[AK])}if(g.grid.show){for(AK in s){F(s[AK],g[AK]);p(s[AK],g[AK]);AN(s[AK],g[AK])}AM()}else{e.left=e.right=e.top=e.bottom=0;I=y;t=Q}for(AK in s){AL(s[AK],g[AK])}if(g.grid.show){h()}AI()}function K(AN,AQ){var AM=+(AQ.min!=null?AQ.min:AN.datamin),AK=+(AQ.max!=null?AQ.max:AN.datamax),AP=AK-AM;if(AP==0){var AL=AK==0?1:0.01;if(AQ.min==null){AM-=AL}if(AQ.max==null||AQ.min!=null){AK+=AL}}else{var AO=AQ.autoscaleMargin;if(AO!=null){if(AQ.min==null){AM-=AP*AO;if(AM<0&&AN.datamin!=null&&AN.datamin>=0){AM=0}}if(AQ.max==null){AK+=AP*AO;if(AK>0&&AN.datamax!=null&&AN.datamax<=0){AK=0}}}}AN.min=AM;AN.max=AK}function F(AP,AS){var AO;if(typeof AS.ticks=="number"&&AS.ticks>0){AO=AS.ticks}else{if(AP==s.xaxis||AP==s.x2axis){AO=0.3*Math.sqrt(y)}else{AO=0.3*Math.sqrt(Q)}}var AX=(AP.max-AP.min)/AO,AZ,AT,AV,AW,AR,AM,AL;if(AS.mode=="time"){var AU={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var AY=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var AN=0;if(AS.minTickSize!=null){if(typeof AS.tickSize=="number"){AN=AS.tickSize}else{AN=AS.minTickSize[0]*AU[AS.minTickSize[1]]}}for(AR=0;AR<AY.length-1;++AR){if(AX<(AY[AR][0]*AU[AY[AR][1]]+AY[AR+1][0]*AU[AY[AR+1][1]])/2&&AY[AR][0]*AU[AY[AR][1]]>=AN){break}}AZ=AY[AR][0];AV=AY[AR][1];if(AV=="year"){AM=Math.pow(10,Math.floor(Math.log(AX/AU.year)/Math.LN10));AL=(AX/AU.year)/AM;if(AL<1.5){AZ=1}else{if(AL<3){AZ=2}else{if(AL<7.5){AZ=5}else{AZ=10}}}AZ*=AM}if(AS.tickSize){AZ=AS.tickSize[0];AV=AS.tickSize[1]}AT=function(Ac){var Ah=[],Af=Ac.tickSize[0],Ai=Ac.tickSize[1],Ag=new Date(Ac.min);var Ab=Af*AU[Ai];if(Ai=="second"){Ag.setUTCSeconds(A(Ag.getUTCSeconds(),Af))}if(Ai=="minute"){Ag.setUTCMinutes(A(Ag.getUTCMinutes(),Af))}if(Ai=="hour"){Ag.setUTCHours(A(Ag.getUTCHours(),Af))}if(Ai=="month"){Ag.setUTCMonth(A(Ag.getUTCMonth(),Af))}if(Ai=="year"){Ag.setUTCFullYear(A(Ag.getUTCFullYear(),Af))}Ag.setUTCMilliseconds(0);if(Ab>=AU.minute){Ag.setUTCSeconds(0)}if(Ab>=AU.hour){Ag.setUTCMinutes(0)}if(Ab>=AU.day){Ag.setUTCHours(0)}if(Ab>=AU.day*4){Ag.setUTCDate(1)}if(Ab>=AU.year){Ag.setUTCMonth(0)}var Ak=0,Aj=Number.NaN,Ad;do{Ad=Aj;Aj=Ag.getTime();Ah.push({v:Aj,label:Ac.tickFormatter(Aj,Ac)});if(Ai=="month"){if(Af<1){Ag.setUTCDate(1);var Aa=Ag.getTime();Ag.setUTCMonth(Ag.getUTCMonth()+1);var Ae=Ag.getTime();Ag.setTime(Aj+Ak*AU.hour+(Ae-Aa)*Af);Ak=Ag.getUTCHours();Ag.setUTCHours(0)}else{Ag.setUTCMonth(Ag.getUTCMonth()+Af)}}else{if(Ai=="year"){Ag.setUTCFullYear(Ag.getUTCFullYear()+Af)}else{Ag.setTime(Aj+Ab)}}}while(Aj<Ac.max&&Aj!=Ad);return Ah};AW=function(Aa,Ad){var Af=new Date(Aa);if(AS.timeformat!=null){return C.plot.formatDate(Af,AS.timeformat,AS.monthNames)}var Ab=Ad.tickSize[0]*AU[Ad.tickSize[1]];var Ac=Ad.max-Ad.min;var Ae=(AS.twelveHourClock)?" %p":"";if(Ab<AU.minute){fmt="%h:%M:%S"+Ae}else{if(Ab<AU.day){if(Ac<2*AU.day){fmt="%h:%M"+Ae}else{fmt="%b %d %h:%M"+Ae}}else{if(Ab<AU.month){fmt="%b %d"}else{if(Ab<AU.year){if(Ac<AU.year){fmt="%b"}else{fmt="%b %y"}}else{fmt="%y"}}}}return C.plot.formatDate(Af,fmt,AS.monthNames)}}else{var AK=AS.tickDecimals;var AQ=-Math.floor(Math.log(AX)/Math.LN10);if(AK!=null&&AQ>AK){AQ=AK}AM=Math.pow(10,-AQ);AL=AX/AM;if(AL<1.5){AZ=1}else{if(AL<3){AZ=2;if(AL>2.25&&(AK==null||AQ+1<=AK)){AZ=2.5;++AQ}}else{if(AL<7.5){AZ=5}else{AZ=10}}}AZ*=AM;if(AS.minTickSize!=null&&AZ<AS.minTickSize){AZ=AS.minTickSize}if(AS.tickSize!=null){AZ=AS.tickSize}AP.tickDecimals=Math.max(0,(AK!=null)?AK:AQ);AT=function(Ac){var Ae=[];var Af=A(Ac.min,Ac.tickSize),Ab=0,Aa=Number.NaN,Ad;do{Ad=Aa;Aa=Af+Ab*Ac.tickSize;Ae.push({v:Aa,label:Ac.tickFormatter(Aa,Ac)});++Ab}while(Aa<Ac.max&&Aa!=Ad);return Ae};AW=function(Aa,Ab){return Aa.toFixed(Ab.tickDecimals)}}AP.tickSize=AV?[AZ,AV]:AZ;AP.tickGenerator=AT;if(C.isFunction(AS.tickFormatter)){AP.tickFormatter=function(Aa,Ab){return""+AS.tickFormatter(Aa,Ab)}}else{AP.tickFormatter=AW}}function p(AO,AQ){AO.ticks=[];if(!AO.used){return }if(AQ.ticks==null){AO.ticks=AO.tickGenerator(AO)}else{if(typeof AQ.ticks=="number"){if(AQ.ticks>0){AO.ticks=AO.tickGenerator(AO)}}else{if(AQ.ticks){var AP=AQ.ticks;if(C.isFunction(AP)){AP=AP({min:AO.min,max:AO.max})}var AN,AK;for(AN=0;AN<AP.length;++AN){var AL=null;var AM=AP[AN];if(typeof AM=="object"){AK=AM[0];if(AM.length>1){AL=AM[1]}}else{AK=AM}if(AL==null){AL=AO.tickFormatter(AK,AO)}AO.ticks[AN]={v:AK,label:AL}}}}}if(AQ.autoscaleMargin!=null&&AO.ticks.length>0){if(AQ.min==null){AO.min=Math.min(AO.min,AO.ticks[0].v)}if(AQ.max==null&&AO.ticks.length>1){AO.max=Math.max(AO.max,AO.ticks[AO.ticks.length-1].v)}}}function AH(){Y.clearRect(0,0,y,Q);var AL=g.grid;if(AL.show&&!AL.aboveData){S()}for(var AK=0;AK<O.length;++AK){AA(O[AK])}Z(L.draw,[Y]);if(AL.show&&AL.aboveData){S()}}function N(AL,AR){var AO=AR+"axis",AK=AR+"2axis",AN,AQ,AP,AM;if(AL[AO]){AN=s[AO];AQ=AL[AO].from;AP=AL[AO].to}else{if(AL[AK]){AN=s[AK];AQ=AL[AK].from;AP=AL[AK].to}else{AN=s[AO];AQ=AL[AR+"1"];AP=AL[AR+"2"]}}if(AQ!=null&&AP!=null&&AQ>AP){return{from:AP,to:AQ,axis:AN}}return{from:AQ,to:AP,axis:AN}}function S(){var AO;Y.save();Y.translate(e.left,e.top);if(g.grid.backgroundColor){Y.fillStyle=R(g.grid.backgroundColor,t,0,"rgba(255, 255, 255, 0)");Y.fillRect(0,0,I,t)}var AL=g.grid.markings;if(AL){if(C.isFunction(AL)){AL=AL({xmin:s.xaxis.min,xmax:s.xaxis.max,ymin:s.yaxis.min,ymax:s.yaxis.max,xaxis:s.xaxis,yaxis:s.yaxis,x2axis:s.x2axis,y2axis:s.y2axis})}for(AO=0;AO<AL.length;++AO){var AK=AL[AO],AQ=N(AK,"x"),AN=N(AK,"y");if(AQ.from==null){AQ.from=AQ.axis.min}if(AQ.to==null){AQ.to=AQ.axis.max}if(AN.from==null){AN.from=AN.axis.min}if(AN.to==null){AN.to=AN.axis.max}if(AQ.to<AQ.axis.min||AQ.from>AQ.axis.max||AN.to<AN.axis.min||AN.from>AN.axis.max){continue}AQ.from=Math.max(AQ.from,AQ.axis.min);AQ.to=Math.min(AQ.to,AQ.axis.max);AN.from=Math.max(AN.from,AN.axis.min);AN.to=Math.min(AN.to,AN.axis.max);if(AQ.from==AQ.to&&AN.from==AN.to){continue}AQ.from=AQ.axis.p2c(AQ.from);AQ.to=AQ.axis.p2c(AQ.to);AN.from=AN.axis.p2c(AN.from);AN.to=AN.axis.p2c(AN.to);if(AQ.from==AQ.to||AN.from==AN.to){Y.beginPath();Y.strokeStyle=AK.color||g.grid.markingsColor;Y.lineWidth=AK.lineWidth||g.grid.markingsLineWidth;Y.moveTo(AQ.from,AN.from);Y.lineTo(AQ.to,AN.to);Y.stroke()}else{Y.fillStyle=AK.color||g.grid.markingsColor;Y.fillRect(AQ.from,AN.to,AQ.to-AQ.from,AN.from-AN.to)}}}Y.lineWidth=1;Y.strokeStyle=g.grid.tickColor;Y.beginPath();var AM,AP=s.xaxis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=s.xaxis.max){continue}Y.moveTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,0);Y.lineTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,t)}AP=s.yaxis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=AP.max){continue}Y.moveTo(0,Math.floor(AP.p2c(AM))+Y.lineWidth/2);Y.lineTo(I,Math.floor(AP.p2c(AM))+Y.lineWidth/2)}AP=s.x2axis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=AP.max){continue}Y.moveTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,-5);Y.lineTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,5)}AP=s.y2axis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=AP.max){continue}Y.moveTo(I-5,Math.floor(AP.p2c(AM))+Y.lineWidth/2);Y.lineTo(I+5,Math.floor(AP.p2c(AM))+Y.lineWidth/2)}Y.stroke();if(g.grid.borderWidth){var AR=g.grid.borderWidth;Y.lineWidth=AR;Y.strokeStyle=g.grid.borderColor;Y.strokeRect(-AR/2,-AR/2,I+AR,t+AR)}Y.restore()}function h(){l.find(".tickLabels").remove();var AK=['<div class="tickLabels" style="font-size:smaller;color:'+g.grid.color+'">'];function AM(AP,AQ){for(var AO=0;AO<AP.ticks.length;++AO){var AN=AP.ticks[AO];if(!AN.label||AN.v<AP.min||AN.v>AP.max){continue}AK.push(AQ(AN,AP))}}var AL=g.grid.labelMargin+g.grid.borderWidth;AM(s.xaxis,function(AN,AO){return'<div style="position:absolute;top:'+(e.top+t+AL)+"px;left:"+Math.round(e.left+AO.p2c(AN.v)-AO.labelWidth/2)+"px;width:"+AO.labelWidth+'px;text-align:center" class="tickLabel">'+AN.label+"</div>"});AM(s.yaxis,function(AN,AO){return'<div style="position:absolute;top:'+Math.round(e.top+AO.p2c(AN.v)-AO.labelHeight/2)+"px;right:"+(e.right+I+AL)+"px;width:"+AO.labelWidth+'px;text-align:right" class="tickLabel">'+AN.label+"</div>"});AM(s.x2axis,function(AN,AO){return'<div style="position:absolute;bottom:'+(e.bottom+t+AL)+"px;left:"+Math.round(e.left+AO.p2c(AN.v)-AO.labelWidth/2)+"px;width:"+AO.labelWidth+'px;text-align:center" class="tickLabel">'+AN.label+"</div>"});AM(s.y2axis,function(AN,AO){return'<div style="position:absolute;top:'+Math.round(e.top+AO.p2c(AN.v)-AO.labelHeight/2)+"px;left:"+(e.left+I+AL)+"px;width:"+AO.labelWidth+'px;text-align:left" class="tickLabel">'+AN.label+"</div>"});AK.push("</div>");l.append(AK.join(""))}function AA(AK){if(AK.lines.show){a(AK)}if(AK.bars.show){n(AK)}if(AK.points.show){o(AK)}}function a(AN){function AM(AY,AZ,AR,Ad,Ac){var Ae=AY.points,AS=AY.pointsize,AW=null,AV=null;Y.beginPath();for(var AX=AS;AX<Ae.length;AX+=AS){var AU=Ae[AX-AS],Ab=Ae[AX-AS+1],AT=Ae[AX],Aa=Ae[AX+1];if(AU==null||AT==null){continue}if(Ab<=Aa&&Ab<Ac.min){if(Aa<Ac.min){continue}AU=(Ac.min-Ab)/(Aa-Ab)*(AT-AU)+AU;Ab=Ac.min}else{if(Aa<=Ab&&Aa<Ac.min){if(Ab<Ac.min){continue}AT=(Ac.min-Ab)/(Aa-Ab)*(AT-AU)+AU;Aa=Ac.min}}if(Ab>=Aa&&Ab>Ac.max){if(Aa>Ac.max){continue}AU=(Ac.max-Ab)/(Aa-Ab)*(AT-AU)+AU;Ab=Ac.max}else{if(Aa>=Ab&&Aa>Ac.max){if(Ab>Ac.max){continue}AT=(Ac.max-Ab)/(Aa-Ab)*(AT-AU)+AU;Aa=Ac.max}}if(AU<=AT&&AU<Ad.min){if(AT<Ad.min){continue}Ab=(Ad.min-AU)/(AT-AU)*(Aa-Ab)+Ab;AU=Ad.min}else{if(AT<=AU&&AT<Ad.min){if(AU<Ad.min){continue}Aa=(Ad.min-AU)/(AT-AU)*(Aa-Ab)+Ab;AT=Ad.min}}if(AU>=AT&&AU>Ad.max){if(AT>Ad.max){continue}Ab=(Ad.max-AU)/(AT-AU)*(Aa-Ab)+Ab;AU=Ad.max}else{if(AT>=AU&&AT>Ad.max){if(AU>Ad.max){continue}Aa=(Ad.max-AU)/(AT-AU)*(Aa-Ab)+Ab;AT=Ad.max}}if(AU!=AW||Ab!=AV){Y.moveTo(Ad.p2c(AU)+AZ,Ac.p2c(Ab)+AR)}AW=AT;AV=Aa;Y.lineTo(Ad.p2c(AT)+AZ,Ac.p2c(Aa)+AR)}Y.stroke()}function AO(AX,Ae,Ac){var Af=AX.points,AR=AX.pointsize,AS=Math.min(Math.max(0,Ac.min),Ac.max),Aa,AV=0,Ad=false;for(var AW=AR;AW<Af.length;AW+=AR){var AU=Af[AW-AR],Ab=Af[AW-AR+1],AT=Af[AW],AZ=Af[AW+1];if(Ad&&AU!=null&&AT==null){Y.lineTo(Ae.p2c(AV),Ac.p2c(AS));Y.fill();Ad=false;continue}if(AU==null||AT==null){continue}if(AU<=AT&&AU<Ae.min){if(AT<Ae.min){continue}Ab=(Ae.min-AU)/(AT-AU)*(AZ-Ab)+Ab;AU=Ae.min}else{if(AT<=AU&&AT<Ae.min){if(AU<Ae.min){continue}AZ=(Ae.min-AU)/(AT-AU)*(AZ-Ab)+Ab;AT=Ae.min}}if(AU>=AT&&AU>Ae.max){if(AT>Ae.max){continue}Ab=(Ae.max-AU)/(AT-AU)*(AZ-Ab)+Ab;AU=Ae.max}else{if(AT>=AU&&AT>Ae.max){if(AU>Ae.max){continue}AZ=(Ae.max-AU)/(AT-AU)*(AZ-Ab)+Ab;AT=Ae.max}}if(!Ad){Y.beginPath();Y.moveTo(Ae.p2c(AU),Ac.p2c(AS));Ad=true}if(Ab>=Ac.max&&AZ>=Ac.max){Y.lineTo(Ae.p2c(AU),Ac.p2c(Ac.max));Y.lineTo(Ae.p2c(AT),Ac.p2c(Ac.max));AV=AT;continue}else{if(Ab<=Ac.min&&AZ<=Ac.min){Y.lineTo(Ae.p2c(AU),Ac.p2c(Ac.min));Y.lineTo(Ae.p2c(AT),Ac.p2c(Ac.min));AV=AT;continue}}var Ag=AU,AY=AT;if(Ab<=AZ&&Ab<Ac.min&&AZ>=Ac.min){AU=(Ac.min-Ab)/(AZ-Ab)*(AT-AU)+AU;Ab=Ac.min}else{if(AZ<=Ab&&AZ<Ac.min&&Ab>=Ac.min){AT=(Ac.min-Ab)/(AZ-Ab)*(AT-AU)+AU;AZ=Ac.min}}if(Ab>=AZ&&Ab>Ac.max&&AZ<=Ac.max){AU=(Ac.max-Ab)/(AZ-Ab)*(AT-AU)+AU;Ab=Ac.max}else{if(AZ>=Ab&&AZ>Ac.max&&Ab<=Ac.max){AT=(Ac.max-Ab)/(AZ-Ab)*(AT-AU)+AU;AZ=Ac.max}}if(AU!=Ag){if(Ab<=Ac.min){Aa=Ac.min}else{Aa=Ac.max}Y.lineTo(Ae.p2c(Ag),Ac.p2c(Aa));Y.lineTo(Ae.p2c(AU),Ac.p2c(Aa))}Y.lineTo(Ae.p2c(AU),Ac.p2c(Ab));Y.lineTo(Ae.p2c(AT),Ac.p2c(AZ));if(AT!=AY){if(AZ<=Ac.min){Aa=Ac.min}else{Aa=Ac.max}Y.lineTo(Ae.p2c(AT),Ac.p2c(Aa));Y.lineTo(Ae.p2c(AY),Ac.p2c(Aa))}AV=Math.max(AT,AY)}if(Ad){Y.lineTo(Ae.p2c(AV),Ac.p2c(AS));Y.fill()}}Y.save();Y.translate(e.left,e.top);Y.lineJoin="round";var AP=AN.lines.lineWidth,AK=AN.shadowSize;if(AP>0&&AK>0){Y.lineWidth=AK;Y.strokeStyle="rgba(0,0,0,0.1)";var AQ=Math.PI/18;AM(AN.datapoints,Math.sin(AQ)*(AP/2+AK/2),Math.cos(AQ)*(AP/2+AK/2),AN.xaxis,AN.yaxis);Y.lineWidth=AK/2;AM(AN.datapoints,Math.sin(AQ)*(AP/2+AK/4),Math.cos(AQ)*(AP/2+AK/4),AN.xaxis,AN.yaxis)}Y.lineWidth=AP;Y.strokeStyle=AN.color;var AL=V(AN.lines,AN.color,0,t);if(AL){Y.fillStyle=AL;AO(AN.datapoints,AN.xaxis,AN.yaxis)}if(AP>0){AM(AN.datapoints,0,0,AN.xaxis,AN.yaxis)}Y.restore()}function o(AN){function AP(AU,AT,Ab,AR,AV,AZ,AY){var Aa=AU.points,AQ=AU.pointsize;for(var AS=0;AS<Aa.length;AS+=AQ){var AX=Aa[AS],AW=Aa[AS+1];if(AX==null||AX<AZ.min||AX>AZ.max||AW<AY.min||AW>AY.max){continue}Y.beginPath();Y.arc(AZ.p2c(AX),AY.p2c(AW)+AR,AT,0,AV,false);if(Ab){Y.fillStyle=Ab;Y.fill()}Y.stroke()}}Y.save();Y.translate(e.left,e.top);var AO=AN.lines.lineWidth,AL=AN.shadowSize,AK=AN.points.radius;if(AO>0&&AL>0){var AM=AL/2;Y.lineWidth=AM;Y.strokeStyle="rgba(0,0,0,0.1)";AP(AN.datapoints,AK,null,AM+AM/2,Math.PI,AN.xaxis,AN.yaxis);Y.strokeStyle="rgba(0,0,0,0.2)";AP(AN.datapoints,AK,null,AM/2,Math.PI,AN.xaxis,AN.yaxis)}Y.lineWidth=AO;Y.strokeStyle=AN.color;AP(AN.datapoints,AK,V(AN.points,AN.color),0,2*Math.PI,AN.xaxis,AN.yaxis);Y.restore()}function AB(AV,AU,Ad,AQ,AY,AN,AL,AT,AS,Ac,AZ){var AM,Ab,AR,AX,AO,AK,AW,AP,Aa;if(AZ){AP=AK=AW=true;AO=false;AM=Ad;Ab=AV;AX=AU+AQ;AR=AU+AY;if(Ab<AM){Aa=Ab;Ab=AM;AM=Aa;AO=true;AK=false}}else{AO=AK=AW=true;AP=false;AM=AV+AQ;Ab=AV+AY;AR=Ad;AX=AU;if(AX<AR){Aa=AX;AX=AR;AR=Aa;AP=true;AW=false}}if(Ab<AT.min||AM>AT.max||AX<AS.min||AR>AS.max){return }if(AM<AT.min){AM=AT.min;AO=false}if(Ab>AT.max){Ab=AT.max;AK=false}if(AR<AS.min){AR=AS.min;AP=false}if(AX>AS.max){AX=AS.max;AW=false}AM=AT.p2c(AM);AR=AS.p2c(AR);Ab=AT.p2c(Ab);AX=AS.p2c(AX);if(AL){Ac.beginPath();Ac.moveTo(AM,AR);Ac.lineTo(AM,AX);Ac.lineTo(Ab,AX);Ac.lineTo(Ab,AR);Ac.fillStyle=AL(AR,AX);Ac.fill()}if(AO||AK||AW||AP){Ac.beginPath();Ac.moveTo(AM,AR+AN);if(AO){Ac.lineTo(AM,AX+AN)}else{Ac.moveTo(AM,AX+AN)}if(AW){Ac.lineTo(Ab,AX+AN)}else{Ac.moveTo(Ab,AX+AN)}if(AK){Ac.lineTo(Ab,AR+AN)}else{Ac.moveTo(Ab,AR+AN)}if(AP){Ac.lineTo(AM,AR+AN)}else{Ac.moveTo(AM,AR+AN)}Ac.stroke()}}function n(AM){function AL(AS,AR,AU,AP,AT,AW,AV){var AX=AS.points,AO=AS.pointsize;for(var AQ=0;AQ<AX.length;AQ+=AO){if(AX[AQ]==null){continue}AB(AX[AQ],AX[AQ+1],AX[AQ+2],AR,AU,AP,AT,AW,AV,Y,AM.bars.horizontal)}}Y.save();Y.translate(e.left,e.top);Y.lineWidth=AM.bars.lineWidth;Y.strokeStyle=AM.color;var AK=AM.bars.align=="left"?0:-AM.bars.barWidth/2;var AN=AM.bars.fill?function(AO,AP){return V(AM.bars,AM.color,AO,AP)}:null;AL(AM.datapoints,AK,AK+AM.bars.barWidth,0,AN,AM.xaxis,AM.yaxis);Y.restore()}function V(AM,AK,AL,AO){var AN=AM.fill;if(!AN){return null}if(AM.fillColor){return R(AM.fillColor,AL,AO,AK)}var AP=C.color.parse(AK);AP.a=typeof AN=="number"?AN:0.4;AP.normalize();return AP.toString()}function AI(){l.find(".legend").remove();if(!g.legend.show){return }var AP=[],AN=false,AV=g.legend.labelFormatter,AU,AR;for(i=0;i<O.length;++i){AU=O[i];AR=AU.label;if(!AR){continue}if(i%g.legend.noColumns==0){if(AN){AP.push("</tr>")}AP.push("<tr>");AN=true}if(AV){AR=AV(AR,AU)}AP.push('<td class="legendColorBox"><div style="border:1px solid '+g.legend.labelBoxBorderColor+';padding:1px"><div style="width:4px;height:0;border:5px solid '+AU.color+';overflow:hidden"></div></div></td><td class="legendLabel">'+AR+"</td>")}if(AN){AP.push("</tr>")}if(AP.length==0){return }var AT='<table style="font-size:smaller;color:'+g.grid.color+'">'+AP.join("")+"</table>";if(g.legend.container!=null){C(g.legend.container).html(AT)}else{var AQ="",AL=g.legend.position,AM=g.legend.margin;if(AM[0]==null){AM=[AM,AM]}if(AL.charAt(0)=="n"){AQ+="top:"+(AM[1]+e.top)+"px;"}else{if(AL.charAt(0)=="s"){AQ+="bottom:"+(AM[1]+e.bottom)+"px;"}}if(AL.charAt(1)=="e"){AQ+="right:"+(AM[0]+e.right)+"px;"}else{if(AL.charAt(1)=="w"){AQ+="left:"+(AM[0]+e.left)+"px;"}}var AS=C('<div class="legend">'+AT.replace('style="','style="position:absolute;'+AQ+";")+"</div>").appendTo(l);if(g.legend.backgroundOpacity!=0){var AO=g.legend.backgroundColor;if(AO==null){AO=g.grid.backgroundColor;if(AO&&typeof AO=="string"){AO=C.color.parse(AO)}else{AO=C.color.extract(AS,"background-color")}AO.a=1;AO=AO.toString()}var AK=AS.children();C('<div style="position:absolute;width:'+AK.width()+"px;height:"+AK.height()+"px;"+AQ+"background-color:"+AO+';"> </div>').prependTo(AS).css("opacity",g.legend.backgroundOpacity)}}}var w=[],J=null;function AF(AR,AP,AM){var AX=g.grid.mouseActiveRadius,Aj=AX*AX+1,Ah=null,Aa=false,Af,Ad;for(Af=0;Af<O.length;++Af){if(!AM(O[Af])){continue}var AY=O[Af],AQ=AY.xaxis,AO=AY.yaxis,Ae=AY.datapoints.points,Ac=AY.datapoints.pointsize,AZ=AQ.c2p(AR),AW=AO.c2p(AP),AL=AX/AQ.scale,AK=AX/AO.scale;if(AY.lines.show||AY.points.show){for(Ad=0;Ad<Ae.length;Ad+=Ac){var AT=Ae[Ad],AS=Ae[Ad+1];if(AT==null){continue}if(AT-AZ>AL||AT-AZ<-AL||AS-AW>AK||AS-AW<-AK){continue}var AV=Math.abs(AQ.p2c(AT)-AR),AU=Math.abs(AO.p2c(AS)-AP),Ab=AV*AV+AU*AU;if(Ab<=Aj){Aj=Ab;Ah=[Af,Ad/Ac]}}}if(AY.bars.show&&!Ah){var AN=AY.bars.align=="left"?0:-AY.bars.barWidth/2,Ag=AN+AY.bars.barWidth;for(Ad=0;Ad<Ae.length;Ad+=Ac){var AT=Ae[Ad],AS=Ae[Ad+1],Ai=Ae[Ad+2];if(AT==null){continue}if(O[Af].bars.horizontal?(AZ<=Math.max(Ai,AT)&&AZ>=Math.min(Ai,AT)&&AW>=AS+AN&&AW<=AS+Ag):(AZ>=AT+AN&&AZ<=AT+Ag&&AW>=Math.min(Ai,AS)&&AW<=Math.max(Ai,AS))){Ah=[Af,Ad/Ac]}}}}if(Ah){Af=Ah[0];Ad=Ah[1];Ac=O[Af].datapoints.pointsize;return{datapoint:O[Af].datapoints.points.slice(Ad*Ac,(Ad+1)*Ac),dataIndex:Ad,series:O[Af],seriesIndex:Af}}return null}function D(AK){if(g.grid.hoverable){H("plothover",AK,function(AL){return AL.hoverable!=false})}}function d(AK){H("plotclick",AK,function(AL){return AL.clickable!=false})}function H(AL,AK,AM){var AN=AD.offset(),AS={pageX:AK.pageX,pageY:AK.pageY},AQ=AK.pageX-AN.left-e.left,AO=AK.pageY-AN.top-e.top;if(s.xaxis.used){AS.x=s.xaxis.c2p(AQ)}if(s.yaxis.used){AS.y=s.yaxis.c2p(AO)}if(s.x2axis.used){AS.x2=s.x2axis.c2p(AQ)}if(s.y2axis.used){AS.y2=s.y2axis.c2p(AO)}var AT=AF(AQ,AO,AM);if(AT){AT.pageX=parseInt(AT.series.xaxis.p2c(AT.datapoint[0])+AN.left+e.left);AT.pageY=parseInt(AT.series.yaxis.p2c(AT.datapoint[1])+AN.top+e.top)}if(g.grid.autoHighlight){for(var AP=0;AP<w.length;++AP){var AR=w[AP];if(AR.auto==AL&&!(AT&&AR.series==AT.series&&AR.point==AT.datapoint)){x(AR.series,AR.point)}}if(AT){AE(AT.series,AT.datapoint,AL)}}l.trigger(AL,[AS,AT])}function q(){if(!J){J=setTimeout(v,30)}}function v(){J=null;AJ.save();AJ.clearRect(0,0,y,Q);AJ.translate(e.left,e.top);var AL,AK;for(AL=0;AL<w.length;++AL){AK=w[AL];if(AK.series.bars.show){z(AK.series,AK.point)}else{u(AK.series,AK.point)}}AJ.restore();Z(L.drawOverlay,[AJ])}function AE(AM,AK,AN){if(typeof AM=="number"){AM=O[AM]}if(typeof AK=="number"){AK=AM.data[AK]}var AL=j(AM,AK);if(AL==-1){w.push({series:AM,point:AK,auto:AN});q()}else{if(!AN){w[AL].auto=false}}}function x(AM,AK){if(AM==null&&AK==null){w=[];q()}if(typeof AM=="number"){AM=O[AM]}if(typeof AK=="number"){AK=AM.data[AK]}var AL=j(AM,AK);if(AL!=-1){w.splice(AL,1);q()}}function j(AM,AN){for(var AK=0;AK<w.length;++AK){var AL=w[AK];if(AL.series==AM&&AL.point[0]==AN[0]&&AL.point[1]==AN[1]){return AK}}return -1}function u(AN,AM){var AL=AM[0],AR=AM[1],AQ=AN.xaxis,AP=AN.yaxis;if(AL<AQ.min||AL>AQ.max||AR<AP.min||AR>AP.max){return }var AO=AN.points.radius+AN.points.lineWidth/2;AJ.lineWidth=AO;AJ.strokeStyle=C.color.parse(AN.color).scale("a",0.5).toString();var AK=1.5*AO;AJ.beginPath();AJ.arc(AQ.p2c(AL),AP.p2c(AR),AK,0,2*Math.PI,false);AJ.stroke()}function z(AN,AK){AJ.lineWidth=AN.bars.lineWidth;AJ.strokeStyle=C.color.parse(AN.color).scale("a",0.5).toString();var AM=C.color.parse(AN.color).scale("a",0.5).toString();var AL=AN.bars.align=="left"?0:-AN.bars.barWidth/2;AB(AK[0],AK[1],AK[2]||0,AL,AL+AN.bars.barWidth,0,function(){return AM},AN.xaxis,AN.yaxis,AJ,AN.bars.horizontal)}function R(AM,AL,AQ,AO){if(typeof AM=="string"){return AM}else{var AP=Y.createLinearGradient(0,AQ,0,AL);for(var AN=0,AK=AM.colors.length;AN<AK;++AN){var AR=AM.colors[AN];if(typeof AR!="string"){AR=C.color.parse(AO).scale("rgb",AR.brightness);AR.a*=AR.opacity;AR=AR.toString()}AP.addColorStop(AN/(AK-1),AR)}return AP}}}C.plot=function(G,E,D){var F=new B(C(G),E,D,C.plot.plugins);return F};C.plot.plugins=[];C.plot.formatDate=function(H,E,G){var L=function(N){N=""+N;return N.length==1?"0"+N:N};var D=[];var M=false;var K=H.getUTCHours();var I=K<12;if(G==null){G=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}if(E.search(/%p|%P/)!=-1){if(K>12){K=K-12}else{if(K==0){K=12}}}for(var F=0;F<E.length;++F){var J=E.charAt(F);if(M){switch(J){case"h":J=""+K;break;case"H":J=L(K);break;case"M":J=L(H.getUTCMinutes());break;case"S":J=L(H.getUTCSeconds());break;case"d":J=""+H.getUTCDate();break;case"m":J=""+(H.getUTCMonth()+1);break;case"y":J=""+H.getUTCFullYear();break;case"b":J=""+G[H.getUTCMonth()];break;case"p":J=(I)?("am"):("pm");break;case"P":J=(I)?("AM"):("PM");break}D.push(J);M=false}else{if(J=="%"){M=true}else{D.push(J)}}}return D.join("")};function A(E,D){return D*Math.floor(E/D)}})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.js
@@ -0,0 +1,272 @@
+/*
+Flot plugin for adding panning and zooming capabilities to a plot.
+
+The default behaviour is double click and scrollwheel up/down to zoom
+in, drag to pan. The plugin defines plot.zoom({ center }),
+plot.zoomOut() and plot.pan(offset) so you easily can add custom
+controls. It also fires a "plotpan" and "plotzoom" event when
+something happens, useful for synchronizing plots.
+
+Example usage:
+
+  plot = $.plot(...);
+  
+  // zoom default amount in on the pixel (100, 200) 
+  plot.zoom({ center: { left: 10, top: 20 } });
+
+  // zoom out again
+  plot.zoomOut({ center: { left: 10, top: 20 } });
+
+  // pan 100 pixels to the left and 20 down
+  plot.pan({ left: -100, top: 20 })
+
+
+Options:
+
+  zoom: {
+    interactive: false
+    trigger: "dblclick" // or "click" for single click
+    amount: 1.5         // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
+  }
+  
+  pan: {
+    interactive: false
+  }
+
+  xaxis, yaxis, x2axis, y2axis: {
+    zoomRange: null  // or [number, number] (min range, max range)
+    panRange: null   // or [number, number] (min, max)
+  }
+  
+"interactive" enables the built-in drag/click behaviour. "amount" is
+the amount to zoom the viewport relative to the current range, so 1 is
+100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out).
+
+"zoomRange" is the interval in which zooming can happen, e.g. with
+zoomRange: [1, 100] the zoom will never scale the axis so that the
+difference between min and max is smaller than 1 or larger than 100.
+You can set either of them to null to ignore.
+
+"panRange" confines the panning to stay within a range, e.g. with
+panRange: [-10, 20] panning stops at -10 in one end and at 20 in the
+other. Either can be null.
+*/
+
+
+// First two dependencies, jquery.event.drag.js and
+// jquery.mousewheel.js, we put them inline here to save people the
+// effort of downloading them.
+
+/*
+jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)  
+Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
+*/
+(function(E){E.fn.drag=function(L,K,J){if(K){this.bind("dragstart",L)}if(J){this.bind("dragend",J)}return !L?this.trigger("drag"):this.bind("drag",K?K:L)};var A=E.event,B=A.special,F=B.drag={not:":input",distance:0,which:1,dragging:false,setup:function(J){J=E.extend({distance:F.distance,which:F.which,not:F.not},J||{});J.distance=I(J.distance);A.add(this,"mousedown",H,J);if(this.attachEvent){this.attachEvent("ondragstart",D)}},teardown:function(){A.remove(this,"mousedown",H);if(this===F.dragging){F.dragging=F.proxy=false}G(this,true);if(this.detachEvent){this.detachEvent("ondragstart",D)}}};B.dragstart=B.dragend={setup:function(){},teardown:function(){}};function H(L){var K=this,J,M=L.data||{};if(M.elem){K=L.dragTarget=M.elem;L.dragProxy=F.proxy||K;L.cursorOffsetX=M.pageX-M.left;L.cursorOffsetY=M.pageY-M.top;L.offsetX=L.pageX-L.cursorOffsetX;L.offsetY=L.pageY-L.cursorOffsetY}else{if(F.dragging||(M.which>0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY)<M.distance){break}L.target=M.target;J=C(L,"dragstart",K);if(J!==false){F.dragging=K;F.proxy=L.dragProxy=E(J||K)[0]}case"mousemove":if(F.dragging){J=C(L,"drag",K);if(B.drop){B.drop.allowed=(J!==false);B.drop.handler(L)}if(J!==false){break}L.type="mouseup"}case"mouseup":A.remove(document,"mousemove mouseup",H);if(F.dragging){if(B.drop){B.drop.handler(L)}C(L,"dragend",K)}G(K,true);F.dragging=F.proxy=M.elem=false;break}return true}function C(M,K,L){M.type=K;var J=E.event.handle.call(L,M);return J===false?false:J||M.result}function I(J){return Math.pow(J,2)}function D(){return(F.dragging===false)}function G(K,J){if(!K){return }K.unselectable=J?"off":"on";K.onselectstart=function(){return J};if(K.style){K.style.MozUserSelect=J?"":"none"}}})(jQuery);
+
+
+/* jquery.mousewheel.min.js
+ * Copyright (c) 2009 Brandon Aaron (http://brandonaaron.net)
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
+ * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
+ *
+ * Version: 3.0.2
+ * 
+ * Requires: 1.2.2+
+ */
+(function(c){var a=["DOMMouseScroll","mousewheel"];c.event.special.mousewheel={setup:function(){if(this.addEventListener){for(var d=a.length;d;){this.addEventListener(a[--d],b,false)}}else{this.onmousewheel=b}},teardown:function(){if(this.removeEventListener){for(var d=a.length;d;){this.removeEventListener(a[--d],b,false)}}else{this.onmousewheel=null}}};c.fn.extend({mousewheel:function(d){return d?this.bind("mousewheel",d):this.trigger("mousewheel")},unmousewheel:function(d){return this.unbind("mousewheel",d)}});function b(f){var d=[].slice.call(arguments,1),g=0,e=true;f=c.event.fix(f||window.event);f.type="mousewheel";if(f.wheelDelta){g=f.wheelDelta/120}if(f.detail){g=-f.detail/3}d.unshift(f,g);return c.event.handle.apply(this,d)}})(jQuery);
+
+
+
+
+(function ($) {
+    var options = {
+        xaxis: {
+            zoomRange: null, // or [number, number] (min range, max range)
+            panRange: null // or [number, number] (min, max)
+        },
+        zoom: {
+            interactive: false,
+            trigger: "dblclick", // or "click" for single click
+            amount: 1.5 // how much to zoom relative to current position, 2 = 200% (zoom in), 0.5 = 50% (zoom out)
+        },
+        pan: {
+            interactive: false
+        }
+    };
+
+    function init(plot) {
+        function bindEvents(plot, eventHolder) {
+            var o = plot.getOptions();
+            if (o.zoom.interactive) {
+                function clickHandler(e, zoomOut) {
+                    var c = plot.offset();
+                    c.left = e.pageX - c.left;
+                    c.top = e.pageY - c.top;
+                    if (zoomOut)
+                        plot.zoomOut({ center: c });
+                    else
+                        plot.zoom({ center: c });
+                }
+                
+                eventHolder[o.zoom.trigger](clickHandler);
+
+                eventHolder.mousewheel(function (e, delta) {
+                    clickHandler(e, delta < 0);
+                    return false;
+                });
+            }
+            if (o.pan.interactive) {
+                var prevCursor = 'default', pageX = 0, pageY = 0;
+                
+                eventHolder.bind("dragstart", { distance: 10 }, function (e) {
+                    if (e.which != 1)  // only accept left-click
+                        return false;
+                    eventHolderCursor = eventHolder.css('cursor');
+                    eventHolder.css('cursor', 'move');
+                    pageX = e.pageX;
+                    pageY = e.pageY;
+                });
+                eventHolder.bind("drag", function (e) {
+                    // unused at the moment, but we need it here to
+                    // trigger the dragstart/dragend events
+                });
+                eventHolder.bind("dragend", function (e) {
+                    eventHolder.css('cursor', prevCursor);
+                    plot.pan({ left: pageX - e.pageX,
+                               top: pageY - e.pageY });
+                });
+            }
+        }
+
+        plot.zoomOut = function (args) {
+            if (!args)
+                args = {};
+            
+            if (!args.amount)
+                args.amount = plot.getOptions().zoom.amount
+
+            args.amount = 1 / args.amount;
+            plot.zoom(args);
+        }
+        
+        plot.zoom = function (args) {
+            if (!args)
+                args = {};
+            
+            var axes = plot.getAxes(),
+                options = plot.getOptions(),
+                c = args.center,
+                amount = args.amount ? args.amount : options.zoom.amount,
+                w = plot.width(), h = plot.height();
+
+            if (!c)
+                c = { left: w / 2, top: h / 2 };
+                
+            var xf = c.left / w,
+                x1 = c.left - xf * w / amount,
+                x2 = c.left + (1 - xf) * w / amount,
+                yf = c.top / h,
+                y1 = c.top - yf * h / amount,
+                y2 = c.top + (1 - yf) * h / amount;
+
+            function scaleAxis(min, max, name) {
+                var axis = axes[name],
+                    axisOptions = options[name];
+                
+                if (!axis.used)
+                    return;
+                    
+                min = axis.c2p(min);
+                max = axis.c2p(max);
+                if (max < min) { // make sure min < max
+                    var tmp = min
+                    min = max;
+                    max = tmp;
+                }
+
+                var range = max - min, zr = axisOptions.zoomRange;
+                if (zr &&
+                    ((zr[0] != null && range < zr[0]) ||
+                     (zr[1] != null && range > zr[1])))
+                    return;
+            
+                axisOptions.min = min;
+                axisOptions.max = max;
+            }
+
+            scaleAxis(x1, x2, 'xaxis');
+            scaleAxis(x1, x2, 'x2axis');
+            scaleAxis(y1, y2, 'yaxis');
+            scaleAxis(y1, y2, 'y2axis');
+            
+            plot.setupGrid();
+            plot.draw();
+            
+            if (!args.preventEvent)
+                plot.getPlaceholder().trigger("plotzoom", [ plot ]);
+        }
+
+        plot.pan = function (args) {
+            var l = +args.left, t = +args.top,
+                axes = plot.getAxes(), options = plot.getOptions();
+
+            if (isNaN(l))
+                l = 0;
+            if (isNaN(t))
+                t = 0;
+
+            function panAxis(delta, name) {
+                var axis = axes[name],
+                    axisOptions = options[name],
+                    min, max;
+                
+                if (!axis.used)
+                    return;
+
+                min = axis.c2p(axis.p2c(axis.min) + delta),
+                max = axis.c2p(axis.p2c(axis.max) + delta);
+
+                var pr = axisOptions.panRange;
+                if (pr) {
+                    // check whether we hit the wall
+                    if (pr[0] != null && pr[0] > min) {
+                        delta = pr[0] - min;
+                        min += delta;
+                        max += delta;
+                    }
+                    
+                    if (pr[1] != null && pr[1] < max) {
+                        delta = pr[1] - max;
+                        min += delta;
+                        max += delta;
+                    }
+                }
+                
+                axisOptions.min = min;
+                axisOptions.max = max;
+            }
+
+            panAxis(l, 'xaxis');
+            panAxis(l, 'x2axis');
+            panAxis(t, 'yaxis');
+            panAxis(t, 'y2axis');
+            
+            plot.setupGrid();
+            plot.draw();
+            
+            if (!args.preventEvent)
+                plot.getPlaceholder().trigger("plotpan", [ plot ]);
+        }
+        
+        plot.hooks.bindEvents.push(bindEvents);
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'navigate',
+        version: '1.1'
+    });
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.min.js
@@ -0,0 +1,1 @@
+(function(R){R.fn.drag=function(A,B,C){if(B){this.bind("dragstart",A)}if(C){this.bind("dragend",C)}return !A?this.trigger("drag"):this.bind("drag",B?B:A)};var M=R.event,L=M.special,Q=L.drag={not:":input",distance:0,which:1,dragging:false,setup:function(A){A=R.extend({distance:Q.distance,which:Q.which,not:Q.not},A||{});A.distance=N(A.distance);M.add(this,"mousedown",O,A);if(this.attachEvent){this.attachEvent("ondragstart",J)}},teardown:function(){M.remove(this,"mousedown",O);if(this===Q.dragging){Q.dragging=Q.proxy=false}P(this,true);if(this.detachEvent){this.detachEvent("ondragstart",J)}}};L.dragstart=L.dragend={setup:function(){},teardown:function(){}};function O(A){var B=this,C,D=A.data||{};if(D.elem){B=A.dragTarget=D.elem;A.dragProxy=Q.proxy||B;A.cursorOffsetX=D.pageX-D.left;A.cursorOffsetY=D.pageY-D.top;A.offsetX=A.pageX-A.cursorOffsetX;A.offsetY=A.pageY-A.cursorOffsetY}else{if(Q.dragging||(D.which>0&&A.which!=D.which)||R(A.target).is(D.not)){return }}switch(A.type){case"mousedown":R.extend(D,R(B).offset(),{elem:B,target:A.target,pageX:A.pageX,pageY:A.pageY});M.add(document,"mousemove mouseup",O,D);P(B,false);Q.dragging=null;return false;case !Q.dragging&&"mousemove":if(N(A.pageX-D.pageX)+N(A.pageY-D.pageY)<D.distance){break}A.target=D.target;C=K(A,"dragstart",B);if(C!==false){Q.dragging=B;Q.proxy=A.dragProxy=R(C||B)[0]}case"mousemove":if(Q.dragging){C=K(A,"drag",B);if(L.drop){L.drop.allowed=(C!==false);L.drop.handler(A)}if(C!==false){break}A.type="mouseup"}case"mouseup":M.remove(document,"mousemove mouseup",O);if(Q.dragging){if(L.drop){L.drop.handler(A)}K(A,"dragend",B)}P(B,true);Q.dragging=Q.proxy=D.elem=false;break}return true}function K(D,B,A){D.type=B;var C=R.event.handle.call(A,D);return C===false?false:C||D.result}function N(A){return Math.pow(A,2)}function J(){return(Q.dragging===false)}function P(A,B){if(!A){return }A.unselectable=B?"off":"on";A.onselectstart=function(){return B};if(A.style){A.style.MozUserSelect=B?"":"none"}}})(jQuery);(function(C){var B=["DOMMouseScroll","mousewheel"];C.event.special.mousewheel={setup:function(){if(this.addEventListener){for(var D=B.length;D;){this.addEventListener(B[--D],A,false)}}else{this.onmousewheel=A}},teardown:function(){if(this.removeEventListener){for(var D=B.length;D;){this.removeEventListener(B[--D],A,false)}}else{this.onmousewheel=null}}};C.fn.extend({mousewheel:function(D){return D?this.bind("mousewheel",D):this.trigger("mousewheel")},unmousewheel:function(D){return this.unbind("mousewheel",D)}});function A(E){var G=[].slice.call(arguments,1),D=0,F=true;E=C.event.fix(E||window.event);E.type="mousewheel";if(E.wheelDelta){D=E.wheelDelta/120}if(E.detail){D=-E.detail/3}G.unshift(E,D);return C.event.handle.apply(this,G)}})(jQuery);(function(B){var A={xaxis:{zoomRange:null,panRange:null},zoom:{interactive:false,trigger:"dblclick",amount:1.5},pan:{interactive:false}};function C(D){function E(J,F){var K=J.getOptions();if(K.zoom.interactive){function L(N,M){var O=J.offset();O.left=N.pageX-O.left;O.top=N.pageY-O.top;if(M){J.zoomOut({center:O})}else{J.zoom({center:O})}}F[K.zoom.trigger](L);F.mousewheel(function(M,N){L(M,N<0);return false})}if(K.pan.interactive){var I="default",H=0,G=0;F.bind("dragstart",{distance:10},function(M){if(M.which!=1){return false}eventHolderCursor=F.css("cursor");F.css("cursor","move");H=M.pageX;G=M.pageY});F.bind("drag",function(M){});F.bind("dragend",function(M){F.css("cursor",I);J.pan({left:H-M.pageX,top:G-M.pageY})})}}D.zoomOut=function(F){if(!F){F={}}if(!F.amount){F.amount=D.getOptions().zoom.amount}F.amount=1/F.amount;D.zoom(F)};D.zoom=function(M){if(!M){M={}}var L=D.getAxes(),S=D.getOptions(),N=M.center,J=M.amount?M.amount:S.zoom.amount,R=D.width(),I=D.height();if(!N){N={left:R/2,top:I/2}}var Q=N.left/R,G=N.left-Q*R/J,F=N.left+(1-Q)*R/J,H=N.top/I,P=N.top-H*I/J,O=N.top+(1-H)*I/J;function K(X,T,V){var Y=L[V],a=S[V];if(!Y.used){return }X=Y.c2p(X);T=Y.c2p(T);if(T<X){var W=X;X=T;T=W}var U=T-X,Z=a.zoomRange;if(Z&&((Z[0]!=null&&U<Z[0])||(Z[1]!=null&&U>Z[1]))){return }a.min=X;a.max=T}K(G,F,"xaxis");K(G,F,"x2axis");K(P,O,"yaxis");K(P,O,"y2axis");D.setupGrid();D.draw();if(!M.preventEvent){D.getPlaceholder().trigger("plotzoom",[D])}};D.pan=function(I){var F=+I.left,J=+I.top,K=D.getAxes(),H=D.getOptions();if(isNaN(F)){F=0}if(isNaN(J)){J=0}function G(R,M){var O=K[M],Q=H[M],N,L;if(!O.used){return }N=O.c2p(O.p2c(O.min)+R),L=O.c2p(O.p2c(O.max)+R);var P=Q.panRange;if(P){if(P[0]!=null&&P[0]>N){R=P[0]-N;N+=R;L+=R}if(P[1]!=null&&P[1]<L){R=P[1]-L;N+=R;L+=R}}Q.min=N;Q.max=L}G(F,"xaxis");G(F,"x2axis");G(J,"yaxis");G(J,"y2axis");D.setupGrid();D.draw();if(!I.preventEvent){D.getPlaceholder().trigger("plotpan",[D])}};D.hooks.bindEvents.push(E)}B.plot.plugins.push({init:C,options:A,name:"navigate",version:"1.1"})})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.selection.js
@@ -0,0 +1,299 @@
+/*
+Flot plugin for selecting regions.
+
+The plugin defines the following options:
+
+  selection: {
+    mode: null or "x" or "y" or "xy",
+    color: color
+  }
+
+You enable selection support by setting the mode to one of "x", "y" or
+"xy". In "x" mode, the user will only be able to specify the x range,
+similarly for "y" mode. For "xy", the selection becomes a rectangle
+where both ranges can be specified. "color" is color of the selection.
+
+When selection support is enabled, a "plotselected" event will be emitted
+on the DOM element you passed into the plot function. The event
+handler gets one extra parameter with the ranges selected on the axes,
+like this:
+
+  placeholder.bind("plotselected", function(event, ranges) {
+    alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
+    // similar for yaxis, secondary axes are in x2axis
+    // and y2axis if present
+  });
+
+The "plotselected" event is only fired when the user has finished
+making the selection. A "plotselecting" event is fired during the
+process with the same parameters as the "plotselected" event, in case
+you want to know what's happening while it's happening,
+
+A "plotunselected" event with no arguments is emitted when the user
+clicks the mouse to remove the selection.
+
+The plugin allso adds the following methods to the plot object:
+
+- setSelection(ranges, preventEvent)
+
+  Set the selection rectangle. The passed in ranges is on the same
+  form as returned in the "plotselected" event. If the selection
+  mode is "x", you should put in either an xaxis (or x2axis) object,
+  if the mode is "y" you need to put in an yaxis (or y2axis) object
+  and both xaxis/x2axis and yaxis/y2axis if the selection mode is
+  "xy", like this:
+
+    setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
+
+  setSelection will trigger the "plotselected" event when called. If
+  you don't want that to happen, e.g. if you're inside a
+  "plotselected" handler, pass true as the second parameter.
+  
+- clearSelection(preventEvent)
+
+  Clear the selection rectangle. Pass in true to avoid getting a
+  "plotunselected" event.
+
+- getSelection()
+
+  Returns the current selection in the same format as the
+  "plotselected" event. If there's currently no selection, the
+  function returns null.
+
+*/
+
+(function ($) {
+    function init(plot) {
+        var selection = {
+                first: { x: -1, y: -1}, second: { x: -1, y: -1},
+                show: false,
+                active: false
+            };
+
+        // FIXME: The drag handling implemented here should be
+        // abstracted out, there's some similar code from a library in
+        // the navigation plugin, this should be massaged a bit to fit
+        // the Flot cases here better and reused. Doing this would
+        // make this plugin much slimmer.
+        var savedhandlers = {};
+
+        function onMouseMove(e) {
+            if (selection.active) {
+                plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
+
+                updateSelection(e);
+            }
+        }
+
+        function onMouseDown(e) {
+            if (e.which != 1)  // only accept left-click
+                return;
+            
+            // cancel out any text selections
+            document.body.focus();
+
+            // prevent text selection and drag in old-school browsers
+            if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
+                savedhandlers.onselectstart = document.onselectstart;
+                document.onselectstart = function () { return false; };
+            }
+            if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
+                savedhandlers.ondrag = document.ondrag;
+                document.ondrag = function () { return false; };
+            }
+
+            setSelectionPos(selection.first, e);
+
+            selection.active = true;
+            
+            $(document).one("mouseup", onMouseUp);
+        }
+
+        function onMouseUp(e) {
+            // revert drag stuff for old-school browsers
+            if (document.onselectstart !== undefined)
+                document.onselectstart = savedhandlers.onselectstart;
+            if (document.ondrag !== undefined)
+                document.ondrag = savedhandlers.ondrag;
+
+            // no more draggy-dee-drag
+            selection.active = false;
+            updateSelection(e);
+
+            if (selectionIsSane())
+                triggerSelectedEvent();
+            else {
+                // this counts as a clear
+                plot.getPlaceholder().trigger("plotunselected", [ ]);
+                plot.getPlaceholder().trigger("plotselecting", [ null ]);
+            }
+
+            return false;
+        }
+
+        function getSelection() {
+            if (!selectionIsSane())
+                return null;
+
+            var x1 = Math.min(selection.first.x, selection.second.x),
+                x2 = Math.max(selection.first.x, selection.second.x),
+                y1 = Math.max(selection.first.y, selection.second.y),
+                y2 = Math.min(selection.first.y, selection.second.y);
+
+            var r = {};
+            var axes = plot.getAxes();
+            if (axes.xaxis.used)
+                r.xaxis = { from: axes.xaxis.c2p(x1), to: axes.xaxis.c2p(x2) };
+            if (axes.x2axis.used)
+                r.x2axis = { from: axes.x2axis.c2p(x1), to: axes.x2axis.c2p(x2) };
+            if (axes.yaxis.used)
+                r.yaxis = { from: axes.yaxis.c2p(y1), to: axes.yaxis.c2p(y2) };
+            if (axes.y2axis.used)
+                r.y2axis = { from: axes.y2axis.c2p(y1), to: axes.y2axis.c2p(y2) };
+            return r;
+        }
+
+        function triggerSelectedEvent() {
+            var r = getSelection();
+
+            plot.getPlaceholder().trigger("plotselected", [ r ]);
+
+            // backwards-compat stuff, to be removed in future
+            var axes = plot.getAxes();
+            if (axes.xaxis.used && axes.yaxis.used)
+                plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
+        }
+
+        function clamp(min, value, max) {
+            return value < min? min: (value > max? max: value);
+        }
+
+        function setSelectionPos(pos, e) {
+            var o = plot.getOptions();
+            var offset = plot.getPlaceholder().offset();
+            var plotOffset = plot.getPlotOffset();
+            pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
+            pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
+
+            if (o.selection.mode == "y")
+                pos.x = pos == selection.first? 0: plot.width();
+
+            if (o.selection.mode == "x")
+                pos.y = pos == selection.first? 0: plot.height();
+        }
+
+        function updateSelection(pos) {
+            if (pos.pageX == null)
+                return;
+
+            setSelectionPos(selection.second, pos);
+            if (selectionIsSane()) {
+                selection.show = true;
+                plot.triggerRedrawOverlay();
+            }
+            else
+                clearSelection(true);
+        }
+
+        function clearSelection(preventEvent) {
+            if (selection.show) {
+                selection.show = false;
+                plot.triggerRedrawOverlay();
+                if (!preventEvent)
+                    plot.getPlaceholder().trigger("plotunselected", [ ]);
+            }
+        }
+
+        function setSelection(ranges, preventEvent) {
+            var axis, range, axes = plot.getAxes();
+            var o = plot.getOptions();
+
+            if (o.selection.mode == "y") {
+                selection.first.x = 0;
+                selection.second.x = plot.width();
+            }
+            else {
+                axis = ranges["xaxis"]? axes["xaxis"]: (ranges["x2axis"]? axes["x2axis"]: axes["xaxis"]);
+                range = ranges["xaxis"] || ranges["x2axis"] || { from:ranges["x1"], to:ranges["x2"] }
+                selection.first.x = axis.p2c(Math.min(range.from, range.to));
+                selection.second.x = axis.p2c(Math.max(range.from, range.to));
+            }
+
+            if (o.selection.mode == "x") {
+                selection.first.y = 0;
+                selection.second.y = plot.height();
+            }
+            else {
+                axis = ranges["yaxis"]? axes["yaxis"]: (ranges["y2axis"]? axes["y2axis"]: axes["yaxis"]);
+                range = ranges["yaxis"] || ranges["y2axis"] || { from:ranges["y1"], to:ranges["y2"] }
+                selection.first.y = axis.p2c(Math.min(range.from, range.to));
+                selection.second.y = axis.p2c(Math.max(range.from, range.to));
+            }
+
+            selection.show = true;
+            plot.triggerRedrawOverlay();
+            if (!preventEvent)
+                triggerSelectedEvent();
+        }
+
+        function selectionIsSane() {
+            var minSize = 5;
+            return Math.abs(selection.second.x - selection.first.x) >= minSize &&
+                Math.abs(selection.second.y - selection.first.y) >= minSize;
+        }
+
+        plot.clearSelection = clearSelection;
+        plot.setSelection = setSelection;
+        plot.getSelection = getSelection;
+
+        plot.hooks.bindEvents.push(function(plot, eventHolder) {
+            var o = plot.getOptions();
+            if (o.selection.mode != null)
+                eventHolder.mousemove(onMouseMove);
+
+            if (o.selection.mode != null)
+                eventHolder.mousedown(onMouseDown);
+        });
+
+
+        plot.hooks.drawOverlay.push(function (plot, ctx) {
+            // draw selection
+            if (selection.show && selectionIsSane()) {
+                var plotOffset = plot.getPlotOffset();
+                var o = plot.getOptions();
+
+                ctx.save();
+                ctx.translate(plotOffset.left, plotOffset.top);
+
+                var c = $.color.parse(o.selection.color);
+
+                ctx.strokeStyle = c.scale('a', 0.8).toString();
+                ctx.lineWidth = 1;
+                ctx.lineJoin = "round";
+                ctx.fillStyle = c.scale('a', 0.4).toString();
+
+                var x = Math.min(selection.first.x, selection.second.x),
+                    y = Math.min(selection.first.y, selection.second.y),
+                    w = Math.abs(selection.second.x - selection.first.x),
+                    h = Math.abs(selection.second.y - selection.first.y);
+
+                ctx.fillRect(x, y, w, h);
+                ctx.strokeRect(x, y, w, h);
+
+                ctx.restore();
+            }
+        });
+    }
+
+    $.plot.plugins.push({
+        init: init,
+        options: {
+            selection: {
+                mode: null, // one of null, "x", "y" or "xy"
+                color: "#e8cfac"
+            }
+        },
+        name: 'selection',
+        version: '1.0'
+    });
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.selection.min.js
@@ -0,0 +1,1 @@
+(function(A){function B(J){var O={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var L={};function D(Q){if(O.active){J.getPlaceholder().trigger("plotselecting",[F()]);K(Q)}}function M(Q){if(Q.which!=1){return }document.body.focus();if(document.onselectstart!==undefined&&L.onselectstart==null){L.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&L.ondrag==null){L.ondrag=document.ondrag;document.ondrag=function(){return false}}C(O.first,Q);O.active=true;A(document).one("mouseup",I)}function I(Q){if(document.onselectstart!==undefined){document.onselectstart=L.onselectstart}if(document.ondrag!==undefined){document.ondrag=L.ondrag}O.active=false;K(Q);if(E()){H()}else{J.getPlaceholder().trigger("plotunselected",[]);J.getPlaceholder().trigger("plotselecting",[null])}return false}function F(){if(!E()){return null}var R=Math.min(O.first.x,O.second.x),Q=Math.max(O.first.x,O.second.x),T=Math.max(O.first.y,O.second.y),S=Math.min(O.first.y,O.second.y);var U={};var V=J.getAxes();if(V.xaxis.used){U.xaxis={from:V.xaxis.c2p(R),to:V.xaxis.c2p(Q)}}if(V.x2axis.used){U.x2axis={from:V.x2axis.c2p(R),to:V.x2axis.c2p(Q)}}if(V.yaxis.used){U.yaxis={from:V.yaxis.c2p(T),to:V.yaxis.c2p(S)}}if(V.y2axis.used){U.y2axis={from:V.y2axis.c2p(T),to:V.y2axis.c2p(S)}}return U}function H(){var Q=F();J.getPlaceholder().trigger("plotselected",[Q]);var R=J.getAxes();if(R.xaxis.used&&R.yaxis.used){J.getPlaceholder().trigger("selected",[{x1:Q.xaxis.from,y1:Q.yaxis.from,x2:Q.xaxis.to,y2:Q.yaxis.to}])}}function G(R,S,Q){return S<R?R:(S>Q?Q:S)}function C(U,R){var T=J.getOptions();var S=J.getPlaceholder().offset();var Q=J.getPlotOffset();U.x=G(0,R.pageX-S.left-Q.left,J.width());U.y=G(0,R.pageY-S.top-Q.top,J.height());if(T.selection.mode=="y"){U.x=U==O.first?0:J.width()}if(T.selection.mode=="x"){U.y=U==O.first?0:J.height()}}function K(Q){if(Q.pageX==null){return }C(O.second,Q);if(E()){O.show=true;J.triggerRedrawOverlay()}else{P(true)}}function P(Q){if(O.show){O.show=false;J.triggerRedrawOverlay();if(!Q){J.getPlaceholder().trigger("plotunselected",[])}}}function N(R,Q){var T,S,U=J.getAxes();var V=J.getOptions();if(V.selection.mode=="y"){O.first.x=0;O.second.x=J.width()}else{T=R.xaxis?U.xaxis:(R.x2axis?U.x2axis:U.xaxis);S=R.xaxis||R.x2axis||{from:R.x1,to:R.x2};O.first.x=T.p2c(Math.min(S.from,S.to));O.second.x=T.p2c(Math.max(S.from,S.to))}if(V.selection.mode=="x"){O.first.y=0;O.second.y=J.height()}else{T=R.yaxis?U.yaxis:(R.y2axis?U.y2axis:U.yaxis);S=R.yaxis||R.y2axis||{from:R.y1,to:R.y2};O.first.y=T.p2c(Math.min(S.from,S.to));O.second.y=T.p2c(Math.max(S.from,S.to))}O.show=true;J.triggerRedrawOverlay();if(!Q){H()}}function E(){var Q=5;return Math.abs(O.second.x-O.first.x)>=Q&&Math.abs(O.second.y-O.first.y)>=Q}J.clearSelection=P;J.setSelection=N;J.getSelection=F;J.hooks.bindEvents.push(function(R,Q){var S=R.getOptions();if(S.selection.mode!=null){Q.mousemove(D)}if(S.selection.mode!=null){Q.mousedown(M)}});J.hooks.drawOverlay.push(function(T,Y){if(O.show&&E()){var R=T.getPlotOffset();var Q=T.getOptions();Y.save();Y.translate(R.left,R.top);var U=A.color.parse(Q.selection.color);Y.strokeStyle=U.scale("a",0.8).toString();Y.lineWidth=1;Y.lineJoin="round";Y.fillStyle=U.scale("a",0.4).toString();var W=Math.min(O.first.x,O.second.x),V=Math.min(O.first.y,O.second.y),X=Math.abs(O.second.x-O.first.x),S=Math.abs(O.second.y-O.first.y);Y.fillRect(W,V,X,S);Y.strokeRect(W,V,X,S);Y.restore()}})}A.plot.plugins.push({init:B,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.0"})})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.stack.js
@@ -0,0 +1,152 @@
+/*
+Flot plugin for stacking data sets, i.e. putting them on top of each
+other, for accumulative graphs. Note that the plugin assumes the data
+is sorted on x. Also note that stacking a mix of positive and negative
+values in most instances doesn't make sense (so it looks weird).
+
+Two or more series are stacked when their "stack" attribute is set to
+the same key (which can be any number or string or just "true"). To
+specify the default stack, you can set
+
+  series: {
+    stack: null or true or key (number/string)
+  }
+
+or specify it for a specific series
+
+  $.plot($("#placeholder"), [{ data: [ ... ], stack: true ])
+  
+The stacking order is determined by the order of the data series in
+the array (later series end up on top of the previous).
+
+Internally, the plugin modifies the datapoints in each series, adding
+an offset to the y value. For line series, extra data points are
+inserted through interpolation. For bar charts, the second y value is
+also adjusted.
+*/
+
+(function ($) {
+    var options = {
+        series: { stack: null } // or number/string
+    };
+    
+    function init(plot) {
+        function findMatchingSeries(s, allseries) {
+            var res = null
+            for (var i = 0; i < allseries.length; ++i) {
+                if (s == allseries[i])
+                    break;
+                
+                if (allseries[i].stack == s.stack)
+                    res = allseries[i];
+            }
+            
+            return res;
+        }
+        
+        function stackData(plot, s, datapoints) {
+            if (s.stack == null)
+                return;
+
+            var other = findMatchingSeries(s, plot.getData());
+            if (!other)
+                return;
+            
+            var ps = datapoints.pointsize,
+                points = datapoints.points,
+                otherps = other.datapoints.pointsize,
+                otherpoints = other.datapoints.points,
+                newpoints = [],
+                px, py, intery, qx, qy, bottom,
+                withlines = s.lines.show, withbars = s.bars.show,
+                withsteps = withlines && s.lines.steps,
+                i = 0, j = 0, l;
+
+            while (true) {
+                if (i >= points.length)
+                    break;
+
+                l = newpoints.length;
+
+                if (j >= otherpoints.length
+                    || otherpoints[j] == null
+                    || points[i] == null) {
+                    // degenerate cases
+                    for (m = 0; m < ps; ++m)
+                        newpoints.push(points[i + m]);
+                    i += ps;
+                }
+                else {
+                    // cases where we actually got two points
+                    px = points[i];
+                    py = points[i + 1];
+                    qx = otherpoints[j];
+                    qy = otherpoints[j + 1];
+                    bottom = 0;
+
+                    if (px == qx) {
+                        for (m = 0; m < ps; ++m)
+                            newpoints.push(points[i + m]);
+
+                        newpoints[l + 1] += qy;
+                        bottom = qy;
+                        
+                        i += ps;
+                        j += otherps;
+                    }
+                    else if (px > qx) {
+                        // we got past point below, might need to
+                        // insert interpolated extra point
+                        if (withlines && i > 0 && points[i - ps] != null) {
+                            intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px);
+                            newpoints.push(qx);
+                            newpoints.push(intery + qy)
+                            for (m = 2; m < ps; ++m)
+                                newpoints.push(points[i + m]);
+                            bottom = qy; 
+                        }
+
+                        j += otherps;
+                    }
+                    else {
+                        for (m = 0; m < ps; ++m)
+                            newpoints.push(points[i + m]);
+                        
+                        // we might be able to interpolate a point below,
+                        // this can give us a better y
+                        if (withlines && j > 0 && otherpoints[j - ps] != null)
+                            bottom = qy + (otherpoints[j - ps + 1] - qy) * (px - qx) / (otherpoints[j - ps] - qx);
+
+                        newpoints[l + 1] += bottom;
+                        
+                        i += ps;
+                    }
+                    
+                    if (l != newpoints.length && withbars)
+                        newpoints[l + 2] += bottom;
+                }
+
+                // maintain the line steps invariant
+                if (withsteps && l != newpoints.length && l > 0
+                    && newpoints[l] != null
+                    && newpoints[l] != newpoints[l - ps]
+                    && newpoints[l + 1] != newpoints[l - ps + 1]) {
+                    for (m = 0; m < ps; ++m)
+                        newpoints[l + ps + m] = newpoints[l + m];
+                    newpoints[l + 1] = newpoints[l - ps + 1];
+                }
+            }
+            
+            datapoints.points = newpoints;
+        }
+        
+        plot.hooks.processDatapoints.push(stackData);
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'stack',
+        version: '1.0'
+    });
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.stack.min.js
@@ -0,0 +1,1 @@
+(function(B){var A={series:{stack:null}};function C(F){function D(J,I){var H=null;for(var G=0;G<I.length;++G){if(J==I[G]){break}if(I[G].stack==J.stack){H=I[G]}}return H}function E(W,P,G){if(P.stack==null){return }var L=D(P,W.getData());if(!L){return }var T=G.pointsize,Y=G.points,H=L.datapoints.pointsize,S=L.datapoints.points,N=[],R,Q,I,a,Z,M,O=P.lines.show,K=P.bars.show,J=O&&P.lines.steps,X=0,V=0,U;while(true){if(X>=Y.length){break}U=N.length;if(V>=S.length||S[V]==null||Y[X]==null){for(m=0;m<T;++m){N.push(Y[X+m])}X+=T}else{R=Y[X];Q=Y[X+1];a=S[V];Z=S[V+1];M=0;if(R==a){for(m=0;m<T;++m){N.push(Y[X+m])}N[U+1]+=Z;M=Z;X+=T;V+=H}else{if(R>a){if(O&&X>0&&Y[X-T]!=null){I=Q+(Y[X-T+1]-Q)*(a-R)/(Y[X-T]-R);N.push(a);N.push(I+Z);for(m=2;m<T;++m){N.push(Y[X+m])}M=Z}V+=H}else{for(m=0;m<T;++m){N.push(Y[X+m])}if(O&&V>0&&S[V-T]!=null){M=Z+(S[V-T+1]-Z)*(R-a)/(S[V-T]-a)}N[U+1]+=M;X+=T}}if(U!=N.length&&K){N[U+2]+=M}}if(J&&U!=N.length&&U>0&&N[U]!=null&&N[U]!=N[U-T]&&N[U+1]!=N[U-T+1]){for(m=0;m<T;++m){N[U+T+m]=N[U+m]}N[U+1]=N[U-T+1]}}G.points=N}F.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"stack",version:"1.0"})})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.js
@@ -0,0 +1,103 @@
+/*
+Flot plugin for thresholding data. Controlled through the option
+"threshold" in either the global series options
+
+  series: {
+    threshold: {
+      below: number
+      color: colorspec
+    }
+  }
+
+or in a specific series
+
+  $.plot($("#placeholder"), [{ data: [ ... ], threshold: { ... }}])
+
+The data points below "below" are drawn with the specified color. This
+makes it easy to mark points below 0, e.g. for budget data.
+
+Internally, the plugin works by splitting the data into two series,
+above and below the threshold. The extra series below the threshold
+will have its label cleared and the special "originSeries" attribute
+set to the original series. You may need to check for this in hover
+events.
+*/
+
+(function ($) {
+    var options = {
+        series: { threshold: null } // or { below: number, color: color spec}
+    };
+    
+    function init(plot) {
+        function thresholdData(plot, s, datapoints) {
+            if (!s.threshold)
+                return;
+            
+            var ps = datapoints.pointsize, i, x, y, p, prevp,
+                thresholded = $.extend({}, s); // note: shallow copy
+
+            thresholded.datapoints = { points: [], pointsize: ps };
+            thresholded.label = null;
+            thresholded.color = s.threshold.color;
+            thresholded.threshold = null;
+            thresholded.originSeries = s;
+            thresholded.data = [];
+
+            var below = s.threshold.below,
+                origpoints = datapoints.points,
+                addCrossingPoints = s.lines.show;
+
+            threspoints = [];
+            newpoints = [];
+
+            for (i = 0; i < origpoints.length; i += ps) {
+                x = origpoints[i]
+                y = origpoints[i + 1];
+
+                prevp = p;
+                if (y < below)
+                    p = threspoints;
+                else
+                    p = newpoints;
+
+                if (addCrossingPoints && prevp != p && x != null
+                    && i > 0 && origpoints[i - ps] != null) {
+                    var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x;
+                    prevp.push(interx);
+                    prevp.push(below);
+                    for (m = 2; m < ps; ++m)
+                        prevp.push(origpoints[i + m]);
+                    
+                    p.push(null); // start new segment
+                    p.push(null);
+                    for (m = 2; m < ps; ++m)
+                        p.push(origpoints[i + m]);
+                    p.push(interx);
+                    p.push(below);
+                    for (m = 2; m < ps; ++m)
+                        p.push(origpoints[i + m]);
+                }
+
+                p.push(x);
+                p.push(y);
+            }
+
+            datapoints.points = newpoints;
+            thresholded.datapoints.points = threspoints;
+            
+            if (thresholded.datapoints.points.length > 0)
+                plot.getData().push(thresholded);
+                
+            // FIXME: there are probably some edge cases left in bars
+        }
+        
+        plot.hooks.processDatapoints.push(thresholdData);
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'threshold',
+        version: '1.0'
+    });
+})(jQuery);
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.min.js
@@ -0,0 +1,1 @@
+(function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I<Q.length;I+=F){O=Q[I];N=Q[I+1];K=G;if(N<P){G=threspoints}else{G=newpoints}if(R&&K!=G&&O!=null&&I>0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m<F;++m){K.push(Q[I+m])}G.push(null);G.push(null);for(m=2;m<F;++m){G.push(Q[I+m])}G.push(J);G.push(P);for(m=2;m<F;++m){G.push(Q[I+m])}}G.push(O);G.push(N)}M.points=newpoints;H.datapoints.points=threspoints;if(H.datapoints.points.length>0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mail/app/profile/extensions/tbtestpilot@labs.mozilla.com/content/flot/jquery.js
@@ -0,0 +1,4376 @@
+/*!
+ * jQuery JavaScript Library v1.3.2
+ * http://jquery.com/
+ *
+ * Copyright (c) 2009 John Resig
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ *
+ * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
+ * Revision: 6246
+ */
+(function(){
+
+var 
+	// Will speed up references to window, and allows munging its name.
+	window = this,
+	// Will speed up references to undefined, and allows munging its name.
+	undefined,
+	// Map over jQuery in case of overwrite
+	_jQuery = window.jQuery,
+	// Map over the $ in case of overwrite
+	_$ = window.$,
+
+	jQuery = window.jQuery = window.$ = function( selector, context ) {
+		// The jQuery object is actually just the init constructor 'enhanced'
+		return new jQuery.fn.init( selector, context );
+	},
+
+	// A simple way to check for HTML strings or ID strings
+	// (both of which we optimize for)
+	quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
+	// Is it a simple selector
+	isSimple = /^.[^:#\[\.,]*$/;
+
+jQuery.fn = jQuery.prototype = {
+	init: function( selector, context ) {
+		// Make sure that a selection was provided
+		selector = selector || document;
+
+		// Handle $(DOMElement)
+		if ( selector.nodeType ) {
+			this[0] = selector;
+			this.length = 1;
+			this.context = selector;
+			return this;
+		}
+		// Handle HTML strings
+		if ( typeof selector === "string" ) {
+			// Are we dealing with HTML string or an ID?
+			var match = quickExpr.exec( selector );
+
+			// Verify a match, and that no context was specified for #id
+			if ( match && (match[1] || !context) ) {
+
+				// HANDLE: $(html) -> $(array)
+				if ( match[1] )
+					selector = jQuery.clean( [ match[1] ], context );
+
+				// HANDLE: $("#id")
+				else {
+					var elem = document.getElementById( match[3] );
+
+					// Handle the case where IE and Opera return items
+					// by name instead of ID
+					if ( elem && elem.id != match[3] )
+						return jQuery().find( selector );
+
+					// Otherwise, we inject the element directly into the jQuery object
+					var ret = jQuery( elem || [] );
+					ret.context = document;
+					ret.selector = selector;
+					return ret;
+				}
+
+			// HANDLE: $(expr, [context])
+			// (which is just equivalent to: $(content).find(expr)
+			} else
+				return jQuery( context ).find( selector );
+
+		// HANDLE: $(function)
+		// Shortcut for document ready
+		} else if ( jQuery.isFunction( selector ) )
+			return jQuery( document ).ready( selector );
+
+		// Make sure that old selector state is passed along
+		if ( selector.selector && selector.context ) {
+			this.selector = selector.selector;
+			this.context = selector.context;
+		}
+
+		return this.setArray(jQuery.isArray( selector ) ?
+			selector :
+			jQuery.makeArray(selector));
+	},
+
+	// Start with an empty selector
+	selector: "",
+
+	// The current version of jQuery being used
+	jquery: "1.3.2",
+
+	// The number of elements contained in the matched element set
+	size: function() {
+		return this.length;
+	},
+
+	// 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 === undefined ?
+
+			// Return a 'clean' array
+			Array.prototype.slice.call( this ) :
+
+			// Return just the object
+			this[ num ];
+	},
+
+	// Take an array of elements and push it onto the stack
+	// (returning the new matched element set)
+	pushStack: function( elems, name, selector ) {
+		// Build a new jQuery matched element set
+		var ret = jQuery( elems );
+
+		// Add the old object onto the stack (as a reference)
+		ret.prevObject = this;
+
+		ret.context = this.context;
+
+		if ( name === "find" )
+			ret.selector = this.selector + (this.selector ? " " : "") + selector;
+		else if ( name )
+			ret.selector = this.selector + "." + name + "(" + selector + ")";
+
+		// Return the newly-formed element set
+		return ret;
+	},
+
+	// Force the current matched set of elements to become
+	// the specified array of elements (destroying the stack in the process)
+	// You should use pushStack() in order to do this, but maintain the stack
+	setArray: function( elems ) {
+		// Resetting the length to 0, then using the native Array push
+		// is a super-fast way to populate an object with array-like properties
+		this.length = 0;
+		Array.prototype.push.apply( this, elems );
+
+		return this;
+	},
+
+	// 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 );
+	},
+
+	// Determine the position of an element within
+	// the matched set of elements
+	index: function( elem ) {
+		// Locate the position of the desired element
+		return jQuery.inArray(
+			// If it receives a jQuery object, the first element is used
+			elem && elem.jquery ? elem[0] : elem
+		, this );
+	},
+
+	attr: function( name, value, type ) {
+		var options = name;
+
+		// Look for the case where we're accessing a style value
+		if ( typeof name === "string" )
+			if ( value === undefined )
+				return this[0] && jQuery[ type || "attr" ]( this[0], name );
+
+			else {
+				options = {};
+				options[ name ] = value;
+			}
+
+		// Check to see if we're setting style values
+		return this.each(function(i){
+			// Set all the styles
+			for ( name in options )
+				jQuery.attr(
+					type ?
+						this.style :
+						this,
+					name, jQuery.prop( this, options[ name ], type, i, name )
+				);
+		});
+	},
+
+	css: function( key, value ) {
+		// ignore negative width and height values
+		if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
+			value = undefined;
+		return this.attr( key, value, "curCSS" );
+	},
+
+	text: function( text ) {
+		if ( typeof text !== "object" && text != null )
+			return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+
+		var ret = "";
+
+		jQuery.each( text || this, function(){
+			jQuery.each( this.childNodes, function(){
+				if ( this.nodeType != 8 )
+					ret += this.nodeType != 1 ?
+						this.nodeValue :
+						jQuery.fn.text( [ this ] );
+			});
+		});
+
+		return ret;
+	},
+
+	wrapAll: function( html ) {
+		if ( this[0] ) {
+			// The elements to wrap the target around
+			var wrap = jQuery( html, this[0].ownerDocument ).clone();
+
+			if ( this[0].parentNode )
+				wrap.insertBefore( this[0] );
+
+			wrap.map(function(){
+				var elem = this;
+
+				while ( elem.firstChild )
+					elem = elem.firstChild;
+
+				return elem;
+			}).append(this);
+		}
+
+		return this;
+	},
+
+	wrapInner: function( html ) {
+		return this.each(function(){
+			jQuery( this ).contents().wrapAll( html );
+		});
+	},
+
+	wrap: function( html ) {
+		return this.each(function(){
+			jQuery( this ).wrapAll( html );
+		});
+	},
+
+	append: function() {
+		return this.domManip(arguments, true, function(elem){
+			if (this.nodeType == 1)
+				this.appendChild( elem );
+		});
+	},
+
+	prepend: function() {
+		return this.domManip(arguments, true, function(elem){
+			if (this.nodeType == 1)
+				this.insertBefore( elem, this.firstChild );
+		});
+	},
+
+	before: function() {
+		return this.domManip(arguments, false, function(elem){
+			this.parentNode.insertBefore( elem, this );
+		});
+	},
+
+	after: function() {
+		return this.domManip(arguments, false, function(elem){
+			this.parentNode.insertBefore( elem, this.nextSibling );
+		});
+	},
+
+	end: function() {
+		return this.prevObject || jQuery( [] );
+	},
+
+	// For internal use only.
+	// Behaves like an Array's method, not like a jQuery method.
+	push: [].push,
+	sort: [].sort,
+	splice: [].splice,
+
+	find: function( selector ) {
+		if ( this.length === 1 ) {
+			var ret = this.pushStack( [], "find", selector );
+			ret.length = 0;
+			jQuery.find( selector, this[0], ret );
+			return ret;
+		} else {
+			return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){
+				return jQuery.find( selector, elem );
+			})), "find", selector );
+		}
+	},
+
+	clone: function( events ) {
+		// Do the clone
+		var ret = this.map(function(){
+			if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
+				// IE copies events bound via attachEvent when
+				// using cloneNode. Calling detachEvent on the
+				// clone will also remove the events from the orignal
+				// In order to get around this, we use innerHTML.
+				// Unfortunately, this means some modifications to
+				// attributes in IE that are actually only stored
+				// as properties will not be copied (such as the
+				// the name attribute on an input).
+				var html = this.outerHTML;
+				if ( !html ) {
+					var div = this.ownerDocument.createElement("div");
+					div.appendChild( this.cloneNode(true) );
+					html = div.innerHTML;
+				}
+
+				return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0];
+			} else
+				return this.cloneNode(true);
+		});
+
+		// Copy the events from the original to the clone
+		if ( events === true ) {
+			var orig = this.find("*").andSelf(), i = 0;
+
+			ret.find("*").andSelf().each(function(){
+				if ( this.nodeName !== orig[i].nodeName )
+					return;
+
+				var events = jQuery.data( orig[i], "events" );
+
+				for ( var type in events ) {
+					for ( var handler in events[ type ] ) {
+						jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
+					}
+				}
+
+				i++;
+			});
+		}
+
+		// Return the cloned set
+		return ret;
+	},
+
+	filter: function( selector ) {
+		return this.pushStack(
+			jQuery.isFunction( selector ) &&
+			jQuery.grep(this, function(elem, i){
+				return selector.call( elem, i );
+			}) ||
+
+			jQuery.multiFilter( selector, jQuery.grep(this, function(elem){
+				return elem.nodeType === 1;
+			}) ), "filter", selector );
+	},
+
+	closest: function( selector ) {
+		var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null,
+			closer = 0;
+
+		return this.map(function(){
+			var cur = this;
+			while ( cur && cur.ownerDocument ) {
+				if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) {
+					jQuery.data(cur, "closest", closer);
+					return cur;
+				}
+				cur = cur.parentNode;
+				closer++;
+			}
+		});
+	},
+
+	not: function( selector ) {
+		if ( typeof selector === "string" )
+			// test special case where just one selector is passed in
+			if ( isSimple.test( selector ) )
+				return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector );
+			else
+				selector = jQuery.multiFilter( selector, this );
+
+		var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
+		return this.filter(function() {
+			return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
+		});
+	},
+
+	add: function( selector ) {
+		return this.pushStack( jQuery.unique( jQuery.merge(
+			this.get(),
+			typeof selector === "string" ?
+				jQuery( selector ) :
+				jQuery.makeArray( selector )
+		)));
+	},
+
+	is: function( selector ) {
+		return !!selector && jQuery.multiFilter( selector, this ).length > 0;
+	},
+
+	hasClass: function( selector ) {
+		return !!selector && this.is( "." + selector );
+	},
+
+	val: function( value ) {
+		if ( value === undefined ) {			
+			var elem = this[0];
+
+			if ( elem ) {
+				if( jQuery.nodeName( elem, 'option' ) )
+					return (elem.attributes.value || {}).specified ? elem.value : elem.text;
+				
+				// We need to handle select boxes special
+				if ( jQuery.nodeName( elem, "select" ) ) {
+					var index = elem.selectedIndex,
+						values = [],
+						options = elem.options,
+						one = elem.type == "select-one";
+
+					// Nothing was selected
+					if ( index < 0 )
+						return null;
+
+					// Loop through all the selected options
+					for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+						var option = options[ i ];
+
+						if ( option.selected ) {
+							// Get the specifc 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;				
+				}
+
+				// Everything else, we just grab the value
+				return (elem.value || "").replace(/\r/g, "");
+
+			}
+
+			return undefined;
+		}
+
+		if ( typeof value === "number" )
+			value += '';
+
+		return this.each(function(){
+			if ( this.nodeType != 1 )
+				return;
+
+			if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) )
+				this.checked = (jQuery.inArray(this.value, value) >= 0 ||
+					jQuery.inArray(this.name, value) >= 0);
+
+			else if ( jQuery.nodeName( this, "select" ) ) {
+				var values = jQuery.makeArray(value);
+
+				jQuery( "option", this ).each(function(){
+					this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
+						jQuery.inArray( this.text, values ) >= 0);
+				});
+
+				if ( !values.length )
+					this.selectedIndex = -1;
+
+			} else
+				this.value = value;
+		});
+	},
+
+	html: function( value ) {
+		return value === undefined ?
+			(this[0] ?
+				this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
+				null) :
+			this.empty().append( value );
+	},
+
+	replaceWith: function( value ) {
+		return this.after( value ).remove();
+	},
+
+	eq: function( i ) {
+		return this.slice( i, +i + 1 );
+	},
+
+	slice: function() {
+		return this.pushStack( Array.prototype.slice.apply( this, arguments ),
+			"slice", Array.prototype.slice.call(arguments).join(",") );
+	},
+
+	map: function( callback ) {
+		return this.pushStack( jQuery.map(this, function(elem, i){
+			return callback.call( elem, i, elem );
+		}));
+	},
+
+	andSelf: function() {
+		return this.add( this.prevObject );
+	},
+
+	domManip: function( args, table, callback ) {
+		if ( this[0] ) {
+			var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
+				scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
+				first = fragment.firstChild;
+
+			if ( first )
+				for ( var i = 0, l = this.length; i < l; i++ )
+					callback.call( root(this[i], first), this.length > 1 || i > 0 ?
+							fragment.cloneNode(true) : fragment );
+		
+			if ( scripts )
+				jQuery.each( scripts, evalScript );
+		}
+
+		return this;
+		
+		function root( elem, cur ) {
+			return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ?
+				(elem.getElementsByTagName("tbody")[0] ||
+				elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
+				elem;
+		}
+	}
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+function evalScript( i, elem ) {
+	if ( elem.src )
+		jQuery.ajax({
+			url: elem.src,
+			async: false,
+			dataType: "script"
+		});
+
+	else
+		jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+
+	if ( elem.parentNode )
+		elem.parentNode.removeChild( elem );
+}
+
+function now(){
+	return +new Date;
+}
+
+jQuery.extend = jQuery.fn.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" && !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 ( 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 ] = jQuery.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;
+};
+
+// exclude the following css properties to add px
+var	exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
+	// cache defaultView
+	defaultView = document.defaultView || {},
+	toString = Object.prototype.toString;
+
+jQuery.extend({
+	noConflict: function( deep ) {
+		window.$ = _$;
+
+		if ( deep )
+			window.jQuery = _jQuery;
+
+		return jQuery;
+	},
+
+	// 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 toString.call(obj) === "[object Function]";
+	},
+
+	isArray: function( obj ) {
+		return toString.call(obj) === "[object Array]";
+	},
+
+	// check if an element is in a (or is an) XML document
+	isXMLDoc: function( elem ) {
+		return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+			!!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument );
+	},
+
+	// Evalulates a script in a global context
+	globalEval: function( data ) {
+		if ( data && /\S/.test(data) ) {
+			// Inspired by code by Andrea Giammarchi
+			// http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
+			var head = document.getElementsByTagName("head")[0] || document.documentElement,
+				script = document.createElement("script");
+
+			script.type = "text/javascript";
+			if ( jQuery.support.scriptEval )
+				script.appendChild( document.createTextNode( data ) );
+			else
+				script.text = data;
+
+			// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+			// This arises when a base node is used (#2709).
+			head.insertBefore( script, head.firstChild );
+			head.removeChild( script );
+		}
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+	},
+
+	// args is for internal usage only
+	each: function( object, callback, args ) {
+		var name, i = 0, length = object.length;
+
+		if ( args ) {
+			if ( length === undefined ) {
+				for ( name in object )
+					if ( callback.apply( object[ name ], args ) === false )
+						break;
+			} else
+				for ( ; i < length; )
+					if ( callback.apply( object[ i++ ], args ) === false )
+						break;
+
+		// A special, fast, case for the most common use of each
+		} else {
+			if ( length === undefined ) {
+				for ( name in object )
+					if ( callback.call( object[ name ], name, object[ name ] ) === false )
+						break;
+			} else
+				for ( var value = object[0];
+					i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
+		}
+
+		return object;
+	},
+
+	prop: function( elem, value, type, i, name ) {
+		// Handle executable functions
+		if ( jQuery.isFunction( value ) )
+			value = value.call( elem, i );
+
+		// Handle passing in a number to a CSS property
+		return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ?
+			value + "px" :
+			value;
+	},
+
+	className: {
+		// internal only, use addClass("class")
+		add: function( elem, classNames ) {
+			jQuery.each((classNames || "").split(/\s+/), function(i, className){
+				if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
+					elem.className += (elem.className ? " " : "") + className;
+			});
+		},
+
+		// internal only, use removeClass("class")
+		remove: function( elem, classNames ) {
+			if (elem.nodeType == 1)
+				elem.className = classNames !== undefined ?
+					jQuery.grep(elem.className.split(/\s+/), function(className){
+						return !jQuery.className.has( classNames, className );
+					}).join(" ") :
+					"";
+		},
+
+		// internal only, use hasClass("class")
+		has: function( elem, className ) {
+			return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+		}
+	},
+
+	// A method for quickly swapping in/out CSS properties to get correct calculations
+	swap: function( elem, options, callback ) {
+		var old = {};
+		// Remember the old values, and insert the new ones
+		for ( var name in options ) {
+			old[ name ] = elem.style[ name ];
+			elem.style[ name ] = options[ name ];
+		}
+
+		callback.call( elem );
+
+		// Revert the old values
+		for ( var name in options )
+			elem.style[ name ] = old[ name ];
+	},
+
+	css: function( elem, name, force, extra ) {
+		if ( name == "width" || name == "height" ) {
+			var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
+
+			function getWH() {
+				val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
+
+				if ( extra === "border" )
+					return;
+
+				jQuery.each( which, function() {
+					if ( !extra )
+						val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
+					if ( extra === "margin" )
+						val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
+					else
+						val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
+				});
+			}
+
+			if ( elem.offsetWidth !== 0 )
+				getWH();
+			else
+				jQuery.swap( elem, props, getWH );
+
+			return Math.max(0, Math.round(val));
+		}
+
+		return jQuery.curCSS( elem, name, force );
+	},
+
+	curCSS: function( elem, name, force ) {
+		var ret, style = elem.style;
+
+		// We need to handle opacity special in IE
+		if ( name == "opacity" && !jQuery.support.opacity ) {
+			ret = jQuery.attr( style, "opacity" );
+
+			return ret == "" ?
+				"1" :
+				ret;
+		}
+
+		// Make sure we're using the right name for getting the float value
+		if ( name.match( /float/i ) )
+			name = styleFloat;
+
+		if ( !force && style && style[ name ] )
+			ret = style[ name ];
+
+		else if ( defaultView.getComputedStyle ) {
+
+			// Only "float" is needed here
+			if ( name.match( /float/i ) )
+				name = "float";
+
+			name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
+
+			var computedStyle = defaultView.getComputedStyle( elem, null );
+
+			if ( computedStyle )
+				ret = computedStyle.getPropertyValue( name );
+
+			// We should always get a number back from opacity
+			if ( name == "opacity" && ret == "" )
+				ret = "1";
+
+		} else if ( elem.currentStyle ) {
+			var camelCase = name.replace(/\-(\w)/g, function(all, letter){
+				return letter.toUpperCase();
+			});
+
+			ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
+
+			// 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
+			if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
+				// Remember the original values
+				var left = style.left, rsLeft = elem.runtimeStyle.left;
+
+				// Put in the new values to get a computed value out
+				elem.runtimeStyle.left = elem.currentStyle.left;
+				style.left = ret || 0;
+				ret = style.pixelLeft + "px";
+
+				// Revert the changed values
+				style.left = left;
+				elem.runtimeStyle.left = rsLeft;
+			}
+		}
+
+		return ret;
+	},
+
+	clean: function( elems, context, fragment ) {
+		context = context || document;
+
+		// !context.createElement fails in IE with an error but returns typeof 'object'
+		if ( typeof context.createElement === "undefined" )
+			context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+
+		// If a single string is passed in and it's a single tag
+		// just do a createElement and skip the rest
+		if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
+			var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
+			if ( match )
+				return [ context.createElement( match[1] ) ];
+		}
+
+		var ret = [], scripts = [], div = context.createElement("div");
+
+		jQuery.each(elems, function(i, elem){
+			if ( typeof elem === "number" )
+				elem += '';
+
+			if ( !elem )
+				return;
+
+			// Convert html string into DOM nodes
+			if ( typeof elem === "string" ) {
+				// Fix "XHTML"-style tags in all browsers
+				elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
+					return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
+						all :
+						front + "></" + tag + ">";
+				});
+
+				// Trim whitespace, otherwise indexOf won't work as expected
+				var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase();
+
+				var wrap =
+					// option or optgroup
+					!tags.indexOf("<opt") &&
+					[ 1, "<select multiple='multiple'>", "</select>" ] ||
+
+					!tags.indexOf("<leg") &&
+					[ 1, "<fieldset>", "</fieldset>" ] ||
+
+					tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
+					[ 1, "<table>", "</table>" ] ||
+
+					!tags.indexOf("<tr") &&
+					[ 2, "<table><tbody>", "</tbody></table>" ] ||
+
+				 	// <thead> matched above
+					(!tags.indexOf("<td") || !tags.indexOf("<th")) &&
+					[ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
+
+					!tags.indexOf("<col") &&
+					[ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
+
+					// IE can't serialize <link> and <script> tags normally
+					!jQuery.support.htmlSerialize &&
+					[ 1, "div<div>", "</div>" ] ||
+
+					[ 0, "", "" ];
+
+				// Go to html and back, then peel off extra wrappers
+				div.innerHTML = wrap[1] + elem + wrap[2];
+
+				// Move to the right depth
+				while ( wrap[0]-- )
+					div = div.lastChild;
+
+				// Remove IE's autoinserted <tbody> from table fragments
+				if ( !jQuery.support.tbody ) {
+
+					// String was a <table>, *may* have spurious <tbody>
+					var hasBody = /<tbody/i.test(elem),
+						tbody = !tags.indexOf("<table") && !hasBody ?
+							div.firstChild && div.firstChild.childNodes :
+
+						// String was a bare <thead> or <tfoot>
+						wrap[1] == "<table>" && !hasBody ?
+							div.childNodes :
+							[];
+
+					for ( var j = tbody.length - 1; j >= 0 ; --j )
+						if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
+							tbody[ j ].parentNode.removeChild( tbody[ j ] );
+
+					}
+
+				// IE completely kills leading whitespace when innerHTML is used
+				if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
+					div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
+				
+				elem = jQuery.makeArray( div.childNodes );
+			}
+
+			if ( elem.nodeType )
+				ret.push( elem );
+			else
+				ret = jQuery.merge( ret, elem );
+
+		});
+
+		if ( fragment ) {
+			for ( var i = 0; ret[i]; i++ ) {
+				if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+					scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+				} else {
+					if ( ret[i].nodeType === 1 )
+						ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
+					fragment.appendChild( ret[i] );
+				}
+			}
+			
+			return scripts;
+		}
+
+		return ret;
+	},
+
+	attr: function( elem, name, value ) {
+		// don't set attributes on text and comment nodes
+		if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
+			return undefined;
+
+		var notxml = !jQuery.isXMLDoc( elem ),
+			// Whether we are setting (or getting)
+			set = value !== undefined;
+
+		// Try to normalize/fix the name
+		name = notxml && jQuery.props[ name ] || name;
+
+		// Only do all the following if this is a node (faster for style)
+		// IE elem.getAttribute passes even for style
+		if ( elem.tagName ) {
+
+			// These attributes require special treatment
+			var special = /href|src|style/.test( name );
+
+			// Safari mis-reports the default selected property of a hidden option
+			// Accessing the parent's selectedIndex property fixes it
+			if ( name == "selected" && elem.parentNode )
+				elem.parentNode.selectedIndex;
+
+			// If applicable, access the attribute via the DOM 0 way
+			if ( name in elem && notxml && !special ) {
+				if ( set ){
+					// We can't allow the type property to be changed (since it causes problems in IE)
+					if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
+						throw "type property can't be changed";
+
+					elem[ name ] = value;
+				}
+
+				// browsers index elements by id/name on forms, give priority to attributes.
+				if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
+					return elem.getAttributeNode( name ).nodeValue;
+
+				// 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/
+				if ( name == "tabIndex" ) {
+					var attributeNode = elem.getAttributeNode( "tabIndex" );
+					return attributeNode && attributeNode.specified
+						? attributeNode.value
+						: elem.nodeName.match(/(button|input|object|select|textarea)/i)
+							? 0
+							: elem.nodeName.match(/^(a|area)$/i) && elem.href
+								? 0
+								: undefined;
+				}
+
+				return elem[ name ];
+			}
+
+			if ( !jQuery.support.style && notxml &&  name == "style" )
+				return jQuery.attr( elem.style, "cssText", value );
+
+			if ( set )
+				// convert the value to a string (all browsers do this but IE) see #1070
+				elem.setAttribute( name, "" + value );
+
+			var attr = !jQuery.support.hrefNormalized && notxml && special
+					// Some attributes require a special call on IE
+					? elem.getAttribute( name, 2 )
+					: elem.getAttribute( name );
+
+			// Non-existent attributes return null, we normalize to undefined
+			return attr === null ? undefined : attr;
+		}
+
+		// elem is actually elem.style ... set the style
+
+		// IE uses filters for opacity
+		if ( !jQuery.support.opacity && name == "opacity" ) {
+			if ( set ) {
+				// IE has trouble with opacity if it does not have layout
+				// Force it by setting the zoom level
+				elem.zoom = 1;
+
+				// Set the alpha filter to set the opacity
+				elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
+					(parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
+			}
+
+			return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
+				(parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
+				"";
+		}
+
+		name = name.replace(/-([a-z])/ig, function(all, letter){
+			return letter.toUpperCase();
+		});
+
+		if ( set )
+			elem[ name ] = value;
+
+		return elem[ name ];
+	},
+
+	trim: function( text ) {
+		return (text || "").replace( /^\s+|\s+$/g, "" );
+	},
+
+	makeArray: function( array ) {
+		var ret = [];
+
+		if( array != null ){
+			var i = array.length;
+			// The window, strings (and functions) also have 'length'
+			if( i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval )
+				ret[0] = array;
+			else
+				while( i )
+					ret[--i] = array[i];
+		}
+
+		return ret;
+	},
+
+	inArray: function( elem, array ) {
+		for ( var i = 0, length = array.length; i < length; i++ )
+		// Use === because on IE, window == document
+			if ( array[ i ] === elem )
+				return i;
+
+		return -1;
+	},
+
+	merge: function( first, second ) {
+		// We have to loop this way because IE & Opera overwrite the length
+		// expando of getElementsByTagName
+		var i = 0, elem, pos = first.length;
+		// Also, we need to make sure that the correct elements are being returned
+		// (IE returns comment nodes in a '*' query)
+		if ( !jQuery.support.getAll ) {
+			while ( (elem = second[ i++ ]) != null )
+				if ( elem.nodeType != 8 )
+					first[ pos++ ] = elem;
+
+		} else
+			while ( (elem = second[ i++ ]) != null )
+				first[ pos++ ] = elem;
+
+		return first;
+	},
+
+	unique: function( array ) {
+		var ret = [], done = {};
+
+		try {
+
+			for ( var i = 0, length = array.length; i < length; i++ ) {
+				var id = jQuery.data( array[ i ] );
+
+				if ( !done[ id ] ) {
+					done[ id ] = true;
+					ret.push( array[ i ] );
+				}
+			}
+
+		} catch( e ) {
+			ret = array;
+		}
+
+		return ret;
+	},
+
+	grep: function( elems, callback, inv ) {
+		var ret = [];
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( var i = 0, length = elems.length; i < length; i++ )
+			if ( !inv != !callback( elems[ i ], i ) )
+				ret.push( elems[ i ] );
+
+		return ret;
+	},
+
+	map: function( elems, callback ) {
+		var ret = [];
+
+		// Go through the array, translating each of the items to their
+		// new value (or values).
+		for ( var i = 0, length = elems.length; i < length; i++ ) {
+			var value = callback( elems[ i ], i );
+
+			if ( value != null )
+				ret[ ret.length ] = value;
+		}
+
+		return ret.concat.apply( [], ret );
+	}
+});
+
+// Use of jQuery.browser is deprecated.
+// It's included for backwards compatibility and plugins,
+// although they should work to migrate away.
+
+var userAgent = navigator.userAgent.toLowerCase();
+
+// Figure out what browser is being used
+jQuery.browser = {
+	version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
+	safari: /webkit/.test( userAgent ),
+	opera: /opera/.test( userAgent ),
+	msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
+	mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
+};
+
+jQuery.each({
+	parent: function(elem){return elem.parentNode;},
+	parents: function(elem){return jQuery.dir(elem,"parentNode");},
+	next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
+	prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
+	nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
+	prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
+	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.makeArray(elem.childNodes);}
+}, function(name, fn){
+	jQuery.fn[ name ] = function( selector ) {
+		var ret = jQuery.map( this, fn );
+
+		if ( selector && typeof selector == "string" )
+			ret = jQuery.multiFilter( selector, ret );
+
+		return this.pushStack( jQuery.unique( ret ), name, selector );
+	};
+});
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after",
+	replaceAll: "replaceWith"
+}, function(name, original){
+	jQuery.fn[ name ] = function( selector ) {
+		var ret = [], insert = jQuery( selector );
+
+		for ( var i = 0, l = insert.length; i < l; i++ ) {
+			var elems = (i > 0 ? this.clone(true) : this).get();
+			jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
+			ret = ret.concat( elems );
+		}
+
+		return this.pushStack( ret, name, selector );
+	};
+});
+
+jQuery.each({
+	removeAttr: function( name ) {
+		jQuery.attr( this, name, "" );
+		if (this.nodeType == 1)
+			this.removeAttribute( name );
+	},
+
+	addClass: function( classNames ) {
+		jQuery.className.add( this, classNames );
+	},
+
+	removeClass: function( classNames ) {
+		jQuery.className.remove( this, classNames );
+	},
+
+	toggleClass: function( classNames, state ) {
+		if( typeof state !== "boolean" )
+			state = !jQuery.className.has( this, classNames );
+		jQuery.className[ state ? "add" : "remove" ]( this, classNames );
+	},
+
+	remove: function( selector ) {
+		if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
+			// Prevent memory leaks
+			jQuery( "*", this ).add([this]).each(function(){
+				jQuery.event.remove(this);
+				jQuery.removeData(this);
+			});
+			if (this.parentNode)
+				this.parentNode.removeChild( this );
+		}
+	},
+
+	empty: function() {
+		// Remove element nodes and prevent memory leaks
+		jQuery(this).children().remove();
+
+		// Remove any remaining nodes
+		while ( this.firstChild )
+			this.removeChild( this.firstChild );
+	}
+}, function(name, fn){
+	jQuery.fn[ name ] = function(){
+		return this.each( fn, arguments );
+	};
+});
+
+// Helper function used by the dimensions and offset modules
+function num(elem, prop) {
+	return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
+}
+var expando = "jQuery" + now(), uuid = 0, windowData = {};
+
+jQuery.extend({
+	cache: {},
+
+	data: function( elem, name, data ) {
+		elem = elem == window ?
+			windowData :
+			elem;
+
+		var id = elem[ expando ];
+
+		// Compute a unique ID for the element
+		if ( !id )
+			id = elem[ expando ] = ++uuid;
+
+		// Only generate the data cache if we're
+		// trying to access or manipulate it
+		if ( name && !jQuery.cache[ id ] )
+			jQuery.cache[ id ] = {};
+
+		// Prevent overriding the named cache with undefined values
+		if ( data !== undefined )
+			jQuery.cache[ id ][ name ] = data;
+
+		// Return the named cache data, or the ID for the element
+		return name ?
+			jQuery.cache[ id ][ name ] :
+			id;
+	},
+