Merge pull request #565 from danvk/gulp
authorDan Vanderkam <danvdk@gmail.com>
Mon, 23 Mar 2015 22:22:15 +0000 (18:22 -0400)
committerDan Vanderkam <danvdk@gmail.com>
Mon, 23 Mar 2015 22:22:15 +0000 (18:22 -0400)
Migrate to gulp & karma

171 files changed:
.dygraph-combined-clean.js [deleted file]
.gitignore
.jshintrc
.travis.yml
CONTRIBUTING.md
DEVELOP.md [new file with mode: 0644]
Makefile [deleted file]
NOTES [deleted file]
README.md
auto_tests/.jshintrc [new file with mode: 0644]
auto_tests/karma.conf.js [new file with mode: 0644]
auto_tests/lib/Asserts.js [deleted file]
auto_tests/lib/JsTestDriver-1.3.3c.jar [deleted file]
auto_tests/lib/coverage-1.3.5.jar [deleted file]
auto_tests/lib/jquery-1.4.2.js [deleted file]
auto_tests/misc/README [deleted file]
auto_tests/misc/fake-jstestdriver.js [deleted file]
auto_tests/misc/filter-lcov.py [deleted file]
auto_tests/misc/local.html [deleted file]
auto_tests/misc/local.js [deleted file]
auto_tests/misc/new-test.sh [deleted file]
auto_tests/tests/CanvasAssertions.js
auto_tests/tests/Util.js
auto_tests/tests/annotations.js
auto_tests/tests/axis_labels-deprecated.js
auto_tests/tests/axis_labels.js
auto_tests/tests/callback.js
auto_tests/tests/connect_separated_points.js
auto_tests/tests/css.js
auto_tests/tests/custom_asserts.js [new file with mode: 0644]
auto_tests/tests/custom_bars.js
auto_tests/tests/data_api.js
auto_tests/tests/date_formats.js
auto_tests/tests/date_ticker.js
auto_tests/tests/dygraph-options-tests.js
auto_tests/tests/error_bars.js
auto_tests/tests/fast_canvas_proxy.js
auto_tests/tests/fill_step_plot.js
auto_tests/tests/formats.js
auto_tests/tests/grid_per_axis.js
auto_tests/tests/hidpi.js
auto_tests/tests/interaction_model.js
auto_tests/tests/missing_points.js
auto_tests/tests/multi_csv.js
auto_tests/tests/multiple_axes.js
auto_tests/tests/no_hours.js
auto_tests/tests/numeric_ticker.js
auto_tests/tests/parser.js
auto_tests/tests/pathological_cases.js
auto_tests/tests/per_axis.js
auto_tests/tests/per_series.js
auto_tests/tests/plugins.js
auto_tests/tests/plugins_legend.js
auto_tests/tests/range_selector.js
auto_tests/tests/range_tests.js
auto_tests/tests/resize.js
auto_tests/tests/rolling_average.js
auto_tests/tests/sanity.js
auto_tests/tests/scientific_notation.js
auto_tests/tests/scrolling_div.js
auto_tests/tests/selection.js
auto_tests/tests/simple_drawing.js
auto_tests/tests/smooth_plotter.js
auto_tests/tests/stacked.js
auto_tests/tests/step_plot_per_series.js
auto_tests/tests/to_dom_coords.js
auto_tests/tests/two_digit_years.js
auto_tests/tests/update_options.js
auto_tests/tests/update_while_panning.js
auto_tests/tests/utils_test.js
auto_tests/tests/visibility.js
check-combined-unaffected.sh [deleted file]
closure-todo.txt [deleted file]
compile-with-closure.sh [deleted file]
dashed-canvas.js [deleted file]
data.js [deleted file]
datahandler/bars-custom.js [deleted file]
datahandler/bars-error.js [deleted file]
datahandler/bars-fractions.js [deleted file]
datahandler/bars.js [deleted file]
datahandler/datahandler.js [deleted file]
datahandler/default-fractions.js [deleted file]
datahandler/default.js [deleted file]
dygraph-canvas.js [deleted file]
dygraph-combined.js [deleted file]
dygraph-dev.js
dygraph-gviz.js [deleted file]
dygraph-interaction-model.js [deleted file]
dygraph-internal.externs.js [deleted file]
dygraph-layout.js [deleted file]
dygraph-options-reference.js [deleted file]
dygraph-options.js [deleted file]
dygraph-plugin-base.js [deleted file]
dygraph-plugin-install.js [deleted file]
dygraph-tickers.js [deleted file]
dygraph-types.js [deleted file]
dygraph-utils.js [deleted file]
dygraph.js [deleted file]
extras/hairlines.js [deleted file]
extras/shapes.js [deleted file]
extras/smooth-plotter.js [deleted file]
extras/super-annotations.js [deleted file]
extras/synchronizer.js [deleted file]
extras/unzoom.js [deleted file]
gadget.xml [deleted file]
generate-combined.sh [deleted file]
generate-documentation.py [deleted file]
generate-download.py [deleted file]
generate-jar.sh [deleted file]
generate-jsdoc.sh [deleted file]
gulpfile.js [new file with mode: 0644]
gviz-api.js [deleted file]
gwt/org/danvk/dygraph-combined.js [deleted symlink]
jsTestDriver.conf [deleted file]
package.json
phantom-driver.js [deleted file]
phantom-perf.js [deleted file]
plugins/README [deleted file]
plugins/annotations.js [deleted file]
plugins/axes.js [deleted file]
plugins/chart-labels.js [deleted file]
plugins/grid.js [deleted file]
plugins/legend.js [deleted file]
plugins/range-selector.js [deleted file]
polyfills/console.js [deleted file]
push-to-web.sh [deleted file]
release.sh [deleted file]
scripts/generate-documentation.py [new file with mode: 0755]
scripts/generate-download.py [new file with mode: 0755]
scripts/generate-jsdoc.sh [new file with mode: 0755]
scripts/push-to-web.sh [new file with mode: 0755]
scripts/release.sh [new file with mode: 0755]
scripts/transform-coverage.js [new file with mode: 0755]
src/datahandler/bars-custom.js [new file with mode: 0644]
src/datahandler/bars-error.js [new file with mode: 0644]
src/datahandler/bars-fractions.js [new file with mode: 0644]
src/datahandler/bars.js [new file with mode: 0644]
src/datahandler/datahandler.js [new file with mode: 0644]
src/datahandler/default-fractions.js [new file with mode: 0644]
src/datahandler/default.js [new file with mode: 0644]
src/dygraph-canvas.js [new file with mode: 0644]
src/dygraph-combined.js [new file with mode: 0644]
src/dygraph-gviz.js [new file with mode: 0644]
src/dygraph-interaction-model.js [new file with mode: 0644]
src/dygraph-internal.externs.js [new file with mode: 0644]
src/dygraph-layout.js [new file with mode: 0644]
src/dygraph-options-reference.js [new file with mode: 0644]
src/dygraph-options.js [new file with mode: 0644]
src/dygraph-plugin-base.js [new file with mode: 0644]
src/dygraph-plugin-install.js [new file with mode: 0644]
src/dygraph-tickers.js [new file with mode: 0644]
src/dygraph-types.js [new file with mode: 0644]
src/dygraph-utils.js [new file with mode: 0644]
src/dygraph.js [new file with mode: 0644]
src/extras/hairlines.js [new file with mode: 0644]
src/extras/shapes.js [new file with mode: 0644]
src/extras/smooth-plotter.js [new file with mode: 0644]
src/extras/super-annotations.js [new file with mode: 0644]
src/extras/synchronizer.js [new file with mode: 0644]
src/extras/unzoom.js [new file with mode: 0644]
src/gviz-api.js [new file with mode: 0644]
src/plugins/README [new file with mode: 0644]
src/plugins/annotations.js [new file with mode: 0644]
src/plugins/axes.js [new file with mode: 0644]
src/plugins/chart-labels.js [new file with mode: 0644]
src/plugins/grid.js [new file with mode: 0644]
src/plugins/legend.js [new file with mode: 0644]
src/plugins/range-selector.js [new file with mode: 0644]
src/polyfills/console.js [new file with mode: 0644]
src/polyfills/dashed-canvas.js [new file with mode: 0644]
test.sh [deleted file]

diff --git a/.dygraph-combined-clean.js b/.dygraph-combined-clean.js
deleted file mode 100644 (file)
index e6380de..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-This is not the file you are looking for.
-A reasonably up-to-date version can be found at http://dygraphs.com/dygraph-combined.js
-
-dygraph-combined.js is a "packed" version of the larger dygraphs JS files. It is
-smaller and loads more quickly, but is harder to debug.
-
-To generate this file, run "make" or generate-combined.sh.
index d6ddfb4..01b10ac 100644 (file)
@@ -2,3 +2,4 @@ jsdoc
 docs/options.html
 node_modules
 env
+dist
index b50bc1f..72e44f8 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -8,6 +8,7 @@
   "latedef": true,
   "nonbsp": true,
   "undef": true,
+  "sub": true,
 
   "browser": true,
   "devel": true,
index c20a913..8425aa8 100644 (file)
@@ -1,6 +1,5 @@
 language: node_js
 node_js:
   - "0.10"
-sudo: false  # use container-based architecture
 
-script: "make travis"
+script: "gulp travis"
index 3b5598b..ee72da0 100644 (file)
@@ -13,9 +13,9 @@ page. For instance, instead of doing this:
 do this:
 
 ```html
-<script type="text/javascript" src="dygraph-dev.js"></script>
+<script type="text/javascript" src="dygraph-combined.dev.js"></script>
 ```
 
 This makes error messages and debugging simpler. The jsfiddle does this automatically.
 
-Sending a Pull Request? See our [guide to making dygraphs contributions](http://dygraphs.com/changes.html).
+Sending a Pull Request? See our [guide to making dygraphs contributions](/DEVELOP.md).
diff --git a/DEVELOP.md b/DEVELOP.md
new file mode 100644 (file)
index 0000000..c8872f0
--- /dev/null
@@ -0,0 +1,46 @@
+## dygraphs developer notes
+
+So you've made a change to dygraphs and would like to contribute it back to the open source project. Wonderful!
+
+This is a step-by-step guide explaining how to do it.
+
+### How-to
+
+To build dygraphs, run
+
+    gulp dist
+
+To run the tests, run:
+
+    gulp test
+
+To iterate on the code, open `tests/demo.html` (or one of the other demos) in your browser.
+
+To iterate on a unit test, change `it` to `it.only` in the Mocha test, change the browser from `PhantomJS` to `Chrome` (or whatever you like) in `auto_tests/karma.conf.js`, run
+
+    ./node_modules/karma/bin/karma start auto_tests/karma.conf.js
+
+and hit "DEBUG" in the Karma UI.
+
+### dygraphs style
+
+When making a change, please try to follow the style of the existing dygraphs code. This will make the review process go much more smoothly.
+
+A few salient points:
+
+1. We try to adhere to Google's [JS style guide][gstyle] and would appreciate it if you try to as well. This means:
+  *   No tabs! Indent using two spaces.
+  *   Use camelCase for variable and function names.
+  *   Limit lines to 80 characters.
+1.  If you've added a new feature, add a test for it (in the tests/ directory) or a gallery entry.
+1.  If you've added an option, document it in `dygraph-options-reference.js`. You'll get lots of warnings if you don't.
+1.  If you've fixed a bug or added a feature, add a unit test (in `auto_tests`) for it.
+
+Adding a unit test ensures that we won't inadvertently break your feature in the future. To do this, either add to an existing test in `auto_tests/tests` or create a new one.
+
+### Sending a Pull Request
+
+To make a change, you'll need to send a Pull Request. See GitHub's documentation [here][pr].
+
+[gstyle]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
+[pr]: http://help.github.com/send-pull-requests/
diff --git a/Makefile b/Makefile
deleted file mode 100644 (file)
index 2ab31de..0000000
--- a/Makefile
+++ /dev/null
@@ -1,50 +0,0 @@
-# You should run "npm install" before running any commands in this Makefile.
-
-all: test generate-combined generate-documentation
-
-clean:
-       @echo cleaning...
-       @cp .dygraph-combined-clean.js dygraph-combined.js
-       rm -f docs/options.html
-
-generate-combined:
-       @echo Generating dygraph-combined.js
-       @./generate-combined.sh
-
-generate-documentation:
-       @echo Generating docs/options.html
-       @./generate-documentation.py > docs/options.html
-       @chmod a+r docs/options.html
-
-gwt: generate-gwt
-
-generate-gwt:
-       @echo Generating GWT JAR file
-       @./generate-jar.sh
-
-test:
-       @./test.sh
-       @./check-combined-unaffected.sh
-
-test-combined: move-combined test clean-combined-test
-
-move-combined: generate-combined
-       mv dygraph-combined.js dygraph-dev.js
-
-clean-combined-test: clean
-       @echo restoring combined
-       git checkout dygraph-dev.js
-       rm dygraph-combined.js.map
-
-lint:
-       @./generate-combined.sh ls \
-           | grep -v 'polyfills' \
-           | xargs ./node_modules/.bin/jshint
-
-# Commands to run for continuous integration on Travis-CI
-travis: test test-combined lint
-
-publish:
-       ./generate-combined.sh
-       npm publish
-       git checkout dygraph-combined.js
diff --git a/NOTES b/NOTES
deleted file mode 100644 (file)
index 211f59a..0000000
--- a/NOTES
+++ /dev/null
@@ -1,37 +0,0 @@
-Axis-related properties:
-includeZero
-valueRange
-labelsKMB
-labelsKMG2
-pixelsPerYLabel
-yAxisLabelWidth
-axisLabelFontSize
-axisTickSize
-
-How is the y-axis determined?
-
-Dygraph.numericTicks: min, max -> set of ticks for axis
-tick = { label: label, v: value }
-
-addYTicks_: min, max -> void
-sets the yAxis and yTicks properties of layout_
-
-drawGraph_:
-if set, uses this.valueRange_ ([low, high] array)
-  -> adds ticks via addYTicks_
-  -> sets displayedYRange_
-
-otherwise, calculates a good axis based on minY and maxY.
-
-this.displayedYRange_ is returned by the yAxisRange function.
-this is, in turn, used by the toDataCoords and toDomCoords methods.
-
-Path of least resistance:
-- in drawGraph_, calculate [minY, maxY] per-series
-- write a function to compute y-axes for all series, ensure only two axes.
-- make yAxis, yTicks into arrays in layout_
-- add a series -> axis mapping to layout_, dygraph
-- add code to Renderer to add second axis.
-- add optional 'series' parameter to toDomCoords/toDataCoords
-
-This won't be compatible with stacked charts.
index b4bb0fc..7e3b0f2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -46,10 +46,14 @@ Learn more by reading [the tutorial](http://www.dygraphs.com/tutorial.html) and
 seeing demonstrations of what dygraphs can do in the
 [gallery](http://www.dygraphs.com/gallery).
 
-## Making Changes
-If you've made a change to dygraphs and would like to contribute it back to the
-community, please follow the [Guide to making dygraphs
-changes](http://dygraphs.com/changes.html).
+## Development
+
+To get going, clone the repo and run:
+
+    npm install
+    gulp dist
+
+Read more about the dygraphs development process in the [developer guide](/DEVELOP.md).
 
 ## License(s)
 dygraphs uses:
diff --git a/auto_tests/.jshintrc b/auto_tests/.jshintrc
new file mode 100644 (file)
index 0000000..46ac7f5
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "extends": "../.jshintrc",
+
+  "asi": true,
+  "mocha": true,
+  "strict": false,
+
+  "laxbreak": true,
+
+  "globals": {
+    "assert": true,
+    "Dygraph": true,
+    "DygraphOps": true,
+    "Util": true,
+    "CanvasAssertions": true,
+    "PixelSampler": true,
+    "smoothPlotter": true,
+    "$": true,
+    "DygraphCanvasRenderer": true,
+    "DygraphOptions": true
+  }
+}
diff --git a/auto_tests/karma.conf.js b/auto_tests/karma.conf.js
new file mode 100644 (file)
index 0000000..591643c
--- /dev/null
@@ -0,0 +1,36 @@
+module.exports = function (config) {
+    config.set({
+        basePath: '../',
+        frameworks: [
+          'mocha',
+          'chai'
+        ],
+        files: [
+            'dist/dygraph-combined-dev.js',
+            'src/extras/smooth-plotter.js',
+            'auto_tests/**/*.js',
+        ],
+        autoWatch: false,
+        singleRun: true,
+        reporters: ['mocha', 'coverage'],  // or 'dots', 'mocha', 'spec'
+        preprocessors: {
+            'dist/dygraph-combined-dev.js': ['coverage']
+        },
+        coverageReporter: {
+            dir: 'dist/coverage',
+            reporters: [
+              { type: 'html', subdir: 'report-html' },
+              { type: 'lcovonly', subdir: 'report-lcov' },
+            ]
+        },
+        browsers: ['PhantomJS'],
+        plugins: [
+            'karma-mocha',
+            'karma-chai-plugins',
+            'karma-phantomjs-launcher',
+            'karma-coverage',
+            'karma-spec-reporter',
+            'karma-mocha-reporter'
+        ]
+    });
+};
diff --git a/auto_tests/lib/Asserts.js b/auto_tests/lib/Asserts.js
deleted file mode 100644 (file)
index aa907c1..0000000
+++ /dev/null
@@ -1,644 +0,0 @@
-/**
- * Copyright 2009 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-function expectAsserts(count) {
-  jstestdriver.expectedAssertCount = count;
-}
-
-
-var fail = function fail(msg) {
-  var err = new Error(msg);
-  err.name = 'AssertError';
-
-  if (!err.message) {
-    err.message = msg;
-  }
-
-  throw err;
-};
-
-
-function isBoolean_(bool) {
-  if (typeof(bool) != 'boolean') {
-    fail('Not a boolean: ' + prettyPrintEntity_(bool));
-  }
-}
-
-
-var isElement_ = (function () {
-  var div = document.createElement('div');
-
-  function isNode(obj) {
-    try {
-      div.appendChild(obj);
-      div.removeChild(obj);
-    } catch (e) {
-      return false;
-    }
-
-    return true;
-  }
-
-  return function isElement(obj) {
-    return obj && obj.nodeType === 1 && isNode(obj);
-  };
-}());
-
-
-function formatElement_(el) {
-  var tagName;
-
-  try {
-    tagName = el.tagName.toLowerCase();
-    var str = '<' + tagName;
-    var attrs = el.attributes, attribute;
-
-    for (var i = 0, l = attrs.length; i < l; i++) {
-      attribute = attrs.item(i);
-
-      if (!!attribute.nodeValue) {
-        str += ' ' + attribute.nodeName + '=\"' + attribute.nodeValue + '\"';
-      }
-    }
-
-    return str + '>...</' + tagName + '>';
-  } catch (e) {
-    return '[Element]' + (!!tagName ? ' ' + tagName : '');
-  }
-}
-
-
-function prettyPrintEntity_(entity) {
-  if (isElement_(entity)) {
-    return formatElement_(entity);
-  }
-
-  var str;
-
-  if (typeof entity == 'function') {
-    try {
-      str = entity.toString().match(/(function [^\(]+\(\))/)[1];
-    } catch (e) {}
-
-    return str || '[function]';
-  }
-
-  try {
-    str = JSON.stringify(entity);
-  } catch (e) {}
-
-  return str || '[' + typeof entity + ']';
-}
-
-
-function argsWithOptionalMsg_(args, length) {
-  var copyOfArgs = [];
-  // make copy because it's bad practice to change a passed in mutable
-  // And to ensure we aren't working with an arguments array. IE gets bitchy.
-  for(var i = 0; i < args.length; i++) {
-    copyOfArgs.push(args[i]);
-  }
-  var min = length - 1;
-
-  if (args.length < min) {
-    fail('expected at least ' + min + ' arguments, got ' + args.length);
-  } else if (args.length == length) {
-    copyOfArgs[0] += ' ';
-  } else {
-    copyOfArgs.unshift('');
-  }
-  return copyOfArgs;
-}
-
-
-function assertTrue(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  isBoolean_(args[1]);
-  if (args[1] != true) {
-    fail(args[0] + 'expected true but was ' + prettyPrintEntity_(args[1]));
-  }
-  return true;
-}
-
-
-function assertFalse(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  isBoolean_(args[1]);
-  if (args[1] != false) {
-    fail(args[0] + 'expected false but was ' + prettyPrintEntity_(args[1]));
-  }
-  return true;
-}
-
-
-function assertEquals(msg, expected, actual) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  jstestdriver.assertCount++;
-  msg = args[0];
-  expected = args[1];
-  actual = args[2];
-
-  if (!compare_(expected, actual)) {
-    fail(msg + 'expected ' + prettyPrintEntity_(expected) + ' but was ' +
-        prettyPrintEntity_(actual) + '');
-  }
-  return true;
-}
-
-
-function compare_(expected, actual) {
-  if (expected === actual) {
-    return true;
-  }
-
-  if (typeof expected != 'object' ||
-      typeof actual != 'object' ||
-      !expected || !actual) {
-    return expected == actual;
-  }
-
-  if (isElement_(expected) || isElement_(actual)) {
-    return false;
-  }
-
-  var key = null;
-  var actualLength   = 0;
-  var expectedLength = 0;
-
-  try {
-    // If an array is expected the length of actual should be simple to
-    // determine. If it is not it is undefined.
-    if (jstestdriver.jQuery.isArray(actual)) {
-      actualLength = actual.length;
-    } else {
-      // In case it is an object it is a little bit more complicated to
-      // get the length.
-      for (key in actual) {
-        if (actual.hasOwnProperty(key)) {
-          ++actualLength;
-        }
-      }
-    }
-
-    // Arguments object
-    if (actualLength == 0 && typeof actual.length == 'number') {
-      actualLength = actual.length;
-
-      for (var i = 0, l = actualLength; i < l; i++) {
-        if (!(i in actual)) {
-          actualLength = 0;
-          break;
-        }
-      }
-    }
-
-    for (key in expected) {
-      if (expected.hasOwnProperty(key)) {
-        if (!compare_(expected[key], actual[key])) {
-          return false;
-        }
-
-        ++expectedLength;
-      }
-    }
-
-    if (expectedLength != actualLength) {
-      return false;
-    }
-
-    return expectedLength == 0 ? expected.toString() == actual.toString() : true;
-  } catch (e) {
-    return false;
-  }
-}
-
-
-function assertNotEquals(msg, expected, actual) {
-  try {
-    assertEquals.apply(this, arguments);
-  } catch (e) {
-    if (e.name == 'AssertError') {
-      return true;
-    }
-
-    throw e;
-  }
-
-  var args = argsWithOptionalMsg_(arguments, 3);
-
-  fail(args[0] + 'expected ' + prettyPrintEntity_(args[1]) +
-      ' not to be equal to ' + prettyPrintEntity_(args[2]));
-}
-
-
-function assertSame(msg, expected, actual) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  jstestdriver.assertCount++;
-
-  if (!isSame_(args[2], args[1])) {
-    fail(args[0] + 'expected ' + prettyPrintEntity_(args[1]) + ' but was ' +
-        prettyPrintEntity_(args[2]));
-  }
-  return true;
-}
-
-
-function assertNotSame(msg, expected, actual) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  jstestdriver.assertCount++;
-
-  if (isSame_(args[2], args[1])) {
-    fail(args[0] + 'expected not same as ' + prettyPrintEntity_(args[1]) +
-        ' but was ' + prettyPrintEntity_(args[2]));
-  }
-  return true;
-}
-
-
-function isSame_(expected, actual) {
-  return actual === expected;
-}
-
-
-function assertNull(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (args[1] !== null) {
-    fail(args[0] + 'expected null but was ' + prettyPrintEntity_(args[1]));
-  }
-  return true;
-}
-
-
-function assertNotNull(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (args[1] === null) {
-    fail(args[0] + 'expected not null but was null');
-  }
-
-  return true;
-}
-
-
-function assertUndefined(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (typeof args[1] != 'undefined') {
-    fail(args[2] + 'expected undefined but was ' + prettyPrintEntity_(args[1]));
-  }
-  return true;
-}
-
-
-function assertNotUndefined(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (typeof args[1] == 'undefined') {
-    fail(args[0] + 'expected not undefined but was undefined');
-  }
-  return true;
-}
-
-
-function assertNaN(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (!isNaN(args[1])) {
-    fail(args[0] + 'expected to be NaN but was ' + args[1]);
-  }
-
-  return true;
-}
-
-
-function assertNotNaN(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (isNaN(args[1])) {
-    fail(args[0] + 'expected not to be NaN');
-  }
-
-  return true;
-}
-
-
-function assertException(msg, callback, error) {
-  if (arguments.length == 1) {
-    // assertThrows(callback)
-    callback = msg;
-    msg = '';
-  } else if (arguments.length == 2) {
-    if (typeof callback != 'function') {
-      // assertThrows(callback, type)
-      error = callback;
-      callback = msg;
-      msg = '';
-    } else {
-      // assertThrows(msg, callback)
-      msg += ' ';
-    }
-  } else {
-    // assertThrows(msg, callback, type)
-    msg += ' ';
-  }
-
-  jstestdriver.assertCount++;
-
-  try {
-    callback();
-  } catch(e) {
-    if (e.name == 'AssertError') {
-      throw e;
-    }
-
-    if (error && e.name != error) {
-      fail(msg + 'expected to throw ' + error + ' but threw ' + e.name);
-    }
-
-    return true;
-  }
-
-  fail(msg + 'expected to throw exception');
-}
-
-
-function assertNoException(msg, callback) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  try {
-    args[1]();
-  } catch(e) {
-    fail(args[0] + 'expected not to throw exception, but threw ' + e.name +
-        ' (' + e.message + ')');
-  }
-}
-
-
-function assertArray(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  jstestdriver.assertCount++;
-
-  if (!jstestdriver.jQuery.isArray(args[1])) {
-    fail(args[0] + 'expected to be array, but was ' +
-        prettyPrintEntity_(args[1]));
-  }
-}
-
-
-function assertTypeOf(msg, expected, value) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  jstestdriver.assertCount++;
-  var actual = typeof args[2];
-
-  if (actual != args[1]) {
-    fail(args[0] + 'expected to be ' + args[1] + ' but was ' + actual);
-  }
-
-  return true;
-}
-
-
-function assertBoolean(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  return assertTypeOf(args[0], 'boolean', args[1]);
-}
-
-
-function assertFunction(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  return assertTypeOf(args[0], 'function', args[1]);
-}
-
-
-function assertObject(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  return assertTypeOf(args[0], 'object', args[1]);
-}
-
-
-function assertNumber(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  return assertTypeOf(args[0], 'number', args[1]);
-}
-
-
-function assertString(msg, actual) {
-  var args = argsWithOptionalMsg_(arguments, 2);
-  return assertTypeOf(args[0], 'string', args[1]);
-}
-
-
-function assertMatch(msg, regexp, actual) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  var isUndef = typeof args[2] == 'undefined';
-  jstestdriver.assertCount++;
-  var _undef;
-
-  if (isUndef || !args[1].test(args[2])) {
-    actual = (isUndef ? _undef : prettyPrintEntity_(args[2]));
-    fail(args[0] + 'expected ' + actual + ' to match ' + args[1]);
-  }
-
-  return true;
-}
-
-
-function assertNoMatch(msg, regexp, actual) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  jstestdriver.assertCount++;
-
-  if (args[1].test(args[2])) {
-    fail(args[0] + 'expected ' + prettyPrintEntity_(args[2]) +
-        ' not to match ' + args[1]);
-  }
-
-  return true;
-}
-
-
-function assertTagName(msg, tagName, element) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  var actual = args[2] && args[2].tagName;
-
-  if (String(actual).toUpperCase() != args[1].toUpperCase()) {
-    fail(args[0] + 'expected tagName to be ' + args[1] + ' but was ' + actual);
-  }
-  return true;
-}
-
-
-function assertClassName(msg, className, element) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  var actual = args[2] && args[2].className;
-  var regexp = new RegExp('(^|\\s)' + args[1] + '(\\s|$)');
-
-  try {
-    assertMatch(args[0], regexp, actual);
-  } catch (e) {
-    actual = prettyPrintEntity_(actual);
-    fail(args[0] + 'expected class name to include ' +
-        prettyPrintEntity_(args[1]) + ' but was ' + actual);
-  }
-
-  return true;
-}
-
-
-function assertElementId(msg, id, element) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  var actual = args[2] && args[2].id;
-  jstestdriver.assertCount++;
-
-  if (actual !== args[1]) {
-    fail(args[0] + 'expected id to be ' + args[1] + ' but was ' + actual);
-  }
-
-  return true;
-}
-
-
-function assertInstanceOf(msg, constructor, actual) {
-  jstestdriver.assertCount++;
-  var args = argsWithOptionalMsg_(arguments, 3);
-  var pretty = prettyPrintEntity_(args[2]);
-  var expected = args[1] && args[1].name || args[1];
-
-  if (args[2] == null) {
-    fail(args[0] + 'expected ' + pretty + ' to be instance of ' + expected);
-  }
-
-  if (!(Object(args[2]) instanceof args[1])) {
-    fail(args[0] + 'expected ' + pretty + ' to be instance of ' + expected);
-  }
-
-  return true;
-}
-
-
-function assertNotInstanceOf(msg, constructor, actual) {
-  var args = argsWithOptionalMsg_(arguments, 3);
-  jstestdriver.assertCount++;
-
-  if (Object(args[2]) instanceof args[1]) {
-    var expected = args[1] && args[1].name || args[1];
-    var pretty = prettyPrintEntity_(args[2]);
-    fail(args[0] + 'expected ' + pretty + ' not to be instance of ' + expected);
-  }
-
-  return true;
-}
-
-/**
- * Asserts that two doubles, or the elements of two arrays of doubles,
- * are equal to within a positive delta.
- */
-function assertEqualsDelta(msg, expected, actual, epsilon) {
-  var args = this.argsWithOptionalMsg_(arguments, 4);
-  jstestdriver.assertCount++;
-  msg = args[0];
-  expected = args[1];
-  actual = args[2];
-  epsilon = args[3];
-
-  if (!compareDelta_(expected, actual, epsilon)) {
-    this.fail(msg + 'expected ' + epsilon + ' within ' +
-              this.prettyPrintEntity_(expected) +
-              ' but was ' + this.prettyPrintEntity_(actual) + '');
-  }
-  return true;
-};
-
-function compareDelta_(expected, actual, epsilon) {
-  var compareDouble = function(e,a,d) {
-    return Math.abs(e - a) <= d;
-  }
-  if (expected === actual) {
-    return true;
-  }
-
-  if (typeof expected == "number" ||
-      typeof actual == "number" ||
-      !expected || !actual) {
-    return compareDouble(expected, actual, epsilon);
-  }
-
-  if (isElement_(expected) || isElement_(actual)) {
-    return false;
-  }
-
-  var key = null;
-  var actualLength   = 0;
-  var expectedLength = 0;
-
-  try {
-    // If an array is expected the length of actual should be simple to
-    // determine. If it is not it is undefined.
-    if (jstestdriver.jQuery.isArray(actual)) {
-      actualLength = actual.length;
-    } else {
-      // In case it is an object it is a little bit more complicated to
-      // get the length.
-      for (key in actual) {
-        if (actual.hasOwnProperty(key)) {
-          ++actualLength;
-        }
-      }
-    }
-
-    // Arguments object
-    if (actualLength == 0 && typeof actual.length == "number") {
-      actualLength = actual.length;
-
-      for (var i = 0, l = actualLength; i < l; i++) {
-        if (!(i in actual)) {
-          actualLength = 0;
-          break;
-        }
-      }
-    }
-
-    for (key in expected) {
-      if (expected.hasOwnProperty(key)) {
-        if (!compareDelta_(expected[key], actual[key], epsilon)) {
-          return false;
-        }
-
-        ++expectedLength;
-      }
-    }
-
-    if (expectedLength != actualLength) {
-      return false;
-    }
-
-    return expectedLength == 0 ? expected.toString() == actual.toString() : true;
-  } catch (e) {
-    return false;
-  }
-};
-
-var assert = assertTrue;
diff --git a/auto_tests/lib/JsTestDriver-1.3.3c.jar b/auto_tests/lib/JsTestDriver-1.3.3c.jar
deleted file mode 100644 (file)
index 39b0054..0000000
Binary files a/auto_tests/lib/JsTestDriver-1.3.3c.jar and /dev/null differ
diff --git a/auto_tests/lib/coverage-1.3.5.jar b/auto_tests/lib/coverage-1.3.5.jar
deleted file mode 100644 (file)
index a54010d..0000000
Binary files a/auto_tests/lib/coverage-1.3.5.jar and /dev/null differ
diff --git a/auto_tests/lib/jquery-1.4.2.js b/auto_tests/lib/jquery-1.4.2.js
deleted file mode 100644 (file)
index fff6776..0000000
+++ /dev/null
@@ -1,6240 +0,0 @@
-/*!
- * jQuery JavaScript Library v1.4.2
- * http://jquery.com/
- *
- * Copyright 2010, John Resig
- * Dual licensed under the MIT or GPL Version 2 licenses.
- * http://jquery.org/license
- *
- * Includes Sizzle.js
- * http://sizzlejs.com/
- * Copyright 2010, The Dojo Foundation
- * Released under the MIT, BSD, and GPL Licenses.
- *
- * Date: Sat Feb 13 22:33:48 2010 -0500
- */
-(function( window, undefined ) {
-
-// Define a local copy of jQuery
-var jQuery = function( selector, context ) {
-               // The jQuery object is actually just the init constructor 'enhanced'
-               return new jQuery.fn.init( selector, context );
-       },
-
-       // Map over jQuery in case of overwrite
-       _jQuery = window.jQuery,
-
-       // Map over the $ in case of overwrite
-       _$ = window.$,
-
-       // Use the correct document accordingly with window argument (sandbox)
-       document = window.document,
-
-       // A central reference to the root jQuery(document)
-       rootjQuery,
-
-       // A simple way to check for HTML strings or ID strings
-       // (both of which we optimize for)
-       quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,
-
-       // Is it a simple selector
-       isSimple = /^.[^:#\[\.,]*$/,
-
-       // Check if a string has a non-whitespace character in it
-       rnotwhite = /\S/,
-
-       // Used for trimming whitespace
-       rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g,
-
-       // Match a standalone tag
-       rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
-
-       // Keep a UserAgent string for use with jQuery.browser
-       userAgent = navigator.userAgent,
-
-       // For matching the engine and version of the browser
-       browserMatch,
-       
-       // Has the ready events already been bound?
-       readyBound = false,
-       
-       // The functions to execute on DOM ready
-       readyList = [],
-
-       // The ready event handler
-       DOMContentLoaded,
-
-       // Save a reference to some core methods
-       toString = Object.prototype.toString,
-       hasOwnProperty = Object.prototype.hasOwnProperty,
-       push = Array.prototype.push,
-       slice = Array.prototype.slice,
-       indexOf = Array.prototype.indexOf;
-
-jQuery.fn = jQuery.prototype = {
-       init: function( selector, context ) {
-               var match, elem, ret, doc;
-
-               // Handle $(""), $(null), or $(undefined)
-               if ( !selector ) {
-                       return this;
-               }
-
-               // Handle $(DOMElement)
-               if ( selector.nodeType ) {
-                       this.context = this[0] = selector;
-                       this.length = 1;
-                       return this;
-               }
-               
-               // The body element only exists once, optimize finding it
-               if ( selector === "body" && !context ) {
-                       this.context = document;
-                       this[0] = document.body;
-                       this.selector = "body";
-                       this.length = 1;
-                       return this;
-               }
-
-               // Handle HTML strings
-               if ( typeof selector === "string" ) {
-                       // Are we dealing with HTML string or an ID?
-                       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] ) {
-                                       doc = (context ? context.ownerDocument || context : document);
-
-                                       // If a single string is passed in and it's a single tag
-                                       // just do a createElement and skip the rest
-                                       ret = rsingleTag.exec( selector );
-
-                                       if ( ret ) {
-                                               if ( jQuery.isPlainObject( context ) ) {
-                                                       selector = [ document.createElement( ret[1] ) ];
-                                                       jQuery.fn.attr.call( selector, context, true );
-
-                                               } else {
-                                                       selector = [ doc.createElement( ret[1] ) ];
-                                               }
-
-                                       } else {
-                                               ret = buildFragment( [ match[1] ], [ doc ] );
-                                               selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes;
-                                       }
-                                       
-                                       return jQuery.merge( this, selector );
-                                       
-                               // HANDLE: $("#id")
-                               } else {
-                                       elem = document.getElementById( match[2] );
-
-                                       if ( elem ) {
-                                               // Handle the case where IE and Opera return items
-                                               // by name instead of ID
-                                               if ( elem.id !== match[2] ) {
-                                                       return rootjQuery.find( selector );
-                                               }
-
-                                               // Otherwise, we inject the element directly into the jQuery object
-                                               this.length = 1;
-                                               this[0] = elem;
-                                       }
-
-                                       this.context = document;
-                                       this.selector = selector;
-                                       return this;
-                               }
-
-                       // HANDLE: $("TAG")
-                       } else if ( !context && /^\w+$/.test( selector ) ) {
-                               this.selector = selector;
-                               this.context = document;
-                               selector = document.getElementsByTagName( selector );
-                               return jQuery.merge( this, selector );
-
-                       // HANDLE: $(expr, $(...))
-                       } else if ( !context || context.jquery ) {
-                               return (context || rootjQuery).find( selector );
-
-                       // HANDLE: $(expr, context)
-                       // (which is just equivalent to: $(context).find(expr)
-                       } else {
-                               return jQuery( context ).find( selector );
-                       }
-
-               // HANDLE: $(function)
-               // Shortcut for document ready
-               } else if ( jQuery.isFunction( selector ) ) {
-                       return rootjQuery.ready( selector );
-               }
-
-               if (selector.selector !== undefined) {
-                       this.selector = selector.selector;
-                       this.context = selector.context;
-               }
-
-               return jQuery.makeArray( selector, this );
-       },
-
-       // Start with an empty selector
-       selector: "",
-
-       // The current version of jQuery being used
-       jquery: "1.4.2",
-
-       // The default length of a jQuery object is 0
-       length: 0,
-
-       // The number of elements contained in the matched element set
-       size: function() {
-               return this.length;
-       },
-
-       toArray: function() {
-               return slice.call( this, 0 );
-       },
-
-       // Get the Nth element in the matched element set OR
-       // Get the whole matched element set as a clean array
-       get: function( num ) {
-               return num == null ?
-
-                       // Return a 'clean' array
-                       this.toArray() :
-
-                       // Return just the object
-                       ( num < 0 ? this.slice(num)[ 0 ] : 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();
-
-               if ( jQuery.isArray( elems ) ) {
-                       push.apply( ret, elems );
-               
-               } else {
-                       jQuery.merge( ret, 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;
-       },
-
-       // Execute a callback for every element in the matched set.
-       // (You can seed the arguments with an array of args, but this is
-       // only used internally.)
-       each: function( callback, args ) {
-               return jQuery.each( this, callback, args );
-       },
-       
-       ready: function( fn ) {
-               // Attach the listeners
-               jQuery.bindReady();
-
-               // If the DOM is already ready
-               if ( jQuery.isReady ) {
-                       // Execute the function immediately
-                       fn.call( document, jQuery );
-
-               // Otherwise, remember the function for later
-               } else if ( readyList ) {
-                       // Add the function to the wait list
-                       readyList.push( fn );
-               }
-
-               return this;
-       },
-       
-       eq: function( i ) {
-               return i === -1 ?
-                       this.slice( i ) :
-                       this.slice( i, +i + 1 );
-       },
-
-       first: function() {
-               return this.eq( 0 );
-       },
-
-       last: function() {
-               return this.eq( -1 );
-       },
-
-       slice: function() {
-               return this.pushStack( slice.apply( this, arguments ),
-                       "slice", slice.call(arguments).join(",") );
-       },
-
-       map: function( callback ) {
-               return this.pushStack( jQuery.map(this, function( elem, i ) {
-                       return callback.call( elem, i, elem );
-               }));
-       },
-       
-       end: function() {
-               return this.prevObject || jQuery(null);
-       },
-
-       // For internal use only.
-       // Behaves like an Array's method, not like a jQuery method.
-       push: push,
-       sort: [].sort,
-       splice: [].splice
-};
-
-// Give the init function the jQuery prototype for later instantiation
-jQuery.fn.init.prototype = jQuery.fn;
-
-jQuery.extend = jQuery.fn.extend = function() {
-       // copy reference to target object
-       var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
-
-       // Handle a deep copy situation
-       if ( typeof target === "boolean" ) {
-               deep = target;
-               target = arguments[1] || {};
-               // skip the boolean and the target
-               i = 2;
-       }
-
-       // Handle case when target is a string or something (possible in deep copy)
-       if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
-               target = {};
-       }
-
-       // extend jQuery itself if only one argument is passed
-       if ( length === i ) {
-               target = this;
-               --i;
-       }
-
-       for ( ; i < length; i++ ) {
-               // Only deal with non-null/undefined values
-               if ( (options = arguments[ i ]) != null ) {
-                       // Extend the base object
-                       for ( name in options ) {
-                               src = target[ name ];
-                               copy = options[ name ];
-
-                               // Prevent never-ending loop
-                               if ( target === copy ) {
-                                       continue;
-                               }
-
-                               // Recurse if we're merging object literal values or arrays
-                               if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) {
-                                       var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src
-                                               : jQuery.isArray(copy) ? [] : {};
-
-                                       // Never move original objects, clone them
-                                       target[ name ] = jQuery.extend( deep, clone, copy );
-
-                               // Don't bring in undefined values
-                               } else if ( copy !== undefined ) {
-                                       target[ name ] = copy;
-                               }
-                       }
-               }
-       }
-
-       // Return the modified object
-       return target;
-};
-
-jQuery.extend({
-       noConflict: function( deep ) {
-               window.$ = _$;
-
-               if ( deep ) {
-                       window.jQuery = _jQuery;
-               }
-
-               return jQuery;
-       },
-       
-       // Is the DOM ready to be used? Set to true once it occurs.
-       isReady: false,
-       
-       // Handle when the DOM is ready
-       ready: function() {
-               // Make sure that the DOM is not already loaded
-               if ( !jQuery.isReady ) {
-                       // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
-                       if ( !document.body ) {
-                               return setTimeout( jQuery.ready, 13 );
-                       }
-
-                       // Remember that the DOM is ready
-                       jQuery.isReady = true;
-
-                       // If there are functions bound, to execute
-                       if ( readyList ) {
-                               // Execute all of them
-                               var fn, i = 0;
-                               while ( (fn = readyList[ i++ ]) ) {
-                                       fn.call( document, jQuery );
-                               }
-
-                               // Reset the list of functions
-                               readyList = null;
-                       }
-
-                       // Trigger any bound ready events
-                       if ( jQuery.fn.triggerHandler ) {
-                               jQuery( document ).triggerHandler( "ready" );
-                       }
-               }
-       },
-       
-       bindReady: function() {
-               if ( readyBound ) {
-                       return;
-               }
-
-               readyBound = true;
-
-               // Catch cases where $(document).ready() is called after the
-               // browser event has already occurred.
-               if ( document.readyState === "complete" ) {
-                       return jQuery.ready();
-               }
-
-               // Mozilla, Opera and webkit nightlies currently support this event
-               if ( document.addEventListener ) {
-                       // Use the handy event callback
-                       document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
-                       
-                       // A fallback to window.onload, that will always work
-                       window.addEventListener( "load", jQuery.ready, false );
-
-               // If IE event model is used
-               } else if ( document.attachEvent ) {
-                       // ensure firing before onload,
-                       // maybe late but safe also for iframes
-                       document.attachEvent("onreadystatechange", DOMContentLoaded);
-                       
-                       // A fallback to window.onload, that will always work
-                       window.attachEvent( "onload", jQuery.ready );
-
-                       // If IE and not a frame
-                       // continually check to see if the document is ready
-                       var toplevel = false;
-
-                       try {
-                               toplevel = window.frameElement == null;
-                       } catch(e) {}
-
-                       if ( document.documentElement.doScroll && toplevel ) {
-                               doScrollCheck();
-                       }
-               }
-       },
-
-       // 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]";
-       },
-
-       isPlainObject: function( obj ) {
-               // Must be an Object.
-               // Because of IE, we also have to check the presence of the constructor property.
-               // Make sure that DOM nodes and window objects don't pass through, as well
-               if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) {
-                       return false;
-               }
-               
-               // Not own constructor property must be Object
-               if ( obj.constructor
-                       && !hasOwnProperty.call(obj, "constructor")
-                       && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) {
-                       return false;
-               }
-               
-               // Own properties are enumerated firstly, so to speed up,
-               // if last one is own, then all properties are own.
-       
-               var key;
-               for ( key in obj ) {}
-               
-               return key === undefined || hasOwnProperty.call( obj, key );
-       },
-
-       isEmptyObject: function( obj ) {
-               for ( var name in obj ) {
-                       return false;
-               }
-               return true;
-       },
-       
-       error: function( msg ) {
-               throw msg;
-       },
-       
-       parseJSON: function( data ) {
-               if ( typeof data !== "string" || !data ) {
-                       return null;
-               }
-
-               // Make sure leading/trailing whitespace is removed (IE can't handle it)
-               data = jQuery.trim( data );
-               
-               // Make sure the incoming data is actual JSON
-               // Logic borrowed from http://json.org/json2.js
-               if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
-                       .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
-                       .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) {
-
-                       // Try to use the native JSON parser first
-                       return window.JSON && window.JSON.parse ?
-                               window.JSON.parse( data ) :
-                               (new Function("return " + data))();
-
-               } else {
-                       jQuery.error( "Invalid JSON: " + data );
-               }
-       },
-
-       noop: function() {},
-
-       // Evalulates a script in a global context
-       globalEval: function( data ) {
-               if ( data && rnotwhite.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,
-                       isObj = length === undefined || jQuery.isFunction(object);
-
-               if ( args ) {
-                       if ( isObj ) {
-                               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 ( isObj ) {
-                               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;
-       },
-
-       trim: function( text ) {
-               return (text || "").replace( rtrim, "" );
-       },
-
-       // results is for internal usage only
-       makeArray: function( array, results ) {
-               var ret = results || [];
-
-               if ( array != null ) {
-                       // The window, strings (and functions) also have 'length'
-                       // The extra typeof function check is to prevent crashes
-                       // in Safari 2 (See: #3039)
-                       if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) {
-                               push.call( ret, array );
-                       } else {
-                               jQuery.merge( ret, array );
-                       }
-               }
-
-               return ret;
-       },
-
-       inArray: function( elem, array ) {
-               if ( array.indexOf ) {
-                       return array.indexOf( elem );
-               }
-
-               for ( var i = 0, length = array.length; i < length; i++ ) {
-                       if ( array[ i ] === elem ) {
-                               return i;
-                       }
-               }
-
-               return -1;
-       },
-
-       merge: function( first, second ) {
-               var i = first.length, j = 0;
-
-               if ( typeof second.length === "number" ) {
-                       for ( var l = second.length; j < l; j++ ) {
-                               first[ i++ ] = second[ j ];
-                       }
-               
-               } else {
-                       while ( second[j] !== undefined ) {
-                               first[ i++ ] = second[ j++ ];
-                       }
-               }
-
-               first.length = i;
-
-               return first;
-       },
-
-       grep: function( elems, callback, inv ) {
-               var 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;
-       },
-
-       // arg is for internal usage only
-       map: function( elems, callback, arg ) {
-               var ret = [], value;
-
-               // 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++ ) {
-                       value = callback( elems[ i ], i, arg );
-
-                       if ( value != null ) {
-                               ret[ ret.length ] = value;
-                       }
-               }
-
-               return ret.concat.apply( [], ret );
-       },
-
-       // A global GUID counter for objects
-       guid: 1,
-
-       proxy: function( fn, proxy, thisObject ) {
-               if ( arguments.length === 2 ) {
-                       if ( typeof proxy === "string" ) {
-                               thisObject = fn;
-                               fn = thisObject[ proxy ];
-                               proxy = undefined;
-
-                       } else if ( proxy && !jQuery.isFunction( proxy ) ) {
-                               thisObject = proxy;
-                               proxy = undefined;
-                       }
-               }
-
-               if ( !proxy && fn ) {
-                       proxy = function() {
-                               return fn.apply( thisObject || this, arguments );
-                       };
-               }
-
-               // Set the guid of unique handler to the same of original handler, so it can be removed
-               if ( fn ) {
-                       proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
-               }
-
-               // So proxy can be declared as an argument
-               return proxy;
-       },
-
-       // Use of jQuery.browser is frowned upon.
-       // More details: http://docs.jquery.com/Utilities/jQuery.browser
-       uaMatch: function( ua ) {
-               ua = ua.toLowerCase();
-
-               var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
-                       /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) ||
-                       /(msie) ([\w.]+)/.exec( ua ) ||
-                       !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) ||
-                       [];
-
-               return { browser: match[1] || "", version: match[2] || "0" };
-       },
-
-       browser: {}
-});
-
-browserMatch = jQuery.uaMatch( userAgent );
-if ( browserMatch.browser ) {
-       jQuery.browser[ browserMatch.browser ] = true;
-       jQuery.browser.version = browserMatch.version;
-}
-
-// Deprecated, use jQuery.browser.webkit instead
-if ( jQuery.browser.webkit ) {
-       jQuery.browser.safari = true;
-}
-
-if ( indexOf ) {
-       jQuery.inArray = function( elem, array ) {
-               return indexOf.call( array, elem );
-       };
-}
-
-// All jQuery objects should point back to these
-rootjQuery = jQuery(document);
-
-// Cleanup functions for the document ready method
-if ( document.addEventListener ) {
-       DOMContentLoaded = function() {
-               document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
-               jQuery.ready();
-       };
-
-} else if ( document.attachEvent ) {
-       DOMContentLoaded = function() {
-               // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
-               if ( document.readyState === "complete" ) {
-                       document.detachEvent( "onreadystatechange", DOMContentLoaded );
-                       jQuery.ready();
-               }
-       };
-}
-
-// The DOM ready check for Internet Explorer
-function doScrollCheck() {
-       if ( jQuery.isReady ) {
-               return;
-       }
-
-       try {
-               // If IE is used, use the trick by Diego Perini
-               // http://javascript.nwbox.com/IEContentLoaded/
-               document.documentElement.doScroll("left");
-       } catch( error ) {
-               setTimeout( doScrollCheck, 1 );
-               return;
-       }
-
-       // and execute any waiting functions
-       jQuery.ready();
-}
-
-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 );
-       }
-}
-
-// Mutifunctional method to get and set values to a collection
-// The value/s can be optionally by executed if its a function
-function access( elems, key, value, exec, fn, pass ) {
-       var length = elems.length;
-       
-       // Setting many attributes
-       if ( typeof key === "object" ) {
-               for ( var k in key ) {
-                       access( elems, k, key[k], exec, fn, value );
-               }
-               return elems;
-       }
-       
-       // Setting one attribute
-       if ( value !== undefined ) {
-               // Optionally, function values get executed if exec is true
-               exec = !pass && exec && jQuery.isFunction(value);
-               
-               for ( var i = 0; i < length; i++ ) {
-                       fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
-               }
-               
-               return elems;
-       }
-       
-       // Getting an attribute
-       return length ? fn( elems[0], key ) : undefined;
-}
-
-function now() {
-       return (new Date).getTime();
-}
-(function() {
-
-       jQuery.support = {};
-
-       var root = document.documentElement,
-               script = document.createElement("script"),
-               div = document.createElement("div"),
-               id = "script" + now();
-
-       div.style.display = "none";
-       div.innerHTML = "   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
-
-       var all = div.getElementsByTagName("*"),
-               a = div.getElementsByTagName("a")[0];
-
-       // Can't get basic test support
-       if ( !all || !all.length || !a ) {
-               return;
-       }
-
-       jQuery.support = {
-               // IE strips leading whitespace when .innerHTML is used
-               leadingWhitespace: div.firstChild.nodeType === 3,
-
-               // Make sure that tbody elements aren't automatically inserted
-               // IE will insert them into empty tables
-               tbody: !div.getElementsByTagName("tbody").length,
-
-               // Make sure that link elements get serialized correctly by innerHTML
-               // This requires a wrapper element in IE
-               htmlSerialize: !!div.getElementsByTagName("link").length,
-
-               // Get the style information from getAttribute
-               // (IE uses .cssText insted)
-               style: /red/.test( a.getAttribute("style") ),
-
-               // Make sure that URLs aren't manipulated
-               // (IE normalizes it by default)
-               hrefNormalized: a.getAttribute("href") === "/a",
-
-               // Make sure that element opacity exists
-               // (IE uses filter instead)
-               // Use a regex to work around a WebKit issue. See #5145
-               opacity: /^0.55$/.test( a.style.opacity ),
-
-               // Verify style float existence
-               // (IE uses styleFloat instead of cssFloat)
-               cssFloat: !!a.style.cssFloat,
-
-               // Make sure that if no value is specified for a checkbox
-               // that it defaults to "on".
-               // (WebKit defaults to "" instead)
-               checkOn: div.getElementsByTagName("input")[0].value === "on",
-
-               // Make sure that a selected-by-default option has a working selected property.
-               // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
-               optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected,
-
-               parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null,
-
-               // Will be defined later
-               deleteExpando: true,
-               checkClone: false,
-               scriptEval: false,
-               noCloneEvent: true,
-               boxModel: null
-       };
-
-       script.type = "text/javascript";
-       try {
-               script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
-       } catch(e) {}
-
-       root.insertBefore( script, root.firstChild );
-
-       // Make sure that the execution of code works by injecting a script
-       // tag with appendChild/createTextNode
-       // (IE doesn't support this, fails, and uses .text instead)
-       if ( window[ id ] ) {
-               jQuery.support.scriptEval = true;
-               delete window[ id ];
-       }
-
-       // Test to see if it's possible to delete an expando from an element
-       // Fails in Internet Explorer
-       try {
-               delete script.test;
-       
-       } catch(e) {
-               jQuery.support.deleteExpando = false;
-       }
-
-       root.removeChild( script );
-
-       if ( div.attachEvent && div.fireEvent ) {
-               div.attachEvent("onclick", function click() {
-                       // Cloning a node shouldn't copy over any
-                       // bound event handlers (IE does this)
-                       jQuery.support.noCloneEvent = false;
-                       div.detachEvent("onclick", click);
-               });
-               div.cloneNode(true).fireEvent("onclick");
-       }
-
-       div = document.createElement("div");
-       div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";
-
-       var fragment = document.createDocumentFragment();
-       fragment.appendChild( div.firstChild );
-
-       // WebKit doesn't clone checked state correctly in fragments
-       jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked;
-
-       // Figure out if the W3C box model works as expected
-       // document.body must exist before we can do this
-       jQuery(function() {
-               var div = document.createElement("div");
-               div.style.width = div.style.paddingLeft = "1px";
-
-               document.body.appendChild( div );
-               jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
-               document.body.removeChild( div ).style.display = 'none';
-
-               div = null;
-       });
-
-       // Technique from Juriy Zaytsev
-       // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
-       var eventSupported = function( eventName ) { 
-               var el = document.createElement("div"); 
-               eventName = "on" + eventName; 
-
-               var isSupported = (eventName in el); 
-               if ( !isSupported ) { 
-                       el.setAttribute(eventName, "return;"); 
-                       isSupported = typeof el[eventName] === "function"; 
-               } 
-               el = null; 
-
-               return isSupported; 
-       };
-       
-       jQuery.support.submitBubbles = eventSupported("submit");
-       jQuery.support.changeBubbles = eventSupported("change");
-
-       // release memory in IE
-       root = script = div = all = a = null;
-})();
-
-jQuery.props = {
-       "for": "htmlFor",
-       "class": "className",
-       readonly: "readOnly",
-       maxlength: "maxLength",
-       cellspacing: "cellSpacing",
-       rowspan: "rowSpan",
-       colspan: "colSpan",
-       tabindex: "tabIndex",
-       usemap: "useMap",
-       frameborder: "frameBorder"
-};
-var expando = "jQuery" + now(), uuid = 0, windowData = {};
-
-jQuery.extend({
-       cache: {},
-       
-       expando:expando,
-
-       // The following elements throw uncatchable exceptions if you
-       // attempt to add expando properties to them.
-       noData: {
-               "embed": true,
-               "object": true,
-               "applet": true
-       },
-
-       data: function( elem, name, data ) {
-               if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
-                       return;
-               }
-
-               elem = elem == window ?
-                       windowData :
-                       elem;
-
-               var id = elem[ expando ], cache = jQuery.cache, thisCache;
-
-               if ( !id && typeof name === "string" && data === undefined ) {
-                       return null;
-               }
-
-               // Compute a unique ID for the element
-               if ( !id ) { 
-                       id = ++uuid;
-               }
-
-               // Avoid generating a new cache unless none exists and we
-               // want to manipulate it.
-               if ( typeof name === "object" ) {
-                       elem[ expando ] = id;
-                       thisCache = cache[ id ] = jQuery.extend(true, {}, name);
-
-               } else if ( !cache[ id ] ) {
-                       elem[ expando ] = id;
-                       cache[ id ] = {};
-               }
-
-               thisCache = cache[ id ];
-
-               // Prevent overriding the named cache with undefined values
-               if ( data !== undefined ) {
-                       thisCache[ name ] = data;
-               }
-
-               return typeof name === "string" ? thisCache[ name ] : thisCache;
-       },
-
-       removeData: function( elem, name ) {
-               if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
-                       return;
-               }
-
-               elem = elem == window ?
-                       windowData :
-                       elem;
-
-               var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ];
-
-               // If we want to remove a specific section of the element's data
-               if ( name ) {
-                       if ( thisCache ) {
-                               // Remove the section of cache data
-                               delete thisCache[ name ];
-
-                               // If we've removed all the data, remove the element's cache
-                               if ( jQuery.isEmptyObject(thisCache) ) {
-                                       jQuery.removeData( elem );
-                               }
-                       }
-
-               // Otherwise, we want to remove all of the element's data
-               } else {
-                       if ( jQuery.support.deleteExpando ) {
-                               delete elem[ jQuery.expando ];
-
-                       } else if ( elem.removeAttribute ) {
-                               elem.removeAttribute( jQuery.expando );
-                       }
-
-                       // Completely remove the data cache
-                       delete cache[ id ];
-               }
-       }
-});
-
-jQuery.fn.extend({
-       data: function( key, value ) {
-               if ( typeof key === "undefined" && this.length ) {
-                       return jQuery.data( this[0] );
-
-               } else if ( typeof key === "object" ) {
-                       return this.each(function() {
-                               jQuery.data( this, key );
-                       });
-               }
-
-               var parts = key.split(".");
-               parts[1] = parts[1] ? "." + parts[1] : "";
-
-               if ( value === undefined ) {
-                       var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
-
-                       if ( data === undefined && this.length ) {
-                               data = jQuery.data( this[0], key );
-                       }
-                       return data === undefined && parts[1] ?
-                               this.data( parts[0] ) :
-                               data;
-               } else {
-                       return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() {
-                               jQuery.data( this, key, value );
-                       });
-               }
-       },
-
-       removeData: function( key ) {
-               return this.each(function() {
-                       jQuery.removeData( this, key );
-               });
-       }
-});
-jQuery.extend({
-       queue: function( elem, type, data ) {
-               if ( !elem ) {
-                       return;
-               }
-
-               type = (type || "fx") + "queue";
-               var q = jQuery.data( elem, type );
-
-               // Speed up dequeue by getting out quickly if this is just a lookup
-               if ( !data ) {
-                       return q || [];
-               }
-
-               if ( !q || jQuery.isArray(data) ) {
-                       q = jQuery.data( elem, type, jQuery.makeArray(data) );
-
-               } else {
-                       q.push( data );
-               }
-
-               return q;
-       },
-
-       dequeue: function( elem, type ) {
-               type = type || "fx";
-
-               var queue = jQuery.queue( elem, type ), fn = queue.shift();
-
-               // If the fx queue is dequeued, always remove the progress sentinel
-               if ( fn === "inprogress" ) {
-                       fn = queue.shift();
-               }
-
-               if ( fn ) {
-                       // Add a progress sentinel to prevent the fx queue from being
-                       // automatically dequeued
-                       if ( type === "fx" ) {
-                               queue.unshift("inprogress");
-                       }
-
-                       fn.call(elem, function() {
-                               jQuery.dequeue(elem, type);
-                       });
-               }
-       }
-});
-
-jQuery.fn.extend({
-       queue: function( type, data ) {
-               if ( typeof type !== "string" ) {
-                       data = type;
-                       type = "fx";
-               }
-
-               if ( data === undefined ) {
-                       return jQuery.queue( this[0], type );
-               }
-               return this.each(function( i, elem ) {
-                       var queue = jQuery.queue( this, type, data );
-
-                       if ( type === "fx" && queue[0] !== "inprogress" ) {
-                               jQuery.dequeue( this, type );
-                       }
-               });
-       },
-       dequeue: function( type ) {
-               return this.each(function() {
-                       jQuery.dequeue( this, type );
-               });
-       },
-
-       // Based off of the plugin by Clint Helfers, with permission.
-       // http://blindsignals.com/index.php/2009/07/jquery-delay/
-       delay: function( time, type ) {
-               time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
-               type = type || "fx";
-
-               return this.queue( type, function() {
-                       var elem = this;
-                       setTimeout(function() {
-                               jQuery.dequeue( elem, type );
-                       }, time );
-               });
-       },
-
-       clearQueue: function( type ) {
-               return this.queue( type || "fx", [] );
-       }
-});
-var rclass = /[\n\t]/g,
-       rspace = /\s+/,
-       rreturn = /\r/g,
-       rspecialurl = /href|src|style/,
-       rtype = /(button|input)/i,
-       rfocusable = /(button|input|object|select|textarea)/i,
-       rclickable = /^(a|area)$/i,
-       rradiocheck = /radio|checkbox/;
-
-jQuery.fn.extend({
-       attr: function( name, value ) {
-               return access( this, name, value, true, jQuery.attr );
-       },
-
-       removeAttr: function( name, fn ) {
-               return this.each(function(){
-                       jQuery.attr( this, name, "" );
-                       if ( this.nodeType === 1 ) {
-                               this.removeAttribute( name );
-                       }
-               });
-       },
-
-       addClass: function( value ) {
-               if ( jQuery.isFunction(value) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.addClass( value.call(this, i, self.attr("class")) );
-                       });
-               }
-
-               if ( value && typeof value === "string" ) {
-                       var classNames = (value || "").split( rspace );
-
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var elem = this[i];
-
-                               if ( elem.nodeType === 1 ) {
-                                       if ( !elem.className ) {
-                                               elem.className = value;
-
-                                       } else {
-                                               var className = " " + elem.className + " ", setClass = elem.className;
-                                               for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
-                                                       if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) {
-                                                               setClass += " " + classNames[c];
-                                                       }
-                                               }
-                                               elem.className = jQuery.trim( setClass );
-                                       }
-                               }
-                       }
-               }
-
-               return this;
-       },
-
-       removeClass: function( value ) {
-               if ( jQuery.isFunction(value) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.removeClass( value.call(this, i, self.attr("class")) );
-                       });
-               }
-
-               if ( (value && typeof value === "string") || value === undefined ) {
-                       var classNames = (value || "").split(rspace);
-
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var elem = this[i];
-
-                               if ( elem.nodeType === 1 && elem.className ) {
-                                       if ( value ) {
-                                               var className = (" " + elem.className + " ").replace(rclass, " ");
-                                               for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
-                                                       className = className.replace(" " + classNames[c] + " ", " ");
-                                               }
-                                               elem.className = jQuery.trim( className );
-
-                                       } else {
-                                               elem.className = "";
-                                       }
-                               }
-                       }
-               }
-
-               return this;
-       },
-
-       toggleClass: function( value, stateVal ) {
-               var type = typeof value, isBool = typeof stateVal === "boolean";
-
-               if ( jQuery.isFunction( value ) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal );
-                       });
-               }
-
-               return this.each(function() {
-                       if ( type === "string" ) {
-                               // toggle individual class names
-                               var className, i = 0, self = jQuery(this),
-                                       state = stateVal,
-                                       classNames = value.split( rspace );
-
-                               while ( (className = classNames[ i++ ]) ) {
-                                       // check each className given, space seperated list
-                                       state = isBool ? state : !self.hasClass( className );
-                                       self[ state ? "addClass" : "removeClass" ]( className );
-                               }
-
-                       } else if ( type === "undefined" || type === "boolean" ) {
-                               if ( this.className ) {
-                                       // store className if set
-                                       jQuery.data( this, "__className__", this.className );
-                               }
-
-                               // toggle whole className
-                               this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || "";
-                       }
-               });
-       },
-
-       hasClass: function( selector ) {
-               var className = " " + selector + " ";
-               for ( var i = 0, l = this.length; i < l; i++ ) {
-                       if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
-                               return true;
-                       }
-               }
-
-               return false;
-       },
-
-       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;
-                               }
-
-                               // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
-                               if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) {
-                                       return elem.getAttribute("value") === null ? "on" : elem.value;
-                               }
-                               
-
-                               // Everything else, we just grab the value
-                               return (elem.value || "").replace(rreturn, "");
-
-                       }
-
-                       return undefined;
-               }
-
-               var isFunction = jQuery.isFunction(value);
-
-               return this.each(function(i) {
-                       var self = jQuery(this), val = value;
-
-                       if ( this.nodeType !== 1 ) {
-                               return;
-                       }
-
-                       if ( isFunction ) {
-                               val = value.call(this, i, self.val());
-                       }
-
-                       // Typecast each time if the value is a Function and the appended
-                       // value is therefore different each time.
-                       if ( typeof val === "number" ) {
-                               val += "";
-                       }
-
-                       if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) {
-                               this.checked = jQuery.inArray( self.val(), val ) >= 0;
-
-                       } else if ( jQuery.nodeName( this, "select" ) ) {
-                               var values = jQuery.makeArray(val);
-
-                               jQuery( "option", this ).each(function() {
-                                       this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
-                               });
-
-                               if ( !values.length ) {
-                                       this.selectedIndex = -1;
-                               }
-
-                       } else {
-                               this.value = val;
-                       }
-               });
-       }
-});
-
-jQuery.extend({
-       attrFn: {
-               val: true,
-               css: true,
-               html: true,
-               text: true,
-               data: true,
-               width: true,
-               height: true,
-               offset: true
-       },
-               
-       attr: function( elem, name, value, pass ) {
-               // don't set attributes on text and comment nodes
-               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return undefined;
-               }
-
-               if ( pass && name in jQuery.attrFn ) {
-                       return jQuery(elem)[name](value);
-               }
-
-               var notxml = elem.nodeType !== 1 || !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)
-               if ( elem.nodeType === 1 ) {
-                       // These attributes require special treatment
-                       var special = rspecialurl.test( name );
-
-                       // Safari mis-reports the default selected property of an option
-                       // Accessing the parent's selectedIndex property fixes it
-                       if ( name === "selected" && !jQuery.support.optSelected ) {
-                               var parent = elem.parentNode;
-                               if ( parent ) {
-                                       parent.selectedIndex;
-       
-                                       // Make sure that it also works with optgroups, see #5701
-                                       if ( parent.parentNode ) {
-                                               parent.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" && rtype.test( elem.nodeName ) && elem.parentNode ) {
-                                               jQuery.error( "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 :
-                                               rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
-                                                       0 :
-                                                       undefined;
-                               }
-
-                               return elem[ name ];
-                       }
-
-                       if ( !jQuery.support.style && notxml && name === "style" ) {
-                               if ( set ) {
-                                       elem.style.cssText = "" + value;
-                               }
-
-                               return elem.style.cssText;
-                       }
-
-                       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
-               // Using attr for specific style information is now deprecated. Use style instead.
-               return jQuery.style( elem, name, value );
-       }
-});
-var rnamespaces = /\.(.*)$/,
-       fcleanup = function( nm ) {
-               return nm.replace(/[^\w\s\.\|`]/g, function( ch ) {
-                       return "\\" + ch;
-               });
-       };
-
-/*
- * A number of helper functions used for managing events.
- * Many of the ideas behind this code originated from
- * Dean Edwards' addEvent library.
- */
-jQuery.event = {
-
-       // Bind an event to an element
-       // Original by Dean Edwards
-       add: function( elem, types, handler, data ) {
-               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return;
-               }
-
-               // For whatever reason, IE has trouble passing the window object
-               // around, causing it to be cloned in the process
-               if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) {
-                       elem = window;
-               }
-
-               var handleObjIn, handleObj;
-
-               if ( handler.handler ) {
-                       handleObjIn = handler;
-                       handler = handleObjIn.handler;
-               }
-
-               // Make sure that the function being executed has a unique ID
-               if ( !handler.guid ) {
-                       handler.guid = jQuery.guid++;
-               }
-
-               // Init the element's event structure
-               var elemData = jQuery.data( elem );
-
-               // If no elemData is found then we must be trying to bind to one of the
-               // banned noData elements
-               if ( !elemData ) {
-                       return;
-               }
-
-               var events = elemData.events = elemData.events || {},
-                       eventHandle = elemData.handle, eventHandle;
-
-               if ( !eventHandle ) {
-                       elemData.handle = eventHandle = function() {
-                               // Handle the second event of a trigger and when
-                               // an event is called after a page has unloaded
-                               return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
-                                       jQuery.event.handle.apply( eventHandle.elem, arguments ) :
-                                       undefined;
-                       };
-               }
-
-               // Add elem as a property of the handle function
-               // This is to prevent a memory leak with non-native events in IE.
-               eventHandle.elem = elem;
-
-               // Handle multiple events separated by a space
-               // jQuery(...).bind("mouseover mouseout", fn);
-               types = types.split(" ");
-
-               var type, i = 0, namespaces;
-
-               while ( (type = types[ i++ ]) ) {
-                       handleObj = handleObjIn ?
-                               jQuery.extend({}, handleObjIn) :
-                               { handler: handler, data: data };
-
-                       // Namespaced event handlers
-                       if ( type.indexOf(".") > -1 ) {
-                               namespaces = type.split(".");
-                               type = namespaces.shift();
-                               handleObj.namespace = namespaces.slice(0).sort().join(".");
-
-                       } else {
-                               namespaces = [];
-                               handleObj.namespace = "";
-                       }
-
-                       handleObj.type = type;
-                       handleObj.guid = handler.guid;
-
-                       // Get the current list of functions bound to this event
-                       var handlers = events[ type ],
-                               special = jQuery.event.special[ type ] || {};
-
-                       // Init the event handler queue
-                       if ( !handlers ) {
-                               handlers = events[ type ] = [];
-
-                               // Check for a special event handler
-                               // Only use addEventListener/attachEvent if the special
-                               // events handler returns false
-                               if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
-                                       // Bind the global event handler to the element
-                                       if ( elem.addEventListener ) {
-                                               elem.addEventListener( type, eventHandle, false );
-
-                                       } else if ( elem.attachEvent ) {
-                                               elem.attachEvent( "on" + type, eventHandle );
-                                       }
-                               }
-                       }
-                       
-                       if ( special.add ) { 
-                               special.add.call( elem, handleObj ); 
-
-                               if ( !handleObj.handler.guid ) {
-                                       handleObj.handler.guid = handler.guid;
-                               }
-                       }
-
-                       // Add the function to the element's handler list
-                       handlers.push( handleObj );
-
-                       // Keep track of which events have been used, for global triggering
-                       jQuery.event.global[ type ] = true;
-               }
-
-               // Nullify elem to prevent memory leaks in IE
-               elem = null;
-       },
-
-       global: {},
-
-       // Detach an event or set of events from an element
-       remove: function( elem, types, handler, pos ) {
-               // don't do events on text and comment nodes
-               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return;
-               }
-
-               var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
-                       elemData = jQuery.data( elem ),
-                       events = elemData && elemData.events;
-
-               if ( !elemData || !events ) {
-                       return;
-               }
-
-               // types is actually an event object here
-               if ( types && types.type ) {
-                       handler = types.handler;
-                       types = types.type;
-               }
-
-               // Unbind all events for the element
-               if ( !types || typeof types === "string" && types.charAt(0) === "." ) {
-                       types = types || "";
-
-                       for ( type in events ) {
-                               jQuery.event.remove( elem, type + types );
-                       }
-
-                       return;
-               }
-
-               // Handle multiple events separated by a space
-               // jQuery(...).unbind("mouseover mouseout", fn);
-               types = types.split(" ");
-
-               while ( (type = types[ i++ ]) ) {
-                       origType = type;
-                       handleObj = null;
-                       all = type.indexOf(".") < 0;
-                       namespaces = [];
-
-                       if ( !all ) {
-                               // Namespaced event handlers
-                               namespaces = type.split(".");
-                               type = namespaces.shift();
-
-                               namespace = new RegExp("(^|\\.)" + 
-                                       jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)")
-                       }
-
-                       eventType = events[ type ];
-
-                       if ( !eventType ) {
-                               continue;
-                       }
-
-                       if ( !handler ) {
-                               for ( var j = 0; j < eventType.length; j++ ) {
-                                       handleObj = eventType[ j ];
-
-                                       if ( all || namespace.test( handleObj.namespace ) ) {
-                                               jQuery.event.remove( elem, origType, handleObj.handler, j );
-                                               eventType.splice( j--, 1 );
-                                       }
-                               }
-
-                               continue;
-                       }
-
-                       special = jQuery.event.special[ type ] || {};
-
-                       for ( var j = pos || 0; j < eventType.length; j++ ) {
-                               handleObj = eventType[ j ];
-
-                               if ( handler.guid === handleObj.guid ) {
-                                       // remove the given handler for the given type
-                                       if ( all || namespace.test( handleObj.namespace ) ) {
-                                               if ( pos == null ) {
-                                                       eventType.splice( j--, 1 );
-                                               }
-
-                                               if ( special.remove ) {
-                                                       special.remove.call( elem, handleObj );
-                                               }
-                                       }
-
-                                       if ( pos != null ) {
-                                               break;
-                                       }
-                               }
-                       }
-
-                       // remove generic event handler if no more handlers exist
-                       if ( eventType.length === 0 || pos != null && eventType.length === 1 ) {
-                               if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
-                                       removeEvent( elem, type, elemData.handle );
-                               }
-
-                               ret = null;
-                               delete events[ type ];
-                       }
-               }
-
-               // Remove the expando if it's no longer used
-               if ( jQuery.isEmptyObject( events ) ) {
-                       var handle = elemData.handle;
-                       if ( handle ) {
-                               handle.elem = null;
-                       }
-
-                       delete elemData.events;
-                       delete elemData.handle;
-
-                       if ( jQuery.isEmptyObject( elemData ) ) {
-                               jQuery.removeData( elem );
-                       }
-               }
-       },
-
-       // bubbling is internal
-       trigger: function( event, data, elem /*, bubbling */ ) {
-               // Event object or event type
-               var type = event.type || event,
-                       bubbling = arguments[3];
-
-               if ( !bubbling ) {
-                       event = typeof event === "object" ?
-                               // jQuery.Event object
-                               event[expando] ? event :
-                               // Object literal
-                               jQuery.extend( jQuery.Event(type), event ) :
-                               // Just the event type (string)
-                               jQuery.Event(type);
-
-                       if ( type.indexOf("!") >= 0 ) {
-                               event.type = type = type.slice(0, -1);
-                               event.exclusive = true;
-                       }
-
-                       // Handle a global trigger
-                       if ( !elem ) {
-                               // Don't bubble custom events when global (to avoid too much overhead)
-                               event.stopPropagation();
-
-                               // Only trigger if we've ever bound an event for it
-                               if ( jQuery.event.global[ type ] ) {
-                                       jQuery.each( jQuery.cache, function() {
-                                               if ( this.events && this.events[type] ) {
-                                                       jQuery.event.trigger( event, data, this.handle.elem );
-                                               }
-                                       });
-                               }
-                       }
-
-                       // Handle triggering a single element
-
-                       // don't do events on text and comment nodes
-                       if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
-                               return undefined;
-                       }
-
-                       // Clean up in case it is reused
-                       event.result = undefined;
-                       event.target = elem;
-
-                       // Clone the incoming data, if any
-                       data = jQuery.makeArray( data );
-                       data.unshift( event );
-               }
-
-               event.currentTarget = elem;
-
-               // Trigger the event, it is assumed that "handle" is a function
-               var handle = jQuery.data( elem, "handle" );
-               if ( handle ) {
-                       handle.apply( elem, data );
-               }
-
-               var parent = elem.parentNode || elem.ownerDocument;
-
-               // Trigger an inline bound script
-               try {
-                       if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
-                               if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) {
-                                       event.result = false;
-                               }
-                       }
-
-               // prevent IE from throwing an error for some elements with some event types, see #3533
-               } catch (e) {}
-
-               if ( !event.isPropagationStopped() && parent ) {
-                       jQuery.event.trigger( event, data, parent, true );
-
-               } else if ( !event.isDefaultPrevented() ) {
-                       var target = event.target, old,
-                               isClick = jQuery.nodeName(target, "a") && type === "click",
-                               special = jQuery.event.special[ type ] || {};
-
-                       if ( (!special._default || special._default.call( elem, event ) === false) && 
-                               !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) {
-
-                               try {
-                                       if ( target[ type ] ) {
-                                               // Make sure that we don't accidentally re-trigger the onFOO events
-                                               old = target[ "on" + type ];
-
-                                               if ( old ) {
-                                                       target[ "on" + type ] = null;
-                                               }
-
-                                               jQuery.event.triggered = true;
-                                               target[ type ]();
-                                       }
-
-                               // prevent IE from throwing an error for some elements with some event types, see #3533
-                               } catch (e) {}
-
-                               if ( old ) {
-                                       target[ "on" + type ] = old;
-                               }
-
-                               jQuery.event.triggered = false;
-                       }
-               }
-       },
-
-       handle: function( event ) {
-               var all, handlers, namespaces, namespace, events;
-
-               event = arguments[0] = jQuery.event.fix( event || window.event );
-               event.currentTarget = this;
-
-               // Namespaced event handlers
-               all = event.type.indexOf(".") < 0 && !event.exclusive;
-
-               if ( !all ) {
-                       namespaces = event.type.split(".");
-                       event.type = namespaces.shift();
-                       namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
-               }
-
-               var events = jQuery.data(this, "events"), handlers = events[ event.type ];
-
-               if ( events && handlers ) {
-                       // Clone the handlers to prevent manipulation
-                       handlers = handlers.slice(0);
-
-                       for ( var j = 0, l = handlers.length; j < l; j++ ) {
-                               var handleObj = handlers[ j ];
-
-                               // Filter the functions by class
-                               if ( all || namespace.test( handleObj.namespace ) ) {
-                                       // Pass in a reference to the handler function itself
-                                       // So that we can later remove it
-                                       event.handler = handleObj.handler;
-                                       event.data = handleObj.data;
-                                       event.handleObj = handleObj;
-       
-                                       var ret = handleObj.handler.apply( this, arguments );
-
-                                       if ( ret !== undefined ) {
-                                               event.result = ret;
-                                               if ( ret === false ) {
-                                                       event.preventDefault();
-                                                       event.stopPropagation();
-                                               }
-                                       }
-
-                                       if ( event.isImmediatePropagationStopped() ) {
-                                               break;
-                                       }
-                               }
-                       }
-               }
-
-               return event.result;
-       },
-
-       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
-
-       fix: function( event ) {
-               if ( event[ expando ] ) {
-                       return event;
-               }
-
-               // store a copy of the original event object
-               // and "clone" to set read-only properties
-               var originalEvent = event;
-               event = jQuery.Event( originalEvent );
-
-               for ( var i = this.props.length, prop; i; ) {
-                       prop = this.props[ --i ];
-                       event[ prop ] = originalEvent[ prop ];
-               }
-
-               // Fix target property, if necessary
-               if ( !event.target ) {
-                       event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
-               }
-
-               // check if target is a textnode (safari)
-               if ( event.target.nodeType === 3 ) {
-                       event.target = event.target.parentNode;
-               }
-
-               // Add relatedTarget, if necessary
-               if ( !event.relatedTarget && event.fromElement ) {
-                       event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
-               }
-
-               // Calculate pageX/Y if missing and clientX/Y available
-               if ( event.pageX == null && event.clientX != null ) {
-                       var doc = document.documentElement, body = document.body;
-                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
-                       event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
-               }
-
-               // Add which for key events
-               if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) {
-                       event.which = event.charCode || event.keyCode;
-               }
-
-               // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
-               if ( !event.metaKey && event.ctrlKey ) {
-                       event.metaKey = event.ctrlKey;
-               }
-
-               // Add which for click: 1 === left; 2 === middle; 3 === right
-               // Note: button is not normalized, so don't use it
-               if ( !event.which && event.button !== undefined ) {
-                       event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
-               }
-
-               return event;
-       },
-
-       // Deprecated, use jQuery.guid instead
-       guid: 1E8,
-
-       // Deprecated, use jQuery.proxy instead
-       proxy: jQuery.proxy,
-
-       special: {
-               ready: {
-                       // Make sure the ready event is setup
-                       setup: jQuery.bindReady,
-                       teardown: jQuery.noop
-               },
-
-               live: {
-                       add: function( handleObj ) {
-                               jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); 
-                       },
-
-                       remove: function( handleObj ) {
-                               var remove = true,
-                                       type = handleObj.origType.replace(rnamespaces, "");
-                               
-                               jQuery.each( jQuery.data(this, "events").live || [], function() {
-                                       if ( type === this.origType.replace(rnamespaces, "") ) {
-                                               remove = false;
-                                               return false;
-                                       }
-                               });
-
-                               if ( remove ) {
-                                       jQuery.event.remove( this, handleObj.origType, liveHandler );
-                               }
-                       }
-
-               },
-
-               beforeunload: {
-                       setup: function( data, namespaces, eventHandle ) {
-                               // We only want to do this special case on windows
-                               if ( this.setInterval ) {
-                                       this.onbeforeunload = eventHandle;
-                               }
-
-                               return false;
-                       },
-                       teardown: function( namespaces, eventHandle ) {
-                               if ( this.onbeforeunload === eventHandle ) {
-                                       this.onbeforeunload = null;
-                               }
-                       }
-               }
-       }
-};
-
-var removeEvent = document.removeEventListener ?
-       function( elem, type, handle ) {
-               elem.removeEventListener( type, handle, false );
-       } : 
-       function( elem, type, handle ) {
-               elem.detachEvent( "on" + type, handle );
-       };
-
-jQuery.Event = function( src ) {
-       // Allow instantiation without the 'new' keyword
-       if ( !this.preventDefault ) {
-               return new jQuery.Event( src );
-       }
-
-       // Event object
-       if ( src && src.type ) {
-               this.originalEvent = src;
-               this.type = src.type;
-       // Event type
-       } else {
-               this.type = src;
-       }
-
-       // timeStamp is buggy for some events on Firefox(#3843)
-       // So we won't rely on the native value
-       this.timeStamp = now();
-
-       // Mark it as fixed
-       this[ expando ] = true;
-};
-
-function returnFalse() {
-       return false;
-}
-function returnTrue() {
-       return true;
-}
-
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
-       preventDefault: function() {
-               this.isDefaultPrevented = returnTrue;
-
-               var e = this.originalEvent;
-               if ( !e ) {
-                       return;
-               }
-               
-               // if preventDefault exists run it on the original event
-               if ( e.preventDefault ) {
-                       e.preventDefault();
-               }
-               // otherwise set the returnValue property of the original event to false (IE)
-               e.returnValue = false;
-       },
-       stopPropagation: function() {
-               this.isPropagationStopped = returnTrue;
-
-               var e = this.originalEvent;
-               if ( !e ) {
-                       return;
-               }
-               // if stopPropagation exists run it on the original event
-               if ( e.stopPropagation ) {
-                       e.stopPropagation();
-               }
-               // otherwise set the cancelBubble property of the original event to true (IE)
-               e.cancelBubble = true;
-       },
-       stopImmediatePropagation: function() {
-               this.isImmediatePropagationStopped = returnTrue;
-               this.stopPropagation();
-       },
-       isDefaultPrevented: returnFalse,
-       isPropagationStopped: returnFalse,
-       isImmediatePropagationStopped: returnFalse
-};
-
-// Checks if an event happened on an element within another element
-// Used in jQuery.event.special.mouseenter and mouseleave handlers
-var withinElement = function( event ) {
-       // Check if mouse(over|out) are still within the same parent element
-       var parent = event.relatedTarget;
-
-       // Firefox sometimes assigns relatedTarget a XUL element
-       // which we cannot access the parentNode property of
-       try {
-               // Traverse up the tree
-               while ( parent && parent !== this ) {
-                       parent = parent.parentNode;
-               }
-
-               if ( parent !== this ) {
-                       // set the correct event type
-                       event.type = event.data;
-
-                       // handle event if we actually just moused on to a non sub-element
-                       jQuery.event.handle.apply( this, arguments );
-               }
-
-       // assuming we've left the element since we most likely mousedover a xul element
-       } catch(e) { }
-},
-
-// In case of event delegation, we only need to rename the event.type,
-// liveHandler will take care of the rest.
-delegate = function( event ) {
-       event.type = event.data;
-       jQuery.event.handle.apply( this, arguments );
-};
-
-// Create mouseenter and mouseleave events
-jQuery.each({
-       mouseenter: "mouseover",
-       mouseleave: "mouseout"
-}, function( orig, fix ) {
-       jQuery.event.special[ orig ] = {
-               setup: function( data ) {
-                       jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
-               },
-               teardown: function( data ) {
-                       jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
-               }
-       };
-});
-
-// submit delegation
-if ( !jQuery.support.submitBubbles ) {
-
-       jQuery.event.special.submit = {
-               setup: function( data, namespaces ) {
-                       if ( this.nodeName.toLowerCase() !== "form" ) {
-                               jQuery.event.add(this, "click.specialSubmit", function( e ) {
-                                       var elem = e.target, type = elem.type;
-
-                                       if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
-                                               return trigger( "submit", this, arguments );
-                                       }
-                               });
-        
-                               jQuery.event.add(this, "keypress.specialSubmit", function( e ) {
-                                       var elem = e.target, type = elem.type;
-
-                                       if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
-                                               return trigger( "submit", this, arguments );
-                                       }
-                               });
-
-                       } else {
-                               return false;
-                       }
-               },
-
-               teardown: function( namespaces ) {
-                       jQuery.event.remove( this, ".specialSubmit" );
-               }
-       };
-
-}
-
-// change delegation, happens here so we have bind.
-if ( !jQuery.support.changeBubbles ) {
-
-       var formElems = /textarea|input|select/i,
-
-       changeFilters,
-
-       getVal = function( elem ) {
-               var type = elem.type, val = elem.value;
-
-               if ( type === "radio" || type === "checkbox" ) {
-                       val = elem.checked;
-
-               } else if ( type === "select-multiple" ) {
-                       val = elem.selectedIndex > -1 ?
-                               jQuery.map( elem.options, function( elem ) {
-                                       return elem.selected;
-                               }).join("-") :
-                               "";
-
-               } else if ( elem.nodeName.toLowerCase() === "select" ) {
-                       val = elem.selectedIndex;
-               }
-
-               return val;
-       },
-
-       testChange = function testChange( e ) {
-               var elem = e.target, data, val;
-
-               if ( !formElems.test( elem.nodeName ) || elem.readOnly ) {
-                       return;
-               }
-
-               data = jQuery.data( elem, "_change_data" );
-               val = getVal(elem);
-
-               // the current data will be also retrieved by beforeactivate
-               if ( e.type !== "focusout" || elem.type !== "radio" ) {
-                       jQuery.data( elem, "_change_data", val );
-               }
-               
-               if ( data === undefined || val === data ) {
-                       return;
-               }
-
-               if ( data != null || val ) {
-                       e.type = "change";
-                       return jQuery.event.trigger( e, arguments[1], elem );
-               }
-       };
-
-       jQuery.event.special.change = {
-               filters: {
-                       focusout: testChange, 
-
-                       click: function( e ) {
-                               var elem = e.target, type = elem.type;
-
-                               if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
-                                       return testChange.call( this, e );
-                               }
-                       },
-
-                       // Change has to be called before submit
-                       // Keydown will be called before keypress, which is used in submit-event delegation
-                       keydown: function( e ) {
-                               var elem = e.target, type = elem.type;
-
-                               if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
-                                       (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
-                                       type === "select-multiple" ) {
-                                       return testChange.call( this, e );
-                               }
-                       },
-
-                       // Beforeactivate happens also before the previous element is blurred
-                       // with this event you can't trigger a change event, but you can store
-                       // information/focus[in] is not needed anymore
-                       beforeactivate: function( e ) {
-                               var elem = e.target;
-                               jQuery.data( elem, "_change_data", getVal(elem) );
-                       }
-               },
-
-               setup: function( data, namespaces ) {
-                       if ( this.type === "file" ) {
-                               return false;
-                       }
-
-                       for ( var type in changeFilters ) {
-                               jQuery.event.add( this, type + ".specialChange", changeFilters[type] );
-                       }
-
-                       return formElems.test( this.nodeName );
-               },
-
-               teardown: function( namespaces ) {
-                       jQuery.event.remove( this, ".specialChange" );
-
-                       return formElems.test( this.nodeName );
-               }
-       };
-
-       changeFilters = jQuery.event.special.change.filters;
-}
-
-function trigger( type, elem, args ) {
-       args[0].type = type;
-       return jQuery.event.handle.apply( elem, args );
-}
-
-// Create "bubbling" focus and blur events
-if ( document.addEventListener ) {
-       jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
-               jQuery.event.special[ fix ] = {
-                       setup: function() {
-                               this.addEventListener( orig, handler, true );
-                       }, 
-                       teardown: function() { 
-                               this.removeEventListener( orig, handler, true );
-                       }
-               };
-
-               function handler( e ) { 
-                       e = jQuery.event.fix( e );
-                       e.type = fix;
-                       return jQuery.event.handle.call( this, e );
-               }
-       });
-}
-
-jQuery.each(["bind", "one"], function( i, name ) {
-       jQuery.fn[ name ] = function( type, data, fn ) {
-               // Handle object literals
-               if ( typeof type === "object" ) {
-                       for ( var key in type ) {
-                               this[ name ](key, data, type[key], fn);
-                       }
-                       return this;
-               }
-               
-               if ( jQuery.isFunction( data ) ) {
-                       fn = data;
-                       data = undefined;
-               }
-
-               var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
-                       jQuery( this ).unbind( event, handler );
-                       return fn.apply( this, arguments );
-               }) : fn;
-
-               if ( type === "unload" && name !== "one" ) {
-                       this.one( type, data, fn );
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               jQuery.event.add( this[i], type, handler, data );
-                       }
-               }
-
-               return this;
-       };
-});
-
-jQuery.fn.extend({
-       unbind: function( type, fn ) {
-               // Handle object literals
-               if ( typeof type === "object" && !type.preventDefault ) {
-                       for ( var key in type ) {
-                               this.unbind(key, type[key]);
-                       }
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               jQuery.event.remove( this[i], type, fn );
-                       }
-               }
-
-               return this;
-       },
-       
-       delegate: function( selector, types, data, fn ) {
-               return this.live( types, data, fn, selector );
-       },
-       
-       undelegate: function( selector, types, fn ) {
-               if ( arguments.length === 0 ) {
-                               return this.unbind( "live" );
-               
-               } else {
-                       return this.die( types, null, fn, selector );
-               }
-       },
-       
-       trigger: function( type, data ) {
-               return this.each(function() {
-                       jQuery.event.trigger( type, data, this );
-               });
-       },
-
-       triggerHandler: function( type, data ) {
-               if ( this[0] ) {
-                       var event = jQuery.Event( type );
-                       event.preventDefault();
-                       event.stopPropagation();
-                       jQuery.event.trigger( event, data, this[0] );
-                       return event.result;
-               }
-       },
-
-       toggle: function( fn ) {
-               // Save reference to arguments for access in closure
-               var args = arguments, i = 1;
-
-               // link all the functions, so any of them can unbind this click handler
-               while ( i < args.length ) {
-                       jQuery.proxy( fn, args[ i++ ] );
-               }
-
-               return this.click( jQuery.proxy( fn, function( event ) {
-                       // Figure out which function to execute
-                       var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
-                       jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
-
-                       // Make sure that clicks stop
-                       event.preventDefault();
-
-                       // and execute the function
-                       return args[ lastToggle ].apply( this, arguments ) || false;
-               }));
-       },
-
-       hover: function( fnOver, fnOut ) {
-               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
-       }
-});
-
-var liveMap = {
-       focus: "focusin",
-       blur: "focusout",
-       mouseenter: "mouseover",
-       mouseleave: "mouseout"
-};
-
-jQuery.each(["live", "die"], function( i, name ) {
-       jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) {
-               var type, i = 0, match, namespaces, preType,
-                       selector = origSelector || this.selector,
-                       context = origSelector ? this : jQuery( this.context );
-
-               if ( jQuery.isFunction( data ) ) {
-                       fn = data;
-                       data = undefined;
-               }
-
-               types = (types || "").split(" ");
-
-               while ( (type = types[ i++ ]) != null ) {
-                       match = rnamespaces.exec( type );
-                       namespaces = "";
-
-                       if ( match )  {
-                               namespaces = match[0];
-                               type = type.replace( rnamespaces, "" );
-                       }
-
-                       if ( type === "hover" ) {
-                               types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
-                               continue;
-                       }
-
-                       preType = type;
-
-                       if ( type === "focus" || type === "blur" ) {
-                               types.push( liveMap[ type ] + namespaces );
-                               type = type + namespaces;
-
-                       } else {
-                               type = (liveMap[ type ] || type) + namespaces;
-                       }
-
-                       if ( name === "live" ) {
-                               // bind live handler
-                               context.each(function(){
-                                       jQuery.event.add( this, liveConvert( type, selector ),
-                                               { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
-                               });
-
-                       } else {
-                               // unbind live handler
-                               context.unbind( liveConvert( type, selector ), fn );
-                       }
-               }
-               
-               return this;
-       }
-});
-
-function liveHandler( event ) {
-       var stop, elems = [], selectors = [], args = arguments,
-               related, match, handleObj, elem, j, i, l, data,
-               events = jQuery.data( this, "events" );
-
-       // Make sure we avoid non-left-click bubbling in Firefox (#3861)
-       if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) {
-               return;
-       }
-
-       event.liveFired = this;
-
-       var live = events.live.slice(0);
-
-       for ( j = 0; j < live.length; j++ ) {
-               handleObj = live[j];
-
-               if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) {
-                       selectors.push( handleObj.selector );
-
-               } else {
-                       live.splice( j--, 1 );
-               }
-       }
-
-       match = jQuery( event.target ).closest( selectors, event.currentTarget );
-
-       for ( i = 0, l = match.length; i < l; i++ ) {
-               for ( j = 0; j < live.length; j++ ) {
-                       handleObj = live[j];
-
-                       if ( match[i].selector === handleObj.selector ) {
-                               elem = match[i].elem;
-                               related = null;
-
-                               // Those two events require additional checking
-                               if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) {
-                                       related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0];
-                               }
-
-                               if ( !related || related !== elem ) {
-                                       elems.push({ elem: elem, handleObj: handleObj });
-                               }
-                       }
-               }
-       }
-
-       for ( i = 0, l = elems.length; i < l; i++ ) {
-               match = elems[i];
-               event.currentTarget = match.elem;
-               event.data = match.handleObj.data;
-               event.handleObj = match.handleObj;
-
-               if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) {
-                       stop = false;
-                       break;
-               }
-       }
-
-       return stop;
-}
-
-function liveConvert( type, selector ) {
-       return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&");
-}
-
-jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
-       "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
-       "change select submit keydown keypress keyup error").split(" "), function( i, name ) {
-
-       // Handle event binding
-       jQuery.fn[ name ] = function( fn ) {
-               return fn ? this.bind( name, fn ) : this.trigger( name );
-       };
-
-       if ( jQuery.attrFn ) {
-               jQuery.attrFn[ name ] = true;
-       }
-});
-
-// Prevent memory leaks in IE
-// Window isn't included so as not to unbind existing unload events
-// More info:
-//  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
-if ( window.attachEvent && !window.addEventListener ) {
-       window.attachEvent("onunload", function() {
-               for ( var id in jQuery.cache ) {
-                       if ( jQuery.cache[ id ].handle ) {
-                               // Try/Catch is to handle iframes being unloaded, see #4280
-                               try {
-                                       jQuery.event.remove( jQuery.cache[ id ].handle.elem );
-                               } catch(e) {}
-                       }
-               }
-       });
-}
-/*!
- * Sizzle CSS Selector Engine - v1.0
- *  Copyright 2009, The Dojo Foundation
- *  Released under the MIT, BSD, and GPL Licenses.
- *  More information: http://sizzlejs.com/
- */
-(function(){
-
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
-       done = 0,
-       toString = Object.prototype.toString,
-       hasDuplicate = false,
-       baseHasDuplicate = true;
-
-// Here we check if the JavaScript engine is using some sort of
-// optimization where it does not always call our comparision
-// function. If that is the case, discard the hasDuplicate value.
-//   Thus far that includes Google Chrome.
-[0, 0].sort(function(){
-       baseHasDuplicate = false;
-       return 0;
-});
-
-var Sizzle = function(selector, context, results, seed) {
-       results = results || [];
-       var origContext = context = context || document;
-
-       if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
-               return [];
-       }
-       
-       if ( !selector || typeof selector !== "string" ) {
-               return results;
-       }
-
-       var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context),
-               soFar = selector;
-       
-       // Reset the position of the chunker regexp (start from head)
-       while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) {
-               soFar = m[3];
-               
-               parts.push( m[1] );
-               
-               if ( m[2] ) {
-                       extra = m[3];
-                       break;
-               }
-       }
-
-       if ( parts.length > 1 && origPOS.exec( selector ) ) {
-               if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
-                       set = posProcess( parts[0] + parts[1], context );
-               } else {
-                       set = Expr.relative[ parts[0] ] ?
-                               [ context ] :
-                               Sizzle( parts.shift(), context );
-
-                       while ( parts.length ) {
-                               selector = parts.shift();
-
-                               if ( Expr.relative[ selector ] ) {
-                                       selector += parts.shift();
-                               }
-                               
-                               set = posProcess( selector, set );
-                       }
-               }
-       } else {
-               // Take a shortcut and set the context if the root selector is an ID
-               // (but not if it'll be faster if the inner selector is an ID)
-               if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
-                               Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
-                       var ret = Sizzle.find( parts.shift(), context, contextXML );
-                       context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
-               }
-
-               if ( context ) {
-                       var ret = seed ?
-                               { expr: parts.pop(), set: makeArray(seed) } :
-                               Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
-                       set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
-
-                       if ( parts.length > 0 ) {
-                               checkSet = makeArray(set);
-                       } else {
-                               prune = false;
-                       }
-
-                       while ( parts.length ) {
-                               var cur = parts.pop(), pop = cur;
-
-                               if ( !Expr.relative[ cur ] ) {
-                                       cur = "";
-                               } else {
-                                       pop = parts.pop();
-                               }
-
-                               if ( pop == null ) {
-                                       pop = context;
-                               }
-
-                               Expr.relative[ cur ]( checkSet, pop, contextXML );
-                       }
-               } else {
-                       checkSet = parts = [];
-               }
-       }
-
-       if ( !checkSet ) {
-               checkSet = set;
-       }
-
-       if ( !checkSet ) {
-               Sizzle.error( cur || selector );
-       }
-
-       if ( toString.call(checkSet) === "[object Array]" ) {
-               if ( !prune ) {
-                       results.push.apply( results, checkSet );
-               } else if ( context && context.nodeType === 1 ) {
-                       for ( var i = 0; checkSet[i] != null; i++ ) {
-                               if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
-                                       results.push( set[i] );
-                               }
-                       }
-               } else {
-                       for ( var i = 0; checkSet[i] != null; i++ ) {
-                               if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
-                                       results.push( set[i] );
-                               }
-                       }
-               }
-       } else {
-               makeArray( checkSet, results );
-       }
-
-       if ( extra ) {
-               Sizzle( extra, origContext, results, seed );
-               Sizzle.uniqueSort( results );
-       }
-
-       return results;
-};
-
-Sizzle.uniqueSort = function(results){
-       if ( sortOrder ) {
-               hasDuplicate = baseHasDuplicate;
-               results.sort(sortOrder);
-
-               if ( hasDuplicate ) {
-                       for ( var i = 1; i < results.length; i++ ) {
-                               if ( results[i] === results[i-1] ) {
-                                       results.splice(i--, 1);
-                               }
-                       }
-               }
-       }
-
-       return results;
-};
-
-Sizzle.matches = function(expr, set){
-       return Sizzle(expr, null, null, set);
-};
-
-Sizzle.find = function(expr, context, isXML){
-       var set, match;
-
-       if ( !expr ) {
-               return [];
-       }
-
-       for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
-               var type = Expr.order[i], match;
-               
-               if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
-                       var left = match[1];
-                       match.splice(1,1);
-
-                       if ( left.substr( left.length - 1 ) !== "\\" ) {
-                               match[1] = (match[1] || "").replace(/\\/g, "");
-                               set = Expr.find[ type ]( match, context, isXML );
-                               if ( set != null ) {
-                                       expr = expr.replace( Expr.match[ type ], "" );
-                                       break;
-                               }
-                       }
-               }
-       }
-
-       if ( !set ) {
-               set = context.getElementsByTagName("*");
-       }
-
-       return {set: set, expr: expr};
-};
-
-Sizzle.filter = function(expr, set, inplace, not){
-       var old = expr, result = [], curLoop = set, match, anyFound,
-               isXMLFilter = set && set[0] && isXML(set[0]);
-
-       while ( expr && set.length ) {
-               for ( var type in Expr.filter ) {
-                       if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
-                               var filter = Expr.filter[ type ], found, item, left = match[1];
-                               anyFound = false;
-
-                               match.splice(1,1);
-
-                               if ( left.substr( left.length - 1 ) === "\\" ) {
-                                       continue;
-                               }
-
-                               if ( curLoop === result ) {
-                                       result = [];
-                               }
-
-                               if ( Expr.preFilter[ type ] ) {
-                                       match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
-
-                                       if ( !match ) {
-                                               anyFound = found = true;
-                                       } else if ( match === true ) {
-                                               continue;
-                                       }
-                               }
-
-                               if ( match ) {
-                                       for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
-                                               if ( item ) {
-                                                       found = filter( item, match, i, curLoop );
-                                                       var pass = not ^ !!found;
-
-                                                       if ( inplace && found != null ) {
-                                                               if ( pass ) {
-                                                                       anyFound = true;
-                                                               } else {
-                                                                       curLoop[i] = false;
-                                                               }
-                                                       } else if ( pass ) {
-                                                               result.push( item );
-                                                               anyFound = true;
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               if ( found !== undefined ) {
-                                       if ( !inplace ) {
-                                               curLoop = result;
-                                       }
-
-                                       expr = expr.replace( Expr.match[ type ], "" );
-
-                                       if ( !anyFound ) {
-                                               return [];
-                                       }
-
-                                       break;
-                               }
-                       }
-               }
-
-               // Improper expression
-               if ( expr === old ) {
-                       if ( anyFound == null ) {
-                               Sizzle.error( expr );
-                       } else {
-                               break;
-                       }
-               }
-
-               old = expr;
-       }
-
-       return curLoop;
-};
-
-Sizzle.error = function( msg ) {
-       throw "Syntax error, unrecognized expression: " + msg;
-};
-
-var Expr = Sizzle.selectors = {
-       order: [ "ID", "NAME", "TAG" ],
-       match: {
-               ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
-               CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
-               NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,
-               ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
-               TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,
-               CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
-               POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
-               PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
-       },
-       leftMatch: {},
-       attrMap: {
-               "class": "className",
-               "for": "htmlFor"
-       },
-       attrHandle: {
-               href: function(elem){
-                       return elem.getAttribute("href");
-               }
-       },
-       relative: {
-               "+": function(checkSet, part){
-                       var isPartStr = typeof part === "string",
-                               isTag = isPartStr && !/\W/.test(part),
-                               isPartStrNotTag = isPartStr && !isTag;
-
-                       if ( isTag ) {
-                               part = part.toLowerCase();
-                       }
-
-                       for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
-                               if ( (elem = checkSet[i]) ) {
-                                       while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
-
-                                       checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
-                                               elem || false :
-                                               elem === part;
-                               }
-                       }
-
-                       if ( isPartStrNotTag ) {
-                               Sizzle.filter( part, checkSet, true );
-                       }
-               },
-               ">": function(checkSet, part){
-                       var isPartStr = typeof part === "string";
-
-                       if ( isPartStr && !/\W/.test(part) ) {
-                               part = part.toLowerCase();
-
-                               for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-                                       var elem = checkSet[i];
-                                       if ( elem ) {
-                                               var parent = elem.parentNode;
-                                               checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
-                                       }
-                               }
-                       } else {
-                               for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-                                       var elem = checkSet[i];
-                                       if ( elem ) {
-                                               checkSet[i] = isPartStr ?
-                                                       elem.parentNode :
-                                                       elem.parentNode === part;
-                                       }
-                               }
-
-                               if ( isPartStr ) {
-                                       Sizzle.filter( part, checkSet, true );
-                               }
-                       }
-               },
-               "": function(checkSet, part, isXML){
-                       var doneName = done++, checkFn = dirCheck;
-
-                       if ( typeof part === "string" && !/\W/.test(part) ) {
-                               var nodeCheck = part = part.toLowerCase();
-                               checkFn = dirNodeCheck;
-                       }
-
-                       checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
-               },
-               "~": function(checkSet, part, isXML){
-                       var doneName = done++, checkFn = dirCheck;
-
-                       if ( typeof part === "string" && !/\W/.test(part) ) {
-                               var nodeCheck = part = part.toLowerCase();
-                               checkFn = dirNodeCheck;
-                       }
-
-                       checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
-               }
-       },
-       find: {
-               ID: function(match, context, isXML){
-                       if ( typeof context.getElementById !== "undefined" && !isXML ) {
-                               var m = context.getElementById(match[1]);
-                               return m ? [m] : [];
-                       }
-               },
-               NAME: function(match, context){
-                       if ( typeof context.getElementsByName !== "undefined" ) {
-                               var ret = [], results = context.getElementsByName(match[1]);
-
-                               for ( var i = 0, l = results.length; i < l; i++ ) {
-                                       if ( results[i].getAttribute("name") === match[1] ) {
-                                               ret.push( results[i] );
-                                       }
-                               }
-
-                               return ret.length === 0 ? null : ret;
-                       }
-               },
-               TAG: function(match, context){
-                       return context.getElementsByTagName(match[1]);
-               }
-       },
-       preFilter: {
-               CLASS: function(match, curLoop, inplace, result, not, isXML){
-                       match = " " + match[1].replace(/\\/g, "") + " ";
-
-                       if ( isXML ) {
-                               return match;
-                       }
-
-                       for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
-                               if ( elem ) {
-                                       if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
-                                               if ( !inplace ) {
-                                                       result.push( elem );
-                                               }
-                                       } else if ( inplace ) {
-                                               curLoop[i] = false;
-                                       }
-                               }
-                       }
-
-                       return false;
-               },
-               ID: function(match){
-                       return match[1].replace(/\\/g, "");
-               },
-               TAG: function(match, curLoop){
-                       return match[1].toLowerCase();
-               },
-               CHILD: function(match){
-                       if ( match[1] === "nth" ) {
-                               // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
-                               var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
-                                       match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
-                                       !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
-
-                               // calculate the numbers (first)n+(last) including if they are negative
-                               match[2] = (test[1] + (test[2] || 1)) - 0;
-                               match[3] = test[3] - 0;
-                       }
-
-                       // TODO: Move to normal caching system
-                       match[0] = done++;
-
-                       return match;
-               },
-               ATTR: function(match, curLoop, inplace, result, not, isXML){
-                       var name = match[1].replace(/\\/g, "");
-                       
-                       if ( !isXML && Expr.attrMap[name] ) {
-                               match[1] = Expr.attrMap[name];
-                       }
-
-                       if ( match[2] === "~=" ) {
-                               match[4] = " " + match[4] + " ";
-                       }
-
-                       return match;
-               },
-               PSEUDO: function(match, curLoop, inplace, result, not){
-                       if ( match[1] === "not" ) {
-                               // If we're dealing with a complex expression, or a simple one
-                               if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
-                                       match[3] = Sizzle(match[3], null, null, curLoop);
-                               } else {
-                                       var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
-                                       if ( !inplace ) {
-                                               result.push.apply( result, ret );
-                                       }
-                                       return false;
-                               }
-                       } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
-                               return true;
-                       }
-                       
-                       return match;
-               },
-               POS: function(match){
-                       match.unshift( true );
-                       return match;
-               }
-       },
-       filters: {
-               enabled: function(elem){
-                       return elem.disabled === false && elem.type !== "hidden";
-               },
-               disabled: function(elem){
-                       return elem.disabled === true;
-               },
-               checked: function(elem){
-                       return elem.checked === true;
-               },
-               selected: function(elem){
-                       // Accessing this property makes selected-by-default
-                       // options in Safari work properly
-                       elem.parentNode.selectedIndex;
-                       return elem.selected === true;
-               },
-               parent: function(elem){
-                       return !!elem.firstChild;
-               },
-               empty: function(elem){
-                       return !elem.firstChild;
-               },
-               has: function(elem, i, match){
-                       return !!Sizzle( match[3], elem ).length;
-               },
-               header: function(elem){
-                       return /h\d/i.test( elem.nodeName );
-               },
-               text: function(elem){
-                       return "text" === elem.type;
-               },
-               radio: function(elem){
-                       return "radio" === elem.type;
-               },
-               checkbox: function(elem){
-                       return "checkbox" === elem.type;
-               },
-               file: function(elem){
-                       return "file" === elem.type;
-               },
-               password: function(elem){
-                       return "password" === elem.type;
-               },
-               submit: function(elem){
-                       return "submit" === elem.type;
-               },
-               image: function(elem){
-                       return "image" === elem.type;
-               },
-               reset: function(elem){
-                       return "reset" === elem.type;
-               },
-               button: function(elem){
-                       return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
-               },
-               input: function(elem){
-                       return /input|select|textarea|button/i.test(elem.nodeName);
-               }
-       },
-       setFilters: {
-               first: function(elem, i){
-                       return i === 0;
-               },
-               last: function(elem, i, match, array){
-                       return i === array.length - 1;
-               },
-               even: function(elem, i){
-                       return i % 2 === 0;
-               },
-               odd: function(elem, i){
-                       return i % 2 === 1;
-               },
-               lt: function(elem, i, match){
-                       return i < match[3] - 0;
-               },
-               gt: function(elem, i, match){
-                       return i > match[3] - 0;
-               },
-               nth: function(elem, i, match){
-                       return match[3] - 0 === i;
-               },
-               eq: function(elem, i, match){
-                       return match[3] - 0 === i;
-               }
-       },
-       filter: {
-               PSEUDO: function(elem, match, i, array){
-                       var name = match[1], filter = Expr.filters[ name ];
-
-                       if ( filter ) {
-                               return filter( elem, i, match, array );
-                       } else if ( name === "contains" ) {
-                               return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
-                       } else if ( name === "not" ) {
-                               var not = match[3];
-
-                               for ( var i = 0, l = not.length; i < l; i++ ) {
-                                       if ( not[i] === elem ) {
-                                               return false;
-                                       }
-                               }
-
-                               return true;
-                       } else {
-                               Sizzle.error( "Syntax error, unrecognized expression: " + name );
-                       }
-               },
-               CHILD: function(elem, match){
-                       var type = match[1], node = elem;
-                       switch (type) {
-                               case 'only':
-                               case 'first':
-                                       while ( (node = node.previousSibling) )  {
-                                               if ( node.nodeType === 1 ) { 
-                                                       return false; 
-                                               }
-                                       }
-                                       if ( type === "first" ) { 
-                                               return true; 
-                                       }
-                                       node = elem;
-                               case 'last':
-                                       while ( (node = node.nextSibling) )      {
-                                               if ( node.nodeType === 1 ) { 
-                                                       return false; 
-                                               }
-                                       }
-                                       return true;
-                               case 'nth':
-                                       var first = match[2], last = match[3];
-
-                                       if ( first === 1 && last === 0 ) {
-                                               return true;
-                                       }
-                                       
-                                       var doneName = match[0],
-                                               parent = elem.parentNode;
-       
-                                       if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
-                                               var count = 0;
-                                               for ( node = parent.firstChild; node; node = node.nextSibling ) {
-                                                       if ( node.nodeType === 1 ) {
-                                                               node.nodeIndex = ++count;
-                                                       }
-                                               } 
-                                               parent.sizcache = doneName;
-                                       }
-                                       
-                                       var diff = elem.nodeIndex - last;
-                                       if ( first === 0 ) {
-                                               return diff === 0;
-                                       } else {
-                                               return ( diff % first === 0 && diff / first >= 0 );
-                                       }
-                       }
-               },
-               ID: function(elem, match){
-                       return elem.nodeType === 1 && elem.getAttribute("id") === match;
-               },
-               TAG: function(elem, match){
-                       return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
-               },
-               CLASS: function(elem, match){
-                       return (" " + (elem.className || elem.getAttribute("class")) + " ")
-                               .indexOf( match ) > -1;
-               },
-               ATTR: function(elem, match){
-                       var name = match[1],
-                               result = Expr.attrHandle[ name ] ?
-                                       Expr.attrHandle[ name ]( elem ) :
-                                       elem[ name ] != null ?
-                                               elem[ name ] :
-                                               elem.getAttribute( name ),
-                               value = result + "",
-                               type = match[2],
-                               check = match[4];
-
-                       return result == null ?
-                               type === "!=" :
-                               type === "=" ?
-                               value === check :
-                               type === "*=" ?
-                               value.indexOf(check) >= 0 :
-                               type === "~=" ?
-                               (" " + value + " ").indexOf(check) >= 0 :
-                               !check ?
-                               value && result !== false :
-                               type === "!=" ?
-                               value !== check :
-                               type === "^=" ?
-                               value.indexOf(check) === 0 :
-                               type === "$=" ?
-                               value.substr(value.length - check.length) === check :
-                               type === "|=" ?
-                               value === check || value.substr(0, check.length + 1) === check + "-" :
-                               false;
-               },
-               POS: function(elem, match, i, array){
-                       var name = match[2], filter = Expr.setFilters[ name ];
-
-                       if ( filter ) {
-                               return filter( elem, i, match, array );
-                       }
-               }
-       }
-};
-
-var origPOS = Expr.match.POS;
-
-for ( var type in Expr.match ) {
-       Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
-       Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){
-               return "\\" + (num - 0 + 1);
-       }));
-}
-
-var makeArray = function(array, results) {
-       array = Array.prototype.slice.call( array, 0 );
-
-       if ( results ) {
-               results.push.apply( results, array );
-               return results;
-       }
-       
-       return array;
-};
-
-// Perform a simple check to determine if the browser is capable of
-// converting a NodeList to an array using builtin methods.
-// Also verifies that the returned array holds DOM nodes
-// (which is not the case in the Blackberry browser)
-try {
-       Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
-
-// Provide a fallback method if it does not work
-} catch(e){
-       makeArray = function(array, results) {
-               var ret = results || [];
-
-               if ( toString.call(array) === "[object Array]" ) {
-                       Array.prototype.push.apply( ret, array );
-               } else {
-                       if ( typeof array.length === "number" ) {
-                               for ( var i = 0, l = array.length; i < l; i++ ) {
-                                       ret.push( array[i] );
-                               }
-                       } else {
-                               for ( var i = 0; array[i]; i++ ) {
-                                       ret.push( array[i] );
-                               }
-                       }
-               }
-
-               return ret;
-       };
-}
-
-var sortOrder;
-
-if ( document.documentElement.compareDocumentPosition ) {
-       sortOrder = function( a, b ) {
-               if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
-                       if ( a == b ) {
-                               hasDuplicate = true;
-                       }
-                       return a.compareDocumentPosition ? -1 : 1;
-               }
-
-               var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
-               if ( ret === 0 ) {
-                       hasDuplicate = true;
-               }
-               return ret;
-       };
-} else if ( "sourceIndex" in document.documentElement ) {
-       sortOrder = function( a, b ) {
-               if ( !a.sourceIndex || !b.sourceIndex ) {
-                       if ( a == b ) {
-                               hasDuplicate = true;
-                       }
-                       return a.sourceIndex ? -1 : 1;
-               }
-
-               var ret = a.sourceIndex - b.sourceIndex;
-               if ( ret === 0 ) {
-                       hasDuplicate = true;
-               }
-               return ret;
-       };
-} else if ( document.createRange ) {
-       sortOrder = function( a, b ) {
-               if ( !a.ownerDocument || !b.ownerDocument ) {
-                       if ( a == b ) {
-                               hasDuplicate = true;
-                       }
-                       return a.ownerDocument ? -1 : 1;
-               }
-
-               var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
-               aRange.setStart(a, 0);
-               aRange.setEnd(a, 0);
-               bRange.setStart(b, 0);
-               bRange.setEnd(b, 0);
-               var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
-               if ( ret === 0 ) {
-                       hasDuplicate = true;
-               }
-               return ret;
-       };
-}
-
-// Utility function for retreiving the text value of an array of DOM nodes
-function getText( elems ) {
-       var ret = "", elem;
-
-       for ( var i = 0; elems[i]; i++ ) {
-               elem = elems[i];
-
-               // Get the text from text nodes and CDATA nodes
-               if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
-                       ret += elem.nodeValue;
-
-               // Traverse everything else, except comment nodes
-               } else if ( elem.nodeType !== 8 ) {
-                       ret += getText( elem.childNodes );
-               }
-       }
-
-       return ret;
-}
-
-// Check to see if the browser returns elements by name when
-// querying by getElementById (and provide a workaround)
-(function(){
-       // We're going to inject a fake input element with a specified name
-       var form = document.createElement("div"),
-               id = "script" + (new Date).getTime();
-       form.innerHTML = "<a name='" + id + "'/>";
-
-       // Inject it into the root element, check its status, and remove it quickly
-       var root = document.documentElement;
-       root.insertBefore( form, root.firstChild );
-
-       // The workaround has to do additional checks after a getElementById
-       // Which slows things down for other browsers (hence the branching)
-       if ( document.getElementById( id ) ) {
-               Expr.find.ID = function(match, context, isXML){
-                       if ( typeof context.getElementById !== "undefined" && !isXML ) {
-                               var m = context.getElementById(match[1]);
-                               return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
-                       }
-               };
-
-               Expr.filter.ID = function(elem, match){
-                       var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
-                       return elem.nodeType === 1 && node && node.nodeValue === match;
-               };
-       }
-
-       root.removeChild( form );
-       root = form = null; // release memory in IE
-})();
-
-(function(){
-       // Check to see if the browser returns only elements
-       // when doing getElementsByTagName("*")
-
-       // Create a fake element
-       var div = document.createElement("div");
-       div.appendChild( document.createComment("") );
-
-       // Make sure no comments are found
-       if ( div.getElementsByTagName("*").length > 0 ) {
-               Expr.find.TAG = function(match, context){
-                       var results = context.getElementsByTagName(match[1]);
-
-                       // Filter out possible comments
-                       if ( match[1] === "*" ) {
-                               var tmp = [];
-
-                               for ( var i = 0; results[i]; i++ ) {
-                                       if ( results[i].nodeType === 1 ) {
-                                               tmp.push( results[i] );
-                                       }
-                               }
-
-                               results = tmp;
-                       }
-
-                       return results;
-               };
-       }
-
-       // Check to see if an attribute returns normalized href attributes
-       div.innerHTML = "<a href='#'></a>";
-       if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
-                       div.firstChild.getAttribute("href") !== "#" ) {
-               Expr.attrHandle.href = function(elem){
-                       return elem.getAttribute("href", 2);
-               };
-       }
-
-       div = null; // release memory in IE
-})();
-
-if ( document.querySelectorAll ) {
-       (function(){
-               var oldSizzle = Sizzle, div = document.createElement("div");
-               div.innerHTML = "<p class='TEST'></p>";
-
-               // Safari can't handle uppercase or unicode characters when
-               // in quirks mode.
-               if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
-                       return;
-               }
-       
-               Sizzle = function(query, context, extra, seed){
-                       context = context || document;
-
-                       // Only use querySelectorAll on non-XML documents
-                       // (ID selectors don't work in non-HTML documents)
-                       if ( !seed && context.nodeType === 9 && !isXML(context) ) {
-                               try {
-                                       return makeArray( context.querySelectorAll(query), extra );
-                               } catch(e){}
-                       }
-               
-                       return oldSizzle(query, context, extra, seed);
-               };
-
-               for ( var prop in oldSizzle ) {
-                       Sizzle[ prop ] = oldSizzle[ prop ];
-               }
-
-               div = null; // release memory in IE
-       })();
-}
-
-(function(){
-       var div = document.createElement("div");
-
-       div.innerHTML = "<div class='test e'></div><div class='test'></div>";
-
-       // Opera can't find a second classname (in 9.6)
-       // Also, make sure that getElementsByClassName actually exists
-       if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
-               return;
-       }
-
-       // Safari caches class attributes, doesn't catch changes (in 3.2)
-       div.lastChild.className = "e";
-
-       if ( div.getElementsByClassName("e").length === 1 ) {
-               return;
-       }
-       
-       Expr.order.splice(1, 0, "CLASS");
-       Expr.find.CLASS = function(match, context, isXML) {
-               if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
-                       return context.getElementsByClassName(match[1]);
-               }
-       };
-
-       div = null; // release memory in IE
-})();
-
-function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
-       for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-               var elem = checkSet[i];
-               if ( elem ) {
-                       elem = elem[dir];
-                       var match = false;
-
-                       while ( elem ) {
-                               if ( elem.sizcache === doneName ) {
-                                       match = checkSet[elem.sizset];
-                                       break;
-                               }
-
-                               if ( elem.nodeType === 1 && !isXML ){
-                                       elem.sizcache = doneName;
-                                       elem.sizset = i;
-                               }
-
-                               if ( elem.nodeName.toLowerCase() === cur ) {
-                                       match = elem;
-                                       break;
-                               }
-
-                               elem = elem[dir];
-                       }
-
-                       checkSet[i] = match;
-               }
-       }
-}
-
-function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
-       for ( var i = 0, l = checkSet.length; i < l; i++ ) {
-               var elem = checkSet[i];
-               if ( elem ) {
-                       elem = elem[dir];
-                       var match = false;
-
-                       while ( elem ) {
-                               if ( elem.sizcache === doneName ) {
-                                       match = checkSet[elem.sizset];
-                                       break;
-                               }
-
-                               if ( elem.nodeType === 1 ) {
-                                       if ( !isXML ) {
-                                               elem.sizcache = doneName;
-                                               elem.sizset = i;
-                                       }
-                                       if ( typeof cur !== "string" ) {
-                                               if ( elem === cur ) {
-                                                       match = true;
-                                                       break;
-                                               }
-
-                                       } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
-                                               match = elem;
-                                               break;
-                                       }
-                               }
-
-                               elem = elem[dir];
-                       }
-
-                       checkSet[i] = match;
-               }
-       }
-}
-
-var contains = document.compareDocumentPosition ? function(a, b){
-       return !!(a.compareDocumentPosition(b) & 16);
-} : function(a, b){
-       return a !== b && (a.contains ? a.contains(b) : true);
-};
-
-var isXML = function(elem){
-       // documentElement is verified for cases where it doesn't yet exist
-       // (such as loading iframes in IE - #4833) 
-       var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
-       return documentElement ? documentElement.nodeName !== "HTML" : false;
-};
-
-var posProcess = function(selector, context){
-       var tmpSet = [], later = "", match,
-               root = context.nodeType ? [context] : context;
-
-       // Position selectors must be done after the filter
-       // And so must :not(positional) so we move all PSEUDOs to the end
-       while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
-               later += match[0];
-               selector = selector.replace( Expr.match.PSEUDO, "" );
-       }
-
-       selector = Expr.relative[selector] ? selector + "*" : selector;
-
-       for ( var i = 0, l = root.length; i < l; i++ ) {
-               Sizzle( selector, root[i], tmpSet );
-       }
-
-       return Sizzle.filter( later, tmpSet );
-};
-
-// EXPOSE
-jQuery.find = Sizzle;
-jQuery.expr = Sizzle.selectors;
-jQuery.expr[":"] = jQuery.expr.filters;
-jQuery.unique = Sizzle.uniqueSort;
-jQuery.text = getText;
-jQuery.isXMLDoc = isXML;
-jQuery.contains = contains;
-
-return;
-
-window.Sizzle = Sizzle;
-
-})();
-var runtil = /Until$/,
-       rparentsprev = /^(?:parents|prevUntil|prevAll)/,
-       // Note: This RegExp should be improved, or likely pulled from Sizzle
-       rmultiselector = /,/,
-       slice = Array.prototype.slice;
-
-// Implement the identical functionality for filter and not
-var winnow = function( elements, qualifier, keep ) {
-       if ( jQuery.isFunction( qualifier ) ) {
-               return jQuery.grep(elements, function( elem, i ) {
-                       return !!qualifier.call( elem, i, elem ) === keep;
-               });
-
-       } else if ( qualifier.nodeType ) {
-               return jQuery.grep(elements, function( elem, i ) {
-                       return (elem === qualifier) === keep;
-               });
-
-       } else if ( typeof qualifier === "string" ) {
-               var filtered = jQuery.grep(elements, function( elem ) {
-                       return elem.nodeType === 1;
-               });
-
-               if ( isSimple.test( qualifier ) ) {
-                       return jQuery.filter(qualifier, filtered, !keep);
-               } else {
-                       qualifier = jQuery.filter( qualifier, filtered );
-               }
-       }
-
-       return jQuery.grep(elements, function( elem, i ) {
-               return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
-       });
-};
-
-jQuery.fn.extend({
-       find: function( selector ) {
-               var ret = this.pushStack( "", "find", selector ), length = 0;
-
-               for ( var i = 0, l = this.length; i < l; i++ ) {
-                       length = ret.length;
-                       jQuery.find( selector, this[i], ret );
-
-                       if ( i > 0 ) {
-                               // Make sure that the results are unique
-                               for ( var n = length; n < ret.length; n++ ) {
-                                       for ( var r = 0; r < length; r++ ) {
-                                               if ( ret[r] === ret[n] ) {
-                                                       ret.splice(n--, 1);
-                                                       break;
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               return ret;
-       },
-
-       has: function( target ) {
-               var targets = jQuery( target );
-               return this.filter(function() {
-                       for ( var i = 0, l = targets.length; i < l; i++ ) {
-                               if ( jQuery.contains( this, targets[i] ) ) {
-                                       return true;
-                               }
-                       }
-               });
-       },
-
-       not: function( selector ) {
-               return this.pushStack( winnow(this, selector, false), "not", selector);
-       },
-
-       filter: function( selector ) {
-               return this.pushStack( winnow(this, selector, true), "filter", selector );
-       },
-       
-       is: function( selector ) {
-               return !!selector && jQuery.filter( selector, this ).length > 0;
-       },
-
-       closest: function( selectors, context ) {
-               if ( jQuery.isArray( selectors ) ) {
-                       var ret = [], cur = this[0], match, matches = {}, selector;
-
-                       if ( cur && selectors.length ) {
-                               for ( var i = 0, l = selectors.length; i < l; i++ ) {
-                                       selector = selectors[i];
-
-                                       if ( !matches[selector] ) {
-                                               matches[selector] = jQuery.expr.match.POS.test( selector ) ? 
-                                                       jQuery( selector, context || this.context ) :
-                                                       selector;
-                                       }
-                               }
-
-                               while ( cur && cur.ownerDocument && cur !== context ) {
-                                       for ( selector in matches ) {
-                                               match = matches[selector];
-
-                                               if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) {
-                                                       ret.push({ selector: selector, elem: cur });
-                                                       delete matches[selector];
-                                               }
-                                       }
-                                       cur = cur.parentNode;
-                               }
-                       }
-
-                       return ret;
-               }
-
-               var pos = jQuery.expr.match.POS.test( selectors ) ? 
-                       jQuery( selectors, context || this.context ) : null;
-
-               return this.map(function( i, cur ) {
-                       while ( cur && cur.ownerDocument && cur !== context ) {
-                               if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) {
-                                       return cur;
-                               }
-                               cur = cur.parentNode;
-                       }
-                       return null;
-               });
-       },
-       
-       // Determine the position of an element within
-       // the matched set of elements
-       index: function( elem ) {
-               if ( !elem || typeof elem === "string" ) {
-                       return jQuery.inArray( this[0],
-                               // If it receives a string, the selector is used
-                               // If it receives nothing, the siblings are used
-                               elem ? jQuery( elem ) : this.parent().children() );
-               }
-               // Locate the position of the desired element
-               return jQuery.inArray(
-                       // If it receives a jQuery object, the first element is used
-                       elem.jquery ? elem[0] : elem, this );
-       },
-
-       add: function( selector, context ) {
-               var set = typeof selector === "string" ?
-                               jQuery( selector, context || this.context ) :
-                               jQuery.makeArray( selector ),
-                       all = jQuery.merge( this.get(), set );
-
-               return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
-                       all :
-                       jQuery.unique( all ) );
-       },
-
-       andSelf: function() {
-               return this.add( this.prevObject );
-       }
-});
-
-// A painfully simple check to see if an element is disconnected
-// from a document (should be improved, where feasible).
-function isDisconnected( node ) {
-       return !node || !node.parentNode || node.parentNode.nodeType === 11;
-}
-
-jQuery.each({
-       parent: function( elem ) {
-               var parent = elem.parentNode;
-               return parent && parent.nodeType !== 11 ? parent : null;
-       },
-       parents: function( elem ) {
-               return jQuery.dir( elem, "parentNode" );
-       },
-       parentsUntil: function( elem, i, until ) {
-               return jQuery.dir( elem, "parentNode", until );
-       },
-       next: function( elem ) {
-               return 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" );
-       },
-       nextUntil: function( elem, i, until ) {
-               return jQuery.dir( elem, "nextSibling", until );
-       },
-       prevUntil: function( elem, i, until ) {
-               return jQuery.dir( elem, "previousSibling", until );
-       },
-       siblings: function( elem ) {
-               return jQuery.sibling( elem.parentNode.firstChild, elem );
-       },
-       children: function( elem ) {
-               return jQuery.sibling( elem.firstChild );
-       },
-       contents: function( elem ) {
-               return jQuery.nodeName( elem, "iframe" ) ?
-                       elem.contentDocument || elem.contentWindow.document :
-                       jQuery.makeArray( elem.childNodes );
-       }
-}, function( name, fn ) {
-       jQuery.fn[ name ] = function( until, selector ) {
-               var ret = jQuery.map( this, fn, until );
-               
-               if ( !runtil.test( name ) ) {
-                       selector = until;
-               }
-
-               if ( selector && typeof selector === "string" ) {
-                       ret = jQuery.filter( selector, ret );
-               }
-
-               ret = this.length > 1 ? jQuery.unique( ret ) : ret;
-
-               if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
-                       ret = ret.reverse();
-               }
-
-               return this.pushStack( ret, name, slice.call(arguments).join(",") );
-       };
-});
-
-jQuery.extend({
-       filter: function( expr, elems, not ) {
-               if ( not ) {
-                       expr = ":not(" + expr + ")";
-               }
-
-               return jQuery.find.matches(expr, elems);
-       },
-       
-       dir: function( elem, dir, until ) {
-               var matched = [], cur = elem[dir];
-               while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
-                       if ( cur.nodeType === 1 ) {
-                               matched.push( cur );
-                       }
-                       cur = cur[dir];
-               }
-               return matched;
-       },
-
-       nth: function( cur, result, dir, elem ) {
-               result = result || 1;
-               var num = 0;
-
-               for ( ; cur; cur = cur[dir] ) {
-                       if ( cur.nodeType === 1 && ++num === result ) {
-                               break;
-                       }
-               }
-
-               return cur;
-       },
-
-       sibling: function( n, elem ) {
-               var r = [];
-
-               for ( ; n; n = n.nextSibling ) {
-                       if ( n.nodeType === 1 && n !== elem ) {
-                               r.push( n );
-                       }
-               }
-
-               return r;
-       }
-});
-var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
-       rleadingWhitespace = /^\s+/,
-       rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g,
-       rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,
-       rtagName = /<([\w:]+)/,
-       rtbody = /<tbody/i,
-       rhtml = /<|&#?\w+;/,
-       rnocache = /<script|<object|<embed|<option|<style/i,
-       rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,  // checked="checked" or checked (html5)
-       fcloseTag = function( all, front, tag ) {
-               return rselfClosing.test( tag ) ?
-                       all :
-                       front + "></" + tag + ">";
-       },
-       wrapMap = {
-               option: [ 1, "<select multiple='multiple'>", "</select>" ],
-               legend: [ 1, "<fieldset>", "</fieldset>" ],
-               thead: [ 1, "<table>", "</table>" ],
-               tr: [ 2, "<table><tbody>", "</tbody></table>" ],
-               td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
-               col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
-               area: [ 1, "<map>", "</map>" ],
-               _default: [ 0, "", "" ]
-       };
-
-wrapMap.optgroup = wrapMap.option;
-wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
-wrapMap.th = wrapMap.td;
-
-// IE can't serialize <link> and <script> tags normally
-if ( !jQuery.support.htmlSerialize ) {
-       wrapMap._default = [ 1, "div<div>", "</div>" ];
-}
-
-jQuery.fn.extend({
-       text: function( text ) {
-               if ( jQuery.isFunction(text) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               self.text( text.call(this, i, self.text()) );
-                       });
-               }
-
-               if ( typeof text !== "object" && text !== undefined ) {
-                       return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
-               }
-
-               return jQuery.text( this );
-       },
-
-       wrapAll: function( html ) {
-               if ( jQuery.isFunction( html ) ) {
-                       return this.each(function(i) {
-                               jQuery(this).wrapAll( html.call(this, i) );
-                       });
-               }
-
-               if ( this[0] ) {
-                       // The elements to wrap the target around
-                       var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
-
-                       if ( this[0].parentNode ) {
-                               wrap.insertBefore( this[0] );
-                       }
-
-                       wrap.map(function() {
-                               var elem = this;
-
-                               while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
-                                       elem = elem.firstChild;
-                               }
-
-                               return elem;
-                       }).append(this);
-               }
-
-               return this;
-       },
-
-       wrapInner: function( html ) {
-               if ( jQuery.isFunction( html ) ) {
-                       return this.each(function(i) {
-                               jQuery(this).wrapInner( html.call(this, i) );
-                       });
-               }
-
-               return this.each(function() {
-                       var self = jQuery( this ), contents = self.contents();
-
-                       if ( contents.length ) {
-                               contents.wrapAll( html );
-
-                       } else {
-                               self.append( html );
-                       }
-               });
-       },
-
-       wrap: function( html ) {
-               return this.each(function() {
-                       jQuery( this ).wrapAll( html );
-               });
-       },
-
-       unwrap: function() {
-               return this.parent().each(function() {
-                       if ( !jQuery.nodeName( this, "body" ) ) {
-                               jQuery( this ).replaceWith( this.childNodes );
-                       }
-               }).end();
-       },
-
-       append: function() {
-               return this.domManip(arguments, true, function( elem ) {
-                       if ( this.nodeType === 1 ) {
-                               this.appendChild( elem );
-                       }
-               });
-       },
-
-       prepend: function() {
-               return this.domManip(arguments, true, function( elem ) {
-                       if ( this.nodeType === 1 ) {
-                               this.insertBefore( elem, this.firstChild );
-                       }
-               });
-       },
-
-       before: function() {
-               if ( this[0] && this[0].parentNode ) {
-                       return this.domManip(arguments, false, function( elem ) {
-                               this.parentNode.insertBefore( elem, this );
-                       });
-               } else if ( arguments.length ) {
-                       var set = jQuery(arguments[0]);
-                       set.push.apply( set, this.toArray() );
-                       return this.pushStack( set, "before", arguments );
-               }
-       },
-
-       after: function() {
-               if ( this[0] && this[0].parentNode ) {
-                       return this.domManip(arguments, false, function( elem ) {
-                               this.parentNode.insertBefore( elem, this.nextSibling );
-                       });
-               } else if ( arguments.length ) {
-                       var set = this.pushStack( this, "after", arguments );
-                       set.push.apply( set, jQuery(arguments[0]).toArray() );
-                       return set;
-               }
-       },
-       
-       // keepData is for internal use only--do not document
-       remove: function( selector, keepData ) {
-               for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
-                       if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
-                               if ( !keepData && elem.nodeType === 1 ) {
-                                       jQuery.cleanData( elem.getElementsByTagName("*") );
-                                       jQuery.cleanData( [ elem ] );
-                               }
-
-                               if ( elem.parentNode ) {
-                                        elem.parentNode.removeChild( elem );
-                               }
-                       }
-               }
-               
-               return this;
-       },
-
-       empty: function() {
-               for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
-                       // Remove element nodes and prevent memory leaks
-                       if ( elem.nodeType === 1 ) {
-                               jQuery.cleanData( elem.getElementsByTagName("*") );
-                       }
-
-                       // Remove any remaining nodes
-                       while ( elem.firstChild ) {
-                               elem.removeChild( elem.firstChild );
-                       }
-               }
-               
-               return this;
-       },
-
-       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, ownerDocument = this.ownerDocument;
-                               if ( !html ) {
-                                       var div = ownerDocument.createElement("div");
-                                       div.appendChild( this.cloneNode(true) );
-                                       html = div.innerHTML;
-                               }
-
-                               return jQuery.clean([html.replace(rinlinejQuery, "")
-                                       // Handle the case in IE 8 where action=/test/> self-closes a tag
-                                       .replace(/=([^="'>\s]+\/)>/g, '="$1">')
-                                       .replace(rleadingWhitespace, "")], ownerDocument)[0];
-                       } else {
-                               return this.cloneNode(true);
-                       }
-               });
-
-               // Copy the events from the original to the clone
-               if ( events === true ) {
-                       cloneCopyEvent( this, ret );
-                       cloneCopyEvent( this.find("*"), ret.find("*") );
-               }
-
-               // Return the cloned set
-               return ret;
-       },
-
-       html: function( value ) {
-               if ( value === undefined ) {
-                       return this[0] && this[0].nodeType === 1 ?
-                               this[0].innerHTML.replace(rinlinejQuery, "") :
-                               null;
-
-               // See if we can take a shortcut and just use innerHTML
-               } else if ( typeof value === "string" && !rnocache.test( value ) &&
-                       (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
-                       !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
-
-                       value = value.replace(rxhtmlTag, fcloseTag);
-
-                       try {
-                               for ( var i = 0, l = this.length; i < l; i++ ) {
-                                       // Remove element nodes and prevent memory leaks
-                                       if ( this[i].nodeType === 1 ) {
-                                               jQuery.cleanData( this[i].getElementsByTagName("*") );
-                                               this[i].innerHTML = value;
-                                       }
-                               }
-
-                       // If using innerHTML throws an exception, use the fallback method
-                       } catch(e) {
-                               this.empty().append( value );
-                       }
-
-               } else if ( jQuery.isFunction( value ) ) {
-                       this.each(function(i){
-                               var self = jQuery(this), old = self.html();
-                               self.empty().append(function(){
-                                       return value.call( this, i, old );
-                               });
-                       });
-
-               } else {
-                       this.empty().append( value );
-               }
-
-               return this;
-       },
-
-       replaceWith: function( value ) {
-               if ( this[0] && this[0].parentNode ) {
-                       // Make sure that the elements are removed from the DOM before they are inserted
-                       // this can help fix replacing a parent with child elements
-                       if ( jQuery.isFunction( value ) ) {
-                               return this.each(function(i) {
-                                       var self = jQuery(this), old = self.html();
-                                       self.replaceWith( value.call( this, i, old ) );
-                               });
-                       }
-
-                       if ( typeof value !== "string" ) {
-                               value = jQuery(value).detach();
-                       }
-
-                       return this.each(function() {
-                               var next = this.nextSibling, parent = this.parentNode;
-
-                               jQuery(this).remove();
-
-                               if ( next ) {
-                                       jQuery(next).before( value );
-                               } else {
-                                       jQuery(parent).append( value );
-                               }
-                       });
-               } else {
-                       return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value );
-               }
-       },
-
-       detach: function( selector ) {
-               return this.remove( selector, true );
-       },
-
-       domManip: function( args, table, callback ) {
-               var results, first, value = args[0], scripts = [], fragment, parent;
-
-               // We can't cloneNode fragments that contain checked, in WebKit
-               if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
-                       return this.each(function() {
-                               jQuery(this).domManip( args, table, callback, true );
-                       });
-               }
-
-               if ( jQuery.isFunction(value) ) {
-                       return this.each(function(i) {
-                               var self = jQuery(this);
-                               args[0] = value.call(this, i, table ? self.html() : undefined);
-                               self.domManip( args, table, callback );
-                       });
-               }
-
-               if ( this[0] ) {
-                       parent = value && value.parentNode;
-
-                       // If we're in a fragment, just use that instead of building a new one
-                       if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
-                               results = { fragment: parent };
-
-                       } else {
-                               results = buildFragment( args, this, scripts );
-                       }
-                       
-                       fragment = results.fragment;
-                       
-                       if ( fragment.childNodes.length === 1 ) {
-                               first = fragment = fragment.firstChild;
-                       } else {
-                               first = fragment.firstChild;
-                       }
-
-                       if ( first ) {
-                               table = table && jQuery.nodeName( first, "tr" );
-
-                               for ( var i = 0, l = this.length; i < l; i++ ) {
-                                       callback.call(
-                                               table ?
-                                                       root(this[i], first) :
-                                                       this[i],
-                                               i > 0 || results.cacheable || this.length > 1  ?
-                                                       fragment.cloneNode(true) :
-                                                       fragment
-                                       );
-                               }
-                       }
-
-                       if ( scripts.length ) {
-                               jQuery.each( scripts, evalScript );
-                       }
-               }
-
-               return this;
-
-               function root( elem, cur ) {
-                       return jQuery.nodeName(elem, "table") ?
-                               (elem.getElementsByTagName("tbody")[0] ||
-                               elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
-                               elem;
-               }
-       }
-});
-
-function cloneCopyEvent(orig, ret) {
-       var i = 0;
-
-       ret.each(function() {
-               if ( this.nodeName !== (orig[i] && orig[i].nodeName) ) {
-                       return;
-               }
-
-               var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events;
-
-               if ( events ) {
-                       delete curData.handle;
-                       curData.events = {};
-
-                       for ( var type in events ) {
-                               for ( var handler in events[ type ] ) {
-                                       jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
-                               }
-                       }
-               }
-       });
-}
-
-function buildFragment( args, nodes, scripts ) {
-       var fragment, cacheable, cacheresults,
-               doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
-
-       // Only cache "small" (1/2 KB) strings that are associated with the main document
-       // Cloning options loses the selected state, so don't cache them
-       // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
-       // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
-       if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document &&
-               !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
-
-               cacheable = true;
-               cacheresults = jQuery.fragments[ args[0] ];
-               if ( cacheresults ) {
-                       if ( cacheresults !== 1 ) {
-                               fragment = cacheresults;
-                       }
-               }
-       }
-
-       if ( !fragment ) {
-               fragment = doc.createDocumentFragment();
-               jQuery.clean( args, doc, fragment, scripts );
-       }
-
-       if ( cacheable ) {
-               jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
-       }
-
-       return { fragment: fragment, cacheable: cacheable };
-}
-
-jQuery.fragments = {};
-
-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 ),
-                       parent = this.length === 1 && this[0].parentNode;
-               
-               if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
-                       insert[ original ]( this[0] );
-                       return this;
-                       
-               } else {
-                       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, insert.selector );
-               }
-       };
-});
-
-jQuery.extend({
-       clean: function( elems, context, fragment, scripts ) {
-               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;
-               }
-
-               var ret = [];
-
-               for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
-                       if ( typeof elem === "number" ) {
-                               elem += "";
-                       }
-
-                       if ( !elem ) {
-                               continue;
-                       }
-
-                       // Convert html string into DOM nodes
-                       if ( typeof elem === "string" && !rhtml.test( elem ) ) {
-                               elem = context.createTextNode( elem );
-
-                       } else if ( typeof elem === "string" ) {
-                               // Fix "XHTML"-style tags in all browsers
-                               elem = elem.replace(rxhtmlTag, fcloseTag);
-
-                               // Trim whitespace, otherwise indexOf won't work as expected
-                               var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
-                                       wrap = wrapMap[ tag ] || wrapMap._default,
-                                       depth = wrap[0],
-                                       div = context.createElement("div");
-
-                               // Go to html and back, then peel off extra wrappers
-                               div.innerHTML = wrap[1] + elem + wrap[2];
-
-                               // Move to the right depth
-                               while ( depth-- ) {
-                                       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 = rtbody.test(elem),
-                                               tbody = tag === "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 && rleadingWhitespace.test( elem ) ) {
-                                       div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
-                               }
-
-                               elem = div.childNodes;
-                       }
-
-                       if ( elem.nodeType ) {
-                               ret.push( elem );
-                       } else {
-                               ret = jQuery.merge( ret, elem );
-                       }
-               }
-
-               if ( fragment ) {
-                       for ( var i = 0; ret[i]; i++ ) {
-                               if ( scripts && 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 ret;
-       },
-       
-       cleanData: function( elems ) {
-               var data, id, cache = jQuery.cache,
-                       special = jQuery.event.special,
-                       deleteExpando = jQuery.support.deleteExpando;
-               
-               for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
-                       id = elem[ jQuery.expando ];
-                       
-                       if ( id ) {
-                               data = cache[ id ];
-                               
-                               if ( data.events ) {
-                                       for ( var type in data.events ) {
-                                               if ( special[ type ] ) {
-                                                       jQuery.event.remove( elem, type );
-
-                                               } else {
-                                                       removeEvent( elem, type, data.handle );
-                                               }
-                                       }
-                               }
-                               
-                               if ( deleteExpando ) {
-                                       delete elem[ jQuery.expando ];
-
-                               } else if ( elem.removeAttribute ) {
-                                       elem.removeAttribute( jQuery.expando );
-                               }
-                               
-                               delete cache[ id ];
-                       }
-               }
-       }
-});
-// exclude the following css properties to add px
-var rexclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
-       ralpha = /alpha\([^)]*\)/,
-       ropacity = /opacity=([^)]*)/,
-       rfloat = /float/i,
-       rdashAlpha = /-([a-z])/ig,
-       rupper = /([A-Z])/g,
-       rnumpx = /^-?\d+(?:px)?$/i,
-       rnum = /^-?\d/,
-
-       cssShow = { position: "absolute", visibility: "hidden", display:"block" },
-       cssWidth = [ "Left", "Right" ],
-       cssHeight = [ "Top", "Bottom" ],
-
-       // cache check for defaultView.getComputedStyle
-       getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
-       // normalize float css property
-       styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat",
-       fcamelCase = function( all, letter ) {
-               return letter.toUpperCase();
-       };
-
-jQuery.fn.css = function( name, value ) {
-       return access( this, name, value, true, function( elem, name, value ) {
-               if ( value === undefined ) {
-                       return jQuery.curCSS( elem, name );
-               }
-               
-               if ( typeof value === "number" && !rexclude.test(name) ) {
-                       value += "px";
-               }
-
-               jQuery.style( elem, name, value );
-       });
-};
-
-jQuery.extend({
-       style: function( elem, name, value ) {
-               // don't set styles on text and comment nodes
-               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
-                       return undefined;
-               }
-
-               // ignore negative width and height values #1599
-               if ( (name === "width" || name === "height") && parseFloat(value) < 0 ) {
-                       value = undefined;
-               }
-
-               var style = elem.style || elem, set = value !== undefined;
-
-               // 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
-                               style.zoom = 1;
-
-                               // Set the alpha filter to set the opacity
-                               var opacity = parseInt( value, 10 ) + "" === "NaN" ? "" : "alpha(opacity=" + value * 100 + ")";
-                               var filter = style.filter || jQuery.curCSS( elem, "filter" ) || "";
-                               style.filter = ralpha.test(filter) ? filter.replace(ralpha, opacity) : opacity;
-                       }
-
-                       return style.filter && style.filter.indexOf("opacity=") >= 0 ?
-                               (parseFloat( ropacity.exec(style.filter)[1] ) / 100) + "":
-                               "";
-               }
-
-               // Make sure we're using the right name for getting the float value
-               if ( rfloat.test( name ) ) {
-                       name = styleFloat;
-               }
-
-               name = name.replace(rdashAlpha, fcamelCase);
-
-               if ( set ) {
-                       style[ name ] = value;
-               }
-
-               return style[ name ];
-       },
-
-       css: function( elem, name, force, extra ) {
-               if ( name === "width" || name === "height" ) {
-                       var val, props = cssShow, which = name === "width" ? cssWidth : cssHeight;
-
-                       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, filter;
-
-               // IE uses filters for opacity
-               if ( !jQuery.support.opacity && name === "opacity" && elem.currentStyle ) {
-                       ret = ropacity.test(elem.currentStyle.filter || "") ?
-                               (parseFloat(RegExp.$1) / 100) + "" :
-                               "";
-
-                       return ret === "" ?
-                               "1" :
-                               ret;
-               }
-
-               // Make sure we're using the right name for getting the float value
-               if ( rfloat.test( name ) ) {
-                       name = styleFloat;
-               }
-
-               if ( !force && style && style[ name ] ) {
-                       ret = style[ name ];
-
-               } else if ( getComputedStyle ) {
-
-                       // Only "float" is needed here
-                       if ( rfloat.test( name ) ) {
-                               name = "float";
-                       }
-
-                       name = name.replace( rupper, "-$1" ).toLowerCase();
-
-                       var defaultView = elem.ownerDocument.defaultView;
-
-                       if ( !defaultView ) {
-                               return null;
-                       }
-
-                       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(rdashAlpha, fcamelCase);
-
-                       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 ( !rnumpx.test( ret ) && rnum.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 = camelCase === "fontSize" ? "1em" : (ret || 0);
-                               ret = style.pixelLeft + "px";
-
-                               // Revert the changed values
-                               style.left = left;
-                               elem.runtimeStyle.left = rsLeft;
-                       }
-               }
-
-               return ret;
-       },
-
-       // 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 ];
-               }
-       }
-});
-
-if ( jQuery.expr && jQuery.expr.filters ) {
-       jQuery.expr.filters.hidden = function( elem ) {
-               var width = elem.offsetWidth, height = elem.offsetHeight,
-                       skip = elem.nodeName.toLowerCase() === "tr";
-
-               return width === 0 && height === 0 && !skip ?
-                       true :
-                       width > 0 && height > 0 && !skip ?
-                               false :
-                               jQuery.curCSS(elem, "display") === "none";
-       };
-
-       jQuery.expr.filters.visible = function( elem ) {
-               return !jQuery.expr.filters.hidden( elem );
-       };
-}
-var jsc = now(),
-       rscript = /<script(.|\s)*?\/script>/gi,
-       rselectTextarea = /select|textarea/i,
-       rinput = /color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,
-       jsre = /=\?(&|$)/,
-       rquery = /\?/,
-       rts = /(\?|&)_=.*?(&|$)/,
-       rurl = /^(\w+:)?\/\/([^\/?#]+)/,
-       r20 = /%20/g,
-
-       // Keep a copy of the old load method
-       _load = jQuery.fn.load;
-
-jQuery.fn.extend({
-       load: function( url, params, callback ) {
-               if ( typeof url !== "string" ) {
-                       return _load.call( this, url );
-
-               // Don't do a request if no elements are being requested
-               } else if ( !this.length ) {
-                       return this;
-               }
-
-               var off = url.indexOf(" ");
-               if ( off >= 0 ) {
-                       var selector = url.slice(off, url.length);
-                       url = url.slice(0, off);
-               }
-
-               // Default to a GET request
-               var type = "GET";
-
-               // If the second parameter was provided
-               if ( params ) {
-                       // If it's a function
-                       if ( jQuery.isFunction( params ) ) {
-                               // We assume that it's the callback
-                               callback = params;
-                               params = null;
-
-                       // Otherwise, build a param string
-                       } else if ( typeof params === "object" ) {
-                               params = jQuery.param( params, jQuery.ajaxSettings.traditional );
-                               type = "POST";
-                       }
-               }
-
-               var self = this;
-
-               // Request the remote document
-               jQuery.ajax({
-                       url: url,
-                       type: type,
-                       dataType: "html",
-                       data: params,
-                       complete: function( res, status ) {
-                               // If successful, inject the HTML into all the matched elements
-                               if ( status === "success" || status === "notmodified" ) {
-                                       // See if a selector was specified
-                                       self.html( selector ?
-                                               // Create a dummy div to hold the results
-                                               jQuery("<div />")
-                                                       // inject the contents of the document in, removing the scripts
-                                                       // to avoid any 'Permission Denied' errors in IE
-                                                       .append(res.responseText.replace(rscript, ""))
-
-                                                       // Locate the specified elements
-                                                       .find(selector) :
-
-                                               // If not, just inject the full result
-                                               res.responseText );
-                               }
-
-                               if ( callback ) {
-                                       self.each( callback, [res.responseText, status, res] );
-                               }
-                       }
-               });
-
-               return this;
-       },
-
-       serialize: function() {
-               return jQuery.param(this.serializeArray());
-       },
-       serializeArray: function() {
-               return this.map(function() {
-                       return this.elements ? jQuery.makeArray(this.elements) : this;
-               })
-               .filter(function() {
-                       return this.name && !this.disabled &&
-                               (this.checked || rselectTextarea.test(this.nodeName) ||
-                                       rinput.test(this.type));
-               })
-               .map(function( i, elem ) {
-                       var val = jQuery(this).val();
-
-                       return val == null ?
-                               null :
-                               jQuery.isArray(val) ?
-                                       jQuery.map( val, function( val, i ) {
-                                               return { name: elem.name, value: val };
-                                       }) :
-                                       { name: elem.name, value: val };
-               }).get();
-       }
-});
-
-// Attach a bunch of functions for handling common AJAX events
-jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) {
-       jQuery.fn[o] = function( f ) {
-               return this.bind(o, f);
-       };
-});
-
-jQuery.extend({
-
-       get: function( url, data, callback, type ) {
-               // shift arguments if data argument was omited
-               if ( jQuery.isFunction( data ) ) {
-                       type = type || callback;
-                       callback = data;
-                       data = null;
-               }
-
-               return jQuery.ajax({
-                       type: "GET",
-                       url: url,
-                       data: data,
-                       success: callback,
-                       dataType: type
-               });
-       },
-
-       getScript: function( url, callback ) {
-               return jQuery.get(url, null, callback, "script");
-       },
-
-       getJSON: function( url, data, callback ) {
-               return jQuery.get(url, data, callback, "json");
-       },
-
-       post: function( url, data, callback, type ) {
-               // shift arguments if data argument was omited
-               if ( jQuery.isFunction( data ) ) {
-                       type = type || callback;
-                       callback = data;
-                       data = {};
-               }
-
-               return jQuery.ajax({
-                       type: "POST",
-                       url: url,
-                       data: data,
-                       success: callback,
-                       dataType: type
-               });
-       },
-
-       ajaxSetup: function( settings ) {
-               jQuery.extend( jQuery.ajaxSettings, settings );
-       },
-
-       ajaxSettings: {
-               url: location.href,
-               global: true,
-               type: "GET",
-               contentType: "application/x-www-form-urlencoded",
-               processData: true,
-               async: true,
-               /*
-               timeout: 0,
-               data: null,
-               username: null,
-               password: null,
-               traditional: false,
-               */
-               // Create the request object; Microsoft failed to properly
-               // implement the XMLHttpRequest in IE7 (can't request local files),
-               // so we use the ActiveXObject when it is available
-               // This function can be overriden by calling jQuery.ajaxSetup
-               xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
-                       function() {
-                               return new window.XMLHttpRequest();
-                       } :
-                       function() {
-                               try {
-                                       return new window.ActiveXObject("Microsoft.XMLHTTP");
-                               } catch(e) {}
-                       },
-               accepts: {
-                       xml: "application/xml, text/xml",
-                       html: "text/html",
-                       script: "text/javascript, application/javascript",
-                       json: "application/json, text/javascript",
-                       text: "text/plain",
-                       _default: "*/*"
-               }
-       },
-
-       // Last-Modified header cache for next request
-       lastModified: {},
-       etag: {},
-
-       ajax: function( origSettings ) {
-               var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
-               
-               var jsonp, status, data,
-                       callbackContext = origSettings && origSettings.context || s,
-                       type = s.type.toUpperCase();
-
-               // convert data if not already a string
-               if ( s.data && s.processData && typeof s.data !== "string" ) {
-                       s.data = jQuery.param( s.data, s.traditional );
-               }
-
-               // Handle JSONP Parameter Callbacks
-               if ( s.dataType === "jsonp" ) {
-                       if ( type === "GET" ) {
-                               if ( !jsre.test( s.url ) ) {
-                                       s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
-                               }
-                       } else if ( !s.data || !jsre.test(s.data) ) {
-                               s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
-                       }
-                       s.dataType = "json";
-               }
-
-               // Build temporary JSONP function
-               if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
-                       jsonp = s.jsonpCallback || ("jsonp" + jsc++);
-
-                       // Replace the =? sequence both in the query string and the data
-                       if ( s.data ) {
-                               s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
-                       }
-
-                       s.url = s.url.replace(jsre, "=" + jsonp + "$1");
-
-                       // We need to make sure
-                       // that a JSONP style response is executed properly
-                       s.dataType = "script";
-
-                       // Handle JSONP-style loading
-                       window[ jsonp ] = window[ jsonp ] || function( tmp ) {
-                               data = tmp;
-                               success();
-                               complete();
-                               // Garbage collect
-                               window[ jsonp ] = undefined;
-
-                               try {
-                                       delete window[ jsonp ];
-                               } catch(e) {}
-
-                               if ( head ) {
-                                       head.removeChild( script );
-                               }
-                       };
-               }
-
-               if ( s.dataType === "script" && s.cache === null ) {
-                       s.cache = false;
-               }
-
-               if ( s.cache === false && type === "GET" ) {
-                       var ts = now();
-
-                       // try replacing _= if it is there
-                       var ret = s.url.replace(rts, "$1_=" + ts + "$2");
-
-                       // if nothing was replaced, add timestamp to the end
-                       s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : "");
-               }
-
-               // If data is available, append data to url for get requests
-               if ( s.data && type === "GET" ) {
-                       s.url += (rquery.test(s.url) ? "&" : "?") + s.data;
-               }
-
-               // Watch for a new set of requests
-               if ( s.global && ! jQuery.active++ ) {
-                       jQuery.event.trigger( "ajaxStart" );
-               }
-
-               // Matches an absolute URL, and saves the domain
-               var parts = rurl.exec( s.url ),
-                       remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
-
-               // If we're requesting a remote document
-               // and trying to load JSON or Script with a GET
-               if ( s.dataType === "script" && type === "GET" && remote ) {
-                       var head = document.getElementsByTagName("head")[0] || document.documentElement;
-                       var script = document.createElement("script");
-                       script.src = s.url;
-                       if ( s.scriptCharset ) {
-                               script.charset = s.scriptCharset;
-                       }
-
-                       // Handle Script loading
-                       if ( !jsonp ) {
-                               var done = false;
-
-                               // Attach handlers for all browsers
-                               script.onload = script.onreadystatechange = function() {
-                                       if ( !done && (!this.readyState ||
-                                                       this.readyState === "loaded" || this.readyState === "complete") ) {
-                                               done = true;
-                                               success();
-                                               complete();
-
-                                               // Handle memory leak in IE
-                                               script.onload = script.onreadystatechange = null;
-                                               if ( head && script.parentNode ) {
-                                                       head.removeChild( script );
-                                               }
-                                       }
-                               };
-                       }
-
-                       // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
-                       // This arises when a base node is used (#2709 and #4378).
-                       head.insertBefore( script, head.firstChild );
-
-                       // We handle everything using the script element injection
-                       return undefined;
-               }
-
-               var requestDone = false;
-
-               // Create the request object
-               var xhr = s.xhr();
-
-               if ( !xhr ) {
-                       return;
-               }
-
-               // Open the socket
-               // Passing null username, generates a login popup on Opera (#2865)
-               if ( s.username ) {
-                       xhr.open(type, s.url, s.async, s.username, s.password);
-               } else {
-                       xhr.open(type, s.url, s.async);
-               }
-
-               // Need an extra try/catch for cross domain requests in Firefox 3
-               try {
-                       // Set the correct header, if data is being sent
-                       if ( s.data || origSettings && origSettings.contentType ) {
-                               xhr.setRequestHeader("Content-Type", s.contentType);
-                       }
-
-                       // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
-                       if ( s.ifModified ) {
-                               if ( jQuery.lastModified[s.url] ) {
-                                       xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]);
-                               }
-
-                               if ( jQuery.etag[s.url] ) {
-                                       xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]);
-                               }
-                       }
-
-                       // Set header so the called script knows that it's an XMLHttpRequest
-                       // Only send the header if it's not a remote XHR
-                       if ( !remote ) {
-                               xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
-                       }
-
-                       // Set the Accepts header for the server, depending on the dataType
-                       xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
-                               s.accepts[ s.dataType ] + ", */*" :
-                               s.accepts._default );
-               } catch(e) {}
-
-               // Allow custom headers/mimetypes and early abort
-               if ( s.beforeSend && s.beforeSend.call(callbackContext, xhr, s) === false ) {
-                       // Handle the global AJAX counter
-                       if ( s.global && ! --jQuery.active ) {
-                               jQuery.event.trigger( "ajaxStop" );
-                       }
-
-                       // close opended socket
-                       xhr.abort();
-                       return false;
-               }
-
-               if ( s.global ) {
-                       trigger("ajaxSend", [xhr, s]);
-               }
-
-               // Wait for a response to come back
-               var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) {
-                       // The request was aborted
-                       if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) {
-                               // Opera doesn't call onreadystatechange before this point
-                               // so we simulate the call
-                               if ( !requestDone ) {
-                                       complete();
-                               }
-
-                               requestDone = true;
-                               if ( xhr ) {
-                                       xhr.onreadystatechange = jQuery.noop;
-                               }
-
-                       // The transfer is complete and the data is available, or the request timed out
-                       } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) {
-                               requestDone = true;
-                               xhr.onreadystatechange = jQuery.noop;
-
-                               status = isTimeout === "timeout" ?
-                                       "timeout" :
-                                       !jQuery.httpSuccess( xhr ) ?
-                                               "error" :
-                                               s.ifModified && jQuery.httpNotModified( xhr, s.url ) ?
-                                                       "notmodified" :
-                                                       "success";
-
-                               var errMsg;
-
-                               if ( status === "success" ) {
-                                       // Watch for, and catch, XML document parse errors
-                                       try {
-                                               // process the data (runs the xml through httpData regardless of callback)
-                                               data = jQuery.httpData( xhr, s.dataType, s );
-                                       } catch(err) {
-                                               status = "parsererror";
-                                               errMsg = err;
-                                       }
-                               }
-
-                               // Make sure that the request was successful or notmodified
-                               if ( status === "success" || status === "notmodified" ) {
-                                       // JSONP handles its own success callback
-                                       if ( !jsonp ) {
-                                               success();
-                                       }
-                               } else {
-                                       jQuery.handleError(s, xhr, status, errMsg);
-                               }
-
-                               // Fire the complete handlers
-                               complete();
-
-                               if ( isTimeout === "timeout" ) {
-                                       xhr.abort();
-                               }
-
-                               // Stop memory leaks
-                               if ( s.async ) {
-                                       xhr = null;
-                               }
-                       }
-               };
-
-               // Override the abort handler, if we can (IE doesn't allow it, but that's OK)
-               // Opera doesn't fire onreadystatechange at all on abort
-               try {
-                       var oldAbort = xhr.abort;
-                       xhr.abort = function() {
-                               if ( xhr ) {
-                                       oldAbort.call( xhr );
-                               }
-
-                               onreadystatechange( "abort" );
-                       };
-               } catch(e) { }
-
-               // Timeout checker
-               if ( s.async && s.timeout > 0 ) {
-                       setTimeout(function() {
-                               // Check to see if the request is still happening
-                               if ( xhr && !requestDone ) {
-                                       onreadystatechange( "timeout" );
-                               }
-                       }, s.timeout);
-               }
-
-               // Send the data
-               try {
-                       xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null );
-               } catch(e) {
-                       jQuery.handleError(s, xhr, null, e);
-                       // Fire the complete handlers
-                       complete();
-               }
-
-               // firefox 1.5 doesn't fire statechange for sync requests
-               if ( !s.async ) {
-                       onreadystatechange();
-               }
-
-               function success() {
-                       // If a local callback was specified, fire it and pass it the data
-                       if ( s.success ) {
-                               s.success.call( callbackContext, data, status, xhr );
-                       }
-
-                       // Fire the global callback
-                       if ( s.global ) {
-                               trigger( "ajaxSuccess", [xhr, s] );
-                       }
-               }
-
-               function complete() {
-                       // Process result
-                       if ( s.complete ) {
-                               s.complete.call( callbackContext, xhr, status);
-                       }
-
-                       // The request was completed
-                       if ( s.global ) {
-                               trigger( "ajaxComplete", [xhr, s] );
-                       }
-
-                       // Handle the global AJAX counter
-                       if ( s.global && ! --jQuery.active ) {
-                               jQuery.event.trigger( "ajaxStop" );
-                       }
-               }
-               
-               function trigger(type, args) {
-                       (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
-               }
-
-               // return XMLHttpRequest to allow aborting the request etc.
-               return xhr;
-       },
-
-       handleError: function( s, xhr, status, e ) {
-               // If a local callback was specified, fire it
-               if ( s.error ) {
-                       s.error.call( s.context || s, xhr, status, e );
-               }
-
-               // Fire the global callback
-               if ( s.global ) {
-                       (s.context ? jQuery(s.context) : jQuery.event).trigger( "ajaxError", [xhr, s, e] );
-               }
-       },
-
-       // Counter for holding the number of active queries
-       active: 0,
-
-       // Determines if an XMLHttpRequest was successful or not
-       httpSuccess: function( xhr ) {
-               try {
-                       // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
-                       return !xhr.status && location.protocol === "file:" ||
-                               // Opera returns 0 when status is 304
-                               ( xhr.status >= 200 && xhr.status < 300 ) ||
-                               xhr.status === 304 || xhr.status === 1223 || xhr.status === 0;
-               } catch(e) {}
-
-               return false;
-       },
-
-       // Determines if an XMLHttpRequest returns NotModified
-       httpNotModified: function( xhr, url ) {
-               var lastModified = xhr.getResponseHeader("Last-Modified"),
-                       etag = xhr.getResponseHeader("Etag");
-
-               if ( lastModified ) {
-                       jQuery.lastModified[url] = lastModified;
-               }
-
-               if ( etag ) {
-                       jQuery.etag[url] = etag;
-               }
-
-               // Opera returns 0 when status is 304
-               return xhr.status === 304 || xhr.status === 0;
-       },
-
-       httpData: function( xhr, type, s ) {
-               var ct = xhr.getResponseHeader("content-type") || "",
-                       xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
-                       data = xml ? xhr.responseXML : xhr.responseText;
-
-               if ( xml && data.documentElement.nodeName === "parsererror" ) {
-                       jQuery.error( "parsererror" );
-               }
-
-               // Allow a pre-filtering function to sanitize the response
-               // s is checked to keep backwards compatibility
-               if ( s && s.dataFilter ) {
-                       data = s.dataFilter( data, type );
-               }
-
-               // The filter can actually parse the response
-               if ( typeof data === "string" ) {
-                       // Get the JavaScript object, if JSON is used.
-                       if ( type === "json" || !type && ct.indexOf("json") >= 0 ) {
-                               data = jQuery.parseJSON( data );
-
-                       // If the type is "script", eval it in global context
-                       } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) {
-                               jQuery.globalEval( data );
-                       }
-               }
-
-               return data;
-       },
-
-       // Serialize an array of form elements or a set of
-       // key/values into a query string
-       param: function( a, traditional ) {
-               var s = [];
-               
-               // Set traditional to true for jQuery <= 1.3.2 behavior.
-               if ( traditional === undefined ) {
-                       traditional = jQuery.ajaxSettings.traditional;
-               }
-               
-               // If an array was passed in, assume that it is an array of form elements.
-               if ( jQuery.isArray(a) || a.jquery ) {
-                       // Serialize the form elements
-                       jQuery.each( a, function() {
-                               add( this.name, this.value );
-                       });
-                       
-               } else {
-                       // If traditional, encode the "old" way (the way 1.3.2 or older
-                       // did it), otherwise encode params recursively.
-                       for ( var prefix in a ) {
-                               buildParams( prefix, a[prefix] );
-                       }
-               }
-
-               // Return the resulting serialization
-               return s.join("&").replace(r20, "+");
-
-               function buildParams( prefix, obj ) {
-                       if ( jQuery.isArray(obj) ) {
-                               // Serialize array item.
-                               jQuery.each( obj, function( i, v ) {
-                                       if ( traditional || /\[\]$/.test( prefix ) ) {
-                                               // Treat each array item as a scalar.
-                                               add( prefix, v );
-                                       } else {
-                                               // If array item is non-scalar (array or object), encode its
-                                               // numeric index to resolve deserialization ambiguity issues.
-                                               // Note that rack (as of 1.0.0) can't currently deserialize
-                                               // nested arrays properly, and attempting to do so may cause
-                                               // a server error. Possible fixes are to modify rack's
-                                               // deserialization algorithm or to provide an option or flag
-                                               // to force array serialization to be shallow.
-                                               buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v );
-                                       }
-                               });
-                                       
-                       } else if ( !traditional && obj != null && typeof obj === "object" ) {
-                               // Serialize object item.
-                               jQuery.each( obj, function( k, v ) {
-                                       buildParams( prefix + "[" + k + "]", v );
-                               });
-                                       
-                       } else {
-                               // Serialize scalar item.
-                               add( prefix, obj );
-                       }
-               }
-
-               function add( key, value ) {
-                       // If value is a function, invoke it and return its value
-                       value = jQuery.isFunction(value) ? value() : value;
-                       s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
-               }
-       }
-});
-var elemdisplay = {},
-       rfxtypes = /toggle|show|hide/,
-       rfxnum = /^([+-]=)?([\d+-.]+)(.*)$/,
-       timerId,
-       fxAttrs = [
-               // height animations
-               [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
-               // width animations
-               [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
-               // opacity animations
-               [ "opacity" ]
-       ];
-
-jQuery.fn.extend({
-       show: function( speed, callback ) {
-               if ( speed || speed === 0) {
-                       return this.animate( genFx("show", 3), speed, callback);
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var old = jQuery.data(this[i], "olddisplay");
-
-                               this[i].style.display = old || "";
-
-                               if ( jQuery.css(this[i], "display") === "none" ) {
-                                       var nodeName = this[i].nodeName, display;
-
-                                       if ( elemdisplay[ nodeName ] ) {
-                                               display = elemdisplay[ nodeName ];
-
-                                       } else {
-                                               var elem = jQuery("<" + nodeName + " />").appendTo("body");
-
-                                               display = elem.css("display");
-
-                                               if ( display === "none" ) {
-                                                       display = "block";
-                                               }
-
-                                               elem.remove();
-
-                                               elemdisplay[ nodeName ] = display;
-                                       }
-
-                                       jQuery.data(this[i], "olddisplay", display);
-                               }
-                       }
-
-                       // Set the display of the elements in a second loop
-                       // to avoid the constant reflow
-                       for ( var j = 0, k = this.length; j < k; j++ ) {
-                               this[j].style.display = jQuery.data(this[j], "olddisplay") || "";
-                       }
-
-                       return this;
-               }
-       },
-
-       hide: function( speed, callback ) {
-               if ( speed || speed === 0 ) {
-                       return this.animate( genFx("hide", 3), speed, callback);
-
-               } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ) {
-                               var old = jQuery.data(this[i], "olddisplay");
-                               if ( !old && old !== "none" ) {
-                                       jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
-                               }
-                       }
-
-                       // Set the display of the elements in a second loop
-                       // to avoid the constant reflow
-                       for ( var j = 0, k = this.length; j < k; j++ ) {
-                               this[j].style.display = "none";
-                       }
-
-                       return this;
-               }
-       },
-
-       // Save the old toggle function
-       _toggle: jQuery.fn.toggle,
-
-       toggle: function( fn, fn2 ) {
-               var bool = typeof fn === "boolean";
-
-               if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
-                       this._toggle.apply( this, arguments );
-
-               } else if ( fn == null || bool ) {
-                       this.each(function() {
-                               var state = bool ? fn : jQuery(this).is(":hidden");
-                               jQuery(this)[ state ? "show" : "hide" ]();
-                       });
-
-               } else {
-                       this.animate(genFx("toggle", 3), fn, fn2);
-               }
-
-               return this;
-       },
-
-       fadeTo: function( speed, to, callback ) {
-               return this.filter(":hidden").css("opacity", 0).show().end()
-                                       .animate({opacity: to}, speed, callback);
-       },
-
-       animate: function( prop, speed, easing, callback ) {
-               var optall = jQuery.speed(speed, easing, callback);
-
-               if ( jQuery.isEmptyObject( prop ) ) {
-                       return this.each( optall.complete );
-               }
-
-               return this[ optall.queue === false ? "each" : "queue" ](function() {
-                       var opt = jQuery.extend({}, optall), p,
-                               hidden = this.nodeType === 1 && jQuery(this).is(":hidden"),
-                               self = this;
-
-                       for ( p in prop ) {
-                               var name = p.replace(rdashAlpha, fcamelCase);
-
-                               if ( p !== name ) {
-                                       prop[ name ] = prop[ p ];
-                                       delete prop[ p ];
-                                       p = name;
-                               }
-
-                               if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) {
-                                       return opt.complete.call(this);
-                               }
-
-                               if ( ( p === "height" || p === "width" ) && this.style ) {
-                                       // Store display property
-                                       opt.display = jQuery.css(this, "display");
-
-                                       // Make sure that nothing sneaks out
-                                       opt.overflow = this.style.overflow;
-                               }
-
-                               if ( jQuery.isArray( prop[p] ) ) {
-                                       // Create (if needed) and add to specialEasing
-                                       (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1];
-                                       prop[p] = prop[p][0];
-                               }
-                       }
-
-                       if ( opt.overflow != null ) {
-                               this.style.overflow = "hidden";
-                       }
-
-                       opt.curAnim = jQuery.extend({}, prop);
-
-                       jQuery.each( prop, function( name, val ) {
-                               var e = new jQuery.fx( self, opt, name );
-
-                               if ( rfxtypes.test(val) ) {
-                                       e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop );
-
-                               } else {
-                                       var parts = rfxnum.exec(val),
-                                               start = e.cur(true) || 0;
-
-                                       if ( parts ) {
-                                               var end = parseFloat( parts[2] ),
-                                                       unit = parts[3] || "px";
-
-                                               // We need to compute starting value
-                                               if ( unit !== "px" ) {
-                                                       self.style[ name ] = (end || 1) + unit;
-                                                       start = ((end || 1) / e.cur(true)) * start;
-                                                       self.style[ name ] = start + unit;
-                                               }
-
-                                               // If a +=/-= token was provided, we're doing a relative animation
-                                               if ( parts[1] ) {
-                                                       end = ((parts[1] === "-=" ? -1 : 1) * end) + start;
-                                               }
-
-                                               e.custom( start, end, unit );
-
-                                       } else {
-                                               e.custom( start, val, "" );
-                                       }
-                               }
-                       });
-
-                       // For JS strict compliance
-                       return true;
-               });
-       },
-
-       stop: function( clearQueue, gotoEnd ) {
-               var timers = jQuery.timers;
-
-               if ( clearQueue ) {
-                       this.queue([]);
-               }
-
-               this.each(function() {
-                       // go in reverse order so anything added to the queue during the loop is ignored
-                       for ( var i = timers.length - 1; i >= 0; i-- ) {
-                               if ( timers[i].elem === this ) {
-                                       if (gotoEnd) {
-                                               // force the next step to be the last
-                                               timers[i](true);
-                                       }
-
-                                       timers.splice(i, 1);
-                               }
-                       }
-               });
-
-               // start the next in the queue if the last step wasn't forced
-               if ( !gotoEnd ) {
-                       this.dequeue();
-               }
-
-               return this;
-       }
-
-});
-
-// Generate shortcuts for custom animations
-jQuery.each({
-       slideDown: genFx("show", 1),
-       slideUp: genFx("hide", 1),
-       slideToggle: genFx("toggle", 1),
-       fadeIn: { opacity: "show" },
-       fadeOut: { opacity: "hide" }
-}, function( name, props ) {
-       jQuery.fn[ name ] = function( speed, callback ) {
-               return this.animate( props, speed, callback );
-       };
-});
-
-jQuery.extend({
-       speed: function( speed, easing, fn ) {
-               var opt = speed && typeof speed === "object" ? speed : {
-                       complete: fn || !fn && easing ||
-                               jQuery.isFunction( speed ) && speed,
-                       duration: speed,
-                       easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
-               };
-
-               opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
-                       jQuery.fx.speeds[opt.duration] || jQuery.fx.speeds._default;
-
-               // Queueing
-               opt.old = opt.complete;
-               opt.complete = function() {
-                       if ( opt.queue !== false ) {
-                               jQuery(this).dequeue();
-                       }
-                       if ( jQuery.isFunction( opt.old ) ) {
-                               opt.old.call( this );
-                       }
-               };
-
-               return opt;
-       },
-
-       easing: {
-               linear: function( p, n, firstNum, diff ) {
-                       return firstNum + diff * p;
-               },
-               swing: function( p, n, firstNum, diff ) {
-                       return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
-               }
-       },
-
-       timers: [],
-
-       fx: function( elem, options, prop ) {
-               this.options = options;
-               this.elem = elem;
-               this.prop = prop;
-
-               if ( !options.orig ) {
-                       options.orig = {};
-               }
-       }
-
-});
-
-jQuery.fx.prototype = {
-       // Simple function for setting a style value
-       update: function() {
-               if ( this.options.step ) {
-                       this.options.step.call( this.elem, this.now, this );
-               }
-
-               (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
-
-               // Set display property to block for height/width animations
-               if ( ( this.prop === "height" || this.prop === "width" ) && this.elem.style ) {
-                       this.elem.style.display = "block";
-               }
-       },
-
-       // Get the current size
-       cur: function( force ) {
-               if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
-                       return this.elem[ this.prop ];
-               }
-
-               var r = parseFloat(jQuery.css(this.elem, this.prop, force));
-               return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
-       },
-
-       // Start an animation from one number to another
-       custom: function( from, to, unit ) {
-               this.startTime = now();
-               this.start = from;
-               this.end = to;
-               this.unit = unit || this.unit || "px";
-               this.now = this.start;
-               this.pos = this.state = 0;
-
-               var self = this;
-               function t( gotoEnd ) {
-                       return self.step(gotoEnd);
-               }
-
-               t.elem = this.elem;
-
-               if ( t() && jQuery.timers.push(t) && !timerId ) {
-                       timerId = setInterval(jQuery.fx.tick, 13);
-               }
-       },
-
-       // Simple 'show' function
-       show: function() {
-               // Remember where we started, so that we can go back to it later
-               this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
-               this.options.show = true;
-
-               // Begin the animation
-               // Make sure that we start at a small width/height to avoid any
-               // flash of content
-               this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur());
-
-               // Start by showing the element
-               jQuery( this.elem ).show();
-       },
-
-       // Simple 'hide' function
-       hide: function() {
-               // Remember where we started, so that we can go back to it later
-               this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
-               this.options.hide = true;
-
-               // Begin the animation
-               this.custom(this.cur(), 0);
-       },
-
-       // Each step of an animation
-       step: function( gotoEnd ) {
-               var t = now(), done = true;
-
-               if ( gotoEnd || t >= this.options.duration + this.startTime ) {
-                       this.now = this.end;
-                       this.pos = this.state = 1;
-                       this.update();
-
-                       this.options.curAnim[ this.prop ] = true;
-
-                       for ( var i in this.options.curAnim ) {
-                               if ( this.options.curAnim[i] !== true ) {
-                                       done = false;
-                               }
-                       }
-
-                       if ( done ) {
-                               if ( this.options.display != null ) {
-                                       // Reset the overflow
-                                       this.elem.style.overflow = this.options.overflow;
-
-                                       // Reset the display
-                                       var old = jQuery.data(this.elem, "olddisplay");
-                                       this.elem.style.display = old ? old : this.options.display;
-
-                                       if ( jQuery.css(this.elem, "display") === "none" ) {
-                                               this.elem.style.display = "block";
-                                       }
-                               }
-
-                               // Hide the element if the "hide" operation was done
-                               if ( this.options.hide ) {
-                                       jQuery(this.elem).hide();
-                               }
-
-                               // Reset the properties, if the item has been hidden or shown
-                               if ( this.options.hide || this.options.show ) {
-                                       for ( var p in this.options.curAnim ) {
-                                               jQuery.style(this.elem, p, this.options.orig[p]);
-                                       }
-                               }
-
-                               // Execute the complete function
-                               this.options.complete.call( this.elem );
-                       }
-
-                       return false;
-
-               } else {
-                       var n = t - this.startTime;
-                       this.state = n / this.options.duration;
-
-                       // Perform the easing function, defaults to swing
-                       var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop];
-                       var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear");
-                       this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration);
-                       this.now = this.start + ((this.end - this.start) * this.pos);
-
-                       // Perform the next step of the animation
-                       this.update();
-               }
-
-               return true;
-       }
-};
-
-jQuery.extend( jQuery.fx, {
-       tick: function() {
-               var timers = jQuery.timers;
-
-               for ( var i = 0; i < timers.length; i++ ) {
-                       if ( !timers[i]() ) {
-                               timers.splice(i--, 1);
-                       }
-               }
-
-               if ( !timers.length ) {
-                       jQuery.fx.stop();
-               }
-       },
-               
-       stop: function() {
-               clearInterval( timerId );
-               timerId = null;
-       },
-       
-       speeds: {
-               slow: 600,
-               fast: 200,
-               // Default speed
-               _default: 400
-       },
-
-       step: {
-               opacity: function( fx ) {
-                       jQuery.style(fx.elem, "opacity", fx.now);
-               },
-
-               _default: function( fx ) {
-                       if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
-                               fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit;
-                       } else {
-                               fx.elem[ fx.prop ] = fx.now;
-                       }
-               }
-       }
-});
-
-if ( jQuery.expr && jQuery.expr.filters ) {
-       jQuery.expr.filters.animated = function( elem ) {
-               return jQuery.grep(jQuery.timers, function( fn ) {
-                       return elem === fn.elem;
-               }).length;
-       };
-}
-
-function genFx( type, num ) {
-       var obj = {};
-
-       jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() {
-               obj[ this ] = type;
-       });
-
-       return obj;
-}
-if ( "getBoundingClientRect" in document.documentElement ) {
-       jQuery.fn.offset = function( options ) {
-               var elem = this[0];
-
-               if ( options ) { 
-                       return this.each(function( i ) {
-                               jQuery.offset.setOffset( this, options, i );
-                       });
-               }
-
-               if ( !elem || !elem.ownerDocument ) {
-                       return null;
-               }
-
-               if ( elem === elem.ownerDocument.body ) {
-                       return jQuery.offset.bodyOffset( elem );
-               }
-
-               var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement,
-                       clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
-                       top  = box.top  + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop ) - clientTop,
-                       left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
-
-               return { top: top, left: left };
-       };
-
-} else {
-       jQuery.fn.offset = function( options ) {
-               var elem = this[0];
-
-               if ( options ) { 
-                       return this.each(function( i ) {
-                               jQuery.offset.setOffset( this, options, i );
-                       });
-               }
-
-               if ( !elem || !elem.ownerDocument ) {
-                       return null;
-               }
-
-               if ( elem === elem.ownerDocument.body ) {
-                       return jQuery.offset.bodyOffset( elem );
-               }
-
-               jQuery.offset.initialize();
-
-               var offsetParent = elem.offsetParent, prevOffsetParent = elem,
-                       doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
-                       body = doc.body, defaultView = doc.defaultView,
-                       prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
-                       top = elem.offsetTop, left = elem.offsetLeft;
-
-               while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
-                       if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
-                               break;
-                       }
-
-                       computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
-                       top  -= elem.scrollTop;
-                       left -= elem.scrollLeft;
-
-                       if ( elem === offsetParent ) {
-                               top  += elem.offsetTop;
-                               left += elem.offsetLeft;
-
-                               if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) {
-                                       top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
-                                       left += parseFloat( computedStyle.borderLeftWidth ) || 0;
-                               }
-
-                               prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
-                       }
-
-                       if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
-                               top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
-                               left += parseFloat( computedStyle.borderLeftWidth ) || 0;
-                       }
-
-                       prevComputedStyle = computedStyle;
-               }
-
-               if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
-                       top  += body.offsetTop;
-                       left += body.offsetLeft;
-               }
-
-               if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
-                       top  += Math.max( docElem.scrollTop, body.scrollTop );
-                       left += Math.max( docElem.scrollLeft, body.scrollLeft );
-               }
-
-               return { top: top, left: left };
-       };
-}
-
-jQuery.offset = {
-       initialize: function() {
-               var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0,
-                       html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
-
-               jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } );
-
-               container.innerHTML = html;
-               body.insertBefore( container, body.firstChild );
-               innerDiv = container.firstChild;
-               checkDiv = innerDiv.firstChild;
-               td = innerDiv.nextSibling.firstChild.firstChild;
-
-               this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
-               this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
-
-               checkDiv.style.position = "fixed", checkDiv.style.top = "20px";
-               // safari subtracts parent border width here which is 5px
-               this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);
-               checkDiv.style.position = checkDiv.style.top = "";
-
-               innerDiv.style.overflow = "hidden", innerDiv.style.position = "relative";
-               this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
-
-               this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
-
-               body.removeChild( container );
-               body = container = innerDiv = checkDiv = table = td = null;
-               jQuery.offset.initialize = jQuery.noop;
-       },
-
-       bodyOffset: function( body ) {
-               var top = body.offsetTop, left = body.offsetLeft;
-
-               jQuery.offset.initialize();
-
-               if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {
-                       top  += parseFloat( jQuery.curCSS(body, "marginTop",  true) ) || 0;
-                       left += parseFloat( jQuery.curCSS(body, "marginLeft", true) ) || 0;
-               }
-
-               return { top: top, left: left };
-       },
-       
-       setOffset: function( elem, options, i ) {
-               // set position first, in-case top/left are set even on static elem
-               if ( /static/.test( jQuery.curCSS( elem, "position" ) ) ) {
-                       elem.style.position = "relative";
-               }
-               var curElem   = jQuery( elem ),
-                       curOffset = curElem.offset(),
-                       curTop    = parseInt( jQuery.curCSS( elem, "top",  true ), 10 ) || 0,
-                       curLeft   = parseInt( jQuery.curCSS( elem, "left", true ), 10 ) || 0;
-
-               if ( jQuery.isFunction( options ) ) {
-                       options = options.call( elem, i, curOffset );
-               }
-
-               var props = {
-                       top:  (options.top  - curOffset.top)  + curTop,
-                       left: (options.left - curOffset.left) + curLeft
-               };
-               
-               if ( "using" in options ) {
-                       options.using.call( elem, props );
-               } else {
-                       curElem.css( props );
-               }
-       }
-};
-
-
-jQuery.fn.extend({
-       position: function() {
-               if ( !this[0] ) {
-                       return null;
-               }
-
-               var elem = this[0],
-
-               // Get *real* offsetParent
-               offsetParent = this.offsetParent(),
-
-               // Get correct offsets
-               offset       = this.offset(),
-               parentOffset = /^body|html$/i.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
-
-               // Subtract element margins
-               // note: when an element has margin: auto the offsetLeft and marginLeft
-               // are the same in Safari causing offset.left to incorrectly be 0
-               offset.top  -= parseFloat( jQuery.curCSS(elem, "marginTop",  true) ) || 0;
-               offset.left -= parseFloat( jQuery.curCSS(elem, "marginLeft", true) ) || 0;
-
-               // Add offsetParent borders
-               parentOffset.top  += parseFloat( jQuery.curCSS(offsetParent[0], "borderTopWidth",  true) ) || 0;
-               parentOffset.left += parseFloat( jQuery.curCSS(offsetParent[0], "borderLeftWidth", true) ) || 0;
-
-               // Subtract the two offsets
-               return {
-                       top:  offset.top  - parentOffset.top,
-                       left: offset.left - parentOffset.left
-               };
-       },
-
-       offsetParent: function() {
-               return this.map(function() {
-                       var offsetParent = this.offsetParent || document.body;
-                       while ( offsetParent && (!/^body|html$/i.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
-                               offsetParent = offsetParent.offsetParent;
-                       }
-                       return offsetParent;
-               });
-       }
-});
-
-
-// Create scrollLeft and scrollTop methods
-jQuery.each( ["Left", "Top"], function( i, name ) {
-       var method = "scroll" + name;
-
-       jQuery.fn[ method ] = function(val) {
-               var elem = this[0], win;
-               
-               if ( !elem ) {
-                       return null;
-               }
-
-               if ( val !== undefined ) {
-                       // Set the scroll offset
-                       return this.each(function() {
-                               win = getWindow( this );
-
-                               if ( win ) {
-                                       win.scrollTo(
-                                               !i ? val : jQuery(win).scrollLeft(),
-                                                i ? val : jQuery(win).scrollTop()
-                                       );
-
-                               } else {
-                                       this[ method ] = val;
-                               }
-                       });
-               } else {
-                       win = getWindow( elem );
-
-                       // Return the scroll offset
-                       return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
-                               jQuery.support.boxModel && win.document.documentElement[ method ] ||
-                                       win.document.body[ method ] :
-                               elem[ method ];
-               }
-       };
-});
-
-function getWindow( elem ) {
-       return ("scrollTo" in elem && elem.document) ?
-               elem :
-               elem.nodeType === 9 ?
-                       elem.defaultView || elem.parentWindow :
-                       false;
-}
-// Create innerHeight, innerWidth, outerHeight and outerWidth methods
-jQuery.each([ "Height", "Width" ], function( i, name ) {
-
-       var type = name.toLowerCase();
-
-       // innerHeight and innerWidth
-       jQuery.fn["inner" + name] = function() {
-               return this[0] ?
-                       jQuery.css( this[0], type, false, "padding" ) :
-                       null;
-       };
-
-       // outerHeight and outerWidth
-       jQuery.fn["outer" + name] = function( margin ) {
-               return this[0] ?
-                       jQuery.css( this[0], type, false, margin ? "margin" : "border" ) :
-                       null;
-       };
-
-       jQuery.fn[ type ] = function( size ) {
-               // Get window width or height
-               var elem = this[0];
-               if ( !elem ) {
-                       return size == null ? null : this;
-               }
-               
-               if ( jQuery.isFunction( size ) ) {
-                       return this.each(function( i ) {
-                               var self = jQuery( this );
-                               self[ type ]( size.call( this, i, self[ type ]() ) );
-                       });
-               }
-
-               return ("scrollTo" in elem && elem.document) ? // does it walk and quack like a window?
-                       // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
-                       elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] ||
-                       elem.document.body[ "client" + name ] :
-
-                       // Get document width or height
-                       (elem.nodeType === 9) ? // is it a document
-                               // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
-                               Math.max(
-                                       elem.documentElement["client" + name],
-                                       elem.body["scroll" + name], elem.documentElement["scroll" + name],
-                                       elem.body["offset" + name], elem.documentElement["offset" + name]
-                               ) :
-
-                               // Get or set width or height on the element
-                               size === undefined ?
-                                       // Get width or height on the element
-                                       jQuery.css( elem, type ) :
-
-                                       // Set the width or height on the element (default to pixels if value is unitless)
-                                       this.css( type, typeof size === "string" ? size : size + "px" );
-       };
-
-});
-// Expose jQuery to the global object
-window.jQuery = window.$ = jQuery;
-
-})(window);
diff --git a/auto_tests/misc/README b/auto_tests/misc/README
deleted file mode 100644 (file)
index b45d5ef..0000000
+++ /dev/null
@@ -1 +0,0 @@
-This directory contains files that aid in local testing.
\ No newline at end of file
diff --git a/auto_tests/misc/fake-jstestdriver.js b/auto_tests/misc/fake-jstestdriver.js
deleted file mode 100644 (file)
index 772e10a..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright (c)  2011 Google, Inc.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-// THE SOFTWARE.
-
-/** 
- * @fileoverview Mocked-out jstestdriver api that lets me test locally.
- *
- * @author konigsberg@google.com (Robert Konigsberg)
- */
-var jstestdriver = {
-  jQuery : jQuery,
-  listeners_ : [],
-  announce_ : function(name, args) {
-    for (var idx = 0; idx < jstestdriver.listeners_.length; idx++) {
-      var listener = jstestdriver.listeners_[idx];
-      if (listener[name]) {
-        listener[name].apply(null, args);
-      }
-    }
-  },
-  attachListener: function(listener) {
-    jstestdriver.listeners_.push(listener);
-  }
-};
-
-if (!console) {
-  var console = {
-    log: function(x) {
-      // ...
-    }
-  };
-}
-
-var jstd = {
-  include : function(name) {
-    this.sucker("Not including " + name);
-  },
-  sucker : function(text) {
-    console.log(text + ", sucker!");
-  }
-};
-
-var testCaseList = [];
-
-function TestCase(name) {
-  var testCase = function() { return this; };
-  testCase.name = name;
-  testCase.toString = function() {
-    return "Fake test case " + name;
-  };
-
-  testCase.prototype.setUp = function() { };
-  testCase.prototype.tearDown = function() { };
-  testCase.prototype.name = name;
-  /**
-   * name can be a string, which is looked up in this object, or it can be a
-   * function, in which case it's run.
-   *
-   * Examples:
-   * var tc = new MyTestCase();
-   * tc.runTest("testThis");
-   * tc.runTest(tc.testThis);
-   *
-   * The duplication tc in runTest is irritating, but it plays well with
-   * Chrome's console completion.
-   */
-  testCase.prototype.runTest = function(func) {
-    var result = false;
-    var ex = null;
-    var name = typeof(func) == "string" ? func : "(anonymous function)";
-    jstestdriver.announce_("start", [this, name]);
-    try {
-      result = this.runTest_(func);
-    } catch (e) {
-      ex = e;
-    }
-    jstestdriver.announce_("finish", [this, name, result, ex]);
-    return result; // TODO(konigsberg): Remove this, and return value from runAllTests.
-  }
-
-  testCase.prototype.runTest_ = function(func) {
-    this.setUp();
-
-    var fn = null;
-    var parameterType = typeof(func);
-    if (typeof(func) == "function") {
-      fn = func;
-    } else if (typeof(func) == "string") {
-      fn = this[func];
-    } else {
-      fail("can't supply " + typeof(func) + " to runTest");
-    }
-
-    fn.apply(this, []);
-    this.tearDown();
-    return true;
-  };
-
-  testCase.prototype.runAllTests = function() {
-    var results = {};
-    var names = this.getTestNames();
-    for (var idx in names) {
-      var name = names[idx];
-      console.log("Running " + name);
-      var result = this.runTest(name);
-      results[name] = result;
-    }
-    console.log(prettyPrintEntity_(results));
-    return results;
-  };
-
-  testCase.prototype.getTestNames = function() {
-    // what's better than for ... in for non-array objects?
-    var tests = [];
-    for (var name in this) {
-      if (name.indexOf('test') == 0 && typeof(this[name]) == 'function') {
-        tests.push(name);
-      }
-    }
-    return tests;
-  }
-
-  testCaseList.push({name : name, testCase : testCase});
-  return testCase;
-};
-
-// Note: this creates a bunch of global variables intentionally.
-function addGlobalTestSymbols() {
-  globalTestDb = {};  // maps test name -> test function wrapper
-
-  var num_tests = 0;
-  for (var i = 0; i < testCaseList.length; i++) {
-    var tc_class = testCaseList[i].testCase;
-    for (var name in tc_class.prototype) {
-      if (name.indexOf('test') == 0 && typeof(tc_class.prototype[name]) == 'function') {
-        if (globalTestDb.hasOwnProperty(name)) {
-          console.log('Duplicated test name: ' + name);
-        } else {
-          globalTestDb[name] = function(name, tc_class) {
-            return function() {
-              var tc = new tc_class;
-              return tc.runTest(name);
-            };
-          }(name, tc_class);
-          eval(name + " = globalTestDb['" + name + "'];");
-          num_tests += 1;
-        }
-      }
-    }
-  }
-  console.log('Loaded ' + num_tests + ' tests in ' +
-              testCaseList.length + ' test cases');
-}
-
-function getAllTestCases() {
-  return testCaseList;
-}
-
-jstestdriver.attachListener({
-  finish : function(tc, name, result, e) {
-    if (e) {
-      console.log(e);
-      if (e.stack) {
-        console.log(e.stack);
-      }
-    }
-  }
-});
-
diff --git a/auto_tests/misc/filter-lcov.py b/auto_tests/misc/filter-lcov.py
deleted file mode 100755 (executable)
index 2d95a3a..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python
-"""Remove unwanted files from LCOV data.
-
-jstd and node-coveralls won't do this themselves, so we have to!
-
-Usage:
-    cat lcov.dat | ./filter-lcov.py > filtered-lcov.dat
-"""
-
-import fileinput
-import re
-
-# Exclude paths which match any of these regular expressions.
-exclude_res = [
-    re.compile(r'auto_tests')
-]
-
-def is_ok(path):
-    for regex in exclude_res:
-        if re.search(regex, path):
-            return False
-    return True
-
-
-writing = False
-for line in fileinput.input():
-    line = line.strip();
-    if line.startswith('SF:'):
-        path = line[3:]
-        writing = is_ok(path)
-    if writing:
-        print line
diff --git a/auto_tests/misc/local.html b/auto_tests/misc/local.html
deleted file mode 100644 (file)
index cb5063f..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-<!-- A local source file that allows dygraph developers to test locally. -->
-<html>
-<head>
-  <!-- Standard Dygraph scripts -->
-  <script type="text/javascript" src="../../dygraph-dev.js"></script>
-  <script type="text/javascript" src="../../extras/smooth-plotter.js"></script>
-
-  <!-- Scripts for library support -->
-  <script type="text/javascript" src="../lib/jquery-1.4.2.js"></script>
-  <script type="text/javascript" src="../lib/Asserts.js"></script>
-  <script type="text/javascript" src="fake-jstestdriver.js"></script>
-  <script type="text/javascript" src="../tests/Proxy.js"></script>
-  <script type="text/javascript" src="../tests/CanvasAssertions.js"></script>
-  <script type="text/javascript" src="../tests/DygraphOps.js"></script>
-  <script type="text/javascript" src="../tests/PixelSampler.js"></script>
-  <script type="text/javascript" src="../tests/Util.js"></script>
-  <script type="text/javascript" src="local.js"></script>
-
-  <!-- Test data -->
-  <script type="text/javascript" src="../data/data.js"></script>
-
-  <!-- Scripts for automated tests -->
-  <script type="text/javascript" src="../tests/annotations.js"></script>
-  <script type="text/javascript" src="../tests/axis_labels.js"></script>
-  <script type="text/javascript" src="../tests/axis_labels-deprecated.js"></script>
-  <script type="text/javascript" src="../tests/callback.js"></script>
-  <script type="text/javascript" src="../tests/connect_separated_points.js"></script>
-  <script type="text/javascript" src="../tests/css.js"></script>
-  <script type="text/javascript" src="../tests/custom_bars.js"></script>
-  <script type="text/javascript" src="../tests/date_formats.js"></script>
-  <script type="text/javascript" src="../tests/date_ticker.js"></script>
-  <script type="text/javascript" src="../tests/dygraph-options-tests.js"></script>
-  <script type="text/javascript" src="../tests/error_bars.js"></script>
-  <script type="text/javascript" src="../tests/fill_step_plot.js"></script>
-  <script type="text/javascript" src="../tests/formats.js"></script>
-  <script type="text/javascript" src="../tests/grid_per_axis.js"></script>
-  <script type="text/javascript" src="../tests/interaction_model.js"></script>
-  <script type="text/javascript" src="../tests/missing_points.js"></script>
-  <script type="text/javascript" src="../tests/multi_csv.js"></script>
-  <script type="text/javascript" src="../tests/multiple_axes.js"></script>
-  <script type="text/javascript" src="../tests/no_hours.js"></script>
-  <script type="text/javascript" src="../tests/numeric_ticker.js"></script>
-  <script type="text/javascript" src="../tests/parser.js"></script>
-  <script type="text/javascript" src="../tests/pathological_cases.js"></script>
-  <script type="text/javascript" src="../tests/per_axis.js"></script>
-  <script type="text/javascript" src="../tests/per_series.js"></script>
-  <script type="text/javascript" src="../tests/plugins.js"></script>
-  <script type="text/javascript" src="../tests/range_selector.js"></script>
-  <script type="text/javascript" src="../tests/range_tests.js"></script>
-  <script type="text/javascript" src="../tests/rolling_average.js"></script>
-  <script type="text/javascript" src="../tests/sanity.js"></script>
-  <script type="text/javascript" src="../tests/scientific_notation.js"></script>
-  <script type="text/javascript" src="../tests/scrolling_div.js"></script>
-  <script type="text/javascript" src="../tests/selection.js"></script>
-  <script type="text/javascript" src="../tests/simple_drawing.js"></script>
-  <script type="text/javascript" src="../tests/step_plot_per_series.js"></script>
-  <script type="text/javascript" src="../tests/stacked.js"></script>
-  <script type="text/javascript" src="../tests/to_dom_coords.js"></script>
-  <script type="text/javascript" src="../tests/resize.js"></script>
-  <script type="text/javascript" src="../tests/plugins_legend.js"></script>
-  <script type="text/javascript" src="../tests/two_digit_years.js"></script>
-  <script type="text/javascript" src="../tests/hidpi.js"></script>
-  <script type="text/javascript" src="../tests/smooth_plotter.js"></script>
-  <script type="text/javascript" src="../tests/fast_canvas_proxy.js"></script>
-  <script type="text/javascript" src="../tests/data_api.js"></script>
-  <script type="text/javascript" src="../tests/update_options.js"></script>
-  <script type="text/javascript" src="../tests/update_while_panning.js"></script>
-  <script type="text/javascript" src="../tests/utils_test.js"></script>
-  <script type="text/javascript" src="../tests/visibility.js"></script>
-
-<style type="text/css">
-  .pass .outcome {
-    color: green;
-  }
-
-  .fail .outcome {
-    color: red;
-  }
-
-  /* Broken: the active anchor isn't gray. Why not? */
-  #results .activeAnchor {
-    color: grey;
-  }
-
-  #results a {
-    text-decoration: none;
-  }
-
-  #results a:visited {
-    color: blue;
-    text-decoration: none;
-  }
-
-  #results a:hover {
-    color: blue;
-    text-decoration: underline;
-  }
-</style>
-</head>
-<body>
-  <div id='graph'></div>
-  <div id="selector"></div>
-  <p>This file is really nothing more than all the tests coalesced into a single
-  HTML file. To run a test, use the selector above, or
-  open a Javascript console and execute, for instance,</p>
-  <code>testDrawSimpleRangePlusOne()</code>
-
-  <p>Alternatively you can use query args: <ul>
-  <li>testCase - for the name of the test case prototype
-  <li>testCaseName - for the name of the test case
-  <li>test - for the name of the test (use command=runTest)
-  <li>command - either runTest or runAllTests.
-  </ul>
-  Example: <code>local.html?testCase=ScrollingDivTestCase&test=testNestedDiv_Scrolled&command=runTest</code>
-  <p/>
-</body>
-<script type="text/javascript">
-  var tester = new DygraphsLocalTester();
-  // tester.overrideWarn(); // uncomment if you want warnings to be errors.
-  tester.processVariables();
-  addGlobalTestSymbols();
-  tester.run();
-</script>
-</html>
diff --git a/auto_tests/misc/local.js b/auto_tests/misc/local.js
deleted file mode 100644 (file)
index 4b1ea40..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-'use strict';
-
-var DygraphsLocalTester = function() {
-  this.tc = null; // Selected test case
-  this.name = null; 
-  this.results = [];
-  this.summary = { failed: 0, passed: 0 };
-  this.start;
-
-  var self = this;
-  jstestdriver.attachListener({
-    start : function(tc) {
-      self.start_(tc);
-    },
-    finish : function(tc, name, result, e) {
-      self.finish_(tc, name, result, e);
-    }
-  });
-};
-
-/**
- * Call this to replace Dygraphs.warn so it throws an error.
- *
- * In some cases we will still allow warnings to be warnings, however.
- */
-DygraphsLocalTester.prototype.overrideWarn = function() {
-  // save console.warn so we can catch warnings.
-  var originalWarn = console.warn;
-  console.warn = function(msg) {
-    // This warning is pervasive enough that we'll let it slide (for now).
-    if (msg == "Using default labels. Set labels explicitly via 'labels' in the options parameter") {
-      originalWarn(msg);
-      return;
-    }
-    throw 'Warnings not permitted: ' + msg;
-  }
-};
-
-DygraphsLocalTester.prototype.processVariables = function() {
-  var splitVariables = function() { // http://www.idealog.us/2006/06/javascript_to_p.html
-    var query = window.location.search.substring(1); 
-    var args = {};
-    var vars = query.split('&'); 
-    for (var i = 0; i < vars.length; i++) { 
-      if (vars[i].length > 0) {
-        var pair = vars[i].split('='); 
-        args[pair[0]] = pair[1];
-      }
-    }
-    return args;
-  }
-
-  var findTestCase = function(stringName, className) {
-    if (stringName) {
-      var testCases = getAllTestCases();
-      for (var idx in testCases) {
-        var entry = testCases[idx];
-        if (entry.name == stringName) {
-          var prototype = entry.testCase;
-          return new entry.testCase();
-        }
-      }
-    } else if (className) {
-      eval('tc__ = new ' + className + '()');
-      return tc__;
-    }
-    return null;
-  }
-
-  var args = splitVariables();
-  this.tc = findTestCase(args.testCaseName, args.testCase);
-  this.test = args.test;
-  this.command = args.command;
-}
-
-DygraphsLocalTester.prototype.createAnchor = function(href, id, text) {
-  var a = document.createElement('a');
-  a.href = href;
-  a.id = id;
-  a.textContent = text;
-  return a;
-}
-
-DygraphsLocalTester.prototype.createResultsDiv = function(summary, durationms) {
-  var div = document.createElement('div');
-  div.id='results';
-
-  var body = document.getElementsByTagName('body')[0];
-  body.insertBefore(div, body.firstChild);
-
-  var addText = function(div, text) {
-    div.appendChild(document.createTextNode(text));
-  };
-
-  var passedAnchor = this.createAnchor('#', 'passed', '' + summary.passed + ' passed');
-  var failedAnchor = this.createAnchor('#', 'failed', '' + summary.failed + ' failed');
-  var allAnchor = this.createAnchor('#', 'all', '(view all)');
-
-  addText(div, 'Test results: ');
-  div.appendChild(passedAnchor);
-  addText(div, ', ');
-  div.appendChild(failedAnchor);
-  addText(div, ', ');
-  div.appendChild(allAnchor);
-  addText(div, ', (' + durationms + ' ms)');
-
-  var table = document.createElement('table');
-  div.appendChild(table);
-  div.appendChild(document.createElement('hr'));
-
-  var setByClassName = function(name, displayStyle) {
-    var elements = table.getElementsByClassName(name);
-    for (var i = 0; i < elements.length; i++) {
-      elements[i].style.display = displayStyle;
-    }
-  }
-
-  passedAnchor.onclick = function() {
-    setByClassName('fail', 'none');
-    setByClassName('pass', 'block');
-
-    passedAnchor.setAttribute('class', 'activeAnchor');
-    failedAnchor.setAttribute('class', '');
-  };
-  failedAnchor.onclick = function() {
-    setByClassName('fail', 'block');
-    setByClassName('pass', 'none');
-    passedAnchor.setAttribute('class', '');
-    failedAnchor.setAttribute('class', 'activeAnchor');
-  };
-  allAnchor.onclick = function() {
-    setByClassName('fail', 'block');
-    setByClassName('pass', 'block');
-    passedAnchor.setAttribute('class', '');
-    failedAnchor.setAttribute('class', '');
-  };
-  return div;
-}
-
-DygraphsLocalTester.prototype.postResults = function(summary, durationms) {
-  var resultsDiv = this.createResultsDiv(summary, durationms);
-
-  var table = resultsDiv.getElementsByTagName('table')[0];
-  for (var idx = 0; idx < this.results.length; idx++) {
-    var result = this.results[idx];
-    var tr = document.createElement('tr');
-    tr.setAttribute('class', result.result ? 'pass' : 'fail');
-
-    var tdResult = document.createElement('td');
-    tdResult.setAttribute('class', 'outcome');
-    tdResult.textContent = result.result ? 'pass' : 'fail';
-    tr.appendChild(tdResult);
-
-    var tdName = document.createElement('td');
-    var s = result.name.split('.');
-    var url = window.location.pathname + '?testCaseName=' + s[0] + '&test=' + s[1] + '&command=runTest';
-    var a = this.createAnchor(url, null, result.name);
-
-    tdName.appendChild(a);
-    tr.appendChild(tdName);
-
-    var tdDuration = document.createElement('td');
-    tdDuration.textContent = result.duration + ' ms';
-    tr.appendChild(tdDuration);
-
-    if (result.e) {
-      var tdDetails = document.createElement('td');
-      var a = this.createAnchor('#', null, '(stack trace)');
-      a.onclick = function(e) {
-        return function() {
-          alert(e + '\n' + e.stack);
-        };
-      }(result.e);
-      tdDetails.appendChild(a);
-      tr.appendChild(tdDetails);
-    }
-
-    table.appendChild(tr);
-  }
-}
-
-DygraphsLocalTester.prototype.listTests = function() {
-  var selector = document.getElementById('selector');
-
-  if (selector != null) { // running a test
-    var createAttached = function(name, parent) {
-      var elem = document.createElement(name);
-      parent.appendChild(elem);
-      return elem;
-    }
-  
-    var description = createAttached('div', selector);
-    var list = createAttached('ul', selector);
-    var parent = list.parentElement;
-    var createLink = function(parent, title, url) {
-      var li = createAttached('li', parent);
-      var a = createAttached('a', li);
-      a.textContent = title;
-      a.href = url;
-      return li;
-    }
-    if (this.tc == null) {
-      description.textContent = 'Test cases:';
-      var testCases = getAllTestCases();
-      createLink(list, '(run all tests)', document.URL + '?command=runAllTests');
-      for (var idx in testCases) {
-        var entryName = testCases[idx].name;
-        createLink(list, entryName, document.URL + '?testCaseName=' + entryName);
-      }
-    } else {
-      description.textContent = 'Tests for ' + name;
-      var names = this.tc.getTestNames();
-      createLink(list, 'Run All Tests', document.URL + '&command=runAllTests');
-      for (var idx in names) {
-        var name = names[idx];
-        createLink(list, name, document.URL + '&test=' + name + '&command=runTest');
-      }
-    }
-  }
-}
-
-DygraphsLocalTester.prototype.run = function() {
-  var executed = false;
-  var start = new Date(). getTime();
-  if (this.tc != null) {
-    if (this.command == 'runAllTests') {
-      console.log('Running all tests for ' + this.tc.name);
-      this.tc.runAllTests();
-      executed = true;
-    } else if (this.command == 'runTest') {
-      console.log('Running test ' + this.tc.name + '.' + this.test);
-      this.tc.runTest(this.test);
-      executed = true;
-    }
-  } else if (this.command == 'runAllTests') {
-    console.log('Running all tests for all test cases');
-    var testCases = getAllTestCases();
-    for (var idx in testCases) {
-      var entry = testCases[idx];
-      var prototype = entry.testCase;
-      this.tc = new entry.testCase();
-      this.tc.runAllTests();
-    }
-    executed = true;
-  }
-
-  var durationms = new Date().getTime() - start;
-
-  if (executed) {
-    this.postResults(this.summary, durationms);
-  } else {
-    this.listTests();
-  }
-}
-
-DygraphsLocalTester.prototype.start_ = function(tc) {
-  this.startms_ = new Date().getTime();
-}
-
-DygraphsLocalTester.prototype.finish_ = function(tc, name, result, e) {
-  var endms_ = new Date().getTime();
-  this.results.push({
-    name : tc.name + '.' + name,
-    result : result,
-    duration : endms_ - this.startms_,
-    e : e
-  });
-  this.summary.passed += result ? 1 : 0;
-  this.summary.failed += result ? 0 : 1;
-}
diff --git a/auto_tests/misc/new-test.sh b/auto_tests/misc/new-test.sh
deleted file mode 100755 (executable)
index 4539202..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/bin/bash
-#
-# Run this to automatically create a new auto_test. Example:
-#
-#   ./auto_tests/misc/new_test.sh axis-labels
-#
-# This will produce a new file in auto_tests/tests/axis_labels.js including
-#
-#   var AxisLabelsTestCase = TestCase("axis-labels");
-#   ...
-#
-# It will also add a reference to this file to auto_tests/misc/local.html.
-
-set -o errexit
-if [ -z $1 ]; then
-  echo Usage: $0 test-case-name-with-dashes
-  exit 1
-fi
-
-dashed_name=$1
-underscore_name=$(echo $1 | sed 's/-/_/g')
-camelCaseName=$(echo $1 | perl -pe 's/-([a-z])/uc $1/ge')
-testCaseName=${camelCaseName}TestCase
-
-test_file=auto_tests/tests/$underscore_name.js
-
-if [ -f $test_file ]; then
-  echo $test_file already exists
-  exit 1
-fi
-
-cat <<END > $test_file;
-/**
- * @fileoverview FILL THIS IN
- *
- * @author $(git config --get user.email) ($(git config --get user.name))
- */
-var $testCaseName = TestCase("$dashed_name");
-
-$testCaseName.prototype.setUp = function() {
-  document.body.innerHTML = "<div id='graph'></div>";
-};
-
-$testCaseName.prototype.tearDown = function() {
-};
-
-$testCaseName.prototype.testNameGoesHere = function() {
-  var opts = {
-    width: 480,
-    height: 320
-  };
-  var data = "X,Y\n" +
-      "0,-1\n" +
-      "1,0\n" +
-      "2,1\n" +
-      "3,0\n"
-  ;
-
-  var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, data, opts);
-
-  ...
-  assertEquals(1, 1);
-};
-
-END
-
-perl -pi -e 'next unless /update_options.js/; print "  <script type=\"text/javascript\" src=\"../tests/'$underscore_name'.js\"></script>\n"' auto_tests/misc/local.html
-
-echo Wrote test to $test_file
index 97279cf..2b37ec5 100644 (file)
@@ -115,8 +115,8 @@ CanvasAssertions.assertLineDrawn = function(proxy, p1, p2, predicate) {
     }
     return s + "}";
   };
-  fail("Can't find a line drawn between " + p1 +
-      " and " + p2 + " with attributes " + toString(predicate));
+  throw "Can't find a line drawn between " + p1 +
+      " and " + p2 + " with attributes " + toString(predicate);
 };
 
 /**
index 305ed47..7c1eaa6 100644 (file)
@@ -62,10 +62,11 @@ Util.getLegend = function(parent) {
  * Assert that all elements have a certain style property.
  */
 Util.assertStyleOfChildren = function(selector, property, expectedValue) {
-  assertTrue(selector.length > 0);
-  $.each(selector, function(idx, child) {
-    assertEquals(expectedValue,  $(child).css(property));
-  });
+  assert.isTrue(selector.length > 0);
+  for (var idx = 0; idx < selector.length; idx++) {
+    var child = selector[idx];
+    assert.equal(expectedValue, window.getComputedStyle(child)[property]);
+  }
 };
 
 
@@ -121,7 +122,7 @@ Util.overrideXMLHttpRequest = function(data) {
     this.responseText = data;
   };
   FakeXMLHttpRequest.restore = function() {
-    XMLHttpRequest = originalXMLHttpRequest;
+    window.XMLHttpRequest = originalXMLHttpRequest;
   };
   FakeXMLHttpRequest.respond = function() {
     for (var i = 0; i < requests.length; i++) {
@@ -129,7 +130,7 @@ Util.overrideXMLHttpRequest = function(data) {
     }
     FakeXMLHttpRequest.restore();
   };
-  XMLHttpRequest = FakeXMLHttpRequest;
+  window.XMLHttpRequest = FakeXMLHttpRequest;
   return FakeXMLHttpRequest;
 };
 
index d0a2f7f..3ea5e7c 100644 (file)
@@ -3,16 +3,16 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var AnnotationsTestCase = TestCase("annotations");
+describe("annotations", function() {
 
-AnnotationsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-AnnotationsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-AnnotationsTestCase.prototype.testAnnotationsDrawn = function() {
+it('testAnnotationsDrawn', function() {
   var opts = {
     width: 480,
     height: 320
@@ -43,23 +43,23 @@ AnnotationsTestCase.prototype.testAnnotationsDrawn = function() {
     }
   ]);
 
-  assertEquals(2, g.annotations().length);
+  assert.equal(2, g.annotations().length);
   var a1 = document.getElementsByClassName('ann1');
-  assertEquals(1, a1.length);
+  assert.equal(1, a1.length);
   a1 = a1[0];
-  assertEquals('A', a1.textContent);
+  assert.equal('A', a1.textContent);
 
   var a2 = document.getElementsByClassName('ann2');
-  assertEquals(1, a2.length);
+  assert.equal(1, a2.length);
   a2 = a2[0];
-  assertEquals('B', a2.textContent);
-};
+  assert.equal('B', a2.textContent);
+});
 
 // Some errors that should be flagged:
 // 1. Invalid series name (e.g. 'X' or 'non-existent')
 // 2. Passing a string as 'x' instead of a number (e.g. x: '1')
 
-AnnotationsTestCase.prototype.testAnnotationsDontDisappearOnResize = function() {
+it('testAnnotationsDontDisappearOnResize', function() {
   var opts = {
   };
   var data = "X,Y\n" +
@@ -82,23 +82,23 @@ AnnotationsTestCase.prototype.testAnnotationsDontDisappearOnResize = function()
   ]);
 
   // Check that it displays at all
-  assertEquals(1, g.annotations().length);
+  assert.equal(1, g.annotations().length);
   var a1 = document.getElementsByClassName('ann1');
-  assertEquals(1, a1.length);
+  assert.equal(1, a1.length);
   a1 = a1[0];
-  assertEquals('A', a1.textContent);
+  assert.equal('A', a1.textContent);
 
   // ... and that resizing doesn't kill it.
   g.resize(400, 300);
-  assertEquals(1, g.annotations().length);
+  assert.equal(1, g.annotations().length);
   var a1 = document.getElementsByClassName('ann1');
-  assertEquals(1, a1.length);
+  assert.equal(1, a1.length);
   a1 = a1[0];
-  assertEquals('A', a1.textContent);
-};
+  assert.equal('A', a1.textContent);
+});
 
 // Verify that annotations outside of the visible x-range are not shown.
-AnnotationsTestCase.prototype.testAnnotationsOutOfRangeX = function() {
+it('testAnnotationsOutOfRangeX', function() {
   var opts = {
   };
   var data = "X,Y\n" +
@@ -121,27 +121,27 @@ AnnotationsTestCase.prototype.testAnnotationsOutOfRangeX = function() {
   ]);
 
   // Check that it displays at all
-  assertEquals(1, g.annotations().length);
+  assert.equal(1, g.annotations().length);
   var a1 = document.getElementsByClassName('ann1');
-  assertEquals(1, a1.length);
+  assert.equal(1, a1.length);
   a1 = a1[0];
-  assertEquals('A', a1.textContent);
+  assert.equal('A', a1.textContent);
 
   // ... and that panning right removes the annotation.
   g.updateOptions({dateWindow: [2, 6]});
-  assertEquals(1, g.annotations().length);
+  assert.equal(1, g.annotations().length);
   a1 = document.getElementsByClassName('ann1');
-  assertEquals(0, a1.length);
+  assert.equal(0, a1.length);
 
   // ... and that panning left brings it back.
   g.updateOptions({dateWindow: [0, 4]});
-  assertEquals(1, g.annotations().length);
+  assert.equal(1, g.annotations().length);
   a1 = document.getElementsByClassName('ann1');
-  assertEquals(1, a1.length);
-};
+  assert.equal(1, a1.length);
+});
 
 // Verify that annotations outside of the visible y-range are not shown.
-AnnotationsTestCase.prototype.testAnnotationsOutOfRangeY = function() {
+it('testAnnotationsOutOfRangeY', function() {
   var opts = {
   };
   var data = "X,Y\n" +
@@ -165,18 +165,18 @@ AnnotationsTestCase.prototype.testAnnotationsOutOfRangeY = function() {
 
   // ... check that panning up removes the annotation.
   g.updateOptions({valueRange: [0.5, 2.5]});
-  assertEquals(1, g.annotations().length);
-  a1 = document.getElementsByClassName('ann1');
-  assertEquals(0, a1.length);
+  assert.equal(1, g.annotations().length);
+  var a1 = document.getElementsByClassName('ann1');
+  assert.equal(0, a1.length);
 
   // ... and that panning down brings it back.
   g.updateOptions({valueRange: [-1, 1]});
-  assertEquals(1, g.annotations().length);
+  assert.equal(1, g.annotations().length);
   a1 = document.getElementsByClassName('ann1');
-  assertEquals(1, a1.length);
-};
+  assert.equal(1, a1.length);
+});
 
-AnnotationsTestCase.prototype.testAnnotationsDrawnInDrawCallback = function() {
+it('testAnnotationsDrawnInDrawCallback', function() {
   var data = "X,Y\n" +
       "0,-1\n" +
       "1,0\n" +
@@ -203,13 +203,13 @@ AnnotationsTestCase.prototype.testAnnotationsDrawnInDrawCallback = function() {
       }
     });
 
-  assertEquals([true, false], calls);
-};
+  assert.deepEqual([true, false], calls);
+});
 
 
 // Test that annotations on the same point are stacked.
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=256
-AnnotationsTestCase.prototype.testAnnotationsStacked = function() {
+it('testAnnotationsStacked', function() {
   var data = 'X,Y1,Y2\n' +
       '0,1,2\n' +
       '1,2,3\n';
@@ -235,15 +235,15 @@ AnnotationsTestCase.prototype.testAnnotationsStacked = function() {
   g.setAnnotations(annotations);
 
   var annEls = document.getElementsByClassName('dygraphDefaultAnnotation');
-  assertEquals(2, annEls.length);
+  assert.equal(2, annEls.length);
 
-  assertEquals(annEls[0].offsetLeft, annEls[1].offsetLeft);
+  assert.equal(annEls[0].offsetLeft, annEls[1].offsetLeft);
   assert(annEls[1].offsetTop < annEls[0].offsetTop - 10);
-};
+});
 
 
 // Test the .ready() method, which is most often used with setAnnotations().
-AnnotationsTestCase.prototype.testReady = function() {
+it('testReady', function() {
   var data = 'X,Y1,Y2\n' +
       '0,1,2\n' +
       '1,2,3\n';
@@ -258,15 +258,17 @@ AnnotationsTestCase.prototype.testReady = function() {
   var ready_calls = 0;
   g.ready(function() { ready_calls++; });
 
-  assertEquals(0, ready_calls);
+  assert.equal(0, ready_calls);
   mockXhr.respond();
-  assertEquals(1, ready_calls);
+  assert.equal(1, ready_calls);
 
   // Make sure that ready isn't called on redraws.
   g.updateOptions({});
-  assertEquals(1, ready_calls);
+  assert.equal(1, ready_calls);
 
   // Or data changes.
   g.updateOptions({file: data});
-  assertEquals(1, ready_calls);
-};
+  assert.equal(1, ready_calls);
+});
+
+});
index da747e5..08a5db2 100644 (file)
@@ -4,16 +4,16 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var DeprecatedAxisLabelsTestCase = TestCase("axis-labels-deprecated");
+describe("axis-labels-deprecated", function() {
 
-DeprecatedAxisLabelsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-DeprecatedAxisLabelsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedDeprecatedXAxisTimeLabelFormatter = function() {
+it('testDeprecatedDeprecatedXAxisTimeLabelFormatter', function() {
   var opts = {
     width: 480,
     height: 320
@@ -39,33 +39,33 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedDeprecatedXAxisTimeLabelFor
     }
   });
 
-  assertEquals(["00:05:00","00:05:12","00:05:24","00:05:36","00:05:48"], Util.getXLabels());
+  assert.deepEqual(["00:05:00","00:05:12","00:05:24","00:05:36","00:05:48"], Util.getXLabels());
 
   // The legend does not use the xAxisLabelFormatter:
   g.setSelection(1);
-  assertEquals('5.1: Y1: 1', Util.getLegend());
-};
+  assert.equal('5.1: Y1: 1', Util.getLegend());
+});
 
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedAxisLabelFormatter = function () {
+it('testDeprecatedAxisLabelFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
     axes : {
       x : {
         axisLabelFormatter: function(x, granularity, opts, dg) {
-          assertEquals('number', typeof(x));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(x));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'x' + x;
         }
       },
       y : {
         axisLabelFormatter: function(y, granularity, opts, dg) {
-          assertEquals('number', typeof(y));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(y));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'y' + y;
         }
       }
@@ -79,34 +79,34 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedAxisLabelFormatter = functi
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['x0','x2','x4','x6','x8'], Util.getXLabels());
-  assertEquals(["y0","y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(['x0','x2','x4','x6','x8'], Util.getXLabels());
+  assert.deepEqual(["y0","y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(2);
-  assertEquals("2: y: 4", Util.getLegend());
-};
+  assert.equal("2: y: 4", Util.getLegend());
+});
 
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedDateAxisLabelFormatter = function () {
+it('testDeprecatedDateAxisLabelFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
     axes : {
       x : {
         axisLabelFormatter: function(x, granularity, opts, dg) {
-          assertTrue(Dygraph.isDateLike(x));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.isTrue(Dygraph.isDateLike(x));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'x' + Util.formatDate(x);
         },
         pixelsPerLabel: 60
       },
       y : {
         axisLabelFormatter: function(y, granularity, opts, dg) {
-          assertEquals('number', typeof(y));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(y));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'y' + y;
         }
       }
@@ -120,36 +120,36 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedDateAxisLabelFormatter = fu
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(["x2011/01/02","x2011/01/04","x2011/01/06","x2011/01/08"], Util.getXLabels());
-  assertEquals(["y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(["x2011/01/02","x2011/01/04","x2011/01/06","x2011/01/08"], Util.getXLabels());
+  assert.deepEqual(["y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(0);
-  assertEquals("2011/01/01: y: 2", Util.getLegend());
-};
+  assert.equal("2011/01/01: y: 2", Util.getLegend());
+});
 
 // This test verifies that when a valueFormatter is set (but not an
 // axisLabelFormatter), then the valueFormatter is used to format the axis
 // labels.
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedValueFormatter = function () {
+it('testDeprecatedValueFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
     axes : {
       x : {
         valueFormatter: function(x, opts, series_name, dg) {
-          assertEquals('number', typeof(x));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(x));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'x' + x;
         }
       },
       y : {
         valueFormatter: function(y, opts, series_name, dg) {
-          assertEquals('number', typeof(y));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(y));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'y' + y;
         }
       }
@@ -164,35 +164,35 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedValueFormatter = function (
   var g = new Dygraph(graph, data, opts);
 
   // the valueFormatter options do not affect the ticks.
-  assertEquals(['0','2','4','6','8'], Util.getXLabels());
-  assertEquals(["0","5","10","15"], Util.getYLabels());
+  assert.deepEqual(['0','2','4','6','8'], Util.getXLabels());
+  assert.deepEqual(["0","5","10","15"], Util.getYLabels());
 
   // they do affect the legend, however.
   g.setSelection(2);
-  assertEquals("x2: y: y4", Util.getLegend());
-};
+  assert.equal("x2: y: y4", Util.getLegend());
+});
 
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedDateValueFormatter = function () {
+it('testDeprecatedDateValueFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
     axes : {
       x : {
         valueFormatter: function(x, opts, series_name, dg) {
-          assertEquals('number', typeof(x));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(x));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'x' + Util.formatDate(x);
         },
         pixelsPerLabel: 60
       },
       y : {
         valueFormatter: function(y, opts, series_name, dg) {
-          assertEquals('number', typeof(y));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(y));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'y' + y;
         }
       }
@@ -208,17 +208,17 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedDateValueFormatter = functi
   var g = new Dygraph(graph, data, opts);
 
   // valueFormatters do not affect ticks.
-  assertEquals(["02 Jan","04 Jan","06 Jan","08 Jan"], Util.getXLabels());
-  assertEquals(["5","10","15"], Util.getYLabels());
+  assert.deepEqual(["02 Jan","04 Jan","06 Jan","08 Jan"], Util.getXLabels());
+  assert.deepEqual(["5","10","15"], Util.getYLabels());
 
   // the valueFormatter options also affect the legend.
   g.setSelection(2);
-  assertEquals('x2011/01/03: y: y6', Util.getLegend());
-};
+  assert.equal('x2011/01/03: y: y6', Util.getLegend());
+});
 
 // This test verifies that when both a valueFormatter and an axisLabelFormatter
 // are specified, the axisLabelFormatter takes precedence.
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedAxisLabelFormatterPrecedence = function () {
+it('testDeprecatedAxisLabelFormatterPrecedence', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -249,16 +249,16 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedAxisLabelFormatterPrecedenc
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['x0','x2','x4','x6','x8'], Util.getXLabels());
-  assertEquals(["y0","y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(['x0','x2','x4','x6','x8'], Util.getXLabels());
+  assert.deepEqual(["y0","y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(9);
-  assertEquals("xvf9: y: yvf18", Util.getLegend());
-};
+  assert.equal("xvf9: y: yvf18", Util.getLegend());
+});
 
 // This is the same as the previous test, except that options are added
 // one-by-one.
-DeprecatedAxisLabelsTestCase.prototype.testDeprecatedAxisLabelFormatterIncremental = function () {
+it('testDeprecatedAxisLabelFormatterIncremental', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -307,9 +307,11 @@ DeprecatedAxisLabelsTestCase.prototype.testDeprecatedAxisLabelFormatterIncrement
     }
   });
 
-  assertEquals(["x0","x2","x4","x6","x8"], Util.getXLabels());
-  assertEquals(["y0","y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(["x0","x2","x4","x6","x8"], Util.getXLabels());
+  assert.deepEqual(["y0","y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(9);
-  assertEquals("xvf9: y: yvf18", Util.getLegend());
-};
+  assert.equal("xvf9: y: yvf18", Util.getLegend());
+});
+
+});
index fd29235..89959ac 100644 (file)
@@ -3,25 +3,25 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var AxisLabelsTestCase = TestCase("axis-labels");
+describe("axis-labels", function() {
 
-AxisLabelsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-AxisLabelsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-AxisLabelsTestCase.simpleData =
+var simpleData =
     "X,Y,Y2\n" +
     "0,-1,.5\n" +
     "1,0,.7\n" +
     "2,1,.4\n" +
     "3,0,.98\n";
 
-AxisLabelsTestCase.prototype.kCloseFloat = 1.0e-10;
+var kCloseFloat = 1.0e-10;
 
-AxisLabelsTestCase.prototype.testMinusOneToOne = function() {
+it('testMinusOneToOne', function() {
   var opts = {
     width: 480,
     height: 320
@@ -37,28 +37,28 @@ AxisLabelsTestCase.prototype.testMinusOneToOne = function() {
   var g = new Dygraph(graph, data, opts);
 
   // TODO(danvk): would ['-1.0','-0.5','0.0','0.5','1.0'] be better?
-  assertEquals(['-1','-0.5','0','0.5','1'], Util.getYLabels());
+  assert.deepEqual(['-1','-0.5','0','0.5','1'], Util.getYLabels());
 
   // Go up to 2
   data += "4,2\n";
   g.updateOptions({file: data});
-  assertEquals(['-1','-0.5','0','0.5','1','1.5','2'], Util.getYLabels());
+  assert.deepEqual(['-1','-0.5','0','0.5','1','1.5','2'], Util.getYLabels());
 
   // Now 10
   data += "5,10\n";
   g.updateOptions({file: data});
-  assertEquals(['-2','0','2','4','6','8','10'], Util.getYLabels());
+  assert.deepEqual(['-2','0','2','4','6','8','10'], Util.getYLabels());
 
   // Now 100
   data += "6,100\n";
   g.updateOptions({file: data});
-  assertEquals(['0','20','40','60','80','100'], Util.getYLabels());
+  assert.deepEqual(['0','20','40','60','80','100'], Util.getYLabels());
 
   g.setSelection(0);
-  assertEquals('0: Y: -1', Util.getLegend());
-};
+  assert.equal('0: Y: -1', Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testSmallRangeNearZero = function() {
+it('testSmallRangeNearZero', function() {
   var opts = {
     drawAxesAtZero: true,
     width: 480,
@@ -74,24 +74,24 @@ AxisLabelsTestCase.prototype.testSmallRangeNearZero = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  assertEqualsDelta([-0.1,-0.05,0,0.05],
-                    Util.makeNumbers(Util.getYLabels()), this.kCloseFloat);
+  assertDeepCloseTo([-0.1,-0.05,0,0.05],
+                    Util.makeNumbers(Util.getYLabels()), kCloseFloat);
 
   opts.valueRange = [-0.05, 0.05];
   g.updateOptions(opts);
-  assertEquals([-0.04,-0.02,0,0.02,0.04],
-               Util.makeNumbers(Util.getYLabels()));
+  assert.deepEqual([-0.04,-0.02,0,0.02,0.04],
+                   Util.makeNumbers(Util.getYLabels()));
 
   opts.valueRange = [-0.01, 0.01];
   g.updateOptions(opts);
-  assertEquals([-0.01,-0.005,0,0.005],
-               Util.makeNumbers(Util.getYLabels()));
+  assert.deepEqual([-0.01,-0.005,0,0.005],
+                   Util.makeNumbers(Util.getYLabels()));
 
   g.setSelection(1);
-  assertEquals('1: Y: 0', Util.getLegend());
-};
+  assert.equal('1: Y: 0', Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testSmallRangeAwayFromZero = function() {
+it('testSmallRangeAwayFromZero', function() {
   var opts = {
     width: 480,
     height: 320
@@ -106,23 +106,23 @@ AxisLabelsTestCase.prototype.testSmallRangeAwayFromZero = function() {
 
   opts.valueRange = [9.9, 10.1];
   var g = new Dygraph(graph, data, opts);
-  assertEquals(["9.9","9.92","9.94","9.96","9.98","10","10.02","10.04","10.06","10.08"], Util.getYLabels());
+  assert.deepEqual(["9.9","9.92","9.94","9.96","9.98","10","10.02","10.04","10.06","10.08"], Util.getYLabels());
 
   opts.valueRange = [9.99, 10.01];
   g.updateOptions(opts);
   // TODO(danvk): this is bad
-  assertEquals(["9.99","9.99","9.99","10","10","10","10","10","10.01","10.01"], Util.getYLabels());
+  assert.deepEqual(["9.99","9.99","9.99","10","10","10","10","10","10.01","10.01"], Util.getYLabels());
 
   opts.valueRange = [9.999, 10.001];
   g.updateOptions(opts);
   // TODO(danvk): this is even worse!
-  assertEquals(["10","10","10","10"], Util.getYLabels());
+  assert.deepEqual(["10","10","10","10"], Util.getYLabels());
 
   g.setSelection(1);
-  assertEquals('1: Y: 0', Util.getLegend());
-};
+  assert.equal('1: Y: 0', Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testXAxisTimeLabelFormatter = function() {
+it('testXAxisTimeLabelFormatter', function() {
   var opts = {
     width: 480,
     height: 320
@@ -148,33 +148,33 @@ AxisLabelsTestCase.prototype.testXAxisTimeLabelFormatter = function() {
     }
   });
 
-  assertEquals(["00:05:00","00:05:12","00:05:24","00:05:36","00:05:48"], Util.getXLabels());
+  assert.deepEqual(["00:05:00","00:05:12","00:05:24","00:05:36","00:05:48"], Util.getXLabels());
 
   // The legend does not use the axisLabelFormatter:
   g.setSelection(1);
-  assertEquals('5.1: Y1: 1', Util.getLegend());
-};
+  assert.equal('5.1: Y1: 1', Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testAxisLabelFormatter = function () {
+it('testAxisLabelFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
     axes : {
       x : {
         axisLabelFormatter : function(x, granularity, opts, dg) {
-          assertEquals('number', typeof(x));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(x));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'x' + x;
         }
       },
       y : {
         axisLabelFormatter : function(y, granularity, opts, dg) {
-          assertEquals('number', typeof(y));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(y));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'y' + y;
         }
       }
@@ -188,14 +188,14 @@ AxisLabelsTestCase.prototype.testAxisLabelFormatter = function () {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['x0','x2','x4','x6','x8'], Util.getXLabels());
-  assertEquals(["y0","y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(['x0','x2','x4','x6','x8'], Util.getXLabels());
+  assert.deepEqual(["y0","y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(2);
-  assertEquals("2: y: 4", Util.getLegend());
-};
+  assert.equal("2: y: 4", Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testDateAxisLabelFormatter = function () {
+it('testDateAxisLabelFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -203,19 +203,19 @@ AxisLabelsTestCase.prototype.testDateAxisLabelFormatter = function () {
       x : {
         pixelsPerLabel: 60,
         axisLabelFormatter : function(x, granularity, opts, dg) {
-          assertTrue(Dygraph.isDateLike(x));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.isTrue(Dygraph.isDateLike(x));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'x' + Util.formatDate(x);
         }
       },
       y : {
         axisLabelFormatter : function(y, granularity, opts, dg) {
-          assertEquals('number', typeof(y));
-          assertEquals('number', typeof(granularity));
-          assertEquals('function', typeof(opts));
-          assertEquals('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(y));
+          assert.equal('number', typeof(granularity));
+          assert.equal('function', typeof(opts));
+          assert.equal('[Dygraph graph]', dg.toString());
           return 'y' + y;
         }
       }
@@ -229,42 +229,42 @@ AxisLabelsTestCase.prototype.testDateAxisLabelFormatter = function () {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(["x2011/01/02","x2011/01/04","x2011/01/06","x2011/01/08"], Util.getXLabels());
-  assertEquals(["y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(["x2011/01/02","x2011/01/04","x2011/01/06","x2011/01/08"], Util.getXLabels());
+  assert.deepEqual(["y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(0);
-  assertEquals("2011/01/01: y: 2", Util.getLegend());
-};
+  assert.equal("2011/01/01: y: 2", Util.getLegend());
+});
 
 // This test verifies that when a valueFormatter is set (but not an
 // axisLabelFormatter), then the valueFormatter is used to format the axis
 // labels.
-AxisLabelsTestCase.prototype.testValueFormatter = function () {
+it('testValueFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
     axes: {
       x: {
         valueFormatter: function(x, opts, series_name, dg, row, col) {
-          assertEquals('number', typeof(x));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
-          assertEquals('number', typeof(row));
-          assertEquals('number', typeof(col));
-          assertEquals(dg, this);
+          assert.equal('number', typeof(x));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(row));
+          assert.equal('number', typeof(col));
+          assert.equal(dg, this);
           return 'x' + x;
         }
       },
       y: {
         valueFormatter: function(y, opts, series_name, dg, row, col) {
-          assertEquals('number', typeof(y));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
-          assertEquals('number', typeof(row));
-          assertEquals('number', typeof(col));
-          assertEquals(dg, this);
+          assert.equal('number', typeof(y));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(row));
+          assert.equal('number', typeof(col));
+          assert.equal(dg, this);
           return 'y' + y;
         }
       }
@@ -279,16 +279,16 @@ AxisLabelsTestCase.prototype.testValueFormatter = function () {
   var g = new Dygraph(graph, data, opts);
 
   // the valueFormatter options do not affect the ticks.
-  assertEquals(['0','2','4','6','8'], Util.getXLabels());
-  assertEquals(["0","5","10","15"],
+  assert.deepEqual(['0','2','4','6','8'], Util.getXLabels());
+  assert.deepEqual(["0","5","10","15"],
                Util.getYLabels());
 
   // they do affect the legend, however.
   g.setSelection(2);
-  assertEquals("x2: y: y4", Util.getLegend());
-};
+  assert.equal("x2: y: y4", Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testDateValueFormatter = function () {
+it('testDateValueFormatter', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -296,25 +296,25 @@ AxisLabelsTestCase.prototype.testDateValueFormatter = function () {
       x : {
         pixelsPerLabel: 60,
         valueFormatter: function(x, opts, series_name, dg, row, col) {
-          assertEquals('number', typeof(x));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
-          assertEquals('number', typeof(row));
-          assertEquals('number', typeof(col));
-          assertEquals(dg, this);
+          assert.equal('number', typeof(x));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(row));
+          assert.equal('number', typeof(col));
+          assert.equal(dg, this);
           return 'x' + Util.formatDate(x);
         }
       },
       y : {
         valueFormatter: function(y, opts, series_name, dg, row, col) {
-          assertEquals('number', typeof(y));
-          assertEquals('function', typeof(opts));
-          assertEquals('string', typeof(series_name));
-          assertEquals('[Dygraph graph]', dg.toString());
-          assertEquals('number', typeof(row));
-          assertEquals('number', typeof(col));
-          assertEquals(dg, this);
+          assert.equal('number', typeof(y));
+          assert.equal('function', typeof(opts));
+          assert.equal('string', typeof(series_name));
+          assert.equal('[Dygraph graph]', dg.toString());
+          assert.equal('number', typeof(row));
+          assert.equal('number', typeof(col));
+          assert.equal(dg, this);
           return 'y' + y;
         }
       }
@@ -330,38 +330,38 @@ AxisLabelsTestCase.prototype.testDateValueFormatter = function () {
   var g = new Dygraph(graph, data, opts);
 
   // valueFormatters do not affect ticks.
-  assertEquals(["02 Jan","04 Jan","06 Jan","08 Jan"], Util.getXLabels());
-  assertEquals(["5","10","15"], Util.getYLabels());
+  assert.deepEqual(["02 Jan","04 Jan","06 Jan","08 Jan"], Util.getXLabels());
+  assert.deepEqual(["5","10","15"], Util.getYLabels());
 
   // the valueFormatter options also affect the legend.
   g.setSelection(2);
-  assertEquals('x2011/01/03: y: y6', Util.getLegend());
-};
+  assert.equal('x2011/01/03: y: y6', Util.getLegend());
+});
 
 // This test verifies that when both a valueFormatter and an axisLabelFormatter
 // are specified, the axisLabelFormatter takes precedence.
-AxisLabelsTestCase.prototype.testAxisLabelFormatterPrecedence = function () {
+it('testAxisLabelFormatterPrecedence', function() {
   var opts = {
     width: 480,
     height: 320,
     axes : {
       x : {
         valueFormatter: function(x) {
-          assertEquals('[Dygraph graph]', this.toString());
+          assert.equal('[Dygraph graph]', this.toString());
           return 'xvf' + x;
         },
         axisLabelFormatter: function(x, granularity) {
-          assertEquals('[Dygraph graph]', this.toString());
+          assert.equal('[Dygraph graph]', this.toString());
           return 'x' + x;
         }
       },
       y : {
         valueFormatter: function(y) {
-          assertEquals('[Dygraph graph]', this.toString());
+          assert.equal('[Dygraph graph]', this.toString());
           return 'yvf' + y;
         },
         axisLabelFormatter: function(y) {
-          assertEquals('[Dygraph graph]', this.toString());
+          assert.equal('[Dygraph graph]', this.toString());
           return 'y' + y;
         }
       }
@@ -375,16 +375,16 @@ AxisLabelsTestCase.prototype.testAxisLabelFormatterPrecedence = function () {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['x0','x2','x4','x6','x8'], Util.getXLabels());
-  assertEquals(["y0","y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(['x0','x2','x4','x6','x8'], Util.getXLabels());
+  assert.deepEqual(["y0","y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(9);
-  assertEquals("xvf9: y: yvf18", Util.getLegend());
-};
+  assert.equal("xvf9: y: yvf18", Util.getLegend());
+});
 
 // This is the same as the previous test, except that options are added
 // one-by-one.
-AxisLabelsTestCase.prototype.testAxisLabelFormatterIncremental = function () {
+it('testAxisLabelFormatterIncremental', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -433,24 +433,24 @@ AxisLabelsTestCase.prototype.testAxisLabelFormatterIncremental = function () {
     }
   });
 
-  assertEquals(["x0","x2","x4","x6","x8"], Util.getXLabels());
-  assertEquals(["y0","y5","y10","y15"], Util.getYLabels());
+  assert.deepEqual(["x0","x2","x4","x6","x8"], Util.getXLabels());
+  assert.deepEqual(["y0","y5","y10","y15"], Util.getYLabels());
 
   g.setSelection(9);
-  assertEquals("xvf9: y: yvf18", Util.getLegend());
-};
+  assert.equal("xvf9: y: yvf18", Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testGlobalFormatters = function() {
+it('testGlobalFormatters', function() {
   var opts = {
     width: 480,
     height: 320,
     labels: ['x', 'y'],
     valueFormatter: function(x) {
-      assertEquals('[Dygraph graph]', this);
+      assert.equal('[Dygraph graph]', this);
       return 'vf' + x;
     },
     axisLabelFormatter: function(x) {
-      assertEquals('[Dygraph graph]', this);
+      assert.equal('[Dygraph graph]', this);
       return 'alf' + x;
     }
   };
@@ -461,14 +461,14 @@ AxisLabelsTestCase.prototype.testGlobalFormatters = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['alf0','alf2','alf4','alf6','alf8'], Util.getXLabels());
-  assertEquals(["alf0","alf5","alf10","alf15"], Util.getYLabels());
+  assert.deepEqual(['alf0','alf2','alf4','alf6','alf8'], Util.getXLabels());
+  assert.deepEqual(["alf0","alf5","alf10","alf15"], Util.getYLabels());
 
   g.setSelection(9);
-  assertEquals("vf9: y: vf18", Util.getLegend());
-};
+  assert.equal("vf9: y: vf18", Util.getLegend());
+});
 
-AxisLabelsTestCase.prototype.testValueFormatterParameters = function() {
+it('testValueFormatterParameters', function() {
   var calls = [];
   // change any functions in list to 'fn' -- functions can't be asserted.
   var killFunctions = function(list) {
@@ -507,9 +507,9 @@ AxisLabelsTestCase.prototype.testValueFormatterParameters = function() {
   var graph = document.getElementById('graph');
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals([], calls);
+  assert.deepEqual([], calls);
   g.setSelection(0);
-  assertEquals([
+  assert.deepEqual([
     // num or millis, opts, series, dygraph, row, col
     [ 'x', g, 0, 'fn',  'x', g, 0, 0],
     [ 'y', g, 1, 'fn', 'y1', g, 0, 1],
@@ -518,14 +518,14 @@ AxisLabelsTestCase.prototype.testValueFormatterParameters = function() {
 
   calls = [];
   g.setSelection(1);
-  assertEquals([
+  assert.deepEqual([
     [ 'x', g, 1, 'fn',  'x', g, 1, 0],
     [ 'y', g, 3, 'fn', 'y1', g, 1, 1],
     ['y2', g, 4, 'fn', 'y2', g, 1, 2]
   ], calls);
-};
+});
 
-AxisLabelsTestCase.prototype.testSeriesOrder = function() {
+it('testSeriesOrder', function() {
   var opts = {
     width: 480,
     height: 320
@@ -541,18 +541,18 @@ AxisLabelsTestCase.prototype.testSeriesOrder = function() {
   var g = new Dygraph(graph, data, opts);
 
   g.setSelection(2);
-  assertEquals('2: 00: 103 01: 203 10: 303 11: 403', Util.getLegend());
+  assert.equal('2: 00: 103 01: 203 10: 303 11: 403', Util.getLegend());
 
   // Sanity checks for indexFromSetName
-  assertEquals(0, g.indexFromSetName("x"));
-  assertEquals(1, g.indexFromSetName("00"));
-  assertEquals(null, g.indexFromSetName("abcde"));
+  assert.equal(0, g.indexFromSetName("x"));
+  assert.equal(1, g.indexFromSetName("00"));
+  assert.equal(null, g.indexFromSetName("abcde"));
 
   // Verify that we get the label list back in the right order
-  assertEquals(["x", "00", "01", "10", "11"], g.getLabels());
-};
+  assert.deepEqual(["x", "00", "01", "10", "11"], g.getLabels());
+});
 
-AxisLabelsTestCase.prototype.testLabelKMB = function() {
+it('testLabelKMB', function() {
   var data = [];
   data.push([0,0]);
   data.push([1,2000]);
@@ -571,10 +571,10 @@ AxisLabelsTestCase.prototype.testLabelKMB = function() {
     }
   );
 
-  assertEquals(["0", "500", "1K", "1.5K", "2K"], Util.getYLabels());
-};
+  assert.deepEqual(["0", "500", "1K", "1.5K", "2K"], Util.getYLabels());
+});
 
-AxisLabelsTestCase.prototype.testLabelKMG2 = function() {
+it('testLabelKMG2', function() {
   var data = [];
   data.push([0,0]);
   data.push([1,2000]);
@@ -593,14 +593,13 @@ AxisLabelsTestCase.prototype.testLabelKMG2 = function() {
     }
   );
 
-  assertEquals(
-      ["0","256","512","768","1k","1.25k","1.5k","1.75k","2k"],
-      Util.getYLabels());
-};
+  assert.deepEqual(["0","256","512","768","1k","1.25k","1.5k","1.75k","2k"],
+                   Util.getYLabels());
+});
 
 // Same as testLabelKMG2 but specifies the option at the
 // top of the option dictionary.
-AxisLabelsTestCase.prototype.testLabelKMG2_top = function() {
+it('testLabelKMG2_top', function() {
   var data = [];
   data.push([0,0]);
   data.push([1,2000]);
@@ -615,51 +614,51 @@ AxisLabelsTestCase.prototype.testLabelKMG2_top = function() {
     }
   );
 
-  assertEquals(
+  assert.deepEqual(
       ["0","256","512","768","1k","1.25k","1.5k","1.75k","2k"],
       Util.getYLabels());
-};
+});
 
 /**
  * Verify that log scale axis range is properly specified.
  */
-AxisLabelsTestCase.prototype.testLogScale = function() {
+it('testLogScale', function() {
   var g = new Dygraph("graph", [[0, 5], [1, 1000]], { logscale : true });
   var nonEmptyLabels = Util.getYLabels().filter(function(x) { return x.length > 0; });
-  assertEquals(["5","10","20","50","100","200","500","1000"], nonEmptyLabels);
+  assert.deepEqual(["5","10","20","50","100","200","500","1000"], nonEmptyLabels);
  
   g.updateOptions({ logscale : false });
-  assertEquals(['0','200','400','600','800','1000'], Util.getYLabels());
-}
+  assert.deepEqual(['0','200','400','600','800','1000'], Util.getYLabels());
+});
 
 /**
  * Verify that include zero range is properly specified.
  */
-AxisLabelsTestCase.prototype.testIncludeZero = function() {
+it('testIncludeZero', function() {
   var g = new Dygraph("graph", [[0, 500], [1, 1000]], { includeZero : true });
-  assertEquals(['0','200','400','600','800','1000'], Util.getYLabels());
+  assert.deepEqual(['0','200','400','600','800','1000'], Util.getYLabels());
  
   g.updateOptions({ includeZero : false });
-  assertEquals(['500','600','700','800','900','1000'], Util.getYLabels());
-}
+  assert.deepEqual(['500','600','700','800','900','1000'], Util.getYLabels());
+});
 
-AxisLabelsTestCase.prototype.testAxisLabelFontSize = function() {
+it('testAxisLabelFontSize', function() {
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, AxisLabelsTestCase.simpleData, {});
+  var g = new Dygraph(graph, simpleData, {});
 
   // Be sure we're dealing with a 14-point default.
-  assertEquals(14, Dygraph.DEFAULT_ATTRS.axisLabelFontSize);
+  assert.equal(14, Dygraph.DEFAULT_ATTRS.axisLabelFontSize);
 
   var assertFontSize = function(selector, expected) {
     Util.assertStyleOfChildren(selector, "font-size", expected);
   }
   
-  assertFontSize($(".dygraph-axis-label-x"), "14px");
-  assertFontSize($(".dygraph-axis-label-y") , "14px");
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-x"), "14px");
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y") , "14px");
 
   g.updateOptions({ axisLabelFontSize : 8});
-  assertFontSize($(".dygraph-axis-label-x"), "8px"); 
-  assertFontSize($(".dygraph-axis-label-y"), "8px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-x"), "8px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y"), "8px"); 
 
   g.updateOptions({
     axisLabelFontSize : null,
@@ -668,8 +667,8 @@ AxisLabelsTestCase.prototype.testAxisLabelFontSize = function() {
     }   
   }); 
 
-  assertFontSize($(".dygraph-axis-label-x"), "5px"); 
-  assertFontSize($(".dygraph-axis-label-y"), "14px");
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-x"), "5px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y"), "14px");
 
   g.updateOptions({
     axes : { 
@@ -677,8 +676,8 @@ AxisLabelsTestCase.prototype.testAxisLabelFontSize = function() {
     }   
   }); 
 
-  assertFontSize($(".dygraph-axis-label-x"), "5px"); 
-  assertFontSize($(".dygraph-axis-label-y"), "20px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-x"), "5px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y"), "20px"); 
 
   g.updateOptions({
     series : { 
@@ -689,14 +688,14 @@ AxisLabelsTestCase.prototype.testAxisLabelFontSize = function() {
     }   
   }); 
 
-  assertFontSize($(".dygraph-axis-label-x"), "5px"); 
-  assertFontSize($(".dygraph-axis-label-y1"), "20px"); 
-  assertFontSize($(".dygraph-axis-label-y2"), "12px"); 
-}
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-x"), "5px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y1"), "20px"); 
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y2"), "12px"); 
+});
 
-AxisLabelsTestCase.prototype.testAxisLabelFontSizeNull = function() {
+it('testAxisLabelFontSizeNull', function() {
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, AxisLabelsTestCase.simpleData,
+  var g = new Dygraph(graph, simpleData,
     {
       axisLabelFontSize: null
     });
@@ -706,29 +705,29 @@ AxisLabelsTestCase.prototype.testAxisLabelFontSizeNull = function() {
   };
 
   // Be sure we're dealing with a 14-point default.
-  assertEquals(14, Dygraph.DEFAULT_ATTRS.axisLabelFontSize);
+  assert.equal(14, Dygraph.DEFAULT_ATTRS.axisLabelFontSize);
 
-  assertFontSize($(".dygraph-axis-label-x"), "14px");
-  assertFontSize($(".dygraph-axis-label-y"), "14px");
-}
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-x"), "14px");
+  assertFontSize(document.querySelectorAll(".dygraph-axis-label-y"), "14px");
+});
 
-AxisLabelsTestCase.prototype.testAxisLabelColor = function() {
+it('testAxisLabelColor', function() {
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, AxisLabelsTestCase.simpleData, {});
+  var g = new Dygraph(graph, simpleData, {});
 
   // Be sure we're dealing with a black default.
-  assertEquals("black", Dygraph.DEFAULT_ATTRS.axisLabelColor);
+  assert.equal("black", Dygraph.DEFAULT_ATTRS.axisLabelColor);
 
   var assertColor = function(selector, expected) {
     Util.assertStyleOfChildren(selector, "color", expected);
   }
 
-  assertColor($(".dygraph-axis-label-x"), "rgb(0, 0, 0)");
-  assertColor($(".dygraph-axis-label-y"), "rgb(0, 0, 0)");
+  assertColor(document.querySelectorAll(".dygraph-axis-label-x"), "rgb(0, 0, 0)");
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y"), "rgb(0, 0, 0)");
 
   g.updateOptions({ axisLabelColor : "red"});
-  assertColor($(".dygraph-axis-label-x"), "rgb(255, 0, 0)"); 
-  assertColor($(".dygraph-axis-label-y"), "rgb(255, 0, 0)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-x"), "rgb(255, 0, 0)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y"), "rgb(255, 0, 0)"); 
 
   g.updateOptions({
     axisLabelColor : null,
@@ -737,8 +736,8 @@ AxisLabelsTestCase.prototype.testAxisLabelColor = function() {
     }   
   }); 
 
-  assertColor($(".dygraph-axis-label-x"), "rgb(0, 0, 255)"); 
-  assertColor($(".dygraph-axis-label-y"), "rgb(0, 0, 0)");
+  assertColor(document.querySelectorAll(".dygraph-axis-label-x"), "rgb(0, 0, 255)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y"), "rgb(0, 0, 0)");
 
   g.updateOptions({
     axes : { 
@@ -746,8 +745,8 @@ AxisLabelsTestCase.prototype.testAxisLabelColor = function() {
     }   
   }); 
 
-  assertColor($(".dygraph-axis-label-x"), "rgb(0, 0, 255)"); 
-  assertColor($(".dygraph-axis-label-y"), "rgb(0, 128, 0)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-x"), "rgb(0, 0, 255)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y"), "rgb(0, 128, 0)"); 
 
   g.updateOptions({
     series : { 
@@ -758,14 +757,14 @@ AxisLabelsTestCase.prototype.testAxisLabelColor = function() {
     }   
   }); 
 
-  assertColor($(".dygraph-axis-label-x"), "rgb(0, 0, 255)"); 
-  assertColor($(".dygraph-axis-label-y1"), "rgb(0, 128, 0)"); 
-  assertColor($(".dygraph-axis-label-y2"), "rgb(255, 255, 0)"); 
-}
+  assertColor(document.querySelectorAll(".dygraph-axis-label-x"), "rgb(0, 0, 255)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y1"), "rgb(0, 128, 0)"); 
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y2"), "rgb(255, 255, 0)"); 
+});
 
-AxisLabelsTestCase.prototype.testAxisLabelColorNull = function() {
+it('testAxisLabelColorNull', function() {
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, AxisLabelsTestCase.simpleData,
+  var g = new Dygraph(graph, simpleData,
     {
       axisLabelColor: null
     });
@@ -775,16 +774,16 @@ AxisLabelsTestCase.prototype.testAxisLabelColorNull = function() {
   }
 
   // Be sure we're dealing with a 14-point default.
-  assertEquals(14, Dygraph.DEFAULT_ATTRS.axisLabelFontSize);
+  assert.equal(14, Dygraph.DEFAULT_ATTRS.axisLabelFontSize);
 
-  assertColor($(".dygraph-axis-label-x"), "rgb(0, 0, 0)");
-  assertColor($(".dygraph-axis-label-y"), "rgb(0, 0, 0)");
-}
+  assertColor(document.querySelectorAll(".dygraph-axis-label-x"), "rgb(0, 0, 0)");
+  assertColor(document.querySelectorAll(".dygraph-axis-label-y"), "rgb(0, 0, 0)");
+});
 
 /*
  * This test shows that the label formatter overrides labelsKMB for all values.
  */
-AxisLabelsTestCase.prototype.testLabelFormatterOverridesLabelsKMB = function() {
+it('testLabelFormatterOverridesLabelsKMB', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "X,a,b\n" +
@@ -797,14 +796,14 @@ AxisLabelsTestCase.prototype.testLabelFormatterOverridesLabelsKMB = function() {
           return v + ":X";
         }
       });
-  assertEquals(["0:X","500:X","1000:X","1500:X","2000:X"], Util.getYLabels());
-  assertEquals(["1:X","2:X","3:X"], Util.getXLabels());
-}
+  assert.deepEqual(["0:X","500:X","1000:X","1500:X","2000:X"], Util.getYLabels());
+  assert.deepEqual(["1:X","2:X","3:X"], Util.getXLabels());
+});
 
 /*
  * This test shows that you can override labelsKMB on the axis level.
  */
-AxisLabelsTestCase.prototype.testLabelsKMBPerAxis = function() {
+it('testLabelsKMBPerAxis', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "x,a,b\n" +
@@ -824,15 +823,15 @@ AxisLabelsTestCase.prototype.testLabelsKMBPerAxis = function() {
 
   // labelsKMB doesn't apply to the x axis. This value should be different.
   // BUG : https://code.google.com/p/dygraphs/issues/detail?id=488
-  assertEquals(["1000","2000","3000"], Util.getXLabels());
-  assertEquals( ["0","500","1000","1500","2000"], Util.getYLabels(1));
-  assertEquals(["0","500","1K","1.5K","2K"], Util.getYLabels(2));
-};
+  assert.deepEqual(["1000","2000","3000"], Util.getXLabels());
+  assert.deepEqual(["0","500","1000","1500","2000"], Util.getYLabels(1));
+  assert.deepEqual(["0","500","1K","1.5K","2K"], Util.getYLabels(2));
+});
 
 /*
  * This test shows that you can override labelsKMG2 on the axis level.
  */
-AxisLabelsTestCase.prototype.testLabelsKMBG2IPerAxis = function() {
+it('testLabelsKMBG2IPerAxis', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "x,a,b\n" +
@@ -854,15 +853,15 @@ AxisLabelsTestCase.prototype.testLabelsKMBG2IPerAxis = function() {
   // Plus I can't be sure they're doing the same thing as they're done in different
   // bits of code.
   // BUG : https://code.google.com/p/dygraphs/issues/detail?id=488
-  assertEquals(["1024","2048","3072"], Util.getXLabels());
-  assertEquals( ["0","500","1000","1500","2000"], Util.getYLabels(1));
-  assertEquals(["0","500","1000","1.46k","1.95k"], Util.getYLabels(2));
-};
+  assert.deepEqual(["1024","2048","3072"], Util.getXLabels());
+  assert.deepEqual(["0","500","1000","1500","2000"], Util.getYLabels(1));
+  assert.deepEqual(["0","500","1000","1.46k","1.95k"], Util.getYLabels(2));
+});
 
 /**
  * This test shows you can override sigFigs on the axis level.
  */
-AxisLabelsTestCase.prototype.testSigFigsPerAxis = function() {
+it('testSigFigsPerAxis', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "x,a,b\n" +
@@ -882,15 +881,15 @@ AxisLabelsTestCase.prototype.testSigFigsPerAxis = function() {
       });
   // sigFigs doesn't apply to the x axis. This value should be different.
   // BUG : https://code.google.com/p/dygraphs/issues/detail?id=488
-  assertEquals(["1000","2000","3000"], Util.getXLabels());
-  assertEquals(["0.0","5.0e+2","1.0e+3","1.5e+3","2.0e+3"], Util.getYLabels(1));
-  assertEquals(["0.00000","500.000","1000.00","1500.00","2000.00"], Util.getYLabels(2));
-}
+  assert.deepEqual(["1000","2000","3000"], Util.getXLabels());
+  assert.deepEqual(["0.0","5.0e+2","1.0e+3","1.5e+3","2.0e+3"], Util.getYLabels(1));
+  assert.deepEqual(["0.00000","500.000","1000.00","1500.00","2000.00"], Util.getYLabels(2));
+});
 
 /**
  * This test shows you can override digitsAfterDecimal on the axis level.
  */
-AxisLabelsTestCase.prototype.testDigitsAfterDecimalPerAxis = function() {
+it('testDigitsAfterDecimalPerAxis', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "x,a,b\n" +
@@ -906,40 +905,40 @@ AxisLabelsTestCase.prototype.testDigitsAfterDecimalPerAxis = function() {
       });
 
   g.updateOptions({ axes: { y: { digitsAfterDecimal: 3 }}});
-  assertEquals(["0.001","0.002","0.002","0.003","0.003","0.004","0.004"], Util.getYLabels(1));
+  assert.deepEqual(["0.001","0.002","0.002","0.003","0.003","0.004","0.004"], Util.getYLabels(1));
   g.updateOptions({ axes: { y: { digitsAfterDecimal: 4 }}});
-  assertEquals(["0.001","0.0015","0.002","0.0025","0.003","0.0035","0.004"], Util.getYLabels(1));
+  assert.deepEqual(["0.001","0.0015","0.002","0.0025","0.003","0.0035","0.004"], Util.getYLabels(1));
   g.updateOptions({ axes: { y: { digitsAfterDecimal: 5 }}});
-  assertEquals(["0.001","0.0015","0.002","0.0025","0.003","0.0035","0.004"], Util.getYLabels(1));
+  assert.deepEqual(["0.001","0.0015","0.002","0.0025","0.003","0.0035","0.004"], Util.getYLabels(1));
   g.updateOptions({ axes: { y: { digitsAfterDecimal: null }}});
-  assertEquals(["1e-3","2e-3","2e-3","3e-3","3e-3","4e-3","4e-3"], Util.getYLabels(1));
+  assert.deepEqual(["1e-3","2e-3","2e-3","3e-3","3e-3","4e-3","4e-3"], Util.getYLabels(1));
 
   g.updateOptions({ axes: { y2: { digitsAfterDecimal: 3 }}});
-  assertEquals(["0.005","0.006","0.006","0.007","0.007","0.008","0.008"], Util.getYLabels(2));
+  assert.deepEqual(["0.005","0.006","0.006","0.007","0.007","0.008","0.008"], Util.getYLabels(2));
   g.updateOptions({ axes: { y2: { digitsAfterDecimal: 4 }}});
-  assertEquals(["0.005","0.0055","0.006","0.0065","0.007","0.0075","0.008"], Util.getYLabels(2));
+  assert.deepEqual(["0.005","0.0055","0.006","0.0065","0.007","0.0075","0.008"], Util.getYLabels(2));
   g.updateOptions({ axes: { y2: { digitsAfterDecimal: 5 }}});
-  assertEquals(["0.005","0.0055","0.006","0.0065","0.007","0.0075","0.008"], Util.getYLabels(2));
+  assert.deepEqual(["0.005","0.0055","0.006","0.0065","0.007","0.0075","0.008"], Util.getYLabels(2));
   g.updateOptions({ axes: { y2: { digitsAfterDecimal: null }}});
-  assertEquals(["5e-3","6e-3","6e-3","7e-3","7e-3","7e-3","8e-3"], Util.getYLabels(2));
+  assert.deepEqual(["5e-3","6e-3","6e-3","7e-3","7e-3","7e-3","8e-3"], Util.getYLabels(2));
 
 
   // digitsAfterDecimal is ignored for the x-axis.
   // BUG : https://code.google.com/p/dygraphs/issues/detail?id=488
   g.updateOptions({ axes: { x: { digitsAfterDecimal: 3 }}});
-  assertEquals(["0.006","0.007","0.008"], Util.getXLabels());
+  assert.deepEqual(["0.006","0.007","0.008"], Util.getXLabels());
   g.updateOptions({ axes: { x: { digitsAfterDecimal: 4 }}});
-  assertEquals(["0.006","0.007","0.008"], Util.getXLabels());
+  assert.deepEqual(["0.006","0.007","0.008"], Util.getXLabels());
   g.updateOptions({ axes: { x: { digitsAfterDecimal: 5 }}});
-  assertEquals(["0.006","0.007","0.008"], Util.getXLabels());
+  assert.deepEqual(["0.006","0.007","0.008"], Util.getXLabels());
   g.updateOptions({ axes: { x: { digitsAfterDecimal: null }}});
-  assertEquals(["0.006","0.007","0.008"], Util.getXLabels());
-}
+  assert.deepEqual(["0.006","0.007","0.008"], Util.getXLabels());
+});
 
 /**
  * This test shows you can override digitsAfterDecimal on the axis level.
  */
-AxisLabelsTestCase.prototype.testMaxNumberWidthPerAxis = function() {
+it('testMaxNumberWidthPerAxis', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "x,a,b\n" +
@@ -954,33 +953,33 @@ AxisLabelsTestCase.prototype.testMaxNumberWidthPerAxis = function() {
       });
 
   g.updateOptions({ axes: { y: { maxNumberWidth: 4 }}});
-  assertEquals(["1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4"] , Util.getYLabels(1));
+  assert.deepEqual(["1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4"] , Util.getYLabels(1));
   g.updateOptions({ axes: { y: { maxNumberWidth: 5 }}});
-  assertEquals(["12601","12601.5","12602","12602.5","12603","12603.5","12604"] , Util.getYLabels(1));
+  assert.deepEqual(["12601","12601.5","12602","12602.5","12603","12603.5","12604"] , Util.getYLabels(1));
   g.updateOptions({ axes: { y: { maxNumberWidth: null }}});
-  assertEquals(["1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4"] , Util.getYLabels(1));
+  assert.deepEqual(["1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4","1.26e+4"] , Util.getYLabels(1));
 
   g.updateOptions({ axes: { y2: { maxNumberWidth: 4 }}});
-  assertEquals(["1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4"], Util.getYLabels(2));
+  assert.deepEqual(["1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4"], Util.getYLabels(2));
   g.updateOptions({ axes: { y2: { maxNumberWidth: 5 }}});
-  assertEquals(["12801","12801.5","12802","12802.5","12803","12803.5","12804"], Util.getYLabels(2));
+  assert.deepEqual(["12801","12801.5","12802","12802.5","12803","12803.5","12804"], Util.getYLabels(2));
   g.updateOptions({ axes: { y2: { maxNumberWidth: null }}});
-  assertEquals(["1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4"], Util.getYLabels(2));
+  assert.deepEqual(["1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4","1.28e+4"], Util.getYLabels(2));
 
   // maxNumberWidth is ignored for the x-axis.
   // BUG : https://code.google.com/p/dygraphs/issues/detail?id=488
   g.updateOptions({ axes: { x: { maxNumberWidth: 4 }}});
-  assertEquals(["12401","12402","12403"], Util.getXLabels());
+  assert.deepEqual(["12401","12402","12403"], Util.getXLabels());
   g.updateOptions({ axes: { x: { maxNumberWidth: 5 }}});
-  assertEquals(["12401","12402","12403"], Util.getXLabels());
+  assert.deepEqual(["12401","12402","12403"], Util.getXLabels());
   g.updateOptions({ axes: { x: { maxNumberWidth: null }}});
-  assertEquals(["12401","12402","12403"], Util.getXLabels());
-}
+  assert.deepEqual(["12401","12402","12403"], Util.getXLabels());
+});
 
 /*
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=147
 // Checks that axis labels stay sane across a DST change.
-AxisLabelsTestCase.prototype.testLabelsCrossDstChange = function() {
+it('testLabelsCrossDstChange', function() {
   // (From tests/daylight-savings.html)
   var g = new Dygraph(
       document.getElementById("graph"),
@@ -1007,7 +1006,7 @@ AxisLabelsTestCase.prototype.testLabelsCrossDstChange = function() {
 
   var xLabels = Util.getXLabels();
   for (var i = 0; i < xLabels.length; i++) {
-    assertTrue(okLabels[xLabels[i]]);
+    assert.isTrue(okLabels[xLabels[i]]);
   }
 
   // This range had issues of its own on tests/daylight-savings.html.
@@ -1016,14 +1015,14 @@ AxisLabelsTestCase.prototype.testLabelsCrossDstChange = function() {
   });
   xLabels = Util.getXLabels();
   for (var i = 0; i < xLabels.length; i++) {
-    assertTrue(okLabels[xLabels[i]]);
+    assert.isTrue(okLabels[xLabels[i]]);
   }
-};
+});
 
 
 // Tests data which crosses a "fall back" at a high enough frequency that you
 // can see both 1:00 A.M.s.
-AxisLabelsTestCase.prototype.testLabelsCrossDstChangeHighFreq = function() {
+it('testLabelsCrossDstChangeHighFreq', function() {
   // Generate data which crosses the EST/EDT boundary.
   var dst_data = [];
   var base_ms = 1383454200000;
@@ -1037,7 +1036,7 @@ AxisLabelsTestCase.prototype.testLabelsCrossDstChangeHighFreq = function() {
       { width: 1024, labels: ['Date', 'Value'] }
       );
 
-  assertEquals([
+  assert.deepEqual([
     '00:50', '00:55',
     '01:00', '01:05', '01:10', '01:15', '01:20', '01:25',
     '01:30', '01:35', '01:40', '01:45', '01:50', '01:55',
@@ -1048,17 +1047,17 @@ AxisLabelsTestCase.prototype.testLabelsCrossDstChangeHighFreq = function() {
   g.updateOptions({
     dateWindow: [1383454200000 + 15*60*1000, g.xAxisExtremes()[1]]}
   );
-  assertEquals([
+  assert.deepEqual([
     '01:05', '01:10', '01:15', '01:20', '01:25',
     '01:30', '01:35', '01:40', '01:45', '01:50', '01:55',
     '01:00', '01:05'  // 1 AM number two!
   ], Util.getXLabels());
-};
+});
 
 
 // Tests data which crosses a "spring forward" at a low frequency.
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=433
-AxisLabelsTestCase.prototype.testLabelsCrossSpringForward = function() {
+it('testLabelsCrossSpringForward', function() {
   var g = new Dygraph(
       document.getElementById("graph"),
       "Date/Time,Purchases\n" +
@@ -1090,11 +1089,11 @@ AxisLabelsTestCase.prototype.testLabelsCrossSpringForward = function() {
 
   var xLabels = Util.getXLabels();
   for (var i = 0; i < xLabels.length; i++) {
-    assertTrue(okLabels[xLabels[i]]);
+    assert.isTrue(okLabels[xLabels[i]]);
   }
-};
+});
 
-AxisLabelsTestCase.prototype.testLabelsCrossSpringForwardHighFreq = function() {
+it('testLabelsCrossSpringForwardHighFreq', function() {
   var base_ms_spring = 1299999000000;
   var dst_data_spring = [];
   for (var x = base_ms_spring; x < base_ms_spring + 1000 * 60 * 80; x += 1000) {
@@ -1107,11 +1106,13 @@ AxisLabelsTestCase.prototype.testLabelsCrossSpringForwardHighFreq = function() {
       { width: 1024, labels: ['Date', 'Value'] }
   );
 
-  assertEquals([
+  assert.deepEqual([
     '01:50', '01:55',
     '03:00', '03:05', '03:10', '03:15', '03:20', '03:25',
     '03:30', '03:35', '03:40', '03:45', '03:50', '03:55',
     '04:00', '04:05'
   ], Util.getXLabels());
-};
+});
 */
+
+});
index 5fe1739..b10cf16 100644 (file)
@@ -4,21 +4,23 @@
  * @author uemit.seren@gmail.com (Ãœmit Seren)
  */
 
-var CallbackTestCase = TestCase("callback");
+describe("callback", function() {
 
-CallbackTestCase.prototype.setUp = function() {
+var xhr, styleSheet;
+
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div><div id='selection'></div>";
-  this.xhr = XMLHttpRequest;
-  this.styleSheet = document.createElement("style");
-  this.styleSheet.type = "text/css";
-  document.getElementsByTagName("head")[0].appendChild(this.styleSheet);
-};
+  xhr = XMLHttpRequest;
+  styleSheet = document.createElement("style");
+  styleSheet.type = "text/css";
+  document.getElementsByTagName("head")[0].appendChild(styleSheet);
+});
 
-CallbackTestCase.prototype.tearDown = function() {
-  XMLHttpRequest = this.xhr;
-};
+afterEach(function() {
+  window.XMLHttpRequest = xhr;
+});
 
-var data = "X,a\,b,c\n" +
+var data = "X,a,b,c\n" +
  "10,-1,1,2\n" +
  "11,0,3,1\n" +
  "12,1,4,2\n" +
@@ -30,12 +32,12 @@ var data = "X,a\,b,c\n" +
  * is properly called when the  first series is hidden (setVisibility = false)
  *
  */
-CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
+it('testHighlightCallbackIsCalled', function() {
   var h_row;
   var h_pts;
 
   var highlightCallback = function(e, x, pts, row) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     h_row = row;
     h_pts = pts;
   };
@@ -52,20 +54,20 @@ CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
   DygraphOps.dispatchMouseMove(g, 13, 10);
 
   //check correct row is returned
-  assertEquals(3, h_row);
+  assert.equal(3, h_row);
   //check there are only two points (because first series is hidden)
-  assertEquals(2, h_pts.length);
-};
+  assert.equal(2, h_pts.length);
+});
 
 
 /**
  * Test that drawPointCallback isn't called when drawPoints is false
  */
-CallbackTestCase.prototype.testDrawPointCallback_disabled = function() {
+it('testDrawPointCallback_disabled', function() {
   var called = false;
 
   var callback = function() {
-    assertEquals(g, this);
+    assert.equal(g, this);
     called = true;
   };
 
@@ -74,13 +76,13 @@ CallbackTestCase.prototype.testDrawPointCallback_disabled = function() {
       drawPointCallback: callback,
     });
 
-  assertFalse(called);
-};
+  assert.isFalse(called);
+});
 
 /**
  * Test that drawPointCallback is called when drawPoints is true
  */
-CallbackTestCase.prototype.testDrawPointCallback_enabled = function() {
+it('testDrawPointCallback_enabled', function() {
   var called = false;
   var callbackThis = null;
 
@@ -95,19 +97,19 @@ CallbackTestCase.prototype.testDrawPointCallback_enabled = function() {
       drawPointCallback: callback
     });
 
-  assertTrue(called);
-  assertEquals(g, callbackThis);
-};
+  assert.isTrue(called);
+  assert.equal(g, callbackThis);
+});
 
 /**
  * Test that drawPointCallback is called when drawPoints is true
  */
-CallbackTestCase.prototype.testDrawPointCallback_pointSize = function() {
+it('testDrawPointCallback_pointSize', function() {
   var pointSize = 0;
   var count = 0;
 
   var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     pointSize = pointSizeParam;
     count++;
   };
@@ -118,8 +120,8 @@ CallbackTestCase.prototype.testDrawPointCallback_pointSize = function() {
       drawPointCallback: callback
     });
 
-  assertEquals(1.5, pointSize);
-  assertEquals(12, count); // one call per data point.
+  assert.equal(1.5, pointSize);
+  assert.equal(12, count); // one call per data point.
 
   var g = new Dygraph(graph, data, {
       drawPoints: true,
@@ -127,19 +129,19 @@ CallbackTestCase.prototype.testDrawPointCallback_pointSize = function() {
       pointSize: 8
     });
 
-  assertEquals(8, pointSize);
-};
+  assert.equal(8, pointSize);
+});
 
 /**
  * Test that drawPointCallback is called for isolated points when
  * drawPoints is false, and also for gap points if that's enabled.
  */
-CallbackTestCase.prototype.testDrawPointCallback_isolated = function() {
+it('testDrawPointCallback_isolated', function() {
   var xvalues = [];
 
   var g;
   var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     var dx = g.toDataXCoord(cx);
     xvalues.push(dx);
     Dygraph.Circles.DEFAULT.apply(this, arguments);
@@ -157,9 +159,9 @@ CallbackTestCase.prototype.testDrawPointCallback_isolated = function() {
 
   // Test that isolated points get drawn
   g = new Dygraph(graph, testdata, graphOpts);
-  assertEquals(2, xvalues.length);
-  assertEquals(13, xvalues[0]);
-  assertEquals(15, xvalues[1]);
+  assert.equal(2, xvalues.length);
+  assert.equal(13, xvalues[0]);
+  assert.equal(15, xvalues[1]);
 
   // Test that isolated points + gap points get drawn when
   // drawGapEdgePoints is set.  This should add one point at the right
@@ -167,22 +169,22 @@ CallbackTestCase.prototype.testDrawPointCallback_isolated = function() {
   xvalues = []; // Reset for new test
   graphOpts.drawGapEdgePoints = true;
   g = new Dygraph(graph, testdata, graphOpts);
-  assertEquals(3, xvalues.length);
-  assertEquals(11, xvalues[0]);
-  assertEquals(13, xvalues[1]);
-  assertEquals(15, xvalues[2]);
-};
+  assert.equal(3, xvalues.length);
+  assert.equal(11, xvalues[0]);
+  assert.equal(13, xvalues[1]);
+  assert.equal(15, xvalues[2]);
+});
 
 /**
  * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
  * is properly called when the first series is hidden (setVisibility = false)
  *
  */
-CallbackTestCase.prototype.testDrawHighlightPointCallbackIsCalled = function() {
+it('testDrawHighlightPointCallbackIsCalled', function() {
   var called = false;
 
   var drawHighlightPointCallback = function() {
-    assertEquals(g, this);
+    assert.equal(g, this);
     called = true;
   };
 
@@ -194,10 +196,10 @@ CallbackTestCase.prototype.testDrawHighlightPointCallbackIsCalled = function() {
         drawHighlightPointCallback: drawHighlightPointCallback
       });
 
-  assertFalse(called);
+  assert.isFalse(called);
   DygraphOps.dispatchMouseMove(g, 13, 10);
-  assertTrue(called);
-};
+  assert.isTrue(called);
+});
 
 /**
  * Test the closest-series highlighting methods for normal and stacked modes.
@@ -228,7 +230,7 @@ var runClosestTest = function(isStacked, widthNormal, widthHighlighted) {
       });
 
   var highlightCallback = function(e, x, pts, row, set) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     h_row = row;
     h_pts = pts;
     h_series = set;
@@ -239,27 +241,27 @@ var runClosestTest = function(isStacked, widthNormal, widthHighlighted) {
 
   if (isStacked) {
     DygraphOps.dispatchMouseMove(g, 11.45, 1.4);
-    assertEquals(1, h_row);
-    assertEquals('c', h_series);
+    assert.equal(1, h_row);
+    assert.equal('c', h_series);
 
     //now move up in the same row
     DygraphOps.dispatchMouseMove(g, 11.45, 1.5);
-    assertEquals(1, h_row);
-    assertEquals('b', h_series);
+    assert.equal(1, h_row);
+    assert.equal('b', h_series);
 
     //and a bit to the right
     DygraphOps.dispatchMouseMove(g, 11.55, 1.5);
-    assertEquals(2, h_row);
-    assertEquals('c', h_series);
+    assert.equal(2, h_row);
+    assert.equal('c', h_series);
   } else {
     DygraphOps.dispatchMouseMove(g, 11, 1.5);
-    assertEquals(1, h_row);
-    assertEquals('c', h_series);
+    assert.equal(1, h_row);
+    assert.equal('c', h_series);
 
     //now move up in the same row
     DygraphOps.dispatchMouseMove(g, 11, 2.5);
-    assertEquals(1, h_row);
-    assertEquals('b', h_series);
+    assert.equal(1, h_row);
+    assert.equal('b', h_series);
   }
 
   return g;
@@ -268,70 +270,70 @@ var runClosestTest = function(isStacked, widthNormal, widthHighlighted) {
 /**
  * Test basic closest-point highlighting.
  */
-CallbackTestCase.prototype.testClosestPointCallback = function() {
+it('testClosestPointCallback', function() {
   runClosestTest(false, 1, 3);
-}
+});
 
 /**
  * Test setSelection() with series name
  */
-CallbackTestCase.prototype.testSetSelection = function() {
+it('testSetSelection', function() {
   var g = runClosestTest(false, 1, 3);
-  assertEquals(1, g.attr_('strokeWidth', 'c'));
+  assert.equal(1, g.attr_('strokeWidth', 'c'));
   g.setSelection(false, 'c');
-  assertEquals(3, g.attr_('strokeWidth', 'c'));
-}
+  assert.equal(3, g.attr_('strokeWidth', 'c'));
+});
 
 /**
  * Test closest-point highlighting for stacked graph
  */
-CallbackTestCase.prototype.testClosestPointStackedCallback = function() {
+it('testClosestPointStackedCallback', function() {
   runClosestTest(true, 1, 3);
-}
+});
 
 /**
  * Closest-point highlighting with legend CSS - border around active series.
  */
-CallbackTestCase.prototype.testClosestPointCallbackCss1 = function() {
+it('testClosestPointCallbackCss1', function() {
   var css = "div.dygraph-legend > span { display: block; }\n" +
       "div.dygraph-legend > span.highlight { border: 1px solid grey; }\n";
-  this.styleSheet.innerHTML = css;
+  styleSheet.innerHTML = css;
   runClosestTest(false, 2, 4);
-  this.styleSheet.innerHTML = '';
-}
+  styleSheet.innerHTML = '';
+});
 
 /**
  * Closest-point highlighting with legend CSS - show only closest series.
  */
-CallbackTestCase.prototype.testClosestPointCallbackCss2 = function() {
+it('testClosestPointCallbackCss2', function() {
   var css = "div.dygraph-legend > span { display: none; }\n" +
       "div.dygraph-legend > span.highlight { display: inline; }\n";
-  this.styleSheet.innerHTML = css;
+  styleSheet.innerHTML = css;
   runClosestTest(false, 10, 15);
-  this.styleSheet.innerHTML = '';
+  styleSheet.innerHTML = '';
   // TODO(klausw): verify that the highlighted line is drawn on top?
-}
+});
 
 /**
  * Closest-point highlighting with locked series.
  */
-CallbackTestCase.prototype.testSetSelectionLocking = function() {
+it('testSetSelectionLocking', function() {
   var g = runClosestTest(false, 2, 4);
 
   // Default behavior, 'b' is closest
   DygraphOps.dispatchMouseMove(g, 11, 4);
-  assertEquals('b', g.getHighlightSeries());
+  assert.equal('b', g.getHighlightSeries());
 
   // Now lock selection to 'c'
   g.setSelection(false, 'c', true);
   DygraphOps.dispatchMouseMove(g, 11, 4);
-  assertEquals('c', g.getHighlightSeries());
+  assert.equal('c', g.getHighlightSeries());
 
   // Unlock, should be back to 'b'
   g.clearSelection();
   DygraphOps.dispatchMouseMove(g, 11, 4);
-  assertEquals('b', g.getHighlightSeries());
-}
+  assert.equal('b', g.getHighlightSeries());
+});
 
 /**
  * This tests that closest point searches work for data containing NaNs.
@@ -339,7 +341,7 @@ CallbackTestCase.prototype.testSetSelectionLocking = function() {
  * It's intended to catch a regression where a NaN Y value confuses the
  * closest-point algorithm, treating it as closer as any previous point.
  */
-CallbackTestCase.prototype.testNaNData = function() {
+it('testNaNData', function() {
   var dataNaN = [
     [9, -1, NaN, NaN],
     [10, -1, 1, 2],
@@ -352,7 +354,7 @@ CallbackTestCase.prototype.testNaNData = function() {
   var h_pts;
 
   var highlightCallback = function(e, x, pts, row) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     h_row = row;
     h_pts = pts;
   };
@@ -369,25 +371,25 @@ CallbackTestCase.prototype.testNaNData = function() {
 
   DygraphOps.dispatchMouseMove(g, 10.1, 0.9);
   //check correct row is returned
-  assertEquals(1, h_row);
+  assert.equal(1, h_row);
 
   // Explicitly test closest point algorithms
   var dom = g.toDomCoords(10.1, 0.9);
-  assertEquals(1, g.findClosestRow(dom[0]));
+  assert.equal(1, g.findClosestRow(dom[0]));
 
   var res = g.findClosestPoint(dom[0], dom[1]);
-  assertEquals(1, res.row);
-  assertEquals('b', res.seriesName);
+  assert.equal(1, res.row);
+  assert.equal('b', res.seriesName);
 
   res = g.findStackedPoint(dom[0], dom[1]);
-  assertEquals(1, res.row);
-  assertEquals('c', res.seriesName);
-};
+  assert.equal(1, res.row);
+  assert.equal('c', res.seriesName);
+});
 
 /**
  * This tests that stacked point searches work for data containing NaNs.
  */
-CallbackTestCase.prototype.testNaNDataStack = function() {
+it('testNaNDataStack', function() {
   var dataNaN = [
     [9, -1, NaN, NaN],
     [10, -1, 1, 2],
@@ -405,7 +407,7 @@ CallbackTestCase.prototype.testNaNDataStack = function() {
   var h_pts;
 
   var highlightCallback = function(e, x, pts, row) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     h_row = row;
     h_pts = pts;
   };
@@ -423,40 +425,40 @@ CallbackTestCase.prototype.testNaNDataStack = function() {
 
   DygraphOps.dispatchMouseMove(g, 10.1, 0.9);
   //check correct row is returned
-  assertEquals(1, h_row);
+  assert.equal(1, h_row);
 
   // Explicitly test stacked point algorithm.
   var dom = g.toDomCoords(10.1, 0.9);
   var res = g.findStackedPoint(dom[0], dom[1]);
-  assertEquals(1, res.row);
-  assertEquals('c', res.seriesName);
+  assert.equal(1, res.row);
+  assert.equal('c', res.seriesName);
 
   // All-NaN area at left, should get no points.
   dom = g.toDomCoords(9.1, 0.9);
   res = g.findStackedPoint(dom[0], dom[1]);
-  assertEquals(0, res.row);
-  assertEquals(undefined, res.seriesName);
+  assert.equal(0, res.row);
+  assert.equal(undefined, res.seriesName);
 
   // First gap, get 'c' since it's non-NaN.
   dom = g.toDomCoords(12.1, 0.9);
   res = g.findStackedPoint(dom[0], dom[1]);
-  assertEquals(3, res.row);
-  assertEquals('c', res.seriesName);
+  assert.equal(3, res.row);
+  assert.equal('c', res.seriesName);
 
   // Second gap, get 'b' since 'c' is NaN.
   dom = g.toDomCoords(15.1, 0.9);
   res = g.findStackedPoint(dom[0], dom[1]);
-  assertEquals(6, res.row);
-  assertEquals('b', res.seriesName);
+  assert.equal(6, res.row);
+  assert.equal('b', res.seriesName);
 
   // Isolated points should work, finding series b in this case.
   dom = g.toDomCoords(15.9, 3.1);
   res = g.findStackedPoint(dom[0], dom[1]);
-  assertEquals(7, res.row);
-  assertEquals('b', res.seriesName);
-};
+  assert.equal(7, res.row);
+  assert.equal('b', res.seriesName);
+});
 
-CallbackTestCase.prototype.testGapHighlight = function() {
+it('testGapHighlight', function() {
   var dataGap = [
     [1, null, 3],
     [2, 2, null],
@@ -471,7 +473,7 @@ CallbackTestCase.prototype.testGapHighlight = function() {
   var h_pts;
 
   var highlightCallback = function(e, x, pts, row) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     h_row = row;
     h_pts = pts;
   };
@@ -489,26 +491,26 @@ CallbackTestCase.prototype.testGapHighlight = function() {
 
   DygraphOps.dispatchMouseMove(g, 1.1, 10);
   //point from series B
-  assertEquals(0, h_row);
-  assertEquals(1, h_pts.length);
-  assertEquals(3, h_pts[0].yval);
-  assertEquals('B', h_pts[0].name);
+  assert.equal(0, h_row);
+  assert.equal(1, h_pts.length);
+  assert.equal(3, h_pts[0].yval);
+  assert.equal('B', h_pts[0].name);
 
   DygraphOps.dispatchMouseMove(g, 6.1, 10);
   // A is NaN at x=6
-  assertEquals(1, h_pts.length);
+  assert.equal(1, h_pts.length);
   assert(isNaN(h_pts[0].yval));
-  assertEquals('A', h_pts[0].name);
+  assert.equal('A', h_pts[0].name);
 
   DygraphOps.dispatchMouseMove(g, 8.1, 10);
   //point from series A
-  assertEquals(6, h_row);
-  assertEquals(1, h_pts.length);
-  assertEquals(8, h_pts[0].yval);
-  assertEquals('A', h_pts[0].name);
-};
+  assert.equal(6, h_row);
+  assert.equal(1, h_pts.length);
+  assert.equal(8, h_pts[0].yval);
+  assert.equal('A', h_pts[0].name);
+});
 
-CallbackTestCase.prototype.testFailedResponse = function() {
+it('testFailedResponse', function() {
 
   // Fake out the XMLHttpRequest so it doesn't do anything.
   XMLHttpRequest = function () {};
@@ -516,7 +518,7 @@ CallbackTestCase.prototype.testFailedResponse = function() {
   XMLHttpRequest.prototype.send = function () {};
 
   var highlightCallback = function(e, x, pts, row) {
-    fail("should not reach here");
+    throw "should not reach here";
   };
 
   var graph = document.getElementById("graph");
@@ -537,15 +539,15 @@ CallbackTestCase.prototype.testFailedResponse = function() {
 
   DygraphOps.dispatchMouseOut_Point(g, 800, 800); // This call should not throw an exception.
 
-  assertFalse("exception thrown during mouseout", failed);
-};
+  assert.isFalse(failed, "exception thrown during mouseout");
+});
 
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=355 
-CallbackTestCase.prototype.testHighlightCallbackRow = function() {
+it('testHighlightCallbackRow', function() {
   var highlightRow;
   var highlightCallback = function(e, x, pts, row) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     highlightRow = row;
   };
 
@@ -566,35 +568,35 @@ CallbackTestCase.prototype.testHighlightCallbackRow = function() {
   // Mouse over each of the points
   DygraphOps.dispatchMouseOver_Point(g, 0, 0);
   DygraphOps.dispatchMouseMove_Point(g, 0, 0);
-  assertEquals(0, highlightRow);
+  assert.equal(0, highlightRow);
   DygraphOps.dispatchMouseMove_Point(g, 100, 0);
-  assertEquals(1, highlightRow);
+  assert.equal(1, highlightRow);
   DygraphOps.dispatchMouseMove_Point(g, 200, 0);
-  assertEquals(2, highlightRow);
+  assert.equal(2, highlightRow);
   DygraphOps.dispatchMouseMove_Point(g, 300, 0);
-  assertEquals(3, highlightRow);
+  assert.equal(3, highlightRow);
   DygraphOps.dispatchMouseMove_Point(g, 400, 0);
-  assertEquals(4, highlightRow);
+  assert.equal(4, highlightRow);
 
   // Now zoom and verify that the row numbers still refer to rows in the data
   // array.
   g.updateOptions({dateWindow: [2, 4]});
   DygraphOps.dispatchMouseOver_Point(g, 0, 0);
   DygraphOps.dispatchMouseMove_Point(g, 0, 0);
-  assertEquals(2, highlightRow);
-  assertEquals('2: Y: 3 Z: 4', Util.getLegend());
-};
+  assert.equal(2, highlightRow);
+  assert.equal('2: Y: 3 Z: 4', Util.getLegend());
+});
 
 /**
  * Test that underlay callback is called even when there are no series,
  * and that the y axis ranges are not NaN.
  */
-CallbackTestCase.prototype.underlayCallback_noSeries = function() {
+it('testUnderlayCallback_noSeries', function() {
   var called = false;
   var yMin, yMax;
 
   var callback = function(canvas, area, g) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     called = true;
     yMin = g.yAxisRange(0)[0];
     yMax = g.yAxisRange(0)[1];
@@ -605,20 +607,20 @@ CallbackTestCase.prototype.underlayCallback_noSeries = function() {
       underlayCallback: callback
     });
 
-  assertTrue(called);
-  assertFalse(isNaN(yMin));
-  assertFalse(isNaN(yMax));
-};
+  assert.isTrue(called);
+  assert.isFalse(isNaN(yMin));
+  assert.isFalse(isNaN(yMax));
+});
 
 /**
  * Test that underlay callback receives the correct y-axis range.
  */
-CallbackTestCase.prototype.underlayCallback_yAxisRange = function() {
+it('testUnderlayCallback_yAxisRange', function() {
   var called = false;
   var yMin, yMax;
 
   var callback = function(canvas, area, g) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     yMin = g.yAxisRange(0)[0];
     yMax = g.yAxisRange(0)[1];
   };
@@ -629,19 +631,19 @@ CallbackTestCase.prototype.underlayCallback_yAxisRange = function() {
       underlayCallback: callback
     });
 
-  assertEquals(0, yMin);
-  assertEquals(10, yMax);
-};
+  assert.equal(0, yMin);
+  assert.equal(10, yMax);
+});
 
 /**
  * Test that drawPointCallback is called for isolated points and correct idx for the point is returned.
  */
-CallbackTestCase.prototype.testDrawPointCallback_idx = function() {
+it('testDrawPointCallback_idx', function() {
   var indices = [];
 
   var g;
   var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam,idx) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     indices.push(idx);
     Dygraph.Circles.DEFAULT.apply(this, arguments);
   };
@@ -659,8 +661,8 @@ CallbackTestCase.prototype.testDrawPointCallback_idx = function() {
 
   // Test that correct idx for isolated points are passed to the callback.
   g = new Dygraph(graph, testdata, graphOpts);
-  assertEquals(2, indices.length);
-  assertEquals([3, 5],indices);
+  assert.equal(2, indices.length);
+  assert.deepEqual([3, 5],indices);
 
   // Test that correct indices for isolated points + gap points are passed to the callback when
   // drawGapEdgePoints is set.  This should add one point at the right
@@ -668,8 +670,8 @@ CallbackTestCase.prototype.testDrawPointCallback_idx = function() {
   indices = []; // Reset for new test
   graphOpts.drawGapEdgePoints = true;
   g = new Dygraph(graph, testdata, graphOpts);
-  assertEquals(3, indices.length);
-  assertEquals([1, 3, 5],indices);
+  assert.equal(3, indices.length);
+  assert.deepEqual([1, 3, 5],indices);
 
 
   //Test that correct indices are passed to the callback when zoomed in.
@@ -678,18 +680,18 @@ CallbackTestCase.prototype.testDrawPointCallback_idx = function() {
   graphOpts.drawPoints = true;
   testdata = [[10, 2], [11, 3], [12, 4], [13, 2], [14, 5], [15, 3]];
   g = new Dygraph(graph, testdata, graphOpts);
-  assertEquals(3, indices.length);
-  assertEquals([2, 3, 4],indices);
-};
+  assert.equal(3, indices.length);
+  assert.deepEqual([2, 3, 4],indices);
+});
 
 /**
  * Test that the correct idx is returned for the point in the onHiglightCallback.
   */
-CallbackTestCase.prototype.testDrawHighlightPointCallback_idx = function() {
+it('testDrawHighlightPointCallback_idx', function() {
   var idxToCheck = null;
 
   var drawHighlightPointCallback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam,idx) {
-    assertEquals(g, this);
+    assert.equal(g, this);
     idxToCheck = idx;
   };
   var testdata = [[1, 2], [2, 3], [3, NaN], [4, 2], [5, NaN], [6, 3]];
@@ -699,13 +701,15 @@ CallbackTestCase.prototype.testDrawHighlightPointCallback_idx = function() {
           drawHighlightPointCallback : drawHighlightPointCallback
       });
 
-  assertNull(idxToCheck);
+  assert.isNull(idxToCheck);
   DygraphOps.dispatchMouseMove(g, 3, 0);
   // check that NaN point is not highlighted
-  assertNull(idxToCheck);
+  assert.isNull(idxToCheck);
   DygraphOps.dispatchMouseMove(g, 1, 2);
   // check that correct index is returned
-  assertEquals(0,idxToCheck);
+  assert.equal(0,idxToCheck);
   DygraphOps.dispatchMouseMove(g, 6, 3);
-  assertEquals(5,idxToCheck);
-};
+  assert.equal(5,idxToCheck);
+});
+
+});
index bec5a52..2636b41 100644 (file)
@@ -3,26 +3,26 @@
  *
  * @author julian.eichstaedt@ch.sauter-bc.com (Fr. Sauter AG)
  */
-var ConnectSeparatedPointsTestCase = TestCase("connect-separated-points");
+describe("connect-separated-points", function() {
 
-ConnectSeparatedPointsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-ConnectSeparatedPointsTestCase.origFunc = Dygraph.getContext;
+var origFunc = Dygraph.getContext;
 
-ConnectSeparatedPointsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(ConnectSeparatedPointsTestCase.origFunc(canvas));
+    return new Proxy(origFunc(canvas));
   };
-};
+});
 
-ConnectSeparatedPointsTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = ConnectSeparatedPointsTestCase.origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = origFunc;
+});
 
-ConnectSeparatedPointsTestCase.prototype.testEdgePointsSimple = function() {
+it('testEdgePointsSimple', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -47,7 +47,7 @@ ConnectSeparatedPointsTestCase.prototype.testEdgePointsSimple = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
   
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -84,9 +84,9 @@ ConnectSeparatedPointsTestCase.prototype.testEdgePointsSimple = function() {
   // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly
   // even if the point is outside the visible range and only one series has a valid value for this point.
   CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs);
-};
+});
 
-ConnectSeparatedPointsTestCase.prototype.testEdgePointsCustomBars = function() {
+it('testEdgePointsCustomBars', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -113,7 +113,7 @@ ConnectSeparatedPointsTestCase.prototype.testEdgePointsCustomBars = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
   
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -213,9 +213,9 @@ ConnectSeparatedPointsTestCase.prototype.testEdgePointsCustomBars = function() {
   // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly
   // even if the point is outside the visible range and only one series has a valid value for this point.
   CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs);
-};
+});
 
-ConnectSeparatedPointsTestCase.prototype.testEdgePointsErrorBars = function() {
+it('testEdgePointsErrorBars', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -243,7 +243,7 @@ ConnectSeparatedPointsTestCase.prototype.testEdgePointsErrorBars = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
   
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -350,16 +350,16 @@ ConnectSeparatedPointsTestCase.prototype.testEdgePointsErrorBars = function() {
   // Check if both points are connected at the right edge of the canvas and if the option "connectSeparatedPoints" works properly
   // even if the point is outside the visible range and only one series has a valid value for this point.
   CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs);
-};
+});
 
-ConnectSeparatedPointsTestCase.prototype.testConnectSeparatedPointsPerSeries = function() {
+it('testConnectSeparatedPointsPerSeries', function() {
   var assertExpectedLinesDrawnPerSeries = function(htx, expectedSeries1, expectedSeries2, expectedSeries3) {
     var expected = [expectedSeries1, expectedSeries2, expectedSeries3];    
     var actual = [ 
         CanvasAssertions.numLinesDrawn(htx, "#ff0000"),
         CanvasAssertions.numLinesDrawn(htx, "#00ff00"),
         CanvasAssertions.numLinesDrawn(htx, "#0000ff")];
-    assertEquals(expected, actual);
+    assert.deepEqual(expected, actual);
   }
 
   var g = new Dygraph(document.getElementById("graph"),
@@ -378,7 +378,7 @@ ConnectSeparatedPointsTestCase.prototype.testConnectSeparatedPointsPerSeries = f
         colors: ["#ff0000", "#00ff00", "#0000ff"]
       });
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
   assertExpectedLinesDrawnPerSeries(htx, 4, 1, 2);
 
   Proxy.reset(htx);
@@ -405,9 +405,9 @@ ConnectSeparatedPointsTestCase.prototype.testConnectSeparatedPointsPerSeries = f
     }
   });
   assertExpectedLinesDrawnPerSeries(htx, 4, 3, 3);
-}
+});
 
-ConnectSeparatedPointsTestCase.prototype.testNaNErrorBars = function() {
+it('testNaNErrorBars', function() {
   var data = [
     [0,[1,2,3]],
     [1,[2,3,4]],
@@ -433,7 +433,7 @@ ConnectSeparatedPointsTestCase.prototype.testNaNErrorBars = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
   
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
   
@@ -445,5 +445,7 @@ ConnectSeparatedPointsTestCase.prototype.testNaNErrorBars = function() {
 
   // No line across the NaN gap, and a single line (not two)
   // across the null gap.
-  assertEquals(8, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
-};
+  assert.equal(8, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+});
+
+});
index b2b0010..89a0680 100644 (file)
@@ -4,62 +4,64 @@
  * @fileoverview Regression test based on some strange customBars data.
  * @author danvk@google.com (Dan Vanderkam)
  */
-var CssTestCase = TestCase("css");
+describe("css", function() {
 
-CssTestCase.data = "X,Y,Z\n1,2,3\n4,5,6\n";
+var data = "X,Y,Z\n1,2,3\n4,5,6\n";
 
-CssTestCase.prototype.setUp = function() {
+var styleSheet;
+
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-  this.styleSheet = document.createElement("style");
-  this.styleSheet.type = "text/css";
-  document.getElementsByTagName("head")[0].appendChild(this.styleSheet);
-};
+  styleSheet = document.createElement("style");
+  styleSheet.type = "text/css";
+  document.getElementsByTagName("head")[0].appendChild(styleSheet);
+});
 
-CssTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 // Verifies that an unstyled, unsized dygraph gets a default size.
-CssTestCase.prototype.testDefaultSize = function() {
+it('testDefaultSize', function() {
   var opts = {
   };
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, CssTestCase.data, opts);
+  var g = new Dygraph(graph, data, opts);
 
-  assertEquals(480, graph.offsetWidth);
-  assertEquals(320, graph.offsetHeight);
-  assertEquals({width: 480, height: 320}, g.size());
-};
+  assert.equal(480, graph.offsetWidth);
+  assert.equal(320, graph.offsetHeight);
+  assert.deepEqual({width: 480, height: 320}, g.size());
+});
 
 // Verifies that the width/height parameters work.
-CssTestCase.prototype.testExplicitParamSize = function() {
+it('testExplicitParamSize', function() {
   var opts = {
     width: 640,
     height: 480
   };
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, CssTestCase.data, opts);
+  var g = new Dygraph(graph, data, opts);
 
-  assertEquals(640, graph.offsetWidth);
-  assertEquals(480, graph.offsetHeight);
-  assertEquals({width: 640, height: 480}, g.size());
-};
+  assert.equal(640, graph.offsetWidth);
+  assert.equal(480, graph.offsetHeight);
+  assert.deepEqual({width: 640, height: 480}, g.size());
+});
 
 // Verifies that setting a style on the div works.
-CssTestCase.prototype.testExplicitStyleSize = function() {
+it('testExplicitStyleSize', function() {
   var opts = {
   };
   var graph = document.getElementById("graph");
   graph.style.width = '600px';
   graph.style.height = '400px';
 
-  var g = new Dygraph(graph, CssTestCase.data, opts);
-  assertEquals(600, graph.offsetWidth);
-  assertEquals(400, graph.offsetHeight);
-  assertEquals({width: 600, height: 400}, g.size());
-};
+  var g = new Dygraph(graph, data, opts);
+  assert.equal(600, graph.offsetWidth);
+  assert.equal(400, graph.offsetHeight);
+  assert.deepEqual({width: 600, height: 400}, g.size());
+});
 
 // Verifies that CSS pixel styles on the div trump explicit parameters.
-CssTestCase.prototype.testPixelStyleWins = function() {
+it('testPixelStyleWins', function() {
   var opts = {
     width: 987,
     height: 654
@@ -68,14 +70,14 @@ CssTestCase.prototype.testPixelStyleWins = function() {
   graph.style.width = '600px';
   graph.style.height = '400px';
 
-  var g = new Dygraph(graph, CssTestCase.data, opts);
-  assertEquals(600, graph.offsetWidth);
-  assertEquals(400, graph.offsetHeight);
-  assertEquals({width: 600, height: 400}, g.size());
-};
+  var g = new Dygraph(graph, data, opts);
+  assert.equal(600, graph.offsetWidth);
+  assert.equal(400, graph.offsetHeight);
+  assert.deepEqual({width: 600, height: 400}, g.size());
+});
 
 // Verifies that a CSS percentage size works.
-CssTestCase.prototype.testPercentageSize = function() {
+it('testPercentageSize', function() {
   document.body.innerHTML =
       '<div style="width: 600px; height: 400px;">' +
       '<div id="graph"></div></div>';
@@ -85,62 +87,62 @@ CssTestCase.prototype.testPercentageSize = function() {
   graph.style.width = '50%';
   graph.style.height = '50%';
 
-  var g = new Dygraph(graph, CssTestCase.data, opts);
-  assertEquals(300, graph.offsetWidth);
-  assertEquals(200, graph.offsetHeight);
-  assertEquals({width: 300, height: 200}, g.size());
-};
+  var g = new Dygraph(graph, data, opts);
+  assert.equal(300, graph.offsetWidth);
+  assert.equal(200, graph.offsetHeight);
+  assert.deepEqual({width: 300, height: 200}, g.size());
+});
 
 // Verifies that a CSS class size works.
-CssTestCase.prototype.testClassPixelSize = function() {
-  this.styleSheet.innerHTML = '.chart { width: 456px; height: 345px; }';
+it('testClassPixelSize', function() {
+  styleSheet.innerHTML = '.chart { width: 456px; height: 345px; }';
 
   var opts = {
   };
   var graph = document.getElementById("graph");
   graph.className = "chart";
-  var g = new Dygraph(graph, CssTestCase.data, opts);
-  assertEquals(456, graph.offsetWidth);
-  assertEquals(345, graph.offsetHeight);
-  assertEquals({width: 456, height: 345}, g.size());
-};
+  var g = new Dygraph(graph, data, opts);
+  assert.equal(456, graph.offsetWidth);
+  assert.equal(345, graph.offsetHeight);
+  assert.deepEqual({width: 456, height: 345}, g.size());
+});
 
 // An invisible chart div shouldn't produce an error.
-CssTestCase.prototype.testInvisibleChart = function() {
+it('testInvisibleChart', function() {
   document.body.innerHTML =
       '<div style="display:none;">' +
       '<div id="graph" style="width: 640px; height: 480px;"></div>' +
       '</div>';
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, CssTestCase.data, {});
-};
+  new Dygraph(graph, data, {});
+});
 
 // An invisible chart div shouldn't produce an error.
-CssTestCase.prototype.testInvisibleChartDate = function() {
+it('testInvisibleChartDate', function() {
   document.body.innerHTML =
       '<div style="display:none;">' +
       '<div id="graph" style="width: 640px; height: 480px;"></div>' +
       '</div>';
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph,
+  new Dygraph(graph,
                   "Date,Y\n" +
                   "2010/01/01,100\n" +
                   "2010/02/01,200\n" +
                   "2010/03/01,300\n" +
                   "2010/04/01,400\n" +
                   "2010/05/01,300\n" +
-                  "2010/06/01,100\n"
-                  {});
-};
+                  "2010/06/01,100\n",
+                  {});
+});
 
 // An invisible chart div that becomes visible.
-CssTestCase.prototype.testInvisibleThenVisibleChart = function() {
+it('testInvisibleThenVisibleChart', function() {
   document.body.innerHTML =
       '<div id="x" style="display:none;">' +
       '<div id="graph" style="width: 640px; height: 480px;"></div>' +
       '</div>';
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph,
+  var g = new Dygraph(graph,
                   "Date,Y\n" +
                   "2010/01/01,100\n" +
                   "2010/02/01,200\n" +
@@ -158,30 +160,32 @@ CssTestCase.prototype.testInvisibleThenVisibleChart = function() {
   // or visibility so we need to let it know ourselves.
   g.resize();
 
-  assertEquals(640, graph.offsetWidth);
-  assertEquals(480, graph.offsetHeight);
-  assertEquals({width: 640, height: 480}, g.size());
-};
+  assert.equal(640, graph.offsetWidth);
+  assert.equal(480, graph.offsetHeight);
+  assert.deepEqual({width: 640, height: 480}, g.size());
+});
 
 // Verifies that a div resize gets picked up.
 /*
   this one isn't quite ready yet.
-CssTestCase.prototype.testDivResize = function() {
+it('testDivResize', function() {
   var opts = {
   };
   var graph = document.getElementById("graph");
   graph.style.width = '640px';
   graph.style.height = '480px';
-  var g = new Dygraph(graph, CssTestCase.data, opts);
+  var g = new Dygraph(graph, data, opts);
 
-  assertEquals(640, graph.offsetWidth);
-  assertEquals(480, graph.offsetHeight);
-  assertEquals({width: 640, height: 480}, g.size());
+  assert.equal(640, graph.offsetWidth);
+  assert.equal(480, graph.offsetHeight);
+  assert.deepEqual({width: 640, height: 480}, g.size());
 
   graph.style.width = '650px';
   graph.style.height = '490px';
-  assertEquals(650, graph.offsetWidth);
-  assertEquals(490, graph.offsetHeight);
-  assertEquals({width: 650, height: 490}, g.size());
-};
+  assert.equal(650, graph.offsetWidth);
+  assert.equal(490, graph.offsetHeight);
+  assert.deepEqual({width: 650, height: 490}, g.size());
+});
 */
+
+});
diff --git a/auto_tests/tests/custom_asserts.js b/auto_tests/tests/custom_asserts.js
new file mode 100644 (file)
index 0000000..06e2444
--- /dev/null
@@ -0,0 +1,11 @@
+/**
+ * @fileoverview Assertions that chai doesn't provide out of the box.
+ */
+
+var assertDeepCloseTo = function(actualArray, expectedArray, epsilon) {
+  assert.isArray(actualArray);
+  assert.isArray(expectedArray);
+  for (var i = 0; i < actualArray.length; i++) {
+    assert.closeTo(actualArray[i], expectedArray[i], epsilon);
+  }
+};
index e8af214..523d93a 100644 (file)
@@ -4,22 +4,22 @@
  * @fileoverview Regression test based on some strange customBars data.
  * @author danvk@google.com (Dan Vanderkam)
  */
-var CustomBarsTestCase = TestCase("custom-bars");
+describe("custom-bars", function() {
 
-CustomBarsTestCase._origFunc = Dygraph.getContext;
-CustomBarsTestCase.prototype.setUp = function() {
+var _origFunc = Dygraph.getContext;
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(CustomBarsTestCase._origFunc(canvas));
+    return new Proxy(_origFunc(canvas));
   }
-};
+});
 
-CustomBarsTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = CustomBarsTestCase._origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = _origFunc;
+});
 
 // This test used to reliably produce an infinite loop.
-CustomBarsTestCase.prototype.testCustomBarsNoHang = function() {
+it('testCustomBarsNoHang', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -61,10 +61,10 @@ CustomBarsTestCase.prototype.testCustomBarsNoHang = function() {
     "35,,0;22437620;0\n";
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-};
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=201
-CustomBarsTestCase.prototype.testCustomBarsZero = function() {
+it('testCustomBarsZero', function() {
   var opts = {
     customBars: true
   };
@@ -77,12 +77,12 @@ CustomBarsTestCase.prototype.testCustomBarsZero = function() {
   var g = new Dygraph(graph, data, opts);
 
   var range = g.yAxisRange();
-  assertTrue('y-axis must include 0', range[0] <= 0);
-  assertTrue('y-axis must include 5', range[1] >= 5);
-};
+  assert.isTrue(range[0] <= 0, 'y-axis must include 0');
+  assert.isTrue(range[1] >= 5, 'y-axis must include 5');
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=229
-CustomBarsTestCase.prototype.testCustomBarsAtTop = function() {
+it('testCustomBarsAtTop', function() {
   var g = new Dygraph(document.getElementById("graph"),
       [
         [1, [10, 10, 100]],
@@ -115,11 +115,11 @@ CustomBarsTestCase.prototype.testCustomBarsAtTop = function() {
       });
 
   var sampler = new PixelSampler(g);
-  assertEquals([0, 255, 0, 38], sampler.colorAtCoordinate(5, 60));
-};
+  assert.deepEqual([0, 255, 0, 38], sampler.colorAtCoordinate(5, 60));
+});
 
 // Tests that custom bars work with log scale.
-CustomBarsTestCase.prototype.testCustomBarsLogScale = function() {
+it('testCustomBarsLogScale', function() {
   var g = new Dygraph(document.getElementById("graph"),
       [
         [1, [10, 10, 100]],
@@ -162,10 +162,9 @@ CustomBarsTestCase.prototype.testCustomBarsLogScale = function() {
        [495, 181.66450704318103],
        [247.5, 152.02209814465604]],
       { fillStyle: "#00ff00" });
-};
+});
 
-CustomBarsTestCase.prototype.testCustomBarsWithNegativeValuesInLogScale =
-    function() {
+it('testCustomBarsWithNegativeValuesInLogScale', function() {
   var graph = document.getElementById("graph");
 
   var count = 0;
@@ -186,10 +185,12 @@ CustomBarsTestCase.prototype.testCustomBarsWithNegativeValuesInLogScale =
       });
 
   // Normally all three points would be drawn.
-  assertEquals(3, count);
+  assert.equal(3, count);
   count = 0;
 
   // In log scale, the third point shouldn't be shown.
   g.updateOptions({ logscale : true });
-  assertEquals(2, count);
-};
+  assert.equal(2, count);
+});
+
+});
index f3180f5..4b0ebe0 100644 (file)
@@ -3,83 +3,85 @@
  *
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
-var dataApiTestCase = TestCase("data-api");
+describe("data-api", function() {
 
-dataApiTestCase.prototype.setUp = function() {
+var opts, graphDiv;
+
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-  this.opts = {
+  opts = {
     width: 480,
     height: 320
   };
 
-  this.graphDiv = document.getElementById("graph");
-};
+  graphDiv = document.getElementById("graph");
+});
 
-dataApiTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-dataApiTestCase.prototype.testBasicAccessors = function() {
-  var g = new Dygraph(this.graphDiv, temperature_data, this.opts);
+it('testBasicAccessors', function() {
+  var g = new Dygraph(graphDiv, temperature_data, opts);
 
-  assertEquals(365, g.numRows());
-  assertEquals(3, g.numColumns());
+  assert.equal(365, g.numRows());
+  assert.equal(3, g.numColumns());
 
   // 2007-01-01,62,39
-  assertEquals(62, g.getValue(0, 1));
-  assertEquals(39, g.getValue(0, 2));
+  assert.equal(62, g.getValue(0, 1));
+  assert.equal(39, g.getValue(0, 2));
 
   // 2007-12-31,57,42
-  assertEquals(57, g.getValue(364, 1));
-  assertEquals(42, g.getValue(364, 2));
-};
+  assert.equal(57, g.getValue(364, 1));
+  assert.equal(42, g.getValue(364, 2));
+});
 
 
-dataApiTestCase.prototype.testAccessorsCustomBars = function() {
-  var g = new Dygraph(this.graphDiv, data_temp_high_low, {
+it('testAccessorsCustomBars', function() {
+  var g = new Dygraph(graphDiv, data_temp_high_low, {
     customBars: true
   });
 
-  assertEquals(1070, g.numRows());
-  assertEquals(3, g.numColumns());
+  assert.equal(1070, g.numRows());
+  assert.equal(3, g.numColumns());
 
   // 2007-01-01,46;51;56,43;45;48
-  assertEquals([46, 51, 56], g.getValue(0, 1));
-  assertEquals([43, 45, 48], g.getValue(0, 2));
+  assert.deepEqual([46, 51, 56], g.getValue(0, 1));
+  assert.deepEqual([43, 45, 48], g.getValue(0, 2));
 
   // 2009-12-05,37;42;47  (i.e. missing second column)
-  assertEquals([37, 42, 47], g.getValue(1069, 1));
-  assertEquals([null, null, null], g.getValue(1069, 2));
-};
+  assert.deepEqual([37, 42, 47], g.getValue(1069, 1));
+  assert.deepEqual([null, null, null], g.getValue(1069, 2));
+});
 
 
 // Regression test for #554.
-dataApiTestCase.prototype.testGetRowForX = function() {
-  var g = new Dygraph(this.graphDiv, [
+it('testGetRowForX', function() {
+  var g = new Dygraph(graphDiv, [
     "x,y",
     "1,2",
     "3,4",
     "5,6",
     "7,8",
     "9,10"
-  ].join('\n'), this.opts);
-
-  assertEquals(null, g.getRowForX(0));
-  assertEquals(0, g.getRowForX(1));
-  assertEquals(null, g.getRowForX(2));
-  assertEquals(1, g.getRowForX(3));
-  assertEquals(null, g.getRowForX(4));
-  assertEquals(2, g.getRowForX(5));
-  assertEquals(null, g.getRowForX(6));
-  assertEquals(3, g.getRowForX(7));
-  assertEquals(null, g.getRowForX(8));
-  assertEquals(4, g.getRowForX(9));
-  assertEquals(null, g.getRowForX(10));
-};
+  ].join('\n'), opts);
+
+  assert.equal(null, g.getRowForX(0));
+  assert.equal(0, g.getRowForX(1));
+  assert.equal(null, g.getRowForX(2));
+  assert.equal(1, g.getRowForX(3));
+  assert.equal(null, g.getRowForX(4));
+  assert.equal(2, g.getRowForX(5));
+  assert.equal(null, g.getRowForX(6));
+  assert.equal(3, g.getRowForX(7));
+  assert.equal(null, g.getRowForX(8));
+  assert.equal(4, g.getRowForX(9));
+  assert.equal(null, g.getRowForX(10));
+});
 
 // If there are rows with identical x-values, getRowForX promises that it will
 // return the first one.
-dataApiTestCase.prototype.testGetRowForXDuplicates = function() {
-  var g = new Dygraph(this.graphDiv, [
+it('testGetRowForXDuplicates', function() {
+  var g = new Dygraph(graphDiv, [
     "x,y",
     "1,2",  // 0
     "1,4",  // 1
@@ -91,9 +93,11 @@ dataApiTestCase.prototype.testGetRowForXDuplicates = function() {
     "9,6",
     "9,8",
     "9,10"
-  ].join('\n'), this.opts);
+  ].join('\n'), opts);
+
+  assert.equal(0, g.getRowForX(1));
+  assert.equal(null, g.getRowForX(2));
+  assert.equal(5, g.getRowForX(9));
+});
 
-  assertEquals(0, g.getRowForX(1));
-  assertEquals(null, g.getRowForX(2));
-  assertEquals(5, g.getRowForX(9));
-};
+});
index 05a62c6..d15567b 100644 (file)
@@ -3,15 +3,15 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var dateFormatsTestCase = TestCase("date-formats");
+describe("date-formats", function() {
 
-dateFormatsTestCase.prototype.setUp = function() {
-};
+beforeEach(function() {
+});
 
-dateFormatsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-dateFormatsTestCase.prototype.testISO8601 = function() {
+it('testISO8601', function() {
   // Format: YYYY-MM-DDTHH:MM:SS.ddddddZ
   // The "Z" indicates UTC, so this test should pass regardless of the time
   // zone of the machine on which it is run.
@@ -19,11 +19,11 @@ dateFormatsTestCase.prototype.testISO8601 = function() {
   // Firefox <4 does not support this format:
   // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
   if (navigator.userAgent.indexOf("Firefox/3.5") == -1) {
-    assertEquals(946816496789, Dygraph.dateParser("2000-01-02T12:34:56.789012Z"));
+    assert.equal(946816496789, Dygraph.dateParser("2000-01-02T12:34:56.789012Z"));
   }
-};
+});
 
-dateFormatsTestCase.prototype.testHyphenatedDate = function() {
+it('testHyphenatedDate', function() {
   // Format: YYYY-MM-DD HH:MM
 
   // Midnight February 2, 2000, UTC
@@ -36,5 +36,7 @@ dateFormatsTestCase.prototype.testHyphenatedDate = function() {
             zp(d.getDate()) + ' ' +
             zp(d.getHours()) + ':' +
             zp(d.getMinutes());
-  assertEquals(Date.UTC(2000, 1, 2), Dygraph.dateParser(str));
-};
+  assert.equal(Date.UTC(2000, 1, 2), Dygraph.dateParser(str));
+});
+
+});
index 0c381c9..554105b 100644 (file)
@@ -6,13 +6,13 @@
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
 
-var DateTickerTestCase = TestCase("date-ticker-tests");
+describe("date-ticker-tests", function() {
 
-DateTickerTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-DateTickerTestCase.prototype.createOptionsViewForAxis = function(axis, dict) {
+var createOptionsViewForAxis = function(axis, dict) {
   return function (x) {
     if (dict && dict.hasOwnProperty(x)) {
       return dict[x];
@@ -37,9 +37,9 @@ function changeNbspToSpace(ticks) {
   }
 }
 
-DateTickerTestCase.prototype.testBasicDateTicker = function() {
+it('testBasicDateTicker', function() {
   var opts = {labelsUTC: true};
-  var options = this.createOptionsViewForAxis('x', opts);
+  var options = createOptionsViewForAxis('x', opts);
   
   var ticks = Dygraph.dateTicker(-1797534000000, 1255579200000, 800, options);
   var expected_ticks = [
@@ -53,7 +53,7 @@ DateTickerTestCase.prototype.testBasicDateTicker = function() {
       {"v": 631152000000, "label":"1990"},
       {"v": 946684800000, "label":"2000"}
   ];
-  assertEquals(expected_ticks, ticks);
+  assert.deepEqual(expected_ticks, ticks);
   
   var start = Date.UTC(1999, 11, 31, 14, 0, 0);
   var end = Date.UTC(2000,  0,  1, 12, 0, 0);
@@ -74,12 +74,12 @@ DateTickerTestCase.prototype.testBasicDateTicker = function() {
       {v: Date.UTC(2000,  0,  1, 10, 0, 0), label: '10:00'},
       {v: Date.UTC(2000,  0,  1, 12, 0, 0), label: '12:00'}
   ];
-  assertEquals(expected_ticks, ticks);
-};
+  assert.deepEqual(expected_ticks, ticks);
+});
 
-DateTickerTestCase.prototype.testAllDateTickers = function() {
+it('testAllDateTickers', function() {
   var opts = {labelsUTC: true, pixelsPerLabel: 60};
-  var options = this.createOptionsViewForAxis('x', opts);
+  var options = createOptionsViewForAxis('x', opts);
 
   // For granularities finer than MONTHLY, the first tick returned tick 
   // could lie outside [start_time, end_time] range in the original code.
@@ -91,175 +91,177 @@ DateTickerTestCase.prototype.testAllDateTickers = function() {
     return ticks;
   };
   
-  assertEquals([{"v":-1577923200000,"label":"1920"},{"v":-1262304000000,"label":"1930"},{"v":-946771200000,"label":"1940"},{"v":-631152000000,"label":"1950"},{"v":-315619200000,"label":"1960"},{"v":0,"label":"1970"},{"v":315532800000,"label":"1980"},{"v":631152000000,"label":"1990"},{"v":946684800000,"label":"2000"}], ticker(-1797552000000, 1255561200000, 800, options));
-  assertEquals([{"v":-5364662400000,"label":"1800"},{"v":-2208988800000,"label":"1900"}], ticker(-6122044800000, 189302400000, 480, options));
-  assertEquals([{"v":1041120000000,"label":"29 Dec"},{"v":1041724800000,"label":"05 Jan"},{"v":1042329600000,"label":"12 Jan"},{"v":1042934400000,"label":"19 Jan"},{"v":1043539200000,"label":"26 Jan"},{"v":1044144000000,"label":"02 Feb"},{"v":1044748800000,"label":"09 Feb"},{"v":1045353600000,"label":"16 Feb"}], ticker(1041120000000, 1045353600000, 640, options));
-  assertEquals([{"v":1041379200000,"label":"Jan 2003"},{"v":1072915200000,"label":"Jan 2004"},{"v":1104537600000,"label":"Jan 2005"},{"v":1136073600000,"label":"Jan 2006"},{"v":1167609600000,"label":"Jan 2007"},{"v":1199145600000,"label":"Jan 2008"},{"v":1230768000000,"label":"Jan 2009"},{"v":1262304000000,"label":"Jan 2010"},{"v":1293840000000,"label":"Jan 2011"}], ticker(1041120000000, 1307833200000, 800, options));
-  assertEquals([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"}], ticker(1159657200000, 1162252800000, 480, options));
-  assertEquals([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"}], ticker(1159657200000, 1162252800000, 640, options));
-  assertEquals([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"},{"v":1163894400000,"label":"19 Nov"},{"v":1164499200000,"label":"26 Nov"}], ticker(1159657200000, 1164758400000, 1150, options));
-  assertEquals([{"v":1159660800000,"label":"Oct 2006"},{"v":1162339200000,"label":"Nov 2006"}], ticker(1159657200000, 1164758400000, 400, options));
-  assertEquals([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"},{"v":1163894400000,"label":"19 Nov"},{"v":1164499200000,"label":"26 Nov"}], ticker(1159657200000, 1164758400000, 500, options));
-  assertEquals([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"},{"v":1163894400000,"label":"19 Nov"},{"v":1164499200000,"label":"26 Nov"}], ticker(1159657200000, 1164758400000, 600, options));
-  assertEquals([{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"}], ticker(1160243979962, 1163887694248, 600, options));
+  assert.deepEqual([{"v":-1577923200000,"label":"1920"},{"v":-1262304000000,"label":"1930"},{"v":-946771200000,"label":"1940"},{"v":-631152000000,"label":"1950"},{"v":-315619200000,"label":"1960"},{"v":0,"label":"1970"},{"v":315532800000,"label":"1980"},{"v":631152000000,"label":"1990"},{"v":946684800000,"label":"2000"}], ticker(-1797552000000, 1255561200000, 800, options));
+  assert.deepEqual([{"v":-5364662400000,"label":"1800"},{"v":-2208988800000,"label":"1900"}], ticker(-6122044800000, 189302400000, 480, options));
+  assert.deepEqual([{"v":1041120000000,"label":"29 Dec"},{"v":1041724800000,"label":"05 Jan"},{"v":1042329600000,"label":"12 Jan"},{"v":1042934400000,"label":"19 Jan"},{"v":1043539200000,"label":"26 Jan"},{"v":1044144000000,"label":"02 Feb"},{"v":1044748800000,"label":"09 Feb"},{"v":1045353600000,"label":"16 Feb"}], ticker(1041120000000, 1045353600000, 640, options));
+  assert.deepEqual([{"v":1041379200000,"label":"Jan 2003"},{"v":1072915200000,"label":"Jan 2004"},{"v":1104537600000,"label":"Jan 2005"},{"v":1136073600000,"label":"Jan 2006"},{"v":1167609600000,"label":"Jan 2007"},{"v":1199145600000,"label":"Jan 2008"},{"v":1230768000000,"label":"Jan 2009"},{"v":1262304000000,"label":"Jan 2010"},{"v":1293840000000,"label":"Jan 2011"}], ticker(1041120000000, 1307833200000, 800, options));
+  assert.deepEqual([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"}], ticker(1159657200000, 1162252800000, 480, options));
+  assert.deepEqual([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"}], ticker(1159657200000, 1162252800000, 640, options));
+  assert.deepEqual([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"},{"v":1163894400000,"label":"19 Nov"},{"v":1164499200000,"label":"26 Nov"}], ticker(1159657200000, 1164758400000, 1150, options));
+  assert.deepEqual([{"v":1159660800000,"label":"Oct 2006"},{"v":1162339200000,"label":"Nov 2006"}], ticker(1159657200000, 1164758400000, 400, options));
+  assert.deepEqual([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"},{"v":1163894400000,"label":"19 Nov"},{"v":1164499200000,"label":"26 Nov"}], ticker(1159657200000, 1164758400000, 500, options));
+  assert.deepEqual([{"v":1159660800000,"label":"01 Oct"},{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"},{"v":1163894400000,"label":"19 Nov"},{"v":1164499200000,"label":"26 Nov"}], ticker(1159657200000, 1164758400000, 600, options));
+  assert.deepEqual([{"v":1160265600000,"label":"08 Oct"},{"v":1160870400000,"label":"15 Oct"},{"v":1161475200000,"label":"22 Oct"},{"v":1162080000000,"label":"29 Oct"},{"v":1162684800000,"label":"05 Nov"},{"v":1163289600000,"label":"12 Nov"}], ticker(1160243979962, 1163887694248, 600, options));
 
-  assertEquals([{"v":1160611200000,"label":"12 Oct"},{"v":1160784000000,"label":"14 Oct"},{"v":1160956800000,"label":"16 Oct"},{"v":1161129600000,"label":"18 Oct"}], ticker(1160521200000, 1161298800000, 480, options));
-  assertEquals([{"v":1161475200000,"label":"22 Oct"},{"v":1161561600000,"label":"23 Oct"},{"v":1161648000000,"label":"24 Oct"},{"v":1161734400000,"label":"25 Oct"},{"v":1161820800000,"label":"26 Oct"},{"v":1161907200000,"label":"27 Oct"},{"v":1161993600000,"label":"28 Oct"}], ticker(1161471164461, 1161994065957, 600, options));
-  assertEquals([{"v":1161561600000,"label":"23 Oct"},{"v":1161583200000,"label":"06:00"},{"v":1161604800000,"label":"12:00"},{"v":1161626400000,"label":"18:00"}], ticker(1161557878860, 1161642991675, 600, options));
-  assertEquals([{"v":1161756000000,"label":"06:00"},{"v":1161759600000,"label":"07:00"},{"v":1161763200000,"label":"08:00"},{"v":1161766800000,"label":"09:00"},{"v":1161770400000,"label":"10:00"},{"v":1161774000000,"label":"11:00"},{"v":1161777600000,"label":"12:00"}], ticker(1161752537840, 1161777663332, 600, options));
-  assertEquals([{"v":1167609600000,"label":"01 Jan"},{"v":1167696000000,"label":"02 Jan"},{"v":1167782400000,"label":"03 Jan"},{"v":1167868800000,"label":"04 Jan"},{"v":1167955200000,"label":"05 Jan"},{"v":1168041600000,"label":"06 Jan"},{"v":1168128000000,"label":"07 Jan"},{"v":1168214400000,"label":"08 Jan"},{"v":1168300800000,"label":"09 Jan"}], ticker(1167609600000, 1168300800000, 480, options));
-  assertEquals([{"v":1167609600000,"label":"Jan 2007"}], ticker(1167609600000, 1199059200000, 100, options));
-  assertEquals([{"v":1167609600000,"label":"Jan 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1191196800000,"label":"Oct 2007"}], ticker(1167609600000, 1199059200000, 300, options));
-  assertEquals([{"v":1167609600000,"label":"Jan 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1191196800000,"label":"Oct 2007"}], ticker(1167609600000, 1199059200000, 480, options));
-  assertEquals([{"v":1167609600000,"label":"Jan 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1191196800000,"label":"Oct 2007"}], ticker(1167609600000, 1199059200000, 600, options));
-  assertEquals([{"v":1160611200000,"label":"12 Oct"},{"v":1160784000000,"label":"14 Oct"},{"v":1160956800000,"label":"16 Oct"},{"v":1161129600000,"label":"18 Oct"}], ticker(1160521200000, 1161298800000, 480, options));
-  assertEquals([{"v":1167609600000,"label":"Jan 2007"},{"v":1170288000000,"label":"Feb 2007"},{"v":1172707200000,"label":"Mar 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1177977600000,"label":"May 2007"},{"v":1180656000000,"label":"Jun 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1185926400000,"label":"Aug 2007"},{"v":1188604800000,"label":"Sep 2007"},{"v":1191196800000,"label":"Oct 2007"},{"v":1193875200000,"label":"Nov 2007"},{"v":1196467200000,"label":"Dec 2007"}], ticker(1167609600000, 1199059200000, 800, options));
+  assert.deepEqual([{"v":1160611200000,"label":"12 Oct"},{"v":1160784000000,"label":"14 Oct"},{"v":1160956800000,"label":"16 Oct"},{"v":1161129600000,"label":"18 Oct"}], ticker(1160521200000, 1161298800000, 480, options));
+  assert.deepEqual([{"v":1161475200000,"label":"22 Oct"},{"v":1161561600000,"label":"23 Oct"},{"v":1161648000000,"label":"24 Oct"},{"v":1161734400000,"label":"25 Oct"},{"v":1161820800000,"label":"26 Oct"},{"v":1161907200000,"label":"27 Oct"},{"v":1161993600000,"label":"28 Oct"}], ticker(1161471164461, 1161994065957, 600, options));
+  assert.deepEqual([{"v":1161561600000,"label":"23 Oct"},{"v":1161583200000,"label":"06:00"},{"v":1161604800000,"label":"12:00"},{"v":1161626400000,"label":"18:00"}], ticker(1161557878860, 1161642991675, 600, options));
+  assert.deepEqual([{"v":1161756000000,"label":"06:00"},{"v":1161759600000,"label":"07:00"},{"v":1161763200000,"label":"08:00"},{"v":1161766800000,"label":"09:00"},{"v":1161770400000,"label":"10:00"},{"v":1161774000000,"label":"11:00"},{"v":1161777600000,"label":"12:00"}], ticker(1161752537840, 1161777663332, 600, options));
+  assert.deepEqual([{"v":1167609600000,"label":"01 Jan"},{"v":1167696000000,"label":"02 Jan"},{"v":1167782400000,"label":"03 Jan"},{"v":1167868800000,"label":"04 Jan"},{"v":1167955200000,"label":"05 Jan"},{"v":1168041600000,"label":"06 Jan"},{"v":1168128000000,"label":"07 Jan"},{"v":1168214400000,"label":"08 Jan"},{"v":1168300800000,"label":"09 Jan"}], ticker(1167609600000, 1168300800000, 480, options));
+  assert.deepEqual([{"v":1167609600000,"label":"Jan 2007"}], ticker(1167609600000, 1199059200000, 100, options));
+  assert.deepEqual([{"v":1167609600000,"label":"Jan 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1191196800000,"label":"Oct 2007"}], ticker(1167609600000, 1199059200000, 300, options));
+  assert.deepEqual([{"v":1167609600000,"label":"Jan 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1191196800000,"label":"Oct 2007"}], ticker(1167609600000, 1199059200000, 480, options));
+  assert.deepEqual([{"v":1167609600000,"label":"Jan 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1191196800000,"label":"Oct 2007"}], ticker(1167609600000, 1199059200000, 600, options));
+  assert.deepEqual([{"v":1160611200000,"label":"12 Oct"},{"v":1160784000000,"label":"14 Oct"},{"v":1160956800000,"label":"16 Oct"},{"v":1161129600000,"label":"18 Oct"}], ticker(1160521200000, 1161298800000, 480, options));
+  assert.deepEqual([{"v":1167609600000,"label":"Jan 2007"},{"v":1170288000000,"label":"Feb 2007"},{"v":1172707200000,"label":"Mar 2007"},{"v":1175385600000,"label":"Apr 2007"},{"v":1177977600000,"label":"May 2007"},{"v":1180656000000,"label":"Jun 2007"},{"v":1183248000000,"label":"Jul 2007"},{"v":1185926400000,"label":"Aug 2007"},{"v":1188604800000,"label":"Sep 2007"},{"v":1191196800000,"label":"Oct 2007"},{"v":1193875200000,"label":"Nov 2007"},{"v":1196467200000,"label":"Dec 2007"}], ticker(1167609600000, 1199059200000, 800, options));
 
-  assertEquals([{"v":1293840000000,"label":"Jan 2011"},{"v":1296518400000,"label":"Feb 2011"},{"v":1298937600000,"label":"Mar 2011"},{"v":1301616000000,"label":"Apr 2011"},{"v":1304208000000,"label":"May 2011"},{"v":1306886400000,"label":"Jun 2011"},{"v":1309478400000,"label":"Jul 2011"},{"v":1312156800000,"label":"Aug 2011"}], ticker(1293753600000, 1312844400000, 727, options));
-  assertEquals([{"v":1201824000000,"label":"01 Feb"},{"v":1201910400000,"label":"02 Feb"},{"v":1201996800000,"label":"03 Feb"},{"v":1202083200000,"label":"04 Feb"},{"v":1202169600000,"label":"05 Feb"},{"v":1202256000000,"label":"06 Feb"}], ticker(1201824000000, 1202256000000, 700, options));
-  assertEquals([{"v":1210118400000,"label":"07 May"},{"v":1210140000000,"label":"06:00"},{"v":1210161600000,"label":"12:00"},{"v":1210183200000,"label":"18:00"},{"v":1210204800000,"label":"08 May"},{"v":1210226400000,"label":"06:00"},{"v":1210248000000,"label":"12:00"},{"v":1210269600000,"label":"18:00"},{"v":1210291200000,"label":"09 May"}], ticker(1210114800000, 1210291200000, 480, options));
-  assertEquals([{"v":1210118400000,"label":"07 May"},{"v":1210204800000,"label":"08 May"},{"v":1210291200000,"label":"09 May"},{"v":1210377600000,"label":"10 May"},{"v":1210464000000,"label":"11 May"}], ticker(1210114800000, 1210464000000, 480, options));
-  assertEquals([{"v":1210118400000,"label":"07 May"},{"v":1210204800000,"label":"08 May"},{"v":1210291200000,"label":"09 May"},{"v":1210377600000,"label":"10 May"},{"v":1210464000000,"label":"11 May"},{"v":1210550400000,"label":"12 May"}], ticker(1210114800000, 1210550400000, 480, options));
-  assertEquals([{"v":1214870400000,"label":"01 Jul"},{"v":1214872200000,"label":"00:30"},{"v":1214874000000,"label":"01:00"},{"v":1214875800000,"label":"01:30"}], ticker(1214870400000, 1214877599000, 600, options));
-  assertEquals([{"v":1214870400000,"label":"Jul 2008"},{"v":1217548800000,"label":"Aug 2008"},{"v":1220227200000,"label":"Sep 2008"}], ticker(1214866800000, 1222747200000, 600, options));
-  assertEquals([{"v":1215820800000,"label":"12 Jul"},{"v":1215842400000,"label":"06:00"},{"v":1215864000000,"label":"12:00"},{"v":1215885600000,"label":"18:00"},{"v":1215907200000,"label":"13 Jul"},{"v":1215928800000,"label":"06:00"},{"v":1215950400000,"label":"12:00"},{"v":1215972000000,"label":"18:00"}], ticker(1215817200000, 1215989940000, 600, options));
-  assertEquals([{"v":1246752000000,"label":"05 Jul"},{"v":1247356800000,"label":"12 Jul"},{"v":1247961600000,"label":"19 Jul"}], ticker(1246402800000, 1248217200000, 600, options));
-  assertEquals([{"v":1246752000000,"label":"05 Jul"},{"v":1247356800000,"label":"12 Jul"},{"v":1247961600000,"label":"19 Jul"},{"v":1248566400000,"label":"26 Jul"},{"v":1249171200000,"label":"02 Aug"}], ticker(1246402800000, 1249340400000, 600, options));
-  assertEquals([{"v":1247356800000,"label":"12 Jul"},{"v":1247360400000,"label":"01:00"},{"v":1247364000000,"label":"02:00"},{"v":1247367600000,"label":"03:00"},{"v":1247371200000,"label":"04:00"},{"v":1247374800000,"label":"05:00"},{"v":1247378400000,"label":"06:00"}], ticker(1247356800000, 1247378400000, 600, options));
+  assert.deepEqual([{"v":1293840000000,"label":"Jan 2011"},{"v":1296518400000,"label":"Feb 2011"},{"v":1298937600000,"label":"Mar 2011"},{"v":1301616000000,"label":"Apr 2011"},{"v":1304208000000,"label":"May 2011"},{"v":1306886400000,"label":"Jun 2011"},{"v":1309478400000,"label":"Jul 2011"},{"v":1312156800000,"label":"Aug 2011"}], ticker(1293753600000, 1312844400000, 727, options));
+  assert.deepEqual([{"v":1201824000000,"label":"01 Feb"},{"v":1201910400000,"label":"02 Feb"},{"v":1201996800000,"label":"03 Feb"},{"v":1202083200000,"label":"04 Feb"},{"v":1202169600000,"label":"05 Feb"},{"v":1202256000000,"label":"06 Feb"}], ticker(1201824000000, 1202256000000, 700, options));
+  assert.deepEqual([{"v":1210118400000,"label":"07 May"},{"v":1210140000000,"label":"06:00"},{"v":1210161600000,"label":"12:00"},{"v":1210183200000,"label":"18:00"},{"v":1210204800000,"label":"08 May"},{"v":1210226400000,"label":"06:00"},{"v":1210248000000,"label":"12:00"},{"v":1210269600000,"label":"18:00"},{"v":1210291200000,"label":"09 May"}], ticker(1210114800000, 1210291200000, 480, options));
+  assert.deepEqual([{"v":1210118400000,"label":"07 May"},{"v":1210204800000,"label":"08 May"},{"v":1210291200000,"label":"09 May"},{"v":1210377600000,"label":"10 May"},{"v":1210464000000,"label":"11 May"}], ticker(1210114800000, 1210464000000, 480, options));
+  assert.deepEqual([{"v":1210118400000,"label":"07 May"},{"v":1210204800000,"label":"08 May"},{"v":1210291200000,"label":"09 May"},{"v":1210377600000,"label":"10 May"},{"v":1210464000000,"label":"11 May"},{"v":1210550400000,"label":"12 May"}], ticker(1210114800000, 1210550400000, 480, options));
+  assert.deepEqual([{"v":1214870400000,"label":"01 Jul"},{"v":1214872200000,"label":"00:30"},{"v":1214874000000,"label":"01:00"},{"v":1214875800000,"label":"01:30"}], ticker(1214870400000, 1214877599000, 600, options));
+  assert.deepEqual([{"v":1214870400000,"label":"Jul 2008"},{"v":1217548800000,"label":"Aug 2008"},{"v":1220227200000,"label":"Sep 2008"}], ticker(1214866800000, 1222747200000, 600, options));
+  assert.deepEqual([{"v":1215820800000,"label":"12 Jul"},{"v":1215842400000,"label":"06:00"},{"v":1215864000000,"label":"12:00"},{"v":1215885600000,"label":"18:00"},{"v":1215907200000,"label":"13 Jul"},{"v":1215928800000,"label":"06:00"},{"v":1215950400000,"label":"12:00"},{"v":1215972000000,"label":"18:00"}], ticker(1215817200000, 1215989940000, 600, options));
+  assert.deepEqual([{"v":1246752000000,"label":"05 Jul"},{"v":1247356800000,"label":"12 Jul"},{"v":1247961600000,"label":"19 Jul"}], ticker(1246402800000, 1248217200000, 600, options));
+  assert.deepEqual([{"v":1246752000000,"label":"05 Jul"},{"v":1247356800000,"label":"12 Jul"},{"v":1247961600000,"label":"19 Jul"},{"v":1248566400000,"label":"26 Jul"},{"v":1249171200000,"label":"02 Aug"}], ticker(1246402800000, 1249340400000, 600, options));
+  assert.deepEqual([{"v":1247356800000,"label":"12 Jul"},{"v":1247360400000,"label":"01:00"},{"v":1247364000000,"label":"02:00"},{"v":1247367600000,"label":"03:00"},{"v":1247371200000,"label":"04:00"},{"v":1247374800000,"label":"05:00"},{"v":1247378400000,"label":"06:00"}], ticker(1247356800000, 1247378400000, 600, options));
 
-  assertEquals([{"v":1247356800000,"label":"12 Jul"},{"v":1247360400000,"label":"01:00"},{"v":1247364000000,"label":"02:00"},{"v":1247367600000,"label":"03:00"},{"v":1247371200000,"label":"04:00"},{"v":1247374800000,"label":"05:00"},{"v":1247378400000,"label":"06:00"}], ticker(1247356800000, 1247378400000, 600, options));
-  assertEquals([{"v":1254268800000,"label":"30 Sep"},{"v":1254355200000,"label":"01 Oct"},{"v":1254441600000,"label":"02 Oct"},{"v":1254528000000,"label":"03 Oct"},{"v":1254614400000,"label":"04 Oct"},{"v":1254700800000,"label":"05 Oct"},{"v":1254787200000,"label":"06 Oct"},{"v":1254873600000,"label":"07 Oct"},{"v":1254960000000,"label":"08 Oct"},{"v":1255046400000,"label":"09 Oct"},{"v":1255132800000,"label":"10 Oct"}], ticker(1254222000000, 1255172400000, 900, options));
-  assertEquals([{"v":1254441600000,"label":"02 Oct"},{"v":1254528000000,"label":"03 Oct"},{"v":1254614400000,"label":"04 Oct"},{"v":1254700800000,"label":"05 Oct"},{"v":1254787200000,"label":"06 Oct"},{"v":1254873600000,"label":"07 Oct"},{"v":1254960000000,"label":"08 Oct"}], ticker(1254394800000, 1254999600000, 900, options));
-  assertEquals([{"v":1259625600000,"label":"01 Dec"},{"v":1259712000000,"label":"02 Dec"},{"v":1259798400000,"label":"03 Dec"},{"v":1259884800000,"label":"04 Dec"},{"v":1259971200000,"label":"05 Dec"},{"v":1260057600000,"label":"06 Dec"},{"v":1260144000000,"label":"07 Dec"}], ticker(1259625600000, 1260144000000, 480, options));
-  assertEquals([{"v":1259625600000,"label":"01 Dec"},{"v":1259712000000,"label":"02 Dec"},{"v":1259798400000,"label":"03 Dec"},{"v":1259884800000,"label":"04 Dec"},{"v":1259971200000,"label":"05 Dec"},{"v":1260057600000,"label":"06 Dec"},{"v":1260144000000,"label":"07 Dec"}], ticker(1259625600000, 1260144000000, 600, options));
-  assertEquals([{"v":1260057600000,"label":"06 Dec"},{"v":1260662400000,"label":"13 Dec"},{"v":1261267200000,"label":"20 Dec"},{"v":1261872000000,"label":"27 Dec"},{"v":1262476800000,"label":"03 Jan"},{"v":1263081600000,"label":"10 Jan"},{"v":1263686400000,"label":"17 Jan"},{"v":1264291200000,"label":"24 Jan"}], ticker(1260057600000, 1264291200000, 640, options));
-  assertEquals([{"v":1262304000000,"label":"Jan 2010"},{"v":1264982400000,"label":"Feb 2010"},{"v":1267401600000,"label":"Mar 2010"},{"v":1270080000000,"label":"Apr 2010"}], ticker(1262304000000, 1270857600000, 640, options));
-  assertEquals([{"v":1288915200000,"label":"05 Nov"},{"v":1288936800000,"label":"06:00"},{"v":1288958400000,"label":"12:00"},{"v":1288980000000,"label":"18:00"},{"v":1289001600000,"label":"06 Nov"},{"v":1289023200000,"label":"06:00"},{"v":1289044800000,"label":"12:00"},{"v":1289066400000,"label":"18:00"},{"v":1289088000000,"label":"07 Nov"},{"v":1289109600000,"label":"06:00"},{"v":1289131200000,"label":"12:00"},{"v":1289152800000,"label":"18:00"},{"v":1289174400000,"label":"08 Nov"},{"v":1289196000000,"label":"06:00"},{"v":1289217600000,"label":"12:00"},{"v":1289239200000,"label":"18:00"},{"v":1289260800000,"label":"09 Nov"}], ticker(1288911600000, 1289260800000, 1024, options));
-  assertEquals([{"v":1291161600000,"label":"01 Dec"},{"v":1291248000000,"label":"02 Dec"},{"v":1291334400000,"label":"03 Dec"},{"v":1291420800000,"label":"04 Dec"},{"v":1291507200000,"label":"05 Dec"},{"v":1291593600000,"label":"06 Dec"},{"v":1291680000000,"label":"07 Dec"},{"v":1291766400000,"label":"08 Dec"},{"v":1291852800000,"label":"09 Dec"}], ticker(1291161600000, 1291852800000, 600, options));
-  assertEquals([{"v":1294358400000,"label":"07 Jan"},{"v":1294444800000,"label":"08 Jan"},{"v":1294531200000,"label":"09 Jan"},{"v":1294617600000,"label":"10 Jan"},{"v":1294704000000,"label":"11 Jan"},{"v":1294790400000,"label":"12 Jan"},{"v":1294876800000,"label":"13 Jan"},{"v":1294963200000,"label":"14 Jan"}], ticker(1294358400000, 1294963200000, 480, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908050165, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908051166, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908052167, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908053167, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908054168, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908055169, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908056169, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908057170, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908058171, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908059172, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908060172, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908061174, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908062176, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908063177, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908064178, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908065000,"label":"19:47:45"}], ticker(1307908000112, 1307908065178, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908065000,"label":"19:47:45"}], ticker(1307908000112, 1307908066178, 800, options));
-  assertEquals([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908065000,"label":"19:47:45"}], ticker(1307908000112, 1307908067179, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908068179, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908069179, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908070180, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908071180, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908072181, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908073181, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908074182, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908075182, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908076183, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908077183, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908078184, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908079185, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908080186, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908081187, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908082188, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908083188, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908084189, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908085190, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908086191, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908087192, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908088192, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908089193, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908090194, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908091194, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908092196, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908093196, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908094197, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908095197, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908096198, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908097199, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908098200, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908099200, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908100201, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908101201, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908102202, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908103203, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908104204, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908105205, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908106205, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908107206, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908108209, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908109209, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908110209, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908111210, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908112211, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908113211, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908114212, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908115213, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908116214, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908117214, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908118215, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908119215, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908120217, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908121218, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908122219, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908123219, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908124220, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908125221, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908126222, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908127222, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908128223, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908129223, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908130224, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908131225, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908132226, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908133227, 800, options));
-  assertEquals([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908134227, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908135227, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908136228, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908137230, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908138231, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908139232, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908140233, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908141233, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908142234, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908143240, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908144240, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908145240, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908146241, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908147241, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908148242, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908149243, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908150243, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908151244, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908152245, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908153245, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908154246, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908155247, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908156247, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908157248, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908158249, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908159250, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908160251, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908161252, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908162252, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908163253, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908164254, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908165254, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908166255, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908167256, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908168256, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908169257, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908170258, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908171258, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908172259, 800, options));
-  assertEquals([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908173260, 800, options));
-  assertEquals([{"v":978307200000,"label":"Jan 2001"},{"v":986083200000,"label":"Apr 2001"},{"v":993945600000,"label":"Jul 2001"},{"v":1001894400000,"label":"Oct 2001"}], ticker(978307200000, 1001894400000, 400, options));
-};
+  assert.deepEqual([{"v":1247356800000,"label":"12 Jul"},{"v":1247360400000,"label":"01:00"},{"v":1247364000000,"label":"02:00"},{"v":1247367600000,"label":"03:00"},{"v":1247371200000,"label":"04:00"},{"v":1247374800000,"label":"05:00"},{"v":1247378400000,"label":"06:00"}], ticker(1247356800000, 1247378400000, 600, options));
+  assert.deepEqual([{"v":1254268800000,"label":"30 Sep"},{"v":1254355200000,"label":"01 Oct"},{"v":1254441600000,"label":"02 Oct"},{"v":1254528000000,"label":"03 Oct"},{"v":1254614400000,"label":"04 Oct"},{"v":1254700800000,"label":"05 Oct"},{"v":1254787200000,"label":"06 Oct"},{"v":1254873600000,"label":"07 Oct"},{"v":1254960000000,"label":"08 Oct"},{"v":1255046400000,"label":"09 Oct"},{"v":1255132800000,"label":"10 Oct"}], ticker(1254222000000, 1255172400000, 900, options));
+  assert.deepEqual([{"v":1254441600000,"label":"02 Oct"},{"v":1254528000000,"label":"03 Oct"},{"v":1254614400000,"label":"04 Oct"},{"v":1254700800000,"label":"05 Oct"},{"v":1254787200000,"label":"06 Oct"},{"v":1254873600000,"label":"07 Oct"},{"v":1254960000000,"label":"08 Oct"}], ticker(1254394800000, 1254999600000, 900, options));
+  assert.deepEqual([{"v":1259625600000,"label":"01 Dec"},{"v":1259712000000,"label":"02 Dec"},{"v":1259798400000,"label":"03 Dec"},{"v":1259884800000,"label":"04 Dec"},{"v":1259971200000,"label":"05 Dec"},{"v":1260057600000,"label":"06 Dec"},{"v":1260144000000,"label":"07 Dec"}], ticker(1259625600000, 1260144000000, 480, options));
+  assert.deepEqual([{"v":1259625600000,"label":"01 Dec"},{"v":1259712000000,"label":"02 Dec"},{"v":1259798400000,"label":"03 Dec"},{"v":1259884800000,"label":"04 Dec"},{"v":1259971200000,"label":"05 Dec"},{"v":1260057600000,"label":"06 Dec"},{"v":1260144000000,"label":"07 Dec"}], ticker(1259625600000, 1260144000000, 600, options));
+  assert.deepEqual([{"v":1260057600000,"label":"06 Dec"},{"v":1260662400000,"label":"13 Dec"},{"v":1261267200000,"label":"20 Dec"},{"v":1261872000000,"label":"27 Dec"},{"v":1262476800000,"label":"03 Jan"},{"v":1263081600000,"label":"10 Jan"},{"v":1263686400000,"label":"17 Jan"},{"v":1264291200000,"label":"24 Jan"}], ticker(1260057600000, 1264291200000, 640, options));
+  assert.deepEqual([{"v":1262304000000,"label":"Jan 2010"},{"v":1264982400000,"label":"Feb 2010"},{"v":1267401600000,"label":"Mar 2010"},{"v":1270080000000,"label":"Apr 2010"}], ticker(1262304000000, 1270857600000, 640, options));
+  assert.deepEqual([{"v":1288915200000,"label":"05 Nov"},{"v":1288936800000,"label":"06:00"},{"v":1288958400000,"label":"12:00"},{"v":1288980000000,"label":"18:00"},{"v":1289001600000,"label":"06 Nov"},{"v":1289023200000,"label":"06:00"},{"v":1289044800000,"label":"12:00"},{"v":1289066400000,"label":"18:00"},{"v":1289088000000,"label":"07 Nov"},{"v":1289109600000,"label":"06:00"},{"v":1289131200000,"label":"12:00"},{"v":1289152800000,"label":"18:00"},{"v":1289174400000,"label":"08 Nov"},{"v":1289196000000,"label":"06:00"},{"v":1289217600000,"label":"12:00"},{"v":1289239200000,"label":"18:00"},{"v":1289260800000,"label":"09 Nov"}], ticker(1288911600000, 1289260800000, 1024, options));
+  assert.deepEqual([{"v":1291161600000,"label":"01 Dec"},{"v":1291248000000,"label":"02 Dec"},{"v":1291334400000,"label":"03 Dec"},{"v":1291420800000,"label":"04 Dec"},{"v":1291507200000,"label":"05 Dec"},{"v":1291593600000,"label":"06 Dec"},{"v":1291680000000,"label":"07 Dec"},{"v":1291766400000,"label":"08 Dec"},{"v":1291852800000,"label":"09 Dec"}], ticker(1291161600000, 1291852800000, 600, options));
+  assert.deepEqual([{"v":1294358400000,"label":"07 Jan"},{"v":1294444800000,"label":"08 Jan"},{"v":1294531200000,"label":"09 Jan"},{"v":1294617600000,"label":"10 Jan"},{"v":1294704000000,"label":"11 Jan"},{"v":1294790400000,"label":"12 Jan"},{"v":1294876800000,"label":"13 Jan"},{"v":1294963200000,"label":"14 Jan"}], ticker(1294358400000, 1294963200000, 480, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908050165, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908051166, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908052167, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908053167, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"}], ticker(1307908000112, 1307908054168, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908055169, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908056169, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908057170, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908058171, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"}], ticker(1307908000112, 1307908059172, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908060172, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908061174, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908062176, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908063177, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908064178, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908065000,"label":"19:47:45"}], ticker(1307908000112, 1307908065178, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908065000,"label":"19:47:45"}], ticker(1307908000112, 1307908066178, 800, options));
+  assert.deepEqual([{"v":1307908005000,"label":"19:46:45"},{"v":1307908010000,"label":"19:46:50"},{"v":1307908015000,"label":"19:46:55"},{"v":1307908020000,"label":"19:47"},{"v":1307908025000,"label":"19:47:05"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908035000,"label":"19:47:15"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908045000,"label":"19:47:25"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908055000,"label":"19:47:35"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908065000,"label":"19:47:45"}], ticker(1307908000112, 1307908067179, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908068179, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"}], ticker(1307908000112, 1307908069179, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908070180, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908071180, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908072181, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908073181, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908074182, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908075182, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908076183, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908077183, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908078184, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"}], ticker(1307908000112, 1307908079185, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908080186, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908081187, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908082188, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908083188, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908084189, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908085190, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908086191, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908087192, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908088192, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"}], ticker(1307908000112, 1307908089193, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908090194, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908091194, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908092196, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908093196, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908094197, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908095197, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908096198, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908097199, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908098200, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"}], ticker(1307908000112, 1307908099200, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908100201, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908101201, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908102202, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908103203, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908104204, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908105205, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908106205, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908107206, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908108209, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"}], ticker(1307908000112, 1307908109209, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908110209, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908111210, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908112211, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908113211, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908114212, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908115213, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908116214, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908117214, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908118215, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908119215, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908120217, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908121218, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908122219, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908123219, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908124220, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908125221, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908126222, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908127222, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908128223, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"}], ticker(1307908000112, 1307908129223, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908130224, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908131225, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908132226, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908133227, 800, options));
+  assert.deepEqual([{"v":1307908010000,"label":"19:46:50"},{"v":1307908020000,"label":"19:47"},{"v":1307908030000,"label":"19:47:10"},{"v":1307908040000,"label":"19:47:20"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908060000,"label":"19:47:40"},{"v":1307908070000,"label":"19:47:50"},{"v":1307908080000,"label":"19:48"},{"v":1307908090000,"label":"19:48:10"},{"v":1307908100000,"label":"19:48:20"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908120000,"label":"19:48:40"},{"v":1307908130000,"label":"19:48:50"}], ticker(1307908000112, 1307908134227, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908135227, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908136228, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908137230, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908138231, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"}], ticker(1307908000112, 1307908139232, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908140233, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908141233, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908142234, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908143240, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908144240, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908145240, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908146241, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908147241, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908148242, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908149243, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908150243, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908151244, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908152245, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908153245, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908154246, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908155247, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908156247, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908157248, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908158249, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908159250, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908160251, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908161252, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908162252, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908163253, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908164254, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908165254, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908166255, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908167256, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908168256, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"}], ticker(1307908000112, 1307908169257, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908170258, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908171258, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908172259, 800, options));
+  assert.deepEqual([{"v":1307908020000,"label":"19:47"},{"v":1307908050000,"label":"19:47:30"},{"v":1307908080000,"label":"19:48"},{"v":1307908110000,"label":"19:48:30"},{"v":1307908140000,"label":"19:49"},{"v":1307908170000,"label":"19:49:30"}], ticker(1307908000112, 1307908173260, 800, options));
+  assert.deepEqual([{"v":978307200000,"label":"Jan 2001"},{"v":986083200000,"label":"Apr 2001"},{"v":993945600000,"label":"Jul 2001"},{"v":1001894400000,"label":"Oct 2001"}], ticker(978307200000, 1001894400000, 400, options));
+});
+
+});
index 7222a21..4311fb5 100644 (file)
@@ -1,19 +1,22 @@
 /** 
  * @fileoverview Test cases for DygraphOptions.
  */
-var DygraphOptionsTestCase = TestCase("dygraph-options-tests");
+describe("dygraph-options-tests", function() {
 
-DygraphOptionsTestCase.prototype.setUp = function() {
+var graph;
+
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+  graph = document.getElementById("graph");
+});
 
-DygraphOptionsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 /*
  * Pathalogical test to ensure getSeriesNames works
  */
-DygraphOptionsTestCase.prototype.testGetSeriesNames = function() {
+it('testGetSeriesNames', function() {
   var opts = {
     width: 480,
     height: 320
@@ -22,20 +25,19 @@ DygraphOptionsTestCase.prototype.testGetSeriesNames = function() {
       "0,-1,0,0";
 
   // Kind of annoying that you need a DOM to test the object.
-  var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
   // We don't need to get at g's attributes_ object just
   // to test DygraphOptions.
   var o = new DygraphOptions(g);
-  assertEquals(["Y", "Y2", "Y3"], o.seriesNames()); 
-};
+  assert.deepEqual(["Y", "Y2", "Y3"], o.seriesNames()); 
+});
 
 /*
  * Ensures that even if logscale is set globally, it doesn't impact the
  * x axis.
  */
-DygraphOptionsTestCase.prototype.testGetLogscaleForX = function() {
+it('testGetLogscaleForX', function() {
   var opts = {
     width: 480,
     height: 320
@@ -44,16 +46,15 @@ DygraphOptionsTestCase.prototype.testGetLogscaleForX = function() {
       "1,-1,2,3";
 
   // Kind of annoying that you need a DOM to test the object.
-  var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertFalse(!!g.getOptionForAxis('logscale', 'x'));
-  assertFalse(!!g.getOptionForAxis('logscale', 'y'));
+  assert.isFalse(!!g.getOptionForAxis('logscale', 'x'));
+  assert.isFalse(!!g.getOptionForAxis('logscale', 'y'));
 
   g.updateOptions({ logscale : true });
-  assertFalse(!!g.getOptionForAxis('logscale', 'x'));
-  assertTrue(!!g.getOptionForAxis('logscale', 'y'));
-};
+  assert.isFalse(!!g.getOptionForAxis('logscale', 'x'));
+  assert.isTrue(!!g.getOptionForAxis('logscale', 'y'));
+});
 
 // Helper to gather all warnings emitted by Dygraph constructor.
 // Removes everything after the first open parenthesis in each warning.
@@ -72,24 +73,23 @@ var getWarnings = function(div, data, opts) {
   return warnings;
 };
 
-DygraphOptionsTestCase.prototype.testLogWarningForNonexistentOption = function() {
+it('testLogWarningForNonexistentOption', function() {
   if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
     return;  // this test won't pass in non-debug mode.
   }
 
-  var graph = document.getElementById("graph");
   var data = "X,Y,Y2,Y3\n" +
       "1,-1,2,3";
 
   var expectWarning = function(opts, badOptionName) {
     DygraphOptions.resetWarnings_();
     var warnings = getWarnings(graph, data, opts);
-    assertEquals(['Unknown option ' + badOptionName], warnings);
+    assert.deepEqual(['Unknown option ' + badOptionName], warnings);
   };
   var expectNoWarning = function(opts) {
     DygraphOptions.resetWarnings_();
     var warnings = getWarnings(graph, data, opts);
-    assertEquals([], warnings);
+    assert.deepEqual([], warnings);
   };
 
   expectNoWarning({});
@@ -100,19 +100,20 @@ DygraphOptionsTestCase.prototype.testLogWarningForNonexistentOption = function()
   expectWarning({highlightSeriesOpts: {anotherNonExistentOption: true}}, 'anotherNonExistentOption');
   expectNoWarning({highlightSeriesOpts: {strokeWidth: 20}});
   expectNoWarning({strokeWidth: 20});
-};
+});
 
-DygraphOptionsTestCase.prototype.testOnlyLogsEachWarningOnce = function() {
+it('testOnlyLogsEachWarningOnce', function() {
   if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
     return;  // this test won't pass in non-debug mode.
   }
 
-  var graph = document.getElementById("graph");
   var data = "X,Y,Y2,Y3\n" +
       "1,-1,2,3";
 
   var warnings1 = getWarnings(graph, data, {nonExistent: true});
   var warnings2 = getWarnings(graph, data, {nonExistent: true});
-  assertEquals(['Unknown option nonExistent'], warnings1);
-  assertEquals([], warnings2);
-};
+  assert.deepEqual(['Unknown option nonExistent'], warnings1);
+  assert.deepEqual([], warnings2);
+});
+
+});
index 2bcee2f..bb6c1d9 100644 (file)
@@ -3,25 +3,25 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var errorBarsTestCase = TestCase("error-bars");
+describe("error-bars", function() {
 
-errorBarsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-errorBarsTestCase._origFunc = Dygraph.getContext;
-errorBarsTestCase.prototype.setUp = function() {
+var _origFunc = Dygraph.getContext;
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(errorBarsTestCase._origFunc(canvas));
+    return new Proxy(_origFunc(canvas));
   }
-};
+});
 
-errorBarsTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = errorBarsTestCase._origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = _origFunc;
+});
 
-errorBarsTestCase.prototype.testErrorBarsDrawn = function() {
+it('testErrorBarsDrawn', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -53,7 +53,7 @@ errorBarsTestCase.prototype.testErrorBarsDrawn = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  // TODO(danvk): fill in
 
@@ -94,9 +94,9 @@ errorBarsTestCase.prototype.testErrorBarsDrawn = function() {
   }
   g.destroy(); // Restore balanced saves and restores.
   CanvasAssertions.assertBalancedSaveRestore(htx);
-};
+});
 
-errorBarsTestCase.prototype.testErrorBarsCorrectColors = function() {
+it('testErrorBarsCorrectColors', function() {
   // Two constant series with constant error.
   var data = [
     [0, [100, 50], [200, 50]],
@@ -137,15 +137,15 @@ errorBarsTestCase.prototype.testErrorBarsCorrectColors = function() {
   // 249-299: empty (white)
   // TODO(danvk): test the edges of these regions.
 
-  assertEquals([0, 0, 255, 38], Util.samplePixel(g.hidden_, 200, 75));
-  assertEquals([0, 0, 255, 38], Util.samplePixel(g.hidden_, 200, 125));
-  assertEquals([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 175));
-  assertEquals([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 225));
-};
+  assert.deepEqual([0, 0, 255, 38], Util.samplePixel(g.hidden_, 200, 75));
+  assert.deepEqual([0, 0, 255, 38], Util.samplePixel(g.hidden_, 200, 125));
+  assert.deepEqual([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 175));
+  assert.deepEqual([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 225));
+});
 
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=392
-errorBarsTestCase.prototype.testRollingAveragePreservesNaNs = function() {
+it('testRollingAveragePreservesNaNs', function() {
   var graph = document.getElementById("graph");
   var data = 
     [
@@ -169,18 +169,20 @@ errorBarsTestCase.prototype.testRollingAveragePreservesNaNs = function() {
 
   var in_series = g.dataHandler_.extractSeries(data, 1, g.attributes_);
 
-  assertEquals(null, in_series[4][1]);
-  assertEquals(null, in_series[4][2][0]);
-  assertEquals(null, in_series[4][2][1]);
-  assertNaN(in_series[5][1]);
-  assertNaN(in_series[5][2][0]);
-  assertNaN(in_series[5][2][1]);
+  assert.equal(null, in_series[4][1]);
+  assert.equal(null, in_series[4][2][0]);
+  assert.equal(null, in_series[4][2][1]);
+  assert(isNaN(in_series[5][1]));
+  assert(isNaN(in_series[5][2][0]));
+  assert(isNaN(in_series[5][2][1]));
 
   var out_series = g.dataHandler_.rollingAverage(in_series, 1, g.attributes_);
-  assertNaN(out_series[5][1]);
-  assertNaN(out_series[5][2][0]);
-  assertNaN(out_series[5][2][1]);
-  assertEquals(null, out_series[4][1]);
-  assertEquals(null, out_series[4][2][0]);
-  assertEquals(null, out_series[4][2][1]);
-};
+  assert(isNaN(out_series[5][1]));
+  assert(isNaN(out_series[5][2][0]));
+  assert(isNaN(out_series[5][2][1]));
+  assert.equal(null, out_series[4][1]);
+  assert.equal(null, out_series[4][2][0]);
+  assert.equal(null, out_series[4][2][1]);
+});
+
+});
index 1e4bfaf..a06fe40 100644 (file)
@@ -3,13 +3,13 @@
  *
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
-var fastCanvasProxyTestCase = TestCase("fast-canvas-proxy");
+describe("fast-canvas-proxy", function() {
 
-fastCanvasProxyTestCase.prototype.setUp = function() {
-};
+beforeEach(function() {
+});
 
-fastCanvasProxyTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 var fakeCanvasContext = {
   moveTo: function() {},
@@ -32,7 +32,7 @@ function extractMoveToAndLineToCalls(proxy) {
   return out;
 }
 
-fastCanvasProxyTestCase.prototype.testExtraMoveTosElided = function() {
+it('testExtraMoveTosElided', function() {
   var htx = new Proxy(fakeCanvasContext);
   var fastProxy = DygraphCanvasRenderer._fastCanvasProxy(htx);
 
@@ -43,12 +43,12 @@ fastCanvasProxyTestCase.prototype.testExtraMoveTosElided = function() {
   fastProxy.moveTo(3, 1);
   fastProxy.stroke();
 
-  assertEquals([['moveTo', 1, 1],
+  assert.deepEqual([['moveTo', 1, 1],
                 ['lineTo', 2, 1],
                 ['lineTo', 3, 1]], extractMoveToAndLineToCalls(htx));
-};
+});
 
-fastCanvasProxyTestCase.prototype.testConsecutiveMoveTosElided = function() {
+it('testConsecutiveMoveTosElided', function() {
   var htx = new Proxy(fakeCanvasContext);
   var fastProxy = DygraphCanvasRenderer._fastCanvasProxy(htx);
 
@@ -59,12 +59,12 @@ fastCanvasProxyTestCase.prototype.testConsecutiveMoveTosElided = function() {
   fastProxy.moveTo(3.2, 3);
   fastProxy.stroke();
 
-  assertEquals([['moveTo', 1, 1],
+  assert.deepEqual([['moveTo', 1, 1],
                 ['lineTo', 2, 1],
                 ['moveTo', 3.2, 3]], extractMoveToAndLineToCalls(htx));
-};
+});
 
-fastCanvasProxyTestCase.prototype.testSuperfluousSegmentsElided = function() {
+it('testSuperfluousSegmentsElided', function() {
   var htx = new Proxy(fakeCanvasContext);
   var fastProxy = DygraphCanvasRenderer._fastCanvasProxy(htx);
 
@@ -90,10 +90,12 @@ fastCanvasProxyTestCase.prototype.testSuperfluousSegmentsElided = function() {
   fastProxy.moveTo(3, 0);  // dodge the "don't touch the last pixel" rule.
   fastProxy.stroke();
 
-  assertEquals([['moveTo', 0.6, 1],
+  assert.deepEqual([['moveTo', 0.6, 1],
                 ['lineTo', 1.0, 5],
                 ['lineTo', 1.2, 0],
                 ['lineTo', 1.7, 30],
                 ['lineTo', 1.8, -30],
                 ['moveTo', 3, 0]], extractMoveToAndLineToCalls(htx));
-};
+});
+
+});
index f40f8a5..6406016 100644 (file)
@@ -4,27 +4,27 @@
  *
  * @author benoitboivin.pro@gmail.com (Benoit Boivin)
  */
-var fillStepPlotTestCase = TestCase("fill-step-plot");
+describe("fill-step-plot", function() {
 
-fillStepPlotTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-fillStepPlotTestCase.origFunc = Dygraph.getContext;
+var origFunc = Dygraph.getContext;
 
-fillStepPlotTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(fillStepPlotTestCase.origFunc(canvas));
+    return new Proxy(origFunc(canvas));
   };
-};
+});
 
-fillStepPlotTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = fillStepPlotTestCase.origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = origFunc;
+});
 
 
-fillStepPlotTestCase.prototype.testFillStepPlotNullValues = function() {
+it('testFillStepPlotNullValues', function() {
   var opts = {
     labels: ["x","y"],
     width: 480,
@@ -45,7 +45,7 @@ fillStepPlotTestCase.prototype.testFillStepPlotNullValues = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
   var x1 = data[3][0];
   var y1 = data[2][1];
   var x2 = data[3][0];
@@ -55,4 +55,6 @@ fillStepPlotTestCase.prototype.testFillStepPlotNullValues = function() {
   
   // Check if a line is drawn between the previous y and the bottom of the chart
   CanvasAssertions.assertLineDrawn(htx, xy1, xy2, {});
-};
\ No newline at end of file
+});
+
+});
index 38ba88e..36b36c9 100644 (file)
@@ -3,62 +3,62 @@
  *
  * @author konigsberg@google.com (Robert Konigsberg)
  */
-var FormatsTestCase = TestCase("formats");
+describe("formats", function() {
 
-FormatsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-FormatsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-FormatsTestCase.prototype.dataString =
+var dataString =
   "X,Y\n" +
   "0,-1\n" +
   "1,0\n" +
   "2,1\n" +
   "3,0\n";
 
-FormatsTestCase.prototype.dataArray =
+var dataArray =
   [[0,-1],
   [1,0],
   [2,1],
   [3,0]];
 
-FormatsTestCase.prototype.testCsv = function() {
-  var data = this.dataString;
+it('testCsv', function() {
+  var data = dataString;
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  this.assertData(g);
-};
+  assertData(g);
+});
 
-FormatsTestCase.prototype.testArray = function() {
-  var data = this.dataArray;
+it('testArray', function() {
+  var data = dataArray;
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  this.assertData(g);
-};
+  assertData(g);
+});
 
-FormatsTestCase.prototype.testFunctionReturnsCsv = function() {
-  var string = this.dataString;
+it('testFunctionReturnsCsv', function() {
+  var string = dataString;
   var data = function() { return string; };
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  // this.assertData(g);
+  // assertData(g);
   console.log("x");
-};
+});
 
-FormatsTestCase.prototype.testFunctionDefinesArray = function() {
-  var array = this.dataArray;
+it('testFunctionDefinesArray', function() {
+  var array = dataArray;
   var data = function() { return array; }
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  this.assertData(g);
-};
+  assertData(g);
+});
 
-FormatsTestCase.prototype.testXValueParser = function() {
+it('testXValueParser', function() {
   var data =
     "X,Y\n" +
     "d,-1\n" +
@@ -69,26 +69,28 @@ FormatsTestCase.prototype.testXValueParser = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {
     xValueParser : function(str) {
-      assertEquals(1, str.length);
+      assert.equal(1, str.length);
       return str.charCodeAt(0) - "a".charCodeAt(0);
     }
   });
 
-  assertEquals(3, g.getValue(0, 0));
-  assertEquals(4, g.getValue(1, 0));
-  assertEquals(5, g.getValue(2, 0));
-  assertEquals(6, g.getValue(3, 0));
-};
+  assert.equal(3, g.getValue(0, 0));
+  assert.equal(4, g.getValue(1, 0));
+  assert.equal(5, g.getValue(2, 0));
+  assert.equal(6, g.getValue(3, 0));
+});
 
-FormatsTestCase.prototype.assertData = function(g) {
-  var expected = this.dataArray;
+var assertData = function(g) {
+  var expected = dataArray;
 
-  assertEquals(4, g.numRows());
-  assertEquals(2, g.numColumns());
+  assert.equal(4, g.numRows());
+  assert.equal(2, g.numColumns());
 
   for (var i = 0; i < 4; i++) {
     for (var j = 0; j < 2; j++) {
-      assertEquals(expected[i][j], g.getValue(i, j));
+      assert.equal(expected[i][j], g.getValue(i, j));
     }
   }
-}
+};
+
+});
index defe25f..6ca239c 100644 (file)
@@ -4,26 +4,26 @@
  * 
  * @author david.eberlein@ch.sauter-bc.com (Fr. Sauter AG)
  */
-var GridPerAxisTestCase = TestCase("grid-per-axis");
+describe("grid-per-axis", function() {
 
-GridPerAxisTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-GridPerAxisTestCase.origFunc = Dygraph.getContext;
+var origFunc = Dygraph.getContext;
 
-GridPerAxisTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(GridPerAxisTestCase.origFunc(canvas));
+    return new Proxy(origFunc(canvas));
   };
-};
+});
 
-GridPerAxisTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = GridPerAxisTestCase.origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = origFunc;
+});
 
-GridPerAxisTestCase.prototype.testIndependentGrids = function() {
+it('testIndependentGrids', function() {
   var opts = {
     width : 480,
     height : 320,
@@ -50,7 +50,7 @@ GridPerAxisTestCase.prototype.testIndependentGrids = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   // The expected gridlines
   var yGridlines = [ 0, 20, 40, 60, 80, 100, 120 ];
@@ -72,7 +72,7 @@ GridPerAxisTestCase.prototype.testIndependentGrids = function() {
     for ( var i = 0; i < gridlines[axis].length; i++) {
       // Check the labels:
       var labels = Util.getYLabels(axis + 1);
-      assertEquals("Expected label not found.", gridlines[axis][i], labels[i]);
+      assert.equal(gridlines[axis][i], labels[i], "Expected label not found.");
 
       // Check that the grid was drawn.
       y = halfDown(g.toDomYCoord(gridlines[axis][i], axis));
@@ -81,9 +81,9 @@ GridPerAxisTestCase.prototype.testIndependentGrids = function() {
       CanvasAssertions.assertLineDrawn(htx, p1, p2, attrs);
     }
   }
-};
+});
 
-GridPerAxisTestCase.prototype.testPerAxisGridColors = function() {
+it('testPerAxisGridColors', function() {
   var opts = {
     width : 480,
     height : 320,
@@ -114,7 +114,7 @@ GridPerAxisTestCase.prototype.testPerAxisGridColors = function() {
       [ 5, 110, 333 ] ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   // The expected gridlines
   var yGridlines = [ 20, 40, 60, 80, 100, 120 ];
@@ -136,12 +136,12 @@ GridPerAxisTestCase.prototype.testPerAxisGridColors = function() {
     for ( var i = 0; i < gridlines[axis].length; i++) {
       y = halfDown(g.toDomYCoord(gridlines[axis][i], axis));
       // Check the grid colors.
-      assertEquals("Unexpected grid color found at pixel: x: " + x + "y: " + y,
-          gridColors[axis], Util.samplePixel(g.hidden_, x, y));
+      assert.deepEqual(gridColors[axis], Util.samplePixel(g.hidden_, x, y),
+          "Unexpected grid color found at pixel: x: " + x + "y: " + y);
     }
   }
-};
-GridPerAxisTestCase.prototype.testPerAxisGridWidth = function() {
+});
+it('testPerAxisGridWidth', function() {
   var opts = {
     width : 480,
     height : 320,
@@ -174,7 +174,7 @@ GridPerAxisTestCase.prototype.testPerAxisGridWidth = function() {
       [ 5, 110, 333 ] ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   // The expected gridlines
   var yGridlines = [ 20, 40, 60, 80 ];
@@ -208,24 +208,16 @@ GridPerAxisTestCase.prototype.testPerAxisGridWidth = function() {
       // Check the grid width.
       switch (axis) {
       case 0: // y with 2 pixels width
-        assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: "
-            + y, emptyColor, drawnPixeldown2);
-        assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: "
-            + y, gridColor, drawnPixeldown1);
-        assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: "
-            + y, gridColor, drawnPixel);
-        assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: "
-            + y, gridColor, drawnPixelup1);
-        assertEquals("Unexpected y-grid color found at pixel: x: " + x + "y: "
-            + y, emptyColor, drawnPixelup2);
+        assert.deepEqual(emptyColor, drawnPixeldown2, "Unexpected y-grid color found at pixel: x: " + x + "y: " + y);
+        assert.deepEqual(gridColor, drawnPixeldown1, "Unexpected y-grid color found at pixel: x: " + x + "y: " + y);
+        assert.deepEqual(gridColor, drawnPixel, "Unexpected y-grid color found at pixel: x: " + x + "y: " + y);
+        assert.deepEqual(gridColor, drawnPixelup1, "Unexpected y-grid color found at pixel: x: " + x + "y: " + y);
+        assert.deepEqual(emptyColor, drawnPixelup2, "Unexpected y-grid color found at pixel: x: " + x + "y: " + y);
         break;
       case 1: // y2 with 1 pixel width
-        assertEquals("Unexpected y2-grid color found at pixel: x: " + x + "y: "
-            + y, emptyColor, drawnPixeldown1);
-        assertEquals("Unexpected y2-grid color found at pixel: x: " + x + "y: "
-            + y, gridColor, drawnPixel);
-        assertEquals("Unexpected y2-grid color found at pixel: x: " + x + "y: "
-            + y, emptyColor, drawnPixelup1);
+        assert.deepEqual(emptyColor, drawnPixeldown1, "Unexpected y2-grid color found at pixel: x: " + x + "y: " + y);
+        assert.deepEqual(gridColor, drawnPixel, "Unexpected y2-grid color found at pixel: x: " + x + "y: " + y);
+        assert.deepEqual(emptyColor, drawnPixelup1, "Unexpected y2-grid color found at pixel: x: " + x + "y: " + y);
         break;
       }
     }
@@ -235,24 +227,24 @@ GridPerAxisTestCase.prototype.testPerAxisGridWidth = function() {
   y = halfDown(g.plotter_.area.y) + 10;
   for ( var i = 0; i < xGridlines.length; i++) {
     x = halfUp(g.toDomXCoord(xGridlines[i]));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        emptyColor, Util.samplePixel(g.hidden_, x - 4, y).slice(0, 3));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        gridColor, Util.samplePixel(g.hidden_, x - 3, y).slice(0, 3));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        gridColor, Util.samplePixel(g.hidden_, x - 2, y).slice(0, 3));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        gridColor, Util.samplePixel(g.hidden_, x - 1, y).slice(0, 3));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        gridColor, Util.samplePixel(g.hidden_, x, y).slice(0, 3));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        gridColor, Util.samplePixel(g.hidden_, x + 1, y).slice(0, 3));
-    assertEquals("Unexpected x-grid color found at pixel: x: " + x + "y: " + y,
-        emptyColor, Util.samplePixel(g.hidden_, x + 2, y).slice(0, 3));
+    assert.deepEqual(emptyColor, Util.samplePixel(g.hidden_, x - 4, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
+    assert.deepEqual(gridColor, Util.samplePixel(g.hidden_, x - 3, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
+    assert.deepEqual(gridColor, Util.samplePixel(g.hidden_, x - 2, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
+    assert.deepEqual(gridColor, Util.samplePixel(g.hidden_, x - 1, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
+    assert.deepEqual(gridColor, Util.samplePixel(g.hidden_, x, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
+    assert.deepEqual(gridColor, Util.samplePixel(g.hidden_, x + 1, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
+    assert.deepEqual(emptyColor, Util.samplePixel(g.hidden_, x + 2, y).slice(0, 3),
+                     "Unexpected x-grid color found at pixel: x: " + x + "y: " + y);
   }
-};
+});
 
-GridPerAxisTestCase.prototype.testGridLinePattern = function() {
+it('testGridLinePattern', function() {
   var opts = {
     width : 120,
     height : 320,
@@ -283,7 +275,7 @@ GridPerAxisTestCase.prototype.testGridLinePattern = function() {
       [ 5, 110, 333 ] ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   // The expected gridlines
   var yGridlines = [ 0, 20, 40, 60, 80, 100, 120 ];
@@ -309,14 +301,16 @@ GridPerAxisTestCase.prototype.testGridLinePattern = function() {
       var pattern = (Math.floor((x) / 10)) % 2;
       switch (pattern) {
       case 0: // fill
-        assertEquals("Unexpected filled grid-pattern color found at pixel: x: " + x + " y: "
-            + y, [ 0, 0, 255 ], drawnPixel);
+        assert.deepEqual([ 0, 0, 255 ], drawnPixel,
+                         "Unexpected filled grid-pattern color found at pixel: x: " + x + " y: " + y);
         break;
       case 1: // no fill
-        assertEquals("Unexpected empty grid-pattern color found at pixel: x: " + x + " y: "
-            + y, [ 0, 0, 0 ], drawnPixel);
+        assert.deepEqual([ 0, 0, 0 ], drawnPixel,
+                         "Unexpected empty grid-pattern color found at pixel: x: " + x + " y: " + y);
         break;
       }
     }
   }
-};
+});
+
+});
index c595065..cdf0ee4 100644 (file)
@@ -3,21 +3,21 @@
  *
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
-var hidpiTestCase = TestCase("hidpi");
+describe("hidpi", function() {
 
 var savePixelRatio;
-hidpiTestCase.prototype.setUp = function() {
+beforeEach(function() {
   savePixelRatio = window.devicePixelRatio;
   window.devicePixelRatio = 2;
 
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-hidpiTestCase.prototype.tearDown = function() {
+afterEach(function() {
   window.devicePixelRatio = savePixelRatio;
-};
+});
 
-hidpiTestCase.prototype.testDoesntCreateScrollbars = function() {
+it('testDoesntCreateScrollbars', function() {
   var sw = document.body.scrollWidth;
   var cw = document.body.clientWidth;
 
@@ -38,7 +38,9 @@ hidpiTestCase.prototype.testDoesntCreateScrollbars = function() {
   // Adding the graph shouldn't cause the width of the page to change.
   // (essentially, we're checking that we don't end up with a scrollbar)
   // See http://stackoverflow.com/a/2146905/388951
-  assertEquals(cw, document.body.clientWidth);
-  assertEquals(sw, document.body.scrollWidth);
-};
+  assert.equal(cw, document.body.clientWidth);
+  assert.equal(sw, document.body.scrollWidth);
+});
 
+
+});
index e36c20b..9ae4efc 100644 (file)
@@ -3,14 +3,14 @@
  *
  * @author konigsberg@google.com (Robert Konigsbrg)
  */
-var InteractionModelTestCase = TestCase("interaction-model");
+describe("interaction-model", function() {
 
-InteractionModelTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-InteractionModelTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 var data1 = "X,Y\n" +
     "20,-1\n" +
@@ -38,7 +38,8 @@ function getXLabels() {
   return ary;
 }
 
-InteractionModelTestCase.prototype.pan = function(g, xRange, yRange) {
+/*
+it('testPan', function() {
   var originalXRange = g.xAxisRange();
   var originalYRange = g.yAxisRange(0);
 
@@ -46,23 +47,24 @@ InteractionModelTestCase.prototype.pan = function(g, xRange, yRange) {
   DygraphOps.dispatchMouseMove(g, xRange[1], yRange[0]); // this is really necessary.
   DygraphOps.dispatchMouseUp(g, xRange[1], yRange[0]);
 
-  assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
-  // assertEqualsDelta(originalYRange, g.yAxisRange(0), 0.2); // Not true, it's something in the middle.
+  assert.closeTo(xRange, g.xAxisRange(), 0.2);
+  // assert.closeTo(originalYRange, g.yAxisRange(0), 0.2); // Not true, it's something in the middle.
 
   var midX = (xRange[1] - xRange[0]) / 2;
   DygraphOps.dispatchMouseDown(g, midX, yRange[0]);
   DygraphOps.dispatchMouseMove(g, midX, yRange[1]); // this is really necessary.
   DygraphOps.dispatchMouseUp(g, midX, yRange[1]);
 
-  assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
-  assertEqualsDelta(yRange, g.yAxisRange(0), 0.2);
-}
+  assert.closeTo(xRange, g.xAxisRange(), 0.2);
+  assert.closeTo(yRange, g.yAxisRange(0), 0.2);
+});
+*/
 
 /**
  * This tests that when changing the interaction model so pan is used instead
  * of zoom as the default behavior, a standard click method is still called.
  */
-InteractionModelTestCase.prototype.testClickCallbackIsCalled = function() {
+it('testClickCallbackIsCalled', function() {
   var clicked;
 
   var clickCallback = function(event, x) {
@@ -81,14 +83,14 @@ InteractionModelTestCase.prototype.testClickCallbackIsCalled = function() {
   DygraphOps.dispatchMouseMove_Point(g, 10, 10);
   DygraphOps.dispatchMouseUp_Point(g, 10, 10);
 
-  assertEquals(20, clicked);
-};
+  assert.equal(20, clicked);
+});
 
 /**
  * This tests that when changing the interaction model so pan is used instead
  * of zoom as the default behavior, a standard click method is still called.
  */
-InteractionModelTestCase.prototype.testClickCallbackIsCalledOnCustomPan = function() {
+it('testClickCallbackIsCalledOnCustomPan', function() {
   var clicked;
 
   var clickCallback = function(event, x) {
@@ -126,19 +128,19 @@ InteractionModelTestCase.prototype.testClickCallbackIsCalledOnCustomPan = functi
   DygraphOps.dispatchMouseMove_Point(g, 10, 10);
   DygraphOps.dispatchMouseUp_Point(g, 10, 10);
 
-  assertEquals(20, clicked);
-};
+  assert.equal(20, clicked);
+});
 
-InteractionModelTestCase.clickAt = function(g, x, y) {
+var clickAt = function(g, x, y) {
   DygraphOps.dispatchMouseDown(g, x, y);
   DygraphOps.dispatchMouseMove(g, x, y);
   DygraphOps.dispatchMouseUp(g, x, y);
-}
+};
 
 /**
  * This tests that clickCallback is still called with the nonInteractiveModel.
  */
-InteractionModelTestCase.prototype.testClickCallbackIsCalledWithNonInteractiveModel = function() {
+it('testClickCallbackIsCalledWithNonInteractiveModel', function() {
   var clicked;
 
   // TODO(danvk): also test pointClickCallback here.
@@ -160,13 +162,13 @@ InteractionModelTestCase.prototype.testClickCallbackIsCalledWithNonInteractiveMo
   DygraphOps.dispatchMouseMove_Point(g, 10, 10);
   DygraphOps.dispatchMouseUp_Point(g, 10, 10);
 
-  assertEquals(20, clicked);
-};
+  assert.equal(20, clicked);
+});
 
 /**
  * A sanity test to ensure pointClickCallback is called.
  */
-InteractionModelTestCase.prototype.testPointClickCallback = function() {
+it('testPointClickCallback', function() {
   var clicked;
   var g = new Dygraph(document.getElementById("graph"), data2, {
     pointClickCallback : function(event, point) {
@@ -174,16 +176,16 @@ InteractionModelTestCase.prototype.testPointClickCallback = function() {
     }
   });
 
-  InteractionModelTestCase.clickAt(g, 4, 40);
+  clickAt(g, 4, 40);
 
-  assertEquals(4, clicked.xval);
-  assertEquals(40, clicked.yval);
-};
+  assert.equal(4, clicked.xval);
+  assert.equal(40, clicked.yval);
+});
 
 /**
  * A sanity test to ensure pointClickCallback is not called when out of range.
  */
-InteractionModelTestCase.prototype.testNoPointClickCallbackWhenOffPoint = function() {
+it('testNoPointClickCallbackWhenOffPoint', function() {
   var clicked;
   var g = new Dygraph(document.getElementById("graph"), data2, {
     pointClickCallback : function(event, point) {
@@ -191,22 +193,22 @@ InteractionModelTestCase.prototype.testNoPointClickCallbackWhenOffPoint = functi
     }
   });
 
-  InteractionModelTestCase.clickAt(g, 5, 40);
+  clickAt(g, 5, 40);
 
-  assertUndefined(clicked);
-};
+  assert.isUndefined(clicked);
+});
 
 /**
  * Ensures pointClickCallback circle size is taken into account.
  */
-InteractionModelTestCase.prototype.testPointClickCallback_circleSize = function() {
+it('testPointClickCallback_circleSize', function() {
   // TODO(konigsberg): Implement.
-};
+});
 
 /**
  * Ensures that pointClickCallback is called prior to clickCallback
  */
-InteractionModelTestCase.prototype.testPointClickCallbackCalledPriorToClickCallback = function() {
+it('testPointClickCallbackCalledPriorToClickCallback', function() {
   var counter = 0;
   var pointClicked;
   var clicked;
@@ -221,16 +223,16 @@ InteractionModelTestCase.prototype.testPointClickCallbackCalledPriorToClickCallb
     }
   });
 
-  InteractionModelTestCase.clickAt(g, 4, 40);
-  assertEquals(1, pointClicked);
-  assertEquals(2, clicked);
-};
+  clickAt(g, 4, 40);
+  assert.equal(1, pointClicked);
+  assert.equal(2, clicked);
+});
 
 /**
  * Ensures that when there's no pointClickCallback, clicking on a point still calls
  * clickCallback
  */
-InteractionModelTestCase.prototype.testClickCallback_clickOnPoint = function() {
+it('testClickCallback_clickOnPoint', function() {
   var clicked;
   var g = new Dygraph(document.getElementById("graph"), data2, {
     clickCallback : function(event, point) {
@@ -238,43 +240,43 @@ InteractionModelTestCase.prototype.testClickCallback_clickOnPoint = function() {
     }
   });
 
-  InteractionModelTestCase.clickAt(g, 4, 40);
-  assertEquals(1, clicked);
-};
+  clickAt(g, 4, 40);
+  assert.equal(1, clicked);
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_none = function() {
+it('testIsZoomed_none', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
-  assertFalse(g.isZoomed());
-  assertFalse(g.isZoomed("x"));
-  assertFalse(g.isZoomed("y"));
-};
+  assert.isFalse(g.isZoomed());
+  assert.isFalse(g.isZoomed("x"));
+  assert.isFalse(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_x = function() {
+it('testIsZoomed_x', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   DygraphOps.dispatchMouseDown_Point(g, 100, 100);
   DygraphOps.dispatchMouseMove_Point(g, 130, 100);
   DygraphOps.dispatchMouseUp_Point(g, 130, 100);
 
-  assertTrue(g.isZoomed());
-  assertTrue(g.isZoomed("x"));
-  assertFalse(g.isZoomed("y"));
-};
+  assert.isTrue(g.isZoomed());
+  assert.isTrue(g.isZoomed("x"));
+  assert.isFalse(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_y = function() {
+it('testIsZoomed_y', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   DygraphOps.dispatchMouseDown_Point(g, 10, 10);
   DygraphOps.dispatchMouseMove_Point(g, 10, 30);
   DygraphOps.dispatchMouseUp_Point(g, 10, 30);
 
-  assertTrue(g.isZoomed());
-  assertFalse(g.isZoomed("x"));
-  assertTrue(g.isZoomed("y"));
-};
+  assert.isTrue(g.isZoomed());
+  assert.isFalse(g.isZoomed("x"));
+  assert.isTrue(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_both = function() {
+it('testIsZoomed_both', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   // Zoom x axis
@@ -288,52 +290,52 @@ InteractionModelTestCase.prototype.testIsZoomed_both = function() {
   DygraphOps.dispatchMouseUp_Point(g, 100, 130);
 
 
-  assertTrue(g.isZoomed());
-  assertTrue(g.isZoomed("x"));
-  assertTrue(g.isZoomed("y"));
-};
+  assert.isTrue(g.isZoomed());
+  assert.isTrue(g.isZoomed("x"));
+  assert.isTrue(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_updateOptions_none = function() {
+it('testIsZoomed_updateOptions_none', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   g.updateOptions({});
 
-  assertFalse(g.isZoomed());
-  assertFalse(g.isZoomed("x"));
-  assertFalse(g.isZoomed("y"));
-};
+  assert.isFalse(g.isZoomed());
+  assert.isFalse(g.isZoomed("x"));
+  assert.isFalse(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_updateOptions_x = function() {
+it('testIsZoomed_updateOptions_x', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   g.updateOptions({dateWindow: [-.5, .3]});
-  assertTrue(g.isZoomed());
-  assertTrue(g.isZoomed("x"));
-  assertFalse(g.isZoomed("y"));
-};
+  assert.isTrue(g.isZoomed());
+  assert.isTrue(g.isZoomed("x"));
+  assert.isFalse(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_updateOptions_y = function() {
+it('testIsZoomed_updateOptions_y', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   g.updateOptions({valueRange: [1, 10]});
 
-  assertTrue(g.isZoomed());
-  assertFalse(g.isZoomed("x"));
-  assertTrue(g.isZoomed("y"));
-};
+  assert.isTrue(g.isZoomed());
+  assert.isFalse(g.isZoomed("x"));
+  assert.isTrue(g.isZoomed("y"));
+});
 
-InteractionModelTestCase.prototype.testIsZoomed_updateOptions_both = function() {
+it('testIsZoomed_updateOptions_both', function() {
   var g = new Dygraph(document.getElementById("graph"), data2, {});
 
   g.updateOptions({dateWindow: [-1, 1], valueRange: [1, 10]});
 
-  assertTrue(g.isZoomed());
-  assertTrue(g.isZoomed("x"));
-  assertTrue(g.isZoomed("y"));
-};
+  assert.isTrue(g.isZoomed());
+  assert.isTrue(g.isZoomed("x"));
+  assert.isTrue(g.isZoomed("y"));
+});
 
 
-InteractionModelTestCase.prototype.testCorrectAxisValueRangeAfterUnzoom = function() {
+it('testCorrectAxisValueRangeAfterUnzoom', function() {
   var g = new Dygraph(document.getElementById("graph"),
       data2, {
         valueRange: [1, 50],
@@ -350,14 +352,14 @@ InteractionModelTestCase.prototype.testCorrectAxisValueRangeAfterUnzoom = functi
   DygraphOps.dispatchMouseDown_Point(g, 100, 100);
   DygraphOps.dispatchMouseMove_Point(g, 100, 130);
   DygraphOps.dispatchMouseUp_Point(g, 100, 130);
-  currentYAxisRange = g.yAxisRange();
-  currentXAxisRange = g.xAxisRange();
+  var currentYAxisRange = g.yAxisRange();
+  var currentXAxisRange = g.xAxisRange();
   
   //check that the range for the axis has changed
-  assertNotEquals(1, currentXAxisRange[0]);
-  assertNotEquals(10, currentXAxisRange[1]);
-  assertNotEquals(1, currentYAxisRange[0]);
-  assertNotEquals(50, currentYAxisRange[1]);
+  assert.notEqual(1, currentXAxisRange[0]);
+  assert.notEqual(10, currentXAxisRange[1]);
+  assert.notEqual(1, currentYAxisRange[0]);
+  assert.notEqual(50, currentYAxisRange[1]);
   
   // unzoom by doubleclick.  This is really the order in which a browser
   // generates events, and we depend on it.
@@ -370,16 +372,16 @@ InteractionModelTestCase.prototype.testCorrectAxisValueRangeAfterUnzoom = functi
   // check if range for y-axis was reset to original value 
   // TODO check if range for x-axis is correct. 
   // Currently not possible because dateRange is set to null and extremes are returned
-  newYAxisRange = g.yAxisRange();
-  assertEquals(1, newYAxisRange[0]);
-  assertEquals(50, newYAxisRange[1]);
-};
+  var newYAxisRange = g.yAxisRange();
+  assert.equal(1, newYAxisRange[0]);
+  assert.equal(50, newYAxisRange[1]);
+});
 
 /**
  * Ensures pointClickCallback is called when some points along the y-axis don't
  * exist.
  */
-InteractionModelTestCase.prototype.testPointClickCallback_missingData = function() {
+it('testPointClickCallback_missingData', function() {
 
   // There's a B-value at 2, but no A-value.
   var data =
@@ -397,8 +399,10 @@ InteractionModelTestCase.prototype.testPointClickCallback_missingData = function
     }
   });
 
-  InteractionModelTestCase.clickAt(g, 2, 110);
+  clickAt(g, 2, 110);
 
-  assertEquals(2, clicked.xval);
-  assertEquals(110, clicked.yval);
-};
+  assert.equal(2, clicked.xval);
+  assert.equal(110, clicked.yval);
+});
+
+});
index eb1c575..cc1ae00 100644 (file)
  */
 var ZERO_TO_FIFTY = [[ 10, 0 ] , [ 20, 50 ]];
 
-var MissingPointsTestCase = TestCase("missing-points");
+describe("missing-points", function() {
 
-MissingPointsTestCase._origFunc = Dygraph.getContext;
-MissingPointsTestCase.prototype.setUp = function() {
+var _origFunc = Dygraph.getContext;
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(MissingPointsTestCase._origFunc(canvas));
+    return new Proxy(_origFunc(canvas));
   }
-};
+});
 
-MissingPointsTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = MissingPointsTestCase._origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = _origFunc;
+});
 
-MissingPointsTestCase.prototype.testSeparatedPointsDontDraw = function() {
+it('testSeparatedPointsDontDraw', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(
       graph,
@@ -48,11 +48,11 @@ MissingPointsTestCase.prototype.testSeparatedPointsDontDraw = function() {
        [3, 12, 13]],
       { colors: ['red', 'blue']});
   var htx = g.hidden_ctx_;
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
-  assertEquals(0, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
-};
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(0, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+});
 
-MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded = function() {
+it('testSeparatedPointsDontDraw_expanded', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(
       graph,
@@ -64,14 +64,14 @@ MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded = function(
       { colors: ['blue']});
   var htx = g.hidden_ctx_;
 
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
   CanvasAssertions.assertLineDrawn(htx, [56, 275], [161, 212],
       { strokeStyle: '#0000ff', });
   CanvasAssertions.assertLineDrawn(htx, [370, 87], [475, 25],
       { strokeStyle: '#0000ff', });
-};
+});
 
-MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded_connected = function() {
+it('testSeparatedPointsDontDraw_expanded_connected', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(
       graph,
@@ -84,16 +84,16 @@ MissingPointsTestCase.prototype.testSeparatedPointsDontDraw_expanded_connected =
   var htx = g.hidden_ctx_;
   var num_lines = 0;
 
-  assertEquals(3, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  assert.equal(3, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [[56, 275], [161, 212], [370, 87], [475, 25]],
       { strokeStyle: '#0000ff' });
-};
+});
 
 /**
  * At the time of writing this test, the blue series is only points, and not lines.
  */
-MissingPointsTestCase.prototype.testConnectSeparatedPoints = function() {
+it('testConnectSeparatedPoints', function() {
   var g = new Dygraph(
     document.getElementById("graph"),
     [
@@ -113,21 +113,21 @@ MissingPointsTestCase.prototype.testConnectSeparatedPoints = function() {
 
   var htx = g.hidden_ctx_;
 
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [[56, 225], [223, 25], [391, 125]],
       { strokeStyle: '#0000ff' });
 
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [[140, 275], [307, 125], [475, 225]],
       { strokeStyle: '#ff0000' });
-};
+});
 
 /**
  * At the time of writing this test, the blue series is only points, and not lines.
  */
-MissingPointsTestCase.prototype.testConnectSeparatedPointsWithNan = function() {
+it('testConnectSeparatedPointsWithNan', function() {
   var g = new Dygraph(
     document.getElementById("graph"),
     "x,A,B  \n" +
@@ -149,16 +149,16 @@ MissingPointsTestCase.prototype.testConnectSeparatedPointsWithNan = function() {
   var htx = g.hidden_ctx_;
 
   // Red has two disconnected line segments
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
   CanvasAssertions.assertLineDrawn(htx, [102, 275], [195, 212], { strokeStyle: '#ff0000' });
   CanvasAssertions.assertLineDrawn(htx, [381, 87], [475, 25], { strokeStyle: '#ff0000' });
 
   // Blue's lines are consecutive, however.
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [[56, 244], [149, 181], [242, 118]],
       { strokeStyle: '#0000ff' });
-};
+});
 
 /* These lines contain awesome powa!
   var lines = CanvasAssertions.getLinesDrawn(htx, {strokeStyle: "#0000ff"});
@@ -168,7 +168,7 @@ MissingPointsTestCase.prototype.testConnectSeparatedPointsWithNan = function() {
   }
 */
 
-MissingPointsTestCase.prototype.testErrorBarsWithMissingPoints = function() {
+it('testErrorBarsWithMissingPoints', function() {
   var data = [
               [1, [2,1]],
               [2, [3,1]],
@@ -188,7 +188,7 @@ MissingPointsTestCase.prototype.testErrorBarsWithMissingPoints = function() {
 
   var htx = g.hidden_ctx_;
 
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
 
   var p0 = g.toDomCoords(data[0][0], data[0][1][0]);
   var p1 = g.toDomCoords(data[1][0], data[1][1][0]);
@@ -198,9 +198,9 @@ MissingPointsTestCase.prototype.testErrorBarsWithMissingPoints = function() {
       [p0, p1], { strokeStyle: '#ff0000' });
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [p2, p3], { strokeStyle: '#ff0000' });
-};
+});
 
-MissingPointsTestCase.prototype.testErrorBarsWithMissingPointsConnected = function() {
+it('testErrorBarsWithMissingPointsConnected', function() {
   var data = [
               [1, [null,1]],
               [2, [2,1]],
@@ -222,7 +222,7 @@ MissingPointsTestCase.prototype.testErrorBarsWithMissingPointsConnected = functi
 
   var htx = g.hidden_ctx_;
 
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
 
   var p1 = g.toDomCoords(data[1][0], data[1][1][0]);
   var p2 = g.toDomCoords(data[3][0], data[3][1][0]);
@@ -230,8 +230,8 @@ MissingPointsTestCase.prototype.testErrorBarsWithMissingPointsConnected = functi
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [p1, p2, p3],
       { strokeStyle: '#ff0000' });
-};
-MissingPointsTestCase.prototype.testCustomBarsWithMissingPoints = function() {
+});
+it('testCustomBarsWithMissingPoints', function() {
   var data = [
               [1, [1,2,3]],
               [2, [2,3,4]],
@@ -257,7 +257,7 @@ MissingPointsTestCase.prototype.testCustomBarsWithMissingPoints = function() {
 
   var htx = g.hidden_ctx_;
 
-  assertEquals(4, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(4, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
 
   var p0 = g.toDomCoords(data[0][0], data[0][1][1]);
   var p1 = g.toDomCoords(data[1][0], data[1][1][1]);
@@ -274,9 +274,9 @@ MissingPointsTestCase.prototype.testCustomBarsWithMissingPoints = function() {
   p0 = g.toDomCoords(data[9][0], data[9][1][1]);
   p1 = g.toDomCoords(data[10][0], data[10][1][1]);
   CanvasAssertions.assertLineDrawn(htx, p0, p1, { strokeStyle: '#ff0000' });
-};
+});
 
-MissingPointsTestCase.prototype.testCustomBarsWithMissingPointsConnected = function() {
+it('testCustomBarsWithMissingPointsConnected', function() {
   var data = [
               [1, [1,null,1]],
               [2, [1,2,3]],
@@ -298,7 +298,7 @@ MissingPointsTestCase.prototype.testCustomBarsWithMissingPointsConnected = funct
 
   var htx = g.hidden_ctx_;
 
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
 
   var p1 = g.toDomCoords(data[1][0], data[1][1][1]);
   var p2 = g.toDomCoords(data[3][0], data[3][1][1]);
@@ -306,9 +306,9 @@ MissingPointsTestCase.prototype.testCustomBarsWithMissingPointsConnected = funct
   CanvasAssertions.assertConsecutiveLinesDrawn(htx,
       [p1, p2, p3],
       { strokeStyle: '#ff0000' });
-};
+});
 
-MissingPointsTestCase.prototype.testLeftBoundaryWithMisingPoints = function() {
+it('testLeftBoundaryWithMisingPoints', function() {
   var data = [
               [1, null, 3],
               [2, 1, null],
@@ -327,26 +327,26 @@ MissingPointsTestCase.prototype.testLeftBoundaryWithMisingPoints = function() {
     }
   );
   g.updateOptions({ dateWindow : [ 2.5, 4.5 ] });
-  assertEquals(1, g.getLeftBoundary_(0));
-  assertEquals(0, g.getLeftBoundary_(1));
+  assert.equal(1, g.getLeftBoundary_(0));
+  assert.equal(0, g.getLeftBoundary_(1));
 
   var domX = g.toDomXCoord(1.9);
   var closestRow = g.findClosestRow(domX);
-  assertEquals(1, closestRow);
+  assert.equal(1, closestRow);
 
   g.setSelection(closestRow);
-  assertEquals(1, g.selPoints_.length);
-  assertEquals(1, g.selPoints_[0].yval);
+  assert.equal(1, g.selPoints_.length);
+  assert.equal(1, g.selPoints_[0].yval);
 
   g.setSelection(3);
-  assertEquals(2, g.selPoints_.length);
-  assertEquals(g.selPoints_[0].xval, g.selPoints_[1].xval);
-  assertEquals(2, g.selPoints_[0].yval);
-  assertEquals(1, g.selPoints_[1].yval);
-};
+  assert.equal(2, g.selPoints_.length);
+  assert.equal(g.selPoints_[0].xval, g.selPoints_[1].xval);
+  assert.equal(2, g.selPoints_[0].yval);
+  assert.equal(1, g.selPoints_[1].yval);
+});
 
 // Regression test for issue #411
-MissingPointsTestCase.prototype.testEmptySeries = function() {
+it('testEmptySeries', function() {
   var graphDiv = document.getElementById("graph");
   var g = new Dygraph(
        graphDiv,
@@ -366,11 +366,11 @@ MissingPointsTestCase.prototype.testEmptySeries = function() {
        });
 
   g.setSelection(6);
-  assertEquals("1381134466: Series 2: 94", Util.getLegend(graphDiv));
-};
+  assert.equal("1381134466: Series 2: 94", Util.getLegend(graphDiv));
+});
 
 // Regression test for issue #485
-MissingPointsTestCase.prototype.testMissingFill = function() {
+it('testMissingFill', function() {
   var graphDiv = document.getElementById("graph");
   var N = null;
   var g = new Dygraph(
@@ -396,8 +396,10 @@ MissingPointsTestCase.prototype.testMissingFill = function() {
     var call = htx.calls__[i];
     if ((call.name == 'moveTo' || call.name == 'lineTo') && call.args) {
       for (var j = 0; j < call.args.length; j++) {
-        assertFalse(isNaN(call.args[j]));
+        assert.isFalse(isNaN(call.args[j]));
       }
     }
   }
-};
+});
+
+});
index 764c402..c6b8d4f 100644 (file)
@@ -3,14 +3,14 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var MultiCsvTestCase = TestCase("multi-csv");
+describe("multi-csv", function() {
 
-MultiCsvTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-MultiCsvTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 function getXLabels() {
   var x_labels = document.getElementsByClassName("dygraph-axis-label-x");
@@ -21,7 +21,7 @@ function getXLabels() {
   return ary;
 }
 
-MultiCsvTestCase.prototype.testOneCSV = function() {
+it('testOneCSV', function() {
   var opts = {
     width: 480,
     height: 320
@@ -36,10 +36,10 @@ MultiCsvTestCase.prototype.testOneCSV = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['0', '1', '2'], getXLabels());
-};
+  assert.deepEqual(['0', '1', '2'], getXLabels());
+});
 
-MultiCsvTestCase.prototype.testTwoCSV = function() {
+it('testTwoCSV', function() {
   var opts = {
     width: 480,
     height: 320
@@ -54,9 +54,11 @@ MultiCsvTestCase.prototype.testTwoCSV = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(['0', '1', '2'], getXLabels());
+  assert.deepEqual(['0', '1', '2'], getXLabels());
 
   g.updateOptions({file: data});
 
-  assertEquals(['0', '1', '2'], getXLabels());
-};
+  assert.deepEqual(['0', '1', '2'], getXLabels());
+});
+
+});
index 5b544be..3935b52 100644 (file)
@@ -4,13 +4,13 @@
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
 
-var MultipleAxesTestCase = TestCase("multiple-axes-tests");
+describe("multiple-axes-tests", function() {
 
-MultipleAxesTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-MultipleAxesTestCase.getData = function() {
+var getData = function() {
   var data = [];
   for (var i = 1; i <= 100; i++) {
     var m = "01", d = i;
@@ -28,8 +28,8 @@ MultipleAxesTestCase.getData = function() {
   return data;
 };
 
-MultipleAxesTestCase.prototype.testBasicMultipleAxes = function() {
-  var data = MultipleAxesTestCase.getData();
+it('testBasicMultipleAxes', function() {
+  var data = getData();
 
   var g = new Dygraph(
     document.getElementById("graph"),
@@ -55,11 +55,11 @@ MultipleAxesTestCase.prototype.testBasicMultipleAxes = function() {
     }
   );
 
-  assertEquals(["0","20","40","60","80","100"], Util.getYLabels("1"));
-  assertEquals(["900K","1.12M","1.34M","1.55M","1.77M","1.99M"], Util.getYLabels("2"));
-};
+  assert.deepEqual(["0","20","40","60","80","100"], Util.getYLabels("1"));
+  assert.deepEqual(["900K","1.12M","1.34M","1.55M","1.77M","1.99M"], Util.getYLabels("2"));
+});
 
-MultipleAxesTestCase.prototype.testTwoAxisVisibility = function() {
+it('testTwoAxisVisibility', function() {
   var data = [];
   data.push([0,0,0]);
   data.push([1,2,2000]);
@@ -83,25 +83,25 @@ MultipleAxesTestCase.prototype.testTwoAxisVisibility = function() {
     }
   );
 
-  assertTrue(document.getElementsByClassName("dygraph-axis-label-y").length > 0);
-  assertTrue(document.getElementsByClassName("dygraph-axis-label-y2").length > 0);
+  assert.isTrue(document.getElementsByClassName("dygraph-axis-label-y").length > 0);
+  assert.isTrue(document.getElementsByClassName("dygraph-axis-label-y2").length > 0);
 
   g.setVisibility(0, false);
 
-  assertTrue(document.getElementsByClassName("dygraph-axis-label-y").length > 0);
-  assertTrue(document.getElementsByClassName("dygraph-axis-label-y2").length > 0);
+  assert.isTrue(document.getElementsByClassName("dygraph-axis-label-y").length > 0);
+  assert.isTrue(document.getElementsByClassName("dygraph-axis-label-y2").length > 0);
 
   g.setVisibility(0, true);
   g.setVisibility(1, false);
 
-  assertTrue(document.getElementsByClassName("dygraph-axis-label-y").length > 0);
-  assertTrue(document.getElementsByClassName("dygraph-axis-label-y2").length > 0);
-};
+  assert.isTrue(document.getElementsByClassName("dygraph-axis-label-y").length > 0);
+  assert.isTrue(document.getElementsByClassName("dygraph-axis-label-y2").length > 0);
+});
 
 // verifies that all four chart labels (title, x-, y-, y2-axis label) can be
 // used simultaneously.
-MultipleAxesTestCase.prototype.testMultiChartLabels = function() {
-  var data = MultipleAxesTestCase.getData();
+it('testMultiChartLabels', function() {
+  var data = getData();
 
   var el = document.getElementById("graph");
   el.style.border = '1px solid black';
@@ -130,22 +130,22 @@ MultipleAxesTestCase.prototype.testMultiChartLabels = function() {
     }
   );
 
-  assertEquals(["Chart title", "x-axis", "y-axis", "y2-axis"],
+  assert.deepEqual(["Chart title", "x-axis", "y-axis", "y2-axis"],
                Util.getClassTexts("dygraph-label"));
-  assertEquals(["Chart title"], Util.getClassTexts("dygraph-title"));
-  assertEquals(["x-axis"], Util.getClassTexts("dygraph-xlabel"));
-  assertEquals(["y-axis"], Util.getClassTexts("dygraph-ylabel"));
-  assertEquals(["y2-axis"], Util.getClassTexts("dygraph-y2label"));
+  assert.deepEqual(["Chart title"], Util.getClassTexts("dygraph-title"));
+  assert.deepEqual(["x-axis"], Util.getClassTexts("dygraph-xlabel"));
+  assert.deepEqual(["y-axis"], Util.getClassTexts("dygraph-ylabel"));
+  assert.deepEqual(["y2-axis"], Util.getClassTexts("dygraph-y2label"));
 
   // TODO(danvk): check relative positioning here: title on top, y left of y2.
-};
+});
 
 // Check that a chart w/o a secondary y-axis will not get a y2label, even if one
 // is specified.
-MultipleAxesTestCase.prototype.testNoY2LabelWithoutSecondaryAxis = function() {
+it('testNoY2LabelWithoutSecondaryAxis', function() {
   var g = new Dygraph(
     document.getElementById("graph"),
-    MultipleAxesTestCase.getData(),
+    getData(),
     {
       labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
       width: 640,
@@ -157,18 +157,18 @@ MultipleAxesTestCase.prototype.testNoY2LabelWithoutSecondaryAxis = function() {
     }
   );
 
-  assertEquals(["Chart title", "x-axis", "y-axis"],
+  assert.deepEqual(["Chart title", "x-axis", "y-axis"],
                Util.getClassTexts("dygraph-label"));
-  assertEquals(["Chart title"], Util.getClassTexts("dygraph-title"));
-  assertEquals(["x-axis"], Util.getClassTexts("dygraph-xlabel"));
-  assertEquals(["y-axis"], Util.getClassTexts("dygraph-ylabel"));
-  assertEquals([], Util.getClassTexts("dygraph-y2label"));
-};
+  assert.deepEqual(["Chart title"], Util.getClassTexts("dygraph-title"));
+  assert.deepEqual(["x-axis"], Util.getClassTexts("dygraph-xlabel"));
+  assert.deepEqual(["y-axis"], Util.getClassTexts("dygraph-ylabel"));
+  assert.deepEqual([], Util.getClassTexts("dygraph-y2label"));
+});
 
-MultipleAxesTestCase.prototype.testValueRangePerAxisOptions = function() {
-  var data = MultipleAxesTestCase.getData();
+it('testValueRangePerAxisOptions', function() {
+  var data = getData();
 
-  g = new Dygraph(
+  var g = new Dygraph(
     document.getElementById("graph"),
     data,
     {
@@ -195,8 +195,8 @@ MultipleAxesTestCase.prototype.testValueRangePerAxisOptions = function() {
       y2label: 'Secondary y-axis',
     }
   );
-  assertEquals(["40", "45", "50", "55", "60", "65"], Util.getYLabels("1"));
-  assertEquals(["900K","1.1M","1.3M","1.5M","1.7M","1.9M"], Util.getYLabels("2"));
+  assert.deepEqual(["40", "45", "50", "55", "60", "65"], Util.getYLabels("1"));
+  assert.deepEqual(["900K","1.1M","1.3M","1.5M","1.7M","1.9M"], Util.getYLabels("2"));
   
   g.updateOptions(
     {
@@ -210,12 +210,12 @@ MultipleAxesTestCase.prototype.testValueRangePerAxisOptions = function() {
      }
     }
   );
-  assertEquals(["40", "45", "50", "55", "60", "65", "70", "75"], Util.getYLabels("1"));
-  assertEquals(["1M", "1.02M", "1.05M", "1.08M", "1.1M", "1.13M", "1.15M", "1.18M"], Util.getYLabels("2"));
-};
+  assert.deepEqual(["40", "45", "50", "55", "60", "65", "70", "75"], Util.getYLabels("1"));
+  assert.deepEqual(["1M", "1.02M", "1.05M", "1.08M", "1.1M", "1.13M", "1.15M", "1.18M"], Util.getYLabels("2"));
+});
 
-MultipleAxesTestCase.prototype.testDrawPointCallback = function() {
-  var data = MultipleAxesTestCase.getData();
+it('testDrawPointCallback', function() {
+  var data = getData();
 
   var results = { y : {}, y2 : {}};
   var firstCallback = function(g, seriesName, ctx, canvasx, canvasy, color, radius) {
@@ -228,7 +228,7 @@ MultipleAxesTestCase.prototype.testDrawPointCallback = function() {
     Dygraph.Circles.DEFAULT(g, seriesName, ctx, canvasx, canvasy, color, radius);
   };
 
-  g = new Dygraph(
+  var g = new Dygraph(
     document.getElementById("graph"),
     data,
     {
@@ -252,19 +252,19 @@ MultipleAxesTestCase.prototype.testDrawPointCallback = function() {
     }
   );
 
-  assertEquals(1, results.y["Y1"]);
-  assertEquals(1, results.y["Y2"]);
-  assertEquals(1, results.y2["Y3"]);
-  assertEquals(1, results.y2["Y4"]);
-};
+  assert.equal(1, results.y["Y1"]);
+  assert.equal(1, results.y["Y2"]);
+  assert.equal(1, results.y2["Y3"]);
+  assert.equal(1, results.y2["Y4"]);
+});
 
 // Test for http://code.google.com/p/dygraphs/issues/detail?id=436
-MultipleAxesTestCase.prototype.testRemovingSecondAxis = function() {
-  var data = MultipleAxesTestCase.getData();
+it('testRemovingSecondAxis', function() {
+  var data = getData();
 
   var results = { y : {}, y2 : {}};
 
-  g = new Dygraph(
+  var g = new Dygraph(
     document.getElementById("graph"),
     data,
     {
@@ -280,4 +280,6 @@ MultipleAxesTestCase.prototype.testRemovingSecondAxis = function() {
   );
 
  g.updateOptions({ series : { Y4 : { axis : 'y' } } });
-};
+});
+
+});
index 1fde9ef..323c84e 100644 (file)
@@ -5,16 +5,16 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var noHoursTestCase = TestCase("no-hours");
+describe("no-hours", function() {
 
-noHoursTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-noHoursTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-noHoursTestCase.prototype.testNoHours = function() {
+it('testNoHours', function() {
   var opts = {
     width: 480,
     height: 320
@@ -30,19 +30,19 @@ noHoursTestCase.prototype.testNoHours = function() {
   var g = new Dygraph(graph, data, opts);
 
   g.setSelection(0);
-  assertEquals("2012/03/13: Y: -1", Util.getLegend());
+  assert.equal("2012/03/13: Y: -1", Util.getLegend());
 
   g.setSelection(1);
-  assertEquals("2012/03/14: Y: 0", Util.getLegend());
+  assert.equal("2012/03/14: Y: 0", Util.getLegend());
 
   g.setSelection(2);
-  assertEquals("2012/03/15: Y: 1", Util.getLegend());
+  assert.equal("2012/03/15: Y: 1", Util.getLegend());
 
   g.setSelection(3);
-  assertEquals("2012/03/16: Y: 0", Util.getLegend());
-};
+  assert.equal("2012/03/16: Y: 0", Util.getLegend());
+});
 
-noHoursTestCase.prototype.testNoHoursDashed = function() {
+it('testNoHoursDashed', function() {
   var opts = {
     width: 480,
     height: 320
@@ -58,15 +58,17 @@ noHoursTestCase.prototype.testNoHoursDashed = function() {
   var g = new Dygraph(graph, data, opts);
 
   g.setSelection(0);
-  assertEquals("2012/03/13: Y: -1", Util.getLegend());
+  assert.equal("2012/03/13: Y: -1", Util.getLegend());
 
   g.setSelection(1);
-  assertEquals("2012/03/14: Y: 0", Util.getLegend());
+  assert.equal("2012/03/14: Y: 0", Util.getLegend());
 
   g.setSelection(2);
-  assertEquals("2012/03/15: Y: 1", Util.getLegend());
+  assert.equal("2012/03/15: Y: 1", Util.getLegend());
 
   g.setSelection(3);
-  assertEquals("2012/03/16: Y: 0", Util.getLegend());
-};
+  assert.equal("2012/03/16: Y: 0", Util.getLegend());
+});
 
+
+});
index 737e4b5..27707d4 100644 (file)
@@ -6,13 +6,13 @@
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
 
-var NumericTickerTestCase = TestCase("numeric-ticker-tests");
+describe("numeric-ticker-tests", function() {
 
-NumericTickerTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-NumericTickerTestCase.prototype.createOptionsViewForAxis = function(axis, dict) {
+var createOptionsViewForAxis = function(axis, dict) {
   return function (x) {
     if (dict && dict.hasOwnProperty(x)) {
       return dict[x];
@@ -28,9 +28,9 @@ NumericTickerTestCase.prototype.createOptionsViewForAxis = function(axis, dict)
   };
 };
 
-NumericTickerTestCase.prototype.testBasicNumericTicker = function() {
+it('testBasicNumericTicker', function() {
   var opts = {"logscale":null,"labelsKMG2":false,"labelsKMB":false};
-  var options = this.createOptionsViewForAxis('y', opts);
+  var options = createOptionsViewForAxis('y', opts);
 
   var ticks = Dygraph.numericTicks(-0.4, 4.4, 320, options);
   var expected_ticks = [
@@ -45,7 +45,7 @@ NumericTickerTestCase.prototype.testBasicNumericTicker = function() {
     {"v":3.5,"label":"3.5"},
     {"v":4,"label":"4"},
     {"v":4.5,"label":"4.5"}];
-  assertEquals(expected_ticks, ticks);
+  assert.deepEqual(expected_ticks, ticks);
 
   ticks = Dygraph.numericTicks(1, 84, 540, options);
   var expected_ticks = [
@@ -68,122 +68,124 @@ NumericTickerTestCase.prototype.testBasicNumericTicker = function() {
     {"v":80,"label":"80"},
     {"v":85,"label":"85"}
   ];
-  assertEquals(expected_ticks, ticks);
-};
+  assert.deepEqual(expected_ticks, ticks);
+});
 
 /*
-NumericTickerTestCase.prototype.testAllNumericTickers = function() {
-  assertEquals([{"v":-0.5,"label":"-0.5"},{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"}], Dygraph.numericTicks(-0.4, 4.4, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-1.5,"label":"-1.5"},{"v":-1,"label":"-1"},{"v":-0.5,"label":"-0.5"},{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"}], Dygraph.numericTicks(-1.4157430939856124, 1.4157430939856124, 400, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-10,"label":"-10"},{"v":-8,"label":"-8"},{"v":-6,"label":"-6"},{"v":-4,"label":"-4"},{"v":-2,"label":"-2"},{"v":0,"label":"0"},{"v":2,"label":"2"},{"v":4,"label":"4"},{"v":6,"label":"6"},{"v":8,"label":"8"}], Dygraph.numericTicks(-10, 9.98046875, 400, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-200,"label":"-200"},{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(-101.10000000000001, 1100.1, 300, this.createOptionsViewForAxis('y',{"logscale":false,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-20,"label":"-20"},{"v":-10,"label":"-10"},{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"}], Dygraph.numericTicks(-11.687459005175139, 42.287459005175144, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-15,"label":"-15"},{"v":-10,"label":"-10"},{"v":-5,"label":"-5"},{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"}], Dygraph.numericTicks(-12, 12, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-15,"label":"-15"},{"v":-10,"label":"-10"},{"v":-5,"label":"-5"},{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"}], Dygraph.numericTicks(-13.19792086872138, 13.197062407353386, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-220,"label":"-220"},{"v":-200,"label":"-200"},{"v":-180,"label":"-180"},{"v":-160,"label":"-160"},{"v":-140,"label":"-140"},{"v":-120,"label":"-120"}], Dygraph.numericTicks(-220, -100, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-40,"label":"-40"},{"v":-20,"label":"-20"},{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"}], Dygraph.numericTicks(-32.8, 132.8, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-40,"label":"-40"},{"v":-30,"label":"-30"},{"v":-20,"label":"-20"},{"v":-10,"label":"-10"},{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(-34.309, 89.279, 400, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-60,"label":"-60"},{"v":-40,"label":"-40"},{"v":-20,"label":"-20"},{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"}], Dygraph.numericTicks(-60, 60, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":-60,"label":"-60"},{"v":-40,"label":"-40"},{"v":-20,"label":"-20"},{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"}], Dygraph.numericTicks(-60, 60, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.0001,"label":"1.00e-4"},{"v":0.0002,"label":"2.00e-4"},{"v":-17999999,"label":"3.00e-4"},{"v":0.0004,"label":"4.00e-4"},{"v":0.0005,"label":"5.00e-4"}], Dygraph.numericTicks(0, 0.00055, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":0},{"v":0.0001,"label":0.0001},{"v":0.0002,"label":0.0002},{"v":-17999999,"label":0.0003},{"v":0.0004,"label":0.0004},{"v":0.0005,"label":0.0005}], Dygraph.numericTicks(0, 0.00055, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.2,"label":"0.2"},{"v":0.4,"label":"0.4"},{"v":-17999999,"label":"0.6"},{"v":0.8,"label":"0.8"}], Dygraph.numericTicks(0, 1, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.2,"label":"0.2"},{"v":0.4,"label":"0.4"},{"v":-17999999,"label":"0.6"},{"v":0.8,"label":"0.8"}], Dygraph.numericTicks(0, 1, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.1,"label":"0.1"},{"v":0.2,"label":"0.2"},{"v":-17999999,"label":"0.3"},{"v":0.4,"label":"0.4"},{"v":0.5,"label":"0.5"},{"v":-17999999,"label":"0.6"},{"v":-17999999,"label":"0.7"},{"v":0.8,"label":"0.8"},{"v":0.9,"label":"0.9"},{"v":1,"label":"1"},{"v":1.1,"label":"1.1"},{"v":-17999998,"label":"1.2"}], Dygraph.numericTicks(0, 1.2, 400, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(0, 100, 400, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 104.53192180924043, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 109.9856877755916, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":2,"label":"2"},{"v":4,"label":"4"},{"v":6,"label":"6"},{"v":8,"label":"8"},{"v":10,"label":"10"}], Dygraph.numericTicks(0, 11, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 110, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 110, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 110, 350, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(0, 1100, 300, this.createOptionsViewForAxis('y',{"logscale":false,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":-17000000,"label":"1M"},{"v":-16000000,"label":"2M"},{"v":-15000000,"label":"3M"},{"v":-14000000,"label":"4M"},{"v":-13000000,"label":"5M"},{"v":-12000000,"label":"6M"},{"v":-11000000,"label":"7M"},{"v":-10000000,"label":"8M"},{"v":-9000000,"label":"9M"},{"v":-8000000,"label":"10M"}], Dygraph.numericTicks(0, 11000000, 480, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 119, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"}], Dygraph.numericTicks(0, 130.9, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"}], Dygraph.numericTicks(0, 131, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":-17998000,"label":"2000"},{"v":-17996000,"label":"4000"},{"v":-17994000,"label":"6000"},{"v":-17992000,"label":"8000"},{"v":-17990000,"label":"10000"},{"v":-17988000,"label":"12000"},{"v":-17986000,"label":"14000"},{"v":-17984000,"label":"16000"}], Dygraph.numericTicks(0, 16977.4, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"}], Dygraph.numericTicks(0, 2, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.2,"label":"0.2"},{"v":0.4,"label":"0.4"},{"v":-17999999,"label":"0.6"},{"v":0.8,"label":"0.8"},{"v":1,"label":"1"},{"v":-17999998,"label":"1.2"},{"v":-17999998,"label":"1.4"},{"v":1.6,"label":"1.6"},{"v":1.8,"label":"1.8"}], Dygraph.numericTicks(0, 2, 400, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"}], Dygraph.numericTicks(0, 2.2, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":-17800000,"label":"200K"},{"v":-17600000,"label":"400K"},{"v":-17400000,"label":"600K"},{"v":-17200000,"label":"800K"},{"v":-17000000,"label":"1M"},{"v":-16800000,"label":"1.2M"},{"v":-16600000,"label":"1.4M"},{"v":-16400000,"label":"1.6M"},{"v":-16200000,"label":"1.8M"},{"v":-16000000,"label":"2M"}], Dygraph.numericTicks(0, 2200000, 350, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":0,"label":"0"},{"v":50,"label":"50"},{"v":100,"label":"100"},{"v":150,"label":"150"},{"v":200,"label":"200"}], Dygraph.numericTicks(0, 249, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":500,"label":"500"},{"v":-17999000,"label":"1000"},{"v":1500,"label":"1500"},{"v":-17998000,"label":"2000"},{"v":2500,"label":"2500"}], Dygraph.numericTicks(0, 2747.9970998900817, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"},{"v":-17999000,"label":"1K"},{"v":1200,"label":"1.2K"},{"v":1400,"label":"1.4K"},{"v":1600,"label":"1.6K"},{"v":1800,"label":"1.8K"},{"v":-17998000,"label":"2K"},{"v":2200,"label":"2.2K"},{"v":2400,"label":"2.4K"},{"v":2600,"label":"2.6K"}], Dygraph.numericTicks(0, 2747.9970998900817, 480, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"},{"v":30,"label":"30"}], Dygraph.numericTicks(0, 32.698942321287205, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":-17500000,"label":"500000"},{"v":-17000000,"label":"1.00e+6"},{"v":-16500000,"label":"1.50e+6"},{"v":-16000000,"label":"2.00e+6"},{"v":-15500000,"label":"2.50e+6"},{"v":-15000000,"label":"3.00e+6"}], Dygraph.numericTicks(0, 3263100.6418021005, 480, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"},{"v":30,"label":"30"}], Dygraph.numericTicks(0, 33.16213467701236, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(0, 4, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"}], Dygraph.numericTicks(0, 4.4, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"},{"v":30,"label":"30"},{"v":35,"label":"35"},{"v":40,"label":"40"}], Dygraph.numericTicks(0, 42, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":0,"label":"0"},{"v":8,"label":"8"},{"v":16,"label":"16"},{"v":24,"label":"24"},{"v":32,"label":"32"},{"v":40,"label":"40"}], Dygraph.numericTicks(0, 42, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":0},{"v":8,"label":8},{"v":16,"label":16},{"v":24,"label":24},{"v":32,"label":32},{"v":40,"label":40}], Dygraph.numericTicks(0, 42, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":999982000000,"label":"1T"},{"v":1999982000000,"label":"2T"},{"v":2999982000000,"label":"3T"},{"v":3999982000000,"label":"4T"}], Dygraph.numericTicks(0, 4837851162214.3, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":0,"label":"0"},{"v":549755813888,"label":"512G"},{"v":1099511627776,"label":"1T"},{"v":1649267441664,"label":"1.5T"},{"v":2199023255552,"label":"2T"},{"v":2748779069440,"label":"2.5T"},{"v":3298534883328,"label":"3T"},{"v":3848290697216,"label":"3.5T"},{"v":4398046511104,"label":"4T"}], Dygraph.numericTicks(0, 4837851162214.3, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":0},{"v":549755813888,"label":"512G"},{"v":1099511627776,"label":"1T"},{"v":1649267441664,"label":"1.5T"},{"v":2199023255552,"label":"2T"},{"v":2748779069440,"label":"2.5T"},{"v":3298534883328,"label":"3T"},{"v":3848290697216,"label":"3.5T"},{"v":4398046511104,"label":"4T"}], Dygraph.numericTicks(0, 4837851162214.3, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":-17999000,"label":"1000"},{"v":-17998000,"label":"2000"},{"v":-17997000,"label":"3000"},{"v":-17996000,"label":"4000"},{"v":-17995000,"label":"5000"}], Dygraph.numericTicks(0, 5451.6, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":100,"label":"100"},{"v":200,"label":"200"},{"v":300,"label":"300"},{"v":400,"label":"400"},{"v":500,"label":"500"}], Dygraph.numericTicks(0, 550, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"}], Dygraph.numericTicks(0, 64.9, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":100,"label":"100"},{"v":200,"label":"200"},{"v":300,"label":"300"},{"v":400,"label":"400"},{"v":500,"label":"500"},{"v":600,"label":"600"}], Dygraph.numericTicks(0, 667.9, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(0, 7.7, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(0, 7.9347329768293005, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"}], Dygraph.numericTicks(0, 72.6, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"}], Dygraph.numericTicks(0, 99, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"}], Dygraph.numericTicks(0, 99, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(0, 99, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"}], Dygraph.numericTicks(0, 999, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0.000001,"label":"1.00e-6"},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":"1.00e-5"},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":0.0001,"label":"1.00e-4"},{"v":0.0002,"label":""},{"v":-17999999,"label":""},{"v":0.0004,"label":""},{"v":0.0005,"label":""},{"v":-17999999,"label":""},{"v":0.0007,"label":""},{"v":0.0008,"label":""},{"v":-17999999,"label":""},{"v":0.001,"label":"1.00e-3"},{"v":0.002,"label":""},{"v":0.003,"label":""},{"v":0.004,"label":""},{"v":0.005,"label":""},{"v":0.006,"label":""},{"v":0.007,"label":""},{"v":0.008,"label":""},{"v":-17999999,"label":""},{"v":0.01,"label":"0.01"},{"v":0.02,"label":""},{"v":0.03,"label":""},{"v":0.04,"label":""},{"v":0.05,"label":""},{"v":0.06,"label":""},{"v":0.07,"label":""},{"v":0.08,"label":""},{"v":0.09,"label":""},{"v":0.1,"label":"0.1"},{"v":0.2,"label":""},{"v":-17999999,"label":""},{"v":0.4,"label":""},{"v":0.5,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":0.8,"label":""},{"v":0.9,"label":""},{"v":1,"label":"1"},{"v":2,"label":""},{"v":3,"label":""},{"v":4,"label":""},{"v":5,"label":""},{"v":6,"label":""},{"v":7,"label":""},{"v":8,"label":""},{"v":9,"label":""},{"v":10,"label":"10"},{"v":20,"label":""},{"v":30,"label":""},{"v":40,"label":""},{"v":50,"label":""},{"v":60,"label":""},{"v":70,"label":""},{"v":80,"label":""},{"v":90,"label":""},{"v":100,"label":"100"},{"v":200,"label":""},{"v":300,"label":""},{"v":400,"label":""},{"v":500,"label":""},{"v":600,"label":""},{"v":700,"label":""},{"v":800,"label":""},{"v":900,"label":""},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(0.000001, 1099.9999999, 300, this.createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"}], Dygraph.numericTicks(0.6, 5.4, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(0.6373123361267239, 4.824406504982038, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(0.6373123361267239, 4.824406504982038, 353, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0.6000000000000001,"label":"0.6"},{"v":0.8,"label":"0.8"},{"v":1,"label":"1"},{"v":-17999998,"label":"1.2"},{"v":-17999998,"label":"1.4"},{"v":1.6,"label":"1.6"},{"v":-17999998,"label":"1.8"},{"v":2,"label":"2"},{"v":2.2,"label":"2.2"},{"v":-17999997,"label":"2.4"},{"v":2.6,"label":"2.6"},{"v":-17999997,"label":"2.8"},{"v":-17999997,"label":"3"},{"v":3.2,"label":"3.2"},{"v":-17999996,"label":"3.4"},{"v":3.6,"label":"3.6"},{"v":-17999996,"label":"3.8"},{"v":4,"label":"4"},{"v":4.2,"label":"4.2"},{"v":4.4,"label":"4.4"},{"v":4.6,"label":"4.6"},{"v":-17999995,"label":"4.8"}], Dygraph.numericTicks(0.6373123361267239, 4.824406504982038, 743, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(0.6386658954698001, 4.8095173522082, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"}], Dygraph.numericTicks(0.7101014279158788, 4.023726495301334, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(1, 109, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"}], Dygraph.numericTicks(1, 3, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(1, 4, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(1, 4, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(1, 5, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":1},{"v":1.5,"label":1.5},{"v":2,"label":2},{"v":2.5,"label":2.5},{"v":3,"label":3},{"v":3.5,"label":3.5},{"v":4,"label":4},{"v":4.5,"label":4.5}], Dygraph.numericTicks(1, 5, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"}], Dygraph.numericTicks(1, 6, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"}], Dygraph.numericTicks(1, 7, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"},{"v":8,"label":"8"}], Dygraph.numericTicks(1, 9, 300, this.createOptionsViewForAxis('y',{"logscale":false,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":""},{"v":7,"label":"7"},{"v":8,"label":""},{"v":9,"label":"9"}], Dygraph.numericTicks(1, 9, 300, this.createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"},{"v":8,"label":"8"}], Dygraph.numericTicks(1, 9, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":2,"label":"2"},{"v":4,"label":"4"},{"v":6,"label":"6"},{"v":8,"label":"8"},{"v":10,"label":"10"}], Dygraph.numericTicks(1.2, 10.8, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(1.2872947778969237, 4.765317192093838, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(1.5, 7.5, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"}], Dygraph.numericTicks(1.7999999999999998, 28.2, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":10,"label":"10"},{"v":10.1,"label":"10.1"},{"v":10.2,"label":"10.2"},{"v":10.3,"label":"10.3"},{"v":10.4,"label":"10.4"},{"v":10.5,"label":"10.5"},{"v":10.6,"label":"10.6"},{"v":10.7,"label":"10.7"},{"v":10.8,"label":"10.8"},{"v":10.9,"label":"10.9"}], Dygraph.numericTicks(10, 11, 480, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":100,"label":"100"},{"v":120,"label":"120"},{"v":140,"label":"140"},{"v":160,"label":"160"},{"v":180,"label":"180"}], Dygraph.numericTicks(100, 200, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":10000,"label":"10000"},{"v":-17988000,"label":"12000"},{"v":-17986000,"label":"14000"},{"v":-17984000,"label":"16000"},{"v":-17982000,"label":"18000"},{"v":-17980000,"label":"20000"},{"v":-17978000,"label":"22000"},{"v":-17976000,"label":"24000"},{"v":-17974000,"label":"26000"},{"v":-17972000,"label":"28000"},{"v":-17970000,"label":"30000"},{"v":-17968000,"label":"32000"},{"v":-17966000,"label":"34000"},{"v":-17964000,"label":"36000"}], Dygraph.numericTicks(10122.8, 36789.2, 480, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":11000,"label":"11000"},{"v":11200,"label":"11200"},{"v":11400,"label":"11400"},{"v":11600,"label":"11600"},{"v":11800,"label":"11800"},{"v":-17988000,"label":"12000"},{"v":12200,"label":"12200"},{"v":12400,"label":"12400"},{"v":12600,"label":"12600"},{"v":12800,"label":"12800"},{"v":-17987000,"label":"13000"},{"v":13200,"label":"13200"},{"v":13400,"label":"13400"}], Dygraph.numericTicks(11110.5, 13579.5, 480, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":162000,"label":"162000"},{"v":-17836000,"label":"164000"},{"v":-17834000,"label":"166000"},{"v":-17832000,"label":"168000"},{"v":-17830000,"label":"170000"},{"v":-17828000,"label":"172000"},{"v":-17826000,"label":"174000"},{"v":-17824000,"label":"176000"},{"v":-17822000,"label":"178000"}], Dygraph.numericTicks(163038.4, 179137.6, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(2, 4, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(2.6, 7.4, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(21.7, 97.3, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(21.7, 97.3, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(24, 96, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 20, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false,pixelsPerLabel:20})));
-  assertEquals([{"v":25,"label":"25"},{"v":30,"label":"30"},{"v":35,"label":"35"},{"v":40,"label":"40"},{"v":45,"label":"45"},{"v":50,"label":"50"},{"v":55,"label":"55"},{"v":60,"label":"60"},{"v":65,"label":"65"},{"v":70,"label":"70"},{"v":75,"label":"75"},{"v":80,"label":"80"},{"v":85,"label":"85"},{"v":90,"label":"90"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false,pixelsPerLabel:20})));
-  assertEquals([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(28.33333333333333, 88.33333333333334, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(3, 5, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":3000,"label":"3K"},{"v":2500,"label":"2.5K"},{"v":-17998000,"label":"2K"},{"v":1500,"label":"1.5K"},{"v":-17999000,"label":"1K"},{"v":500,"label":"500"}], Dygraph.numericTicks(3000, 0, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(33.11333333333334, 83.75333333333333, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(36.921241050119335, 88.32696897374701, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":50,"label":""},{"v":60,"label":"60"},{"v":70,"label":""},{"v":80,"label":""},{"v":90,"label":""},{"v":100,"label":"100"},{"v":200,"label":""},{"v":300,"label":"300"},{"v":400,"label":""},{"v":500,"label":""},{"v":600,"label":"600"},{"v":700,"label":""},{"v":800,"label":""},{"v":900,"label":""},{"v":-17999000,"label":"1000"},{"v":-17998000,"label":""},{"v":-17997000,"label":"3000"},{"v":-17996000,"label":""},{"v":-17995000,"label":""},{"v":-17994000,"label":"6000"},{"v":-17993000,"label":""},{"v":-17992000,"label":""},{"v":-17991000,"label":""},{"v":-17990000,"label":"10000"}], Dygraph.numericTicks(41.220000000000084, 15576.828000000018, 400, this.createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(44.5, 98.5, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":5,"label":"5"},{"v":6,"label":""},{"v":7,"label":""},{"v":8,"label":""},{"v":9,"label":""},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":""},{"v":40,"label":""},{"v":50,"label":"50"},{"v":60,"label":""},{"v":70,"label":""},{"v":80,"label":""},{"v":90,"label":""},{"v":100,"label":"100"},{"v":200,"label":"200"},{"v":300,"label":""},{"v":400,"label":""},{"v":500,"label":"500"},{"v":600,"label":""},{"v":700,"label":""},{"v":800,"label":""},{"v":900,"label":""},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(5, 1099.5, 300, this.createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":50,"label":"50"},{"v":55,"label":"55"},{"v":60,"label":"60"},{"v":65,"label":"65"},{"v":70,"label":"70"},{"v":75,"label":"75"},{"v":80,"label":"80"}], Dygraph.numericTicks(52.5, 82.5, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":68,"label":"68"},{"v":70,"label":"70"},{"v":72,"label":"72"},{"v":74,"label":"74"},{"v":76,"label":"76"},{"v":78,"label":"78"},{"v":80,"label":"80"}], Dygraph.numericTicks(69, 81, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":0,"label":"0"},{"v":-17980000,"label":"20K"},{"v":-17960000,"label":"40K"},{"v":-17940000,"label":"60K"},{"v":-17920000,"label":"80K"}], Dygraph.numericTicks(7921.099999999999, 81407.9, 240, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":8,"label":"8"},{"v":10,"label":"10"},{"v":12,"label":"12"},{"v":14,"label":"14"},{"v":16,"label":"16"},{"v":18,"label":"18"},{"v":20,"label":"20"}], Dygraph.numericTicks(9, 21, 300, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":8,"label":"8"},{"v":10,"label":"10"},{"v":12,"label":"12"},{"v":14,"label":"14"},{"v":16,"label":"16"},{"v":18,"label":"18"},{"v":20,"label":"20"}], Dygraph.numericTicks(9, 21, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":9,"label":"9"},{"v":10,"label":"10"},{"v":11,"label":"11"},{"v":12,"label":"12"},{"v":13,"label":"13"},{"v":14,"label":"14"},{"v":15,"label":"15"},{"v":16,"label":"16"},{"v":17,"label":"17"},{"v":18,"label":"18"}], Dygraph.numericTicks(9.2, 18.8, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"},{"v":140,"label":"140"},{"v":160,"label":"160"},{"v":180,"label":"180"},{"v":200,"label":"200"}], Dygraph.numericTicks(90, 210, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
-  assertEquals([{"v":95,"label":"95"},{"v":96,"label":"96"},{"v":97,"label":"97"},{"v":98,"label":"98"},{"v":99,"label":"99"},{"v":100,"label":"100"},{"v":101,"label":"101"},{"v":102,"label":"102"},{"v":103,"label":"103"},{"v":104,"label":"104"}], Dygraph.numericTicks(95.71121718377088, 104.23150357995226, 320, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-  assertEquals([{"v":950,"label":"950"},{"v":-17999000,"label":"1000"},{"v":1050,"label":"1050"},{"v":1100,"label":"1100"},{"v":1150,"label":"1150"},{"v":1200,"label":"1200"}], Dygraph.numericTicks(980.1, 1218.9, 200, this.createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
-};
+it('testAllNumericTickers', function() {
+  assert.deepEqual([{"v":-0.5,"label":"-0.5"},{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"}], Dygraph.numericTicks(-0.4, 4.4, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-1.5,"label":"-1.5"},{"v":-1,"label":"-1"},{"v":-0.5,"label":"-0.5"},{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"}], Dygraph.numericTicks(-1.4157430939856124, 1.4157430939856124, 400, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-10,"label":"-10"},{"v":-8,"label":"-8"},{"v":-6,"label":"-6"},{"v":-4,"label":"-4"},{"v":-2,"label":"-2"},{"v":0,"label":"0"},{"v":2,"label":"2"},{"v":4,"label":"4"},{"v":6,"label":"6"},{"v":8,"label":"8"}], Dygraph.numericTicks(-10, 9.98046875, 400, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-200,"label":"-200"},{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(-101.10000000000001, 1100.1, 300, createOptionsViewForAxis('y',{"logscale":false,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-20,"label":"-20"},{"v":-10,"label":"-10"},{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"}], Dygraph.numericTicks(-11.687459005175139, 42.287459005175144, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-15,"label":"-15"},{"v":-10,"label":"-10"},{"v":-5,"label":"-5"},{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"}], Dygraph.numericTicks(-12, 12, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-15,"label":"-15"},{"v":-10,"label":"-10"},{"v":-5,"label":"-5"},{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"}], Dygraph.numericTicks(-13.19792086872138, 13.197062407353386, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-220,"label":"-220"},{"v":-200,"label":"-200"},{"v":-180,"label":"-180"},{"v":-160,"label":"-160"},{"v":-140,"label":"-140"},{"v":-120,"label":"-120"}], Dygraph.numericTicks(-220, -100, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-40,"label":"-40"},{"v":-20,"label":"-20"},{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"}], Dygraph.numericTicks(-32.8, 132.8, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-40,"label":"-40"},{"v":-30,"label":"-30"},{"v":-20,"label":"-20"},{"v":-10,"label":"-10"},{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(-34.309, 89.279, 400, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-60,"label":"-60"},{"v":-40,"label":"-40"},{"v":-20,"label":"-20"},{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"}], Dygraph.numericTicks(-60, 60, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":-60,"label":"-60"},{"v":-40,"label":"-40"},{"v":-20,"label":"-20"},{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"}], Dygraph.numericTicks(-60, 60, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.0001,"label":"1.00e-4"},{"v":0.0002,"label":"2.00e-4"},{"v":-17999999,"label":"3.00e-4"},{"v":0.0004,"label":"4.00e-4"},{"v":0.0005,"label":"5.00e-4"}], Dygraph.numericTicks(0, 0.00055, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":0},{"v":0.0001,"label":0.0001},{"v":0.0002,"label":0.0002},{"v":-17999999,"label":0.0003},{"v":0.0004,"label":0.0004},{"v":0.0005,"label":0.0005}], Dygraph.numericTicks(0, 0.00055, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.2,"label":"0.2"},{"v":0.4,"label":"0.4"},{"v":-17999999,"label":"0.6"},{"v":0.8,"label":"0.8"}], Dygraph.numericTicks(0, 1, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.2,"label":"0.2"},{"v":0.4,"label":"0.4"},{"v":-17999999,"label":"0.6"},{"v":0.8,"label":"0.8"}], Dygraph.numericTicks(0, 1, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.1,"label":"0.1"},{"v":0.2,"label":"0.2"},{"v":-17999999,"label":"0.3"},{"v":0.4,"label":"0.4"},{"v":0.5,"label":"0.5"},{"v":-17999999,"label":"0.6"},{"v":-17999999,"label":"0.7"},{"v":0.8,"label":"0.8"},{"v":0.9,"label":"0.9"},{"v":1,"label":"1"},{"v":1.1,"label":"1.1"},{"v":-17999998,"label":"1.2"}], Dygraph.numericTicks(0, 1.2, 400, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(0, 100, 400, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 104.53192180924043, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 109.9856877755916, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":2,"label":"2"},{"v":4,"label":"4"},{"v":6,"label":"6"},{"v":8,"label":"8"},{"v":10,"label":"10"}], Dygraph.numericTicks(0, 11, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 110, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 110, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 110, 350, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(0, 1100, 300, createOptionsViewForAxis('y',{"logscale":false,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":-17000000,"label":"1M"},{"v":-16000000,"label":"2M"},{"v":-15000000,"label":"3M"},{"v":-14000000,"label":"4M"},{"v":-13000000,"label":"5M"},{"v":-12000000,"label":"6M"},{"v":-11000000,"label":"7M"},{"v":-10000000,"label":"8M"},{"v":-9000000,"label":"9M"},{"v":-8000000,"label":"10M"}], Dygraph.numericTicks(0, 11000000, 480, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(0, 119, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"}], Dygraph.numericTicks(0, 130.9, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"}], Dygraph.numericTicks(0, 131, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":-17998000,"label":"2000"},{"v":-17996000,"label":"4000"},{"v":-17994000,"label":"6000"},{"v":-17992000,"label":"8000"},{"v":-17990000,"label":"10000"},{"v":-17988000,"label":"12000"},{"v":-17986000,"label":"14000"},{"v":-17984000,"label":"16000"}], Dygraph.numericTicks(0, 16977.4, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"}], Dygraph.numericTicks(0, 2, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.2,"label":"0.2"},{"v":0.4,"label":"0.4"},{"v":-17999999,"label":"0.6"},{"v":0.8,"label":"0.8"},{"v":1,"label":"1"},{"v":-17999998,"label":"1.2"},{"v":-17999998,"label":"1.4"},{"v":1.6,"label":"1.6"},{"v":1.8,"label":"1.8"}], Dygraph.numericTicks(0, 2, 400, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"}], Dygraph.numericTicks(0, 2.2, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":-17800000,"label":"200K"},{"v":-17600000,"label":"400K"},{"v":-17400000,"label":"600K"},{"v":-17200000,"label":"800K"},{"v":-17000000,"label":"1M"},{"v":-16800000,"label":"1.2M"},{"v":-16600000,"label":"1.4M"},{"v":-16400000,"label":"1.6M"},{"v":-16200000,"label":"1.8M"},{"v":-16000000,"label":"2M"}], Dygraph.numericTicks(0, 2200000, 350, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":50,"label":"50"},{"v":100,"label":"100"},{"v":150,"label":"150"},{"v":200,"label":"200"}], Dygraph.numericTicks(0, 249, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":500,"label":"500"},{"v":-17999000,"label":"1000"},{"v":1500,"label":"1500"},{"v":-17998000,"label":"2000"},{"v":2500,"label":"2500"}], Dygraph.numericTicks(0, 2747.9970998900817, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"},{"v":-17999000,"label":"1K"},{"v":1200,"label":"1.2K"},{"v":1400,"label":"1.4K"},{"v":1600,"label":"1.6K"},{"v":1800,"label":"1.8K"},{"v":-17998000,"label":"2K"},{"v":2200,"label":"2.2K"},{"v":2400,"label":"2.4K"},{"v":2600,"label":"2.6K"}], Dygraph.numericTicks(0, 2747.9970998900817, 480, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"},{"v":30,"label":"30"}], Dygraph.numericTicks(0, 32.698942321287205, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":-17500000,"label":"500000"},{"v":-17000000,"label":"1.00e+6"},{"v":-16500000,"label":"1.50e+6"},{"v":-16000000,"label":"2.00e+6"},{"v":-15500000,"label":"2.50e+6"},{"v":-15000000,"label":"3.00e+6"}], Dygraph.numericTicks(0, 3263100.6418021005, 480, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"},{"v":30,"label":"30"}], Dygraph.numericTicks(0, 33.16213467701236, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(0, 4, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"}], Dygraph.numericTicks(0, 4.4, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"},{"v":30,"label":"30"},{"v":35,"label":"35"},{"v":40,"label":"40"}], Dygraph.numericTicks(0, 42, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":8,"label":"8"},{"v":16,"label":"16"},{"v":24,"label":"24"},{"v":32,"label":"32"},{"v":40,"label":"40"}], Dygraph.numericTicks(0, 42, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":0},{"v":8,"label":8},{"v":16,"label":16},{"v":24,"label":24},{"v":32,"label":32},{"v":40,"label":40}], Dygraph.numericTicks(0, 42, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":999982000000,"label":"1T"},{"v":1999982000000,"label":"2T"},{"v":2999982000000,"label":"3T"},{"v":3999982000000,"label":"4T"}], Dygraph.numericTicks(0, 4837851162214.3, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":549755813888,"label":"512G"},{"v":1099511627776,"label":"1T"},{"v":1649267441664,"label":"1.5T"},{"v":2199023255552,"label":"2T"},{"v":2748779069440,"label":"2.5T"},{"v":3298534883328,"label":"3T"},{"v":3848290697216,"label":"3.5T"},{"v":4398046511104,"label":"4T"}], Dygraph.numericTicks(0, 4837851162214.3, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":0},{"v":549755813888,"label":"512G"},{"v":1099511627776,"label":"1T"},{"v":1649267441664,"label":"1.5T"},{"v":2199023255552,"label":"2T"},{"v":2748779069440,"label":"2.5T"},{"v":3298534883328,"label":"3T"},{"v":3848290697216,"label":"3.5T"},{"v":4398046511104,"label":"4T"}], Dygraph.numericTicks(0, 4837851162214.3, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":true,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":-17999000,"label":"1000"},{"v":-17998000,"label":"2000"},{"v":-17997000,"label":"3000"},{"v":-17996000,"label":"4000"},{"v":-17995000,"label":"5000"}], Dygraph.numericTicks(0, 5451.6, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":100,"label":"100"},{"v":200,"label":"200"},{"v":300,"label":"300"},{"v":400,"label":"400"},{"v":500,"label":"500"}], Dygraph.numericTicks(0, 550, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"}], Dygraph.numericTicks(0, 64.9, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":100,"label":"100"},{"v":200,"label":"200"},{"v":300,"label":"300"},{"v":400,"label":"400"},{"v":500,"label":"500"},{"v":600,"label":"600"}], Dygraph.numericTicks(0, 667.9, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(0, 7.7, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(0, 7.9347329768293005, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"}], Dygraph.numericTicks(0, 72.6, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"}], Dygraph.numericTicks(0, 99, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"}], Dygraph.numericTicks(0, 99, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(0, 99, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":200,"label":"200"},{"v":400,"label":"400"},{"v":600,"label":"600"},{"v":800,"label":"800"}], Dygraph.numericTicks(0, 999, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0.000001,"label":"1.00e-6"},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":"1.00e-5"},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":0.0001,"label":"1.00e-4"},{"v":0.0002,"label":""},{"v":-17999999,"label":""},{"v":0.0004,"label":""},{"v":0.0005,"label":""},{"v":-17999999,"label":""},{"v":0.0007,"label":""},{"v":0.0008,"label":""},{"v":-17999999,"label":""},{"v":0.001,"label":"1.00e-3"},{"v":0.002,"label":""},{"v":0.003,"label":""},{"v":0.004,"label":""},{"v":0.005,"label":""},{"v":0.006,"label":""},{"v":0.007,"label":""},{"v":0.008,"label":""},{"v":-17999999,"label":""},{"v":0.01,"label":"0.01"},{"v":0.02,"label":""},{"v":0.03,"label":""},{"v":0.04,"label":""},{"v":0.05,"label":""},{"v":0.06,"label":""},{"v":0.07,"label":""},{"v":0.08,"label":""},{"v":0.09,"label":""},{"v":0.1,"label":"0.1"},{"v":0.2,"label":""},{"v":-17999999,"label":""},{"v":0.4,"label":""},{"v":0.5,"label":""},{"v":-17999999,"label":""},{"v":-17999999,"label":""},{"v":0.8,"label":""},{"v":0.9,"label":""},{"v":1,"label":"1"},{"v":2,"label":""},{"v":3,"label":""},{"v":4,"label":""},{"v":5,"label":""},{"v":6,"label":""},{"v":7,"label":""},{"v":8,"label":""},{"v":9,"label":""},{"v":10,"label":"10"},{"v":20,"label":""},{"v":30,"label":""},{"v":40,"label":""},{"v":50,"label":""},{"v":60,"label":""},{"v":70,"label":""},{"v":80,"label":""},{"v":90,"label":""},{"v":100,"label":"100"},{"v":200,"label":""},{"v":300,"label":""},{"v":400,"label":""},{"v":500,"label":""},{"v":600,"label":""},{"v":700,"label":""},{"v":800,"label":""},{"v":900,"label":""},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(0.000001, 1099.9999999, 300, createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"}], Dygraph.numericTicks(0.6, 5.4, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(0.6373123361267239, 4.824406504982038, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(0.6373123361267239, 4.824406504982038, 353, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0.6000000000000001,"label":"0.6"},{"v":0.8,"label":"0.8"},{"v":1,"label":"1"},{"v":-17999998,"label":"1.2"},{"v":-17999998,"label":"1.4"},{"v":1.6,"label":"1.6"},{"v":-17999998,"label":"1.8"},{"v":2,"label":"2"},{"v":2.2,"label":"2.2"},{"v":-17999997,"label":"2.4"},{"v":2.6,"label":"2.6"},{"v":-17999997,"label":"2.8"},{"v":-17999997,"label":"3"},{"v":3.2,"label":"3.2"},{"v":-17999996,"label":"3.4"},{"v":3.6,"label":"3.6"},{"v":-17999996,"label":"3.8"},{"v":4,"label":"4"},{"v":4.2,"label":"4.2"},{"v":4.4,"label":"4.4"},{"v":4.6,"label":"4.6"},{"v":-17999995,"label":"4.8"}], Dygraph.numericTicks(0.6373123361267239, 4.824406504982038, 743, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(0.6386658954698001, 4.8095173522082, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0.5,"label":"0.5"},{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"}], Dygraph.numericTicks(0.7101014279158788, 4.023726495301334, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":20,"label":"20"},{"v":40,"label":"40"},{"v":60,"label":"60"},{"v":80,"label":"80"},{"v":100,"label":"100"}], Dygraph.numericTicks(1, 109, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"}], Dygraph.numericTicks(1, 3, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(1, 4, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(1, 4, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(1, 5, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":1},{"v":1.5,"label":1.5},{"v":2,"label":2},{"v":2.5,"label":2.5},{"v":3,"label":3},{"v":3.5,"label":3.5},{"v":4,"label":4},{"v":4.5,"label":4.5}], Dygraph.numericTicks(1, 5, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"}], Dygraph.numericTicks(1, 6, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"}], Dygraph.numericTicks(1, 7, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"},{"v":8,"label":"8"}], Dygraph.numericTicks(1, 9, 300, createOptionsViewForAxis('y',{"logscale":false,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":""},{"v":7,"label":"7"},{"v":8,"label":""},{"v":9,"label":"9"}], Dygraph.numericTicks(1, 9, 300, createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"},{"v":8,"label":"8"}], Dygraph.numericTicks(1, 9, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":2,"label":"2"},{"v":4,"label":"4"},{"v":6,"label":"6"},{"v":8,"label":"8"},{"v":10,"label":"10"}], Dygraph.numericTicks(1.2, 10.8, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":1.5,"label":"1.5"},{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(1.2872947778969237, 4.765317192093838, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":1,"label":"1"},{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(1.5, 7.5, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":5,"label":"5"},{"v":10,"label":"10"},{"v":15,"label":"15"},{"v":20,"label":"20"},{"v":25,"label":"25"}], Dygraph.numericTicks(1.7999999999999998, 28.2, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":10,"label":"10"},{"v":10.1,"label":"10.1"},{"v":10.2,"label":"10.2"},{"v":10.3,"label":"10.3"},{"v":10.4,"label":"10.4"},{"v":10.5,"label":"10.5"},{"v":10.6,"label":"10.6"},{"v":10.7,"label":"10.7"},{"v":10.8,"label":"10.8"},{"v":10.9,"label":"10.9"}], Dygraph.numericTicks(10, 11, 480, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":100,"label":"100"},{"v":120,"label":"120"},{"v":140,"label":"140"},{"v":160,"label":"160"},{"v":180,"label":"180"}], Dygraph.numericTicks(100, 200, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":10000,"label":"10000"},{"v":-17988000,"label":"12000"},{"v":-17986000,"label":"14000"},{"v":-17984000,"label":"16000"},{"v":-17982000,"label":"18000"},{"v":-17980000,"label":"20000"},{"v":-17978000,"label":"22000"},{"v":-17976000,"label":"24000"},{"v":-17974000,"label":"26000"},{"v":-17972000,"label":"28000"},{"v":-17970000,"label":"30000"},{"v":-17968000,"label":"32000"},{"v":-17966000,"label":"34000"},{"v":-17964000,"label":"36000"}], Dygraph.numericTicks(10122.8, 36789.2, 480, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":11000,"label":"11000"},{"v":11200,"label":"11200"},{"v":11400,"label":"11400"},{"v":11600,"label":"11600"},{"v":11800,"label":"11800"},{"v":-17988000,"label":"12000"},{"v":12200,"label":"12200"},{"v":12400,"label":"12400"},{"v":12600,"label":"12600"},{"v":12800,"label":"12800"},{"v":-17987000,"label":"13000"},{"v":13200,"label":"13200"},{"v":13400,"label":"13400"}], Dygraph.numericTicks(11110.5, 13579.5, 480, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":162000,"label":"162000"},{"v":-17836000,"label":"164000"},{"v":-17834000,"label":"166000"},{"v":-17832000,"label":"168000"},{"v":-17830000,"label":"170000"},{"v":-17828000,"label":"172000"},{"v":-17826000,"label":"174000"},{"v":-17824000,"label":"176000"},{"v":-17822000,"label":"178000"}], Dygraph.numericTicks(163038.4, 179137.6, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":2,"label":"2"},{"v":2.5,"label":"2.5"},{"v":3,"label":"3"},{"v":3.5,"label":"3.5"}], Dygraph.numericTicks(2, 4, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":2,"label":"2"},{"v":3,"label":"3"},{"v":4,"label":"4"},{"v":5,"label":"5"},{"v":6,"label":"6"},{"v":7,"label":"7"}], Dygraph.numericTicks(2.6, 7.4, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(21.7, 97.3, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(21.7, 97.3, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(24, 96, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 20, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false,pixelsPerLabel:20})));
+  assert.deepEqual([{"v":25,"label":"25"},{"v":30,"label":"30"},{"v":35,"label":"35"},{"v":40,"label":"40"},{"v":45,"label":"45"},{"v":50,"label":"50"},{"v":55,"label":"55"},{"v":60,"label":"60"},{"v":65,"label":"65"},{"v":70,"label":"70"},{"v":75,"label":"75"},{"v":80,"label":"80"},{"v":85,"label":"85"},{"v":90,"label":"90"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false,pixelsPerLabel:20})));
+  assert.deepEqual([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(26.185714285714287, 90.81428571428572, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":20,"label":"20"},{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(28.33333333333333, 88.33333333333334, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":3,"label":"3"},{"v":3.5,"label":"3.5"},{"v":4,"label":"4"},{"v":4.5,"label":"4.5"}], Dygraph.numericTicks(3, 5, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":3000,"label":"3K"},{"v":2500,"label":"2.5K"},{"v":-17998000,"label":"2K"},{"v":1500,"label":"1.5K"},{"v":-17999000,"label":"1K"},{"v":500,"label":"500"}], Dygraph.numericTicks(3000, 0, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(33.11333333333334, 83.75333333333333, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":30,"label":"30"},{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"}], Dygraph.numericTicks(36.921241050119335, 88.32696897374701, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":50,"label":""},{"v":60,"label":"60"},{"v":70,"label":""},{"v":80,"label":""},{"v":90,"label":""},{"v":100,"label":"100"},{"v":200,"label":""},{"v":300,"label":"300"},{"v":400,"label":""},{"v":500,"label":""},{"v":600,"label":"600"},{"v":700,"label":""},{"v":800,"label":""},{"v":900,"label":""},{"v":-17999000,"label":"1000"},{"v":-17998000,"label":""},{"v":-17997000,"label":"3000"},{"v":-17996000,"label":""},{"v":-17995000,"label":""},{"v":-17994000,"label":"6000"},{"v":-17993000,"label":""},{"v":-17992000,"label":""},{"v":-17991000,"label":""},{"v":-17990000,"label":"10000"}], Dygraph.numericTicks(41.220000000000084, 15576.828000000018, 400, createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":40,"label":"40"},{"v":50,"label":"50"},{"v":60,"label":"60"},{"v":70,"label":"70"},{"v":80,"label":"80"},{"v":90,"label":"90"}], Dygraph.numericTicks(44.5, 98.5, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":5,"label":"5"},{"v":6,"label":""},{"v":7,"label":""},{"v":8,"label":""},{"v":9,"label":""},{"v":10,"label":"10"},{"v":20,"label":"20"},{"v":30,"label":""},{"v":40,"label":""},{"v":50,"label":"50"},{"v":60,"label":""},{"v":70,"label":""},{"v":80,"label":""},{"v":90,"label":""},{"v":100,"label":"100"},{"v":200,"label":"200"},{"v":300,"label":""},{"v":400,"label":""},{"v":500,"label":"500"},{"v":600,"label":""},{"v":700,"label":""},{"v":800,"label":""},{"v":900,"label":""},{"v":-17999000,"label":"1000"}], Dygraph.numericTicks(5, 1099.5, 300, createOptionsViewForAxis('y',{"logscale":true,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":50,"label":"50"},{"v":55,"label":"55"},{"v":60,"label":"60"},{"v":65,"label":"65"},{"v":70,"label":"70"},{"v":75,"label":"75"},{"v":80,"label":"80"}], Dygraph.numericTicks(52.5, 82.5, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":68,"label":"68"},{"v":70,"label":"70"},{"v":72,"label":"72"},{"v":74,"label":"74"},{"v":76,"label":"76"},{"v":78,"label":"78"},{"v":80,"label":"80"}], Dygraph.numericTicks(69, 81, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":0,"label":"0"},{"v":-17980000,"label":"20K"},{"v":-17960000,"label":"40K"},{"v":-17940000,"label":"60K"},{"v":-17920000,"label":"80K"}], Dygraph.numericTicks(7921.099999999999, 81407.9, 240, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":8,"label":"8"},{"v":10,"label":"10"},{"v":12,"label":"12"},{"v":14,"label":"14"},{"v":16,"label":"16"},{"v":18,"label":"18"},{"v":20,"label":"20"}], Dygraph.numericTicks(9, 21, 300, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":8,"label":"8"},{"v":10,"label":"10"},{"v":12,"label":"12"},{"v":14,"label":"14"},{"v":16,"label":"16"},{"v":18,"label":"18"},{"v":20,"label":"20"}], Dygraph.numericTicks(9, 21, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":9,"label":"9"},{"v":10,"label":"10"},{"v":11,"label":"11"},{"v":12,"label":"12"},{"v":13,"label":"13"},{"v":14,"label":"14"},{"v":15,"label":"15"},{"v":16,"label":"16"},{"v":17,"label":"17"},{"v":18,"label":"18"}], Dygraph.numericTicks(9.2, 18.8, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":80,"label":"80"},{"v":100,"label":"100"},{"v":120,"label":"120"},{"v":140,"label":"140"},{"v":160,"label":"160"},{"v":180,"label":"180"},{"v":200,"label":"200"}], Dygraph.numericTicks(90, 210, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":true})));
+  assert.deepEqual([{"v":95,"label":"95"},{"v":96,"label":"96"},{"v":97,"label":"97"},{"v":98,"label":"98"},{"v":99,"label":"99"},{"v":100,"label":"100"},{"v":101,"label":"101"},{"v":102,"label":"102"},{"v":103,"label":"103"},{"v":104,"label":"104"}], Dygraph.numericTicks(95.71121718377088, 104.23150357995226, 320, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+  assert.deepEqual([{"v":950,"label":"950"},{"v":-17999000,"label":"1000"},{"v":1050,"label":"1050"},{"v":1100,"label":"1100"},{"v":1150,"label":"1150"},{"v":1200,"label":"1200"}], Dygraph.numericTicks(980.1, 1218.9, 200, createOptionsViewForAxis('y',{"logscale":null,"labelsKMG2":false,"labelsKMB":false})));
+});
 */
+
+});
index dc1d5b2..55df841 100644 (file)
@@ -3,23 +3,23 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var parserTestCase = TestCase("parser");
+describe("parser", function() {
 
-parserTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-parserTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-parserTestCase.prototype.testDetectLineDelimiter = function() {
+it('testDetectLineDelimiter', function() {
   var data = "X,Y\r" +
       "0,-1\r" +
       "1,0\r" +
       "2,1\r" +
       "3,0\r"
   ;
-  assertEquals("\r", Dygraph.detectLineDelimiter(data));
+  assert.equal("\r", Dygraph.detectLineDelimiter(data));
 
   data = "X,Y\n" +
       "0,-1\n" +
@@ -27,7 +27,7 @@ parserTestCase.prototype.testDetectLineDelimiter = function() {
       "2,1\n" +
       "3,0\n"
   ;
-  assertEquals("\n", Dygraph.detectLineDelimiter(data));
+  assert.equal("\n", Dygraph.detectLineDelimiter(data));
 
   data = "X,Y\n\r" +
       "0,-1\n\r" +
@@ -35,10 +35,10 @@ parserTestCase.prototype.testDetectLineDelimiter = function() {
       "2,1\n\r" +
       "3,0\n\r"
   ;
-  assertEquals("\n\r", Dygraph.detectLineDelimiter(data));
-};
+  assert.equal("\n\r", Dygraph.detectLineDelimiter(data));
+});
 
-parserTestCase.prototype.testParseDosNewlines = function() {
+it('testParseDosNewlines', function() {
   var opts = {
     width: 480,
     height: 320
@@ -53,10 +53,12 @@ parserTestCase.prototype.testParseDosNewlines = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEquals(0, g.getValue(0, 0));
-  assertEquals(-1, g.getValue(0, 1));
-  assertEquals(1, g.getValue(1, 0));
-  assertEquals(0, g.getValue(1, 1));
-  assertEquals(['X', 'Y'], g.getLabels());
-};
+  assert.equal(0, g.getValue(0, 0));
+  assert.equal(-1, g.getValue(0, 1));
+  assert.equal(1, g.getValue(1, 0));
+  assert.equal(0, g.getValue(1, 1));
+  assert.deepEqual(['X', 'Y'], g.getLabels());
+});
 
+
+});
index 985241f..ade714c 100644 (file)
@@ -4,16 +4,16 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var pathologicalCasesTestCase = TestCase("pathological-cases");
+describe("pathological-cases", function() {
 
-pathologicalCasesTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-pathologicalCasesTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-pathologicalCasesTestCase.prototype.testZeroPoint = function() {
+it('testZeroPoint', function() {
   var opts = {
     width: 480,
     height: 320
@@ -22,9 +22,9 @@ pathologicalCasesTestCase.prototype.testZeroPoint = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-};
+});
 
-pathologicalCasesTestCase.prototype.testOnePoint = function() {
+it('testOnePoint', function() {
   var opts = {
     width: 480,
     height: 320
@@ -34,9 +34,9 @@ pathologicalCasesTestCase.prototype.testOnePoint = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-};
+});
 
-pathologicalCasesTestCase.prototype.testCombinations = function() {
+it('testCombinations', function() {
   var dataSets = {
     empty: [],
     onePoint: [[10, 2]],
@@ -114,9 +114,9 @@ pathologicalCasesTestCase.prototype.testCombinations = function() {
       }
     }
   }
-};
+});
 
-pathologicalCasesTestCase.prototype.testNullLegend = function() {
+it('testNullLegend', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -127,35 +127,37 @@ pathologicalCasesTestCase.prototype.testNullLegend = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-};
+});
 
-pathologicalCasesTestCase.prototype.testDivAsString = function() {
+it('testDivAsString', function() {
   var data = "X,Y\n" +
              "1,2\n";
 
   var g = new Dygraph('graph', data, {});
-};
+});
 
 
-pathologicalCasesTestCase.prototype.testConstantSeriesNegative = function() {
+it('testConstantSeriesNegative', function() {
   var data = "X,Y\n" +
              "1,-1\n" +
              "2,-1\n";
 
-  g = new Dygraph('graph', data, {});
+  var g = new Dygraph('graph', data, {});
   // This check could be loosened to
   // g.yAxisRange()[0] < g.yAxisRange()[1] if it breaks in the future.
-  assertEquals([-1.1, -0.9], g.yAxisRange());
-};
+  assert.deepEqual([-1.1, -0.9], g.yAxisRange());
+});
 
 
-pathologicalCasesTestCase.prototype.testConstantSeriesNegativeIncludeZero = function() {
+it('testConstantSeriesNegativeIncludeZero', function() {
   var data = "X,Y\n" +
              "1,-1\n" +
              "2,-1\n";
 
-  g = new Dygraph('graph', data, {includeZero: true});
+  var g = new Dygraph('graph', data, {includeZero: true});
   // This check could be loosened to
   // g.yAxisRange()[0] < g.yAxisRange()[1] if it breaks in the future.
-  assertEquals([-1.1, 0], g.yAxisRange());
-};
+  assert.deepEqual([-1.1, 0], g.yAxisRange());
+});
+
+});
index 279765e..4ae6208 100644 (file)
@@ -3,30 +3,32 @@
  *
  * @author konigsberg@google.com (Robert Konigsberg)
  */
-var perAxisTestCase = TestCase("per-axis");
+describe("per-axis", function() {
 
-perAxisTestCase._origGetContext = Dygraph.getContext;
+var _origGetContext = Dygraph.getContext;
 
-perAxisTestCase.prototype.setUp = function() {
+var xAxisLineColor = "#00ffff";
+var yAxisLineColor = "#ffff00";
+
+var g, graph;
+
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(perAxisTestCase._origGetContext(canvas));
+    return new Proxy(_origGetContext(canvas));
   }
 
-  this.xAxisLineColor = "#00ffff";
-  this.yAxisLineColor = "#ffff00";
-
   var opts = {
     axes : {
       x : {
         drawAxis : false,
         drawGrid : false,
-        gridLineColor : this.xAxisLineColor
+        gridLineColor : xAxisLineColor
       },
       y : {
         drawAxis : false,
         drawGrid : false,
-        gridLineColor : this.yAxisLineColor
+        gridLineColor : yAxisLineColor
       }
     },
     colors: [ '#ff0000', '#0000ff' ]
@@ -36,36 +38,38 @@ perAxisTestCase.prototype.setUp = function() {
       "1,1,0\n" +
       "8,0,1\n"
   ;
-  this.graph = document.getElementById('graph');
-  this.g = new Dygraph(this.graph, data, opts);
-};
+  graph = document.getElementById('graph');
+  g = new Dygraph(graph, data, opts);
+});
+
+afterEach(function() {
+  Dygraph.getContext = _origGetContext;
+});
 
-perAxisTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = perAxisTestCase._origGetContext;
-};
+it('testDrawXAxis', function() {
+  g.updateOptions({ axes : { x : { drawAxis: true }} });
+  assert.isTrue(graph.getElementsByClassName('dygraph-axis-label-x').length > 0);
+  assert.isTrue(graph.getElementsByClassName('dygraph-axis-label-y').length == 0);
+});
 
-perAxisTestCase.prototype.testDrawXAxis = function() {
-  this.g.updateOptions({ axes : { x : { drawAxis: true }} });
-  assertTrue(this.graph.getElementsByClassName('dygraph-axis-label-x').length > 0);
-  assertTrue(this.graph.getElementsByClassName('dygraph-axis-label-y').length == 0);
-}
+it('testDrawYAxis', function() {
+  g.updateOptions({ axes : { y : { drawAxis: true }} });
+  assert.isTrue(graph.getElementsByClassName('dygraph-axis-label-x').length ==0);
+  assert.isTrue(graph.getElementsByClassName('dygraph-axis-label-y').length > 0);
+});
 
-perAxisTestCase.prototype.testDrawYAxis = function() {
-  this.g.updateOptions({ axes : { y : { drawAxis: true }} });
-  assertTrue(this.graph.getElementsByClassName('dygraph-axis-label-x').length ==0);
-  assertTrue(this.graph.getElementsByClassName('dygraph-axis-label-y').length > 0);
-}
+it('testDrawXGrid', function() {
+  g.updateOptions({ axes : { x : { drawGrid : true }}});
+  var htx = g.hidden_ctx_;
+  assert.isTrue(CanvasAssertions.numLinesDrawn(htx, xAxisLineColor) > 0);
+  assert.isTrue(CanvasAssertions.numLinesDrawn(htx, yAxisLineColor) == 0);
+});
 
-perAxisTestCase.prototype.testDrawXGrid = function() {
-  this.g.updateOptions({ axes : { x : { drawGrid : true }}});
-  var htx = this.g.hidden_ctx_;
-  assertTrue(CanvasAssertions.numLinesDrawn(htx, this.xAxisLineColor) > 0);
-  assertTrue(CanvasAssertions.numLinesDrawn(htx, this.yAxisLineColor) == 0);
-}
+it('testDrawYGrid', function() {
+  g.updateOptions({ axes : { y : { drawGrid : true }}});
+  var htx = g.hidden_ctx_;
+  assert.isTrue(CanvasAssertions.numLinesDrawn(htx, xAxisLineColor) == 0);
+  assert.isTrue(CanvasAssertions.numLinesDrawn(htx, yAxisLineColor) > 0);
+});
 
-perAxisTestCase.prototype.testDrawYGrid = function() {
-  this.g.updateOptions({ axes : { y : { drawGrid : true }}});
-  var htx = this.g.hidden_ctx_;
-  assertTrue(CanvasAssertions.numLinesDrawn(htx, this.xAxisLineColor) == 0);
-  assertTrue(CanvasAssertions.numLinesDrawn(htx, this.yAxisLineColor) > 0);
-}
+});
index b1b672e..a09f912 100644 (file)
@@ -3,16 +3,16 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var perSeriesTestCase = TestCase("per-series");
+describe("per-series", function() {
 
-perSeriesTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-perSeriesTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-perSeriesTestCase.prototype.testPerSeriesFill = function() {
+it('testPerSeriesFill', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -44,18 +44,18 @@ perSeriesTestCase.prototype.testPerSeriesFill = function() {
   ;
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   var sampler = new PixelSampler(g);
 
   // Inside of the "Z" bump -- no fill.
-  assertEquals([0,0,0,0], sampler.colorAtCoordinate(2.5, 0.5));
+  assert.deepEqual([0,0,0,0], sampler.colorAtCoordinate(2.5, 0.5));
 
   // Inside of the "Y" bump -- filled in.
-  assertEquals([255,0,0,38], sampler.colorAtCoordinate(6.5, 0.5));
-};
+  assert.deepEqual([255,0,0,38], sampler.colorAtCoordinate(6.5, 0.5));
+});
 
-perSeriesTestCase.prototype.testNewStyleSeries = function() {
+it('testNewStyleSeries', function() {
   var opts = {
     pointSize : 5,
     series : {
@@ -64,15 +64,15 @@ perSeriesTestCase.prototype.testNewStyleSeries = function() {
   };
   var graph = document.getElementById("graph");
   var data = "X,Y,Z\n1,0,0\n";
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
-  assertEquals(5, g.getOption("pointSize"));
-  assertEquals(4, g.getOption("pointSize", "Y"));
-  assertEquals(5, g.getOption("pointSize", "Z"));
-};
+  assert.equal(5, g.getOption("pointSize"));
+  assert.equal(4, g.getOption("pointSize", "Y"));
+  assert.equal(5, g.getOption("pointSize", "Z"));
+});
 
 // TODO(konigsberg): move to multiple_axes.js
-perSeriesTestCase.prototype.testAxisInNewSeries = function() {
+it('testAxisInNewSeries', function() {
   var opts = {
     series : {
       D : { axis : 'y2' },
@@ -83,14 +83,14 @@ perSeriesTestCase.prototype.testAxisInNewSeries = function() {
   };
   var graph = document.getElementById("graph");
   var data = "X,A,B,C,D,E\n0,1,2,3,4,5\n";
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
-  assertEquals(["A", "B", "E"], g.attributes_.seriesForAxis(0));
-  assertEquals(["C", "D"], g.attributes_.seriesForAxis(1));
-};
+  assert.deepEqual(["A", "B", "E"], g.attributes_.seriesForAxis(0));
+  assert.deepEqual(["C", "D"], g.attributes_.seriesForAxis(1));
+});
 
 // TODO(konigsberg): move to multiple_axes.js
-perSeriesTestCase.prototype.testAxisInNewSeries_withAxes = function() {
+it('testAxisInNewSeries_withAxes', function() {
   var opts = {
     series : {
       D : { axis : 'y2' },
@@ -105,21 +105,21 @@ perSeriesTestCase.prototype.testAxisInNewSeries_withAxes = function() {
   };
   var graph = document.getElementById("graph");
   var data = "X,A,B,C,D,E\n0,1,2,3,4,5\n";
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
-  assertEquals(["A", "B", "E"], g.attributes_.seriesForAxis(0));
-  assertEquals(["C", "D"], g.attributes_.seriesForAxis(1));
+  assert.deepEqual(["A", "B", "E"], g.attributes_.seriesForAxis(0));
+  assert.deepEqual(["C", "D"], g.attributes_.seriesForAxis(1));
 
-  assertEquals(1.5, g.getOption("pointSize"));
-  assertEquals(7, g.getOption("pointSize", "A"));
-  assertEquals(7, g.getOption("pointSize", "B"));
-  assertEquals(6, g.getOption("pointSize", "C"));
-  assertEquals(6, g.getOption("pointSize", "D"));
-  assertEquals(7, g.getOption("pointSize", "E"));
-};
+  assert.equal(1.5, g.getOption("pointSize"));
+  assert.equal(7, g.getOption("pointSize", "A"));
+  assert.equal(7, g.getOption("pointSize", "B"));
+  assert.equal(6, g.getOption("pointSize", "C"));
+  assert.equal(6, g.getOption("pointSize", "D"));
+  assert.equal(7, g.getOption("pointSize", "E"));
+});
 
 // TODO(konigsberg): move to multiple_axes.js
-perSeriesTestCase.prototype.testOldAxisSpecInNewSeriesThrows = function() {
+it('testOldAxisSpecInNewSeriesThrows', function() {
   var opts = {
     series : {
       D : { axis : {} },
@@ -134,14 +134,16 @@ perSeriesTestCase.prototype.testOldAxisSpecInNewSeriesThrows = function() {
     threw = true;
   }
 
-  assertTrue(threw);
-}
+  assert.isTrue(threw);
+});
 
-perSeriesTestCase.prototype.testColorOption = function() {
+it('testColorOption', function() {
   var graph = document.getElementById("graph");
   var data = "X,A,B,C\n0,1,2,3\n";
   var g = new Dygraph(graph, data, {});
-  assertEquals(['rgb(64,128,0)', 'rgb(64,0,128)', 'rgb(0,128,128)'], g.getColors());
+  assert.deepEqual(['rgb(64,128,0)', 'rgb(64,0,128)', 'rgb(0,128,128)'], g.getColors());
   g.updateOptions({series : { B : { color : 'purple' }}});
-  assertEquals(['rgb(64,128,0)', 'purple', 'rgb(0,128,128)'], g.getColors());
-}
+  assert.deepEqual(['rgb(64,128,0)', 'purple', 'rgb(0,128,128)'], g.getColors());
+});
+
+});
index 3605113..1abccd4 100644 (file)
@@ -3,23 +3,25 @@
  *
  * @author konigsberg@google.com (Robert Konigsberg)
  */
-var pluginsTestCase = TestCase("plugins");
+describe("plugins", function() {
 
-pluginsTestCase.prototype.setUp = function() {
+var data;
+
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
 
-  this.data = "X,Y1,Y2\n" +
+  data = "X,Y1,Y2\n" +
       "0,1,2\n" +
       "1,2,1\n" +
       "2,1,2\n" +
       "3,2,1\n"
   ;
-};
+});
 
-pluginsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-pluginsTestCase.prototype.testWillDrawChart = function() {
+it('testWillDrawChart', function() {
   var draw = 0;
 
   var plugin = (function() {
@@ -40,12 +42,12 @@ pluginsTestCase.prototype.testWillDrawChart = function() {
   })();
 
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, this.data, {plugins: [plugin]});
+  var g = new Dygraph(graph, data, {plugins: [plugin]});
 
-  assertEquals(1, draw);
-};
+  assert.equal(1, draw);
+});
 
-pluginsTestCase.prototype.testPassingInstance = function() {
+it('testPassingInstance', function() {
   // You can also pass an instance of a plugin instead of a Plugin class.
   var draw = 0;
   var p = {
@@ -60,12 +62,12 @@ pluginsTestCase.prototype.testPassingInstance = function() {
   };
 
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, this.data, {plugins: [p]});
+  var g = new Dygraph(graph, data, {plugins: [p]});
 
-  assertEquals(1, draw);
-};
+  assert.equal(1, draw);
+});
 
-pluginsTestCase.prototype.testPreventDefault = function() {
+it('testPreventDefault', function() {
   var data1 = "X,Y\n" +
       "20,-1\n" +
       "21,0\n" +
@@ -119,7 +121,7 @@ pluginsTestCase.prototype.testPreventDefault = function() {
   p.pointClickPreventDefault = false;
   p.clickPreventDefault = false;
   clickOnPoint();
-  assertEquals([
+  assert.deepEqual([
     ['plugin.pointClick', 20, -1],
     ['pointClickCallback', 20, -1],
     ['plugin.click', 20],
@@ -130,7 +132,7 @@ pluginsTestCase.prototype.testPreventDefault = function() {
   p.pointClickPreventDefault = true;
   p.clickPreventDefault = false;
   clickOnPoint();
-  assertEquals([
+  assert.deepEqual([
     ['plugin.pointClick', 20, -1]
   ], events);
 
@@ -138,14 +140,14 @@ pluginsTestCase.prototype.testPreventDefault = function() {
   p.pointClickPreventDefault = false;
   p.clickPreventDefault = true;
   clickOnPoint();
-  assertEquals([
+  assert.deepEqual([
     ['plugin.pointClick', 20, -1],
     ['pointClickCallback', 20, -1],
     ['plugin.click', 20]
   ], events);
-};
+});
 
-pluginsTestCase.prototype.testEventSequence = function() {
+it('testEventSequence', function() {
   var events = [];
 
   var eventLogger = function(name) {
@@ -168,10 +170,10 @@ pluginsTestCase.prototype.testEventSequence = function() {
   };
 
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, this.data, {plugins: [p]});
+  var g = new Dygraph(graph, data, {plugins: [p]});
 
   // Initial draw sequence
-  assertEquals([
+  assert.deepEqual([
    "dataDidUpdate",  // should dataWillUpdate be called here, too?
    "predraw",
    "clearChart",
@@ -182,7 +184,7 @@ pluginsTestCase.prototype.testEventSequence = function() {
   // An options change triggers a redraw, but doesn't change the data.
   events = [];
   g.updateOptions({series: {Y1: {color: 'blue'}}});
-  assertEquals([
+  assert.deepEqual([
    "predraw",
    "clearChart",
    "willDrawChart",
@@ -194,7 +196,7 @@ pluginsTestCase.prototype.testEventSequence = function() {
   DygraphOps.dispatchMouseDown_Point(g, 100, 100, {shiftKey: true});
   DygraphOps.dispatchMouseMove_Point(g, 200, 100, {shiftKey: true});
   DygraphOps.dispatchMouseUp_Point(g, 200, 100, {shiftKey: true});
-  assertEquals([
+  assert.deepEqual([
    "clearChart",
    "willDrawChart",
    "didDrawChart"
@@ -202,8 +204,8 @@ pluginsTestCase.prototype.testEventSequence = function() {
 
   // New data triggers the full sequence.
   events = [];
-  g.updateOptions({file: this.data + '\n4,1,2'});
-  assertEquals([
+  g.updateOptions({file: data + '\n4,1,2'});
+  assert.deepEqual([
    "dataWillUpdate",
    "dataDidUpdate",
    "predraw",
@@ -211,9 +213,9 @@ pluginsTestCase.prototype.testEventSequence = function() {
    "willDrawChart",
    "didDrawChart"
   ], events);
-};
+});
 
-pluginsTestCase.prototype.testDestroyCalledInOrder = function() {
+it('testDestroyCalledInOrder', function() {
   var destructions = [];
   var makePlugin = function(name) {
     return {
@@ -225,11 +227,13 @@ pluginsTestCase.prototype.testDestroyCalledInOrder = function() {
   };
 
   var graph = document.getElementById("graph");
-  var g = new Dygraph(graph, this.data, {
+  var g = new Dygraph(graph, data, {
     plugins: [makePlugin('p'), makePlugin('q')]
   });
 
-  assertEquals([], destructions);
+  assert.deepEqual([], destructions);
   g.destroy();
-  assertEquals(['q', 'p'], destructions);
-};
+  assert.deepEqual(['q', 'p'], destructions);
+});
+
+});
index 3288329..9d5df46 100644 (file)
@@ -3,16 +3,16 @@
  *
  * @author akiya.mizukoshi@gmail.com (Akiyah)
  */
-var pluginsLegendTestCase = TestCase("plugins-legend");
+describe("plugins-legend", function() {
 
-pluginsLegendTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-pluginsLegendTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-pluginsLegendTestCase.prototype.testLegendEscape = function() {
+it('testLegendEscape', function() {
   var opts = {
     width: 480,
     height: 320
@@ -40,7 +40,9 @@ pluginsLegendTestCase.prototype.testLegendEscape = function() {
   }
   legendPlugin.select(e);
 
-  var legendSpan = $(legendPlugin.legend_div_).find("span b span");
-  assertEquals("&lt;script&gt;alert('XSS')&lt;/script&gt;", legendSpan.html());
-};
+  var legendSpan = legendPlugin.legend_div_.querySelector("span b span");
+  assert.equal(legendSpan.innerHTML, "&lt;script&gt;alert('XSS')&lt;/script&gt;");
+});
 
+
+});
index 3349ab5..933cacf 100644 (file)
@@ -4,16 +4,16 @@
  * @fileoverview Regression tests for range selector.
  * @author paul.eric.felix@gmail.com (Paul Felix)
  */
-var RangeSelectorTestCase = TestCase("range-selector");
+describe("range-selector", function() {
 
-RangeSelectorTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-RangeSelectorTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-RangeSelectorTestCase.prototype.testRangeSelector = function() {
+it('testRangeSelector', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -33,10 +33,10 @@ RangeSelectorTestCase.prototype.testRangeSelector = function() {
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorWithErrorBars = function() {
+it('testRangeSelectorWithErrorBars', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -57,10 +57,10 @@ RangeSelectorTestCase.prototype.testRangeSelectorWithErrorBars = function() {
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorWithCustomBars = function() {
+it('testRangeSelectorWithCustomBars', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -81,10 +81,10 @@ RangeSelectorTestCase.prototype.testRangeSelectorWithCustomBars = function() {
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorWithLogScale = function() {
+it('testRangeSelectorWithLogScale', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -105,10 +105,10 @@ RangeSelectorTestCase.prototype.testRangeSelectorWithLogScale = function() {
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorOptions = function() {
+it('testRangeSelectorOptions', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -131,10 +131,10 @@ RangeSelectorTestCase.prototype.testRangeSelectorOptions = function() {
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
-RangeSelectorTestCase.prototype.testAdditionalRangeSelectorOptions = function() {
+it('testAdditionalRangeSelectorOptions', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -161,10 +161,10 @@ RangeSelectorTestCase.prototype.testAdditionalRangeSelectorOptions = function()
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorEnablingAfterCreation = function() {
+it('testRangeSelectorEnablingAfterCreation', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -184,11 +184,11 @@ RangeSelectorTestCase.prototype.testRangeSelectorEnablingAfterCreation = functio
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
   g.updateOptions({showRangeSelector: true});
-  this.assertGraphExistence(g, graph);
-};
+  assertGraphExistence(g, graph);
+});
 
 // The animatedZooms option does not work with the range selector. Make sure it gets turned off.
-RangeSelectorTestCase.prototype.testRangeSelectorWithAnimatedZoomsOption = function() {
+it('testRangeSelectorWithAnimatedZoomsOption', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -209,11 +209,11 @@ RangeSelectorTestCase.prototype.testRangeSelectorWithAnimatedZoomsOption = funct
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
-  assertFalse(g.getOption('animatedZooms'));
-};
+  assertGraphExistence(g, graph);
+  assert.isFalse(g.getOption('animatedZooms'));
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorWithAnimatedZoomsOption2 = function() {
+it('testRangeSelectorWithAnimatedZoomsOption2', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -234,11 +234,11 @@ RangeSelectorTestCase.prototype.testRangeSelectorWithAnimatedZoomsOption2 = func
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
   g.updateOptions({showRangeSelector: true});
-  this.assertGraphExistence(g, graph);
-  assertFalse(g.getOption('animatedZooms'));
-};
+  assertGraphExistence(g, graph);
+  assert.isFalse(g.getOption('animatedZooms'));
+});
 
-RangeSelectorTestCase.prototype.testRangeSelectorInteraction = function() {
+it('testRangeSelectorInteraction', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -258,7 +258,7 @@ RangeSelectorTestCase.prototype.testRangeSelectorInteraction = function() {
              ];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  this.assertGraphExistence(g, graph);
+  assertGraphExistence(g, graph);
   var zoomhandles = graph.getElementsByClassName('dygraph-rangesel-zoomhandle');
 
   // Move left zoomhandle in
@@ -288,8 +288,8 @@ RangeSelectorTestCase.prototype.testRangeSelectorInteraction = function() {
   zoomhandles[0].dispatchEvent(mouseUpEvent);
 
   var newXRange = g.xAxisRange().slice();
-  assert('left zoomhandle should have moved: '+newXRange[0]+'>'+xRange[0], newXRange[0] > xRange[0]);
-  assertEquals('right zoomhandle should not have moved', xRange[1], newXRange[1]);
+  assert(newXRange[0] > xRange[0], 'left zoomhandle should have moved: '+newXRange[0]+'>'+xRange[0]);
+  assert.equal(xRange[1], newXRange[1], 'right zoomhandle should not have moved');
 
   // Move right zoomhandle in
   xRange = newXRange;
@@ -318,8 +318,8 @@ RangeSelectorTestCase.prototype.testRangeSelectorInteraction = function() {
   zoomhandles[1].dispatchEvent(mouseUpEvent);
 
   var newXRange = g.xAxisRange().slice();
-  assert('right zoomhandle should have moved: '+newXRange[1]+'<'+xRange[1], newXRange[1] < xRange[1]);
-  assertEquals('left zoomhandle should not have moved', xRange[0], newXRange[0]);
+  assert(newXRange[1] < xRange[1], 'right zoomhandle should have moved: '+newXRange[1]+'<'+xRange[1]);
+  assert.equal(xRange[0], newXRange[0], 'left zoomhandle should not have moved');
 
   // Pan left
   xRange = newXRange;
@@ -353,12 +353,12 @@ RangeSelectorTestCase.prototype.testRangeSelectorInteraction = function() {
   fgcanvas.dispatchEvent(mouseUpEvent);
 
   var newXRange = g.xAxisRange().slice();
-  assert(newXRange[0]+'<'+xRange[0], newXRange[0] < xRange[0]);
-  assert(newXRange[1]+'<'+xRange[1], newXRange[1] < xRange[1]);
-};
+  assert(newXRange[0] < xRange[0], newXRange[0]+'<'+xRange[0]);
+  assert(newXRange[1] < xRange[1], newXRange[1]+'<'+xRange[1]);
+});
 
 
-RangeSelectorTestCase.prototype.testRangeSelectorPositionIfXAxisNotDrawn = function() {
+it('testRangeSelectorPositionIfXAxisNotDrawn', function() {
   var opts = {
     width: 480,
     height: 100,
@@ -377,14 +377,14 @@ RangeSelectorTestCase.prototype.testRangeSelectorPositionIfXAxisNotDrawn = funct
 
   //assert, that the range selector is at top position 70 since the 30px of the
   // xAxis shouldn't be reserved since it isn't drawn.
-  this.assertGraphExistence(g, graph);
+  assertGraphExistence(g, graph);
   var bgcanvas = graph.getElementsByClassName('dygraph-rangesel-bgcanvas')[0];
-  assertEquals("Range selector is not at the expected position.","70px", bgcanvas.style.top);
+  assert.equal("70px", bgcanvas.style.top, "Range selector is not at the expected position.");
   var fgcanvas = graph.getElementsByClassName('dygraph-rangesel-fgcanvas')[0];
-  assertEquals("Range selector is not at the expected position.","70px", fgcanvas.style.top);
-};
+  assert.equal("70px", fgcanvas.style.top, "Range selector is not at the expected position.");
+});
 
-RangeSelectorTestCase.prototype.testMiniPlotDrawn = function() {
+it('testMiniPlotDrawn', function() {
   // Install Proxy to track canvas calls.
   var origFunc = Dygraph.getContext;
   var miniHtx;
@@ -416,14 +416,14 @@ RangeSelectorTestCase.prototype.testMiniPlotDrawn = function() {
   var g = new Dygraph(graph, data, opts);
 
   // TODO(danvk): more precise tests.
-  assertNotNull(miniHtx);
-  assertTrue(0 < CanvasAssertions.numLinesDrawn(miniHtx, '#ff0000'));
+  assert.isNotNull(miniHtx);
+  assert.isTrue(0 < CanvasAssertions.numLinesDrawn(miniHtx, '#ff0000'));
 
   Dygraph.getContext = origFunc;
-};
+});
 
 // Tests data computation for the mini plot with a single series.
-RangeSelectorTestCase.prototype.testSingleCombinedSeries = function() {
+it('testSingleCombinedSeries', function() {
   var opts = {
     showRangeSelector: true,
     labels: ['X', 'Y1']
@@ -437,10 +437,10 @@ RangeSelectorTestCase.prototype.testSingleCombinedSeries = function() {
   var g = new Dygraph(graph, data, opts);
 
   var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
-  assertNotNull(rangeSelector);
+  assert.isNotNull(rangeSelector);
 
   var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
-  assertEquals({
+  assert.deepEqual({
     yMin: 1 - 7 * 0.25,  // 25% padding
     yMax: 8 + 7 * 0.25,
     data: [
@@ -449,11 +449,11 @@ RangeSelectorTestCase.prototype.testSingleCombinedSeries = function() {
       [10, 8]
     ]
   }, combinedSeries);
-};
+});
 
 
 // Tests that multiple series are averaged for the miniplot.
-RangeSelectorTestCase.prototype.testCombinedSeries = function() {
+it('testCombinedSeries', function() {
   var opts = {
     showRangeSelector: true,
     labels: ['X', 'Y1', 'Y2']
@@ -467,10 +467,10 @@ RangeSelectorTestCase.prototype.testCombinedSeries = function() {
   var g = new Dygraph(graph, data, opts);
 
   var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
-  assertNotNull(rangeSelector);
+  assert.isNotNull(rangeSelector);
 
   var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
-  assertEquals({
+  assert.deepEqual({
     yMin: 2 - 6 * 0.25,  // 25% padding on combined series range.
     yMax: 8 + 6 * 0.25,
     data: [
@@ -479,10 +479,10 @@ RangeSelectorTestCase.prototype.testCombinedSeries = function() {
       [10, 8]
     ]
   }, combinedSeries);
-};
+});
 
 // Tests selection of a specific series to average for the mini plot.
-RangeSelectorTestCase.prototype.testSelectedCombinedSeries = function() {
+it('testSelectedCombinedSeries', function() {
   var opts = {
     showRangeSelector: true,
     labels: ['X', 'Y1', 'Y2', 'Y3', 'Y4'],
@@ -500,10 +500,10 @@ RangeSelectorTestCase.prototype.testSelectedCombinedSeries = function() {
   var g = new Dygraph(graph, data, opts);
 
   var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
-  assertNotNull(rangeSelector);
+  assert.isNotNull(rangeSelector);
 
   var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
-  assertEquals({
+  assert.deepEqual({
     yMin: 4 - 5 * 0.25,  // 25% padding on combined series range.
     yMax: 9 + 5 * 0.25,
     data: [
@@ -512,10 +512,10 @@ RangeSelectorTestCase.prototype.testSelectedCombinedSeries = function() {
       [10, 5]
     ]
   }, combinedSeries);
-};
+});
 
 // Tests data computation for the mini plot with a single error bar series.
-RangeSelectorTestCase.prototype.testSingleCombinedSeriesCustomBars = function() {
+it('testSingleCombinedSeriesCustomBars', function() {
   var opts = {
     customBars: true,
     showRangeSelector: true,
@@ -530,10 +530,10 @@ RangeSelectorTestCase.prototype.testSingleCombinedSeriesCustomBars = function()
   var g = new Dygraph(graph, data, opts);
 
   var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
-  assertNotNull(rangeSelector);
+  assert.isNotNull(rangeSelector);
 
   var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
-  assertEquals({
+  assert.deepEqual({
     yMin: 1 - 7 * 0.25,  // 25% padding
     yMax: 8 + 7 * 0.25,
     data: [
@@ -542,9 +542,9 @@ RangeSelectorTestCase.prototype.testSingleCombinedSeriesCustomBars = function()
       [10, 8]
     ]
   }, combinedSeries);
-};
+});
 
-RangeSelectorTestCase.prototype.testSingleCombinedSeriesErrorBars = function() {
+it('testSingleCombinedSeriesErrorBars', function() {
   var opts = {
     errorBars: true,
     showRangeSelector: true,
@@ -559,10 +559,10 @@ RangeSelectorTestCase.prototype.testSingleCombinedSeriesErrorBars = function() {
   var g = new Dygraph(graph, data, opts);
 
   var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
-  assertNotNull(rangeSelector);
+  assert.isNotNull(rangeSelector);
 
   var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
-  assertEquals({
+  assert.deepEqual({
     yMin: 1 - 7 * 0.25,  // 25% padding
     yMax: 8 + 7 * 0.25,
     data: [
@@ -571,10 +571,10 @@ RangeSelectorTestCase.prototype.testSingleCombinedSeriesErrorBars = function() {
       [10, 8]
     ]
   }, combinedSeries);
-};
+});
 
 // Tests data computation for the mini plot with two custom bar series.
-RangeSelectorTestCase.prototype.testTwoCombinedSeriesCustomBars = function() {
+it('testTwoCombinedSeriesCustomBars', function() {
   var opts = {
     customBars: true,
     showRangeSelector: true,
@@ -589,10 +589,10 @@ RangeSelectorTestCase.prototype.testTwoCombinedSeriesCustomBars = function() {
   var g = new Dygraph(graph, data, opts);
 
   var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
-  assertNotNull(rangeSelector);
+  assert.isNotNull(rangeSelector);
 
   var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
-  assertEquals({
+  assert.deepEqual({
     yMin: 3 - 7 * 0.25,  // 25% padding
     yMax: 10 + 7 * 0.25,
     data: [
@@ -601,15 +601,17 @@ RangeSelectorTestCase.prototype.testTwoCombinedSeriesCustomBars = function() {
       [10, 10]
     ]
   }, combinedSeries);
-};
+});
 
 
-RangeSelectorTestCase.prototype.assertGraphExistence = function(g, graph) {
-  assertNotNull(g);
+var assertGraphExistence = function(g, graph) {
+  assert.isNotNull(g);
   var zoomhandles = graph.getElementsByClassName('dygraph-rangesel-zoomhandle');
-  assertEquals(2, zoomhandles.length);
+  assert.equal(2, zoomhandles.length);
   var bgcanvas = graph.getElementsByClassName('dygraph-rangesel-bgcanvas');
-  assertEquals(1, bgcanvas.length);
+  assert.equal(1, bgcanvas.length);
   var fgcanvas = graph.getElementsByClassName('dygraph-rangesel-fgcanvas');
-  assertEquals(1, fgcanvas.length);
-}
+  assert.equal(1, fgcanvas.length);
+};
+
+});
index 051c1e3..6cef4f9 100644 (file)
@@ -25,7 +25,7 @@
  * @author konigsberg@google.com (Robert Konigsberg)
  */
 var ZERO_TO_FIFTY = [[ 10, 0 ] , [ 20, 50 ]];
-var ZERO_TO_FIFTY_STEPS = function() {
+var ZERO_TO_FIFTY_STEPS = (function() {
   var a = [];
   var x = 10;
   var y = 0;
@@ -34,26 +34,27 @@ var ZERO_TO_FIFTY_STEPS = function() {
     a.push([x + (step * .2), y + step]);
   }
   return a;
-} ();
+}());
+
 var FIVE_TO_ONE_THOUSAND = [
     [ 1, 10 ], [ 2, 20 ], [ 3, 30 ], [ 4, 40 ] , [ 5, 50 ], 
     [ 6, 60 ], [ 7, 70 ], [ 8, 80 ], [ 9, 90 ] , [ 10, 1000 ]];
 
-var RangeTestCase = TestCase("range-tests");
+describe("range-tests", function() {
 
-RangeTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-RangeTestCase.prototype.createGraph = function(opts, data, expectRangeX, expectRangeY) {
+var createGraph = function(opts, data, expectRangeX, expectRangeY) {
   if (data === undefined) data = ZERO_TO_FIFTY_STEPS;
   if (expectRangeX === undefined) expectRangeX = [10, 20];
   if (expectRangeY === undefined) expectRangeY = [0, 55];
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  assertEqualsDelta(expectRangeX, g.xAxisRange(), 0.01);
-  assertEqualsDelta(expectRangeY, g.yAxisRange(0), 0.01);
+  assertDeepCloseTo(expectRangeX, g.xAxisRange(), 0.01);
+  assertDeepCloseTo(expectRangeY, g.yAxisRange(0), 0.01);
 
   return g;
 };
@@ -62,59 +63,59 @@ RangeTestCase.prototype.createGraph = function(opts, data, expectRangeX, expectR
  * Test that changes to valueRange and dateWindow are reflected
  * appropriately.
  */
-RangeTestCase.prototype.testRangeSetOperations = function() {
-  var g = this.createGraph({valueRange : [ 0, 55 ]});
+it('testRangeSetOperations', function() {
+  var g = createGraph({valueRange : [ 0, 55 ]});
 
   g.updateOptions({ dateWindow : [ 12, 18 ] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([0, 55], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([0, 55], g.yAxisRange(0));
 
   g.updateOptions({ valueRange : [ 10, 40 ] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 40], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 40], g.yAxisRange(0));
 
   g.updateOptions({ valueRange: [10, NaN] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 44.2], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 44.2], g.yAxisRange(0));
 
   g.updateOptions({ valueRange: [10, 40] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 40], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 40], g.yAxisRange(0));
 
   g.updateOptions({ valueRange: [10, null] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 44.2], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 44.2], g.yAxisRange(0));
 
   g.updateOptions({ valueRange: [10, 40] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 40], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 40], g.yAxisRange(0));
 
   g.updateOptions({ valueRange: [10, undefined] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 44.2], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 44.2], g.yAxisRange(0));
 
   g.updateOptions({ valueRange: [10, 40] });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 40], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 40], g.yAxisRange(0));
 
   g.updateOptions({  });
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([10, 40], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([10, 40], g.yAxisRange(0));
   
   g.updateOptions({valueRange : null, axes: {y:{valueRange : [15, 20]}}});
-  assertEquals([12, 18], g.xAxisRange());
-  assertEquals([15, 20], g.yAxisRange(0));
+  assert.deepEqual([12, 18], g.xAxisRange());
+  assert.deepEqual([15, 20], g.yAxisRange(0));
 
   g.updateOptions({ dateWindow : null, valueRange : null, axes: null });
-  assertEquals([10, 20], g.xAxisRange());
-  assertEquals([0, 55], g.yAxisRange(0));
-};
+  assert.deepEqual([10, 20], g.xAxisRange());
+  assert.deepEqual([0, 55], g.yAxisRange(0));
+});
 
 /**
  * Verify that when zoomed in by mouse operations, an empty call to
  * updateOptions doesn't change the displayed ranges.
  */
-RangeTestCase.prototype.zoom = function(g, xRange, yRange) {
+var zoom = function(g, xRange, yRange) {
   var originalXRange = g.xAxisRange();
   var originalYRange = g.yAxisRange(0);
 
@@ -122,16 +123,16 @@ RangeTestCase.prototype.zoom = function(g, xRange, yRange) {
   DygraphOps.dispatchMouseMove(g, xRange[1], yRange[0]); // this is really necessary.
   DygraphOps.dispatchMouseUp(g, xRange[1], yRange[0]);
 
-  assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
-  // assertEqualsDelta(originalYRange, g.yAxisRange(0), 0.2); // Not true, it's something in the middle.
+  assertDeepCloseTo(xRange, g.xAxisRange(), 0.2);
+  // assert.closeTo(originalYRange, g.yAxisRange(0), 0.2); // Not true, it's something in the middle.
 
   var midX = (xRange[1] - xRange[0]) / 2;
   DygraphOps.dispatchMouseDown(g, midX, yRange[0]);
   DygraphOps.dispatchMouseMove(g, midX, yRange[1]); // this is really necessary.
   DygraphOps.dispatchMouseUp(g, midX, yRange[1]);
 
-  assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
-  assertEqualsDelta(yRange, g.yAxisRange(0), 0.2);
+  assertDeepCloseTo(xRange, g.xAxisRange(), 0.2);
+  assertDeepCloseTo(yRange, g.yAxisRange(0), 0.2);
 }
 
 
@@ -139,61 +140,61 @@ RangeTestCase.prototype.zoom = function(g, xRange, yRange) {
  * Verify that when zoomed in by mouse operations, an empty call to
  * updateOptions doesn't change the displayed ranges.
  */
-RangeTestCase.prototype.testEmptyUpdateOptions_doesntUnzoom = function() {
-  var g = this.createGraph();
-  this.zoom(g, [ 11, 18 ], [ 35, 40 ]);
+it('testEmptyUpdateOptions_doesntUnzoom', function() {
+  var g = createGraph();
+  zoom(g, [ 11, 18 ], [ 35, 40 ]);
 
-  assertEqualsDelta([11, 18], g.xAxisRange(), 0.1);
-  assertEqualsDelta([35, 40], g.yAxisRange(0), 0.2);
+  assertDeepCloseTo([11, 18], g.xAxisRange(), 0.1);
+  assertDeepCloseTo([35, 40], g.yAxisRange(0), 0.2);
 
   g.updateOptions({});
 
-  assertEqualsDelta([11, 18], g.xAxisRange(), 0.1);
-  assertEqualsDelta([35, 40], g.yAxisRange(0), 0.2);
-}
+  assertDeepCloseTo([11, 18], g.xAxisRange(), 0.1);
+  assertDeepCloseTo([35, 40], g.yAxisRange(0), 0.2);
+});
 
 /**
  * Verify that when zoomed in by mouse operations, a call to
  * updateOptions({ dateWindow : null, valueRange : null }) fully
  * unzooms.
  */
-RangeTestCase.prototype.testRestoreOriginalRanges_viaUpdateOptions = function() {
-  var g = this.createGraph();
-  this.zoom(g, [ 11, 18 ], [ 35, 40 ]);
+it('testRestoreOriginalRanges_viaUpdateOptions', function() {
+  var g = createGraph();
+  zoom(g, [ 11, 18 ], [ 35, 40 ]);
 
   g.updateOptions({ dateWindow : null, valueRange : null });
 
-  assertEquals([0, 55], g.yAxisRange(0));
-  assertEquals([10, 20], g.xAxisRange());
-}
+  assert.deepEqual([0, 55], g.yAxisRange(0));
+  assert.deepEqual([10, 20], g.xAxisRange());
+});
 
 /**
  * Verify that log scale axis range is properly specified.
  */
-RangeTestCase.prototype.testLogScaleExcludesZero = function() {
+it('testLogScaleExcludesZero', function() {
   var g = new Dygraph("graph", FIVE_TO_ONE_THOUSAND, { logscale : true });
-  assertEquals([10, 1099], g.yAxisRange(0));
+  assert.deepEqual([10, 1099], g.yAxisRange(0));
  
   g.updateOptions({ logscale : false });
-  assertEquals([0, 1099], g.yAxisRange(0));
-}
+  assert.deepEqual([0, 1099], g.yAxisRange(0));
+});
 
 /**
  * Verify that includeZero range is properly specified.
  */
-RangeTestCase.prototype.testIncludeZeroIncludesZero = function() {
+it('testIncludeZeroIncludesZero', function() {
   var g = new Dygraph("graph", [[0, 500], [500, 1000]], { includeZero : true });
-  assertEquals([0, 1100], g.yAxisRange(0));
+  assert.deepEqual([0, 1100], g.yAxisRange(0));
  
   g.updateOptions({ includeZero : false });
-  assertEquals([450, 1050], g.yAxisRange(0));
-}
+  assert.deepEqual([450, 1050], g.yAxisRange(0));
+});
 
 
 /**
  * Verify that includeZero range is properly specified per axis.
  */
-RangeTestCase.prototype.testIncludeZeroPerAxis = function() {
+it('testIncludeZeroPerAxis', function() {
   var g = new Dygraph("graph", 
     'X,A,B\n'+
     '0,50,50\n'+
@@ -216,8 +217,8 @@ RangeTestCase.prototype.testIncludeZeroPerAxis = function() {
     });
 
 
-  assertEquals([44, 116], g.yAxisRange(0));
-  assertEquals([0, 121], g.yAxisRange(1));
+  assert.deepEqual([44, 116], g.yAxisRange(0));
+  assert.deepEqual([0, 121], g.yAxisRange(1));
 
   g.updateOptions({
     axes: {
@@ -225,77 +226,77 @@ RangeTestCase.prototype.testIncludeZeroPerAxis = function() {
     }
   });
 
-  assertEquals([44, 116], g.yAxisRange(1));
-}
+  assert.deepEqual([44, 116], g.yAxisRange(1));
+});
 
 /**
  * Verify that very large Y ranges don't break things.
  */ 
-RangeTestCase.prototype.testHugeRange = function() {
+it('testHugeRange', function() {
   var g = new Dygraph("graph", [[0, -1e120], [1, 1e230]], { includeZero : true });
-  assertEqualsDelta(1, -1e229 / g.yAxisRange(0)[0], 0.001);
-  assertEqualsDelta(1, 1.1e230 / g.yAxisRange(0)[1], 0.001);
-}
+  assert.closeTo(1, -1e229 / g.yAxisRange(0)[0], 0.001);
+  assert.closeTo(1, 1.1e230 / g.yAxisRange(0)[1], 0.001);
+});
 
 /**
  * Verify old-style avoidMinZero option.
  */
-RangeTestCase.prototype.testAvoidMinZero = function() {
-  var g = this.createGraph({
+it('testAvoidMinZero', function() {
+  var g = createGraph({
       avoidMinZero: true,
     }, ZERO_TO_FIFTY_STEPS, [10, 20], [-5, 55]);
-};
+});
 
 /**
  * Verify ranges with user-specified padding, implicit avoidMinZero.
  */
-RangeTestCase.prototype.testPaddingAuto = function() {
-  var g = this.createGraph({
+it('testPaddingAuto', function() {
+  var g = createGraph({
       xRangePad: 42,
       yRangePad: 30
     }, ZERO_TO_FIFTY_STEPS, [9, 21], [-5, 55]);
-};
+});
 
 /**
  * Verify auto range with drawAxesAtZero.
  */
-RangeTestCase.prototype.testPaddingAutoAxisAtZero = function() {
-  var g = this.createGraph({
+it('testPaddingAutoAxisAtZero', function() {
+  var g = createGraph({
       drawAxesAtZero: true,
     }, ZERO_TO_FIFTY_STEPS, [10, 20], [0, 55]);
-};
+});
 
 /**
  * Verify user-specified range with padding and drawAxesAtZero options.
  * Try explicit range matching the auto range, should have identical results.
  */
-RangeTestCase.prototype.testPaddingRange1 = function() {
-  var g = this.createGraph({
+it('testPaddingRange1', function() {
+  var g = createGraph({
       valueRange: [0, 50],
       xRangePad: 42,
       yRangePad: 30,
       drawAxesAtZero: true
     }, ZERO_TO_FIFTY_STEPS, [9, 21], [-5, 55]);
-};
+});
 
 /**
  * Verify user-specified range with padding and drawAxesAtZero options.
  * User-supplied range differs from the auto range.
  */
-RangeTestCase.prototype.testPaddingRange2 = function() {
-  var g = this.createGraph({
+it('testPaddingRange2', function() {
+  var g = createGraph({
       valueRange: [10, 60],
       xRangePad: 42,
       yRangePad: 30,
       drawAxesAtZero: true,
     }, ZERO_TO_FIFTY_STEPS, [9, 21], [5, 65]);
-};
+});
 
 /**
  * Verify drawAxesAtZero and includeZero.
  */
-RangeTestCase.prototype.testPaddingYAtZero = function() {
-  var g = this.createGraph({
+it('testPaddingYAtZero', function() {
+  var g = createGraph({
       includeZero: true,
       xRangePad: 42,
       yRangePad: 30,
@@ -305,36 +306,36 @@ RangeTestCase.prototype.testPaddingYAtZero = function() {
       [10, 20],
       [30, 50]
     ], [-14, 34], [-5, 55]);
-};
+});
 
 /**
  * Verify logscale, compat mode.
  */
-RangeTestCase.prototype.testLogscaleCompat = function() {
-  var g = this.createGraph({
+it('testLogscaleCompat', function() {
+  var g = createGraph({
       logscale: true
     },
     [[-10, 10], [10, 10], [30, 1000]],
     [-10, 30], [10, 1099]);
-};
+});
 
 /**
  * Verify logscale, new mode.
  */
-RangeTestCase.prototype.testLogscalePad = function() {
-  var g = this.createGraph({
+it('testLogscalePad', function() {
+  var g = createGraph({
       logscale: true,
       yRangePad: 30
     },
     [[-10, 10], [10, 10], [30, 1000]],
     [-10, 30], [5.01691, 1993.25801]);
-};
+});
 
 /**
  * Verify scrolling all-zero region, traditional.
  */
-RangeTestCase.prototype.testZeroScroll = function() {
-  g = new Dygraph(
+it('testZeroScroll', function() {
+  var g = new Dygraph(
       document.getElementById("graph"),
       "X,Y\n" +
       "1,0\n" +
@@ -345,13 +346,13 @@ RangeTestCase.prototype.testZeroScroll = function() {
         animatedZooms: true,
         avoidMinZero: true
       });
-};
+});
 
 /**
  * Verify scrolling all-zero region, new-style.
  */
-RangeTestCase.prototype.testZeroScroll2 = function() {
-  g = new Dygraph(
+it('testZeroScroll2', function() {
+  var g = new Dygraph(
       document.getElementById("graph"),
       "X,Y\n" +
       "1,0\n" +
@@ -363,4 +364,6 @@ RangeTestCase.prototype.testZeroScroll2 = function() {
         xRangePad: 4,
         yRangePad: 4
       });
-};
+});
+
+});
index 3ce99ad..9a8cffe 100644 (file)
@@ -3,9 +3,9 @@
  *
  * @author konigsberg@google.com (Robert Konigsberg)
  */
-var ResizeTestCase = TestCase("resize");
+describe("resize", function() {
 
-ResizeTestCase.data =
+var data =
       "X,Y\n" +
       "1,100\n" +
       "2,200\n" +
@@ -14,14 +14,14 @@ ResizeTestCase.data =
       "5,300\n" +
       "6,100\n";
 
-ResizeTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-ResizeTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-ResizeTestCase.prototype.testResizeMaintainsMouseOperations = function() {
+it('testResizeMaintainsMouseOperations', function() {
   document.body.innerHTML =
       '<div id="graph" style="width: 640px; height: 480px;"></div>' +
       '</div>';
@@ -41,34 +41,34 @@ ResizeTestCase.prototype.testResizeMaintainsMouseOperations = function() {
     DygraphOps.dispatchMouseUp_Point(g, x2 - 1, y);
   }
 
-  g = new Dygraph(graph, ResizeTestCase.data, {highlightCallback: callback});
+  var g = new Dygraph(graph, data, {highlightCallback: callback});
 
   strum(g, 300, 640);
-  assertEquals(6, callbackCount);
+  assert.equal(6, callbackCount);
 
   document.getElementById("graph").style.width = "500px";
   g.resize();
 
   callbackCount = 0;
   strum(g, 300, 500);
-  assertEquals(6, callbackCount);
-};
+  assert.equal(6, callbackCount);
+});
 
 /**
  * Tests that a graph created in a not-displayed div works as expected
  * if the graph options include height and width. Resize not needed.
  */
-ResizeTestCase.prototype.testHiddenDivWithSizedGraph = function() {
+it('testHiddenDivWithSizedGraph', function() {
   var div = document.getElementById("graph");
 
   div.style.display = 'none';
-  var g = new Dygraph(div, ResizeTestCase.data, {width: 400, height: 300});
+  var g = new Dygraph(div, data, {width: 400, height: 300});
   div.style.display = '';
 
   var area = g.getArea();
-  assertTrue(area.w > 0);
-  assertTrue(area.h > 0);
-};
+  assert.isTrue(area.w > 0);
+  assert.isTrue(area.h > 0);
+});
 
 /**
  * Tests that a graph created in a not-displayed div with
@@ -76,7 +76,7 @@ ResizeTestCase.prototype.testHiddenDivWithSizedGraph = function() {
  * expected. The user needs to call resize() on it after displaying
  * it.
  */
-ResizeTestCase.prototype.testHiddenDivWithResize = function() {
+it('testHiddenDivWithResize', function() {
   var div = document.getElementById("graph");
 
   div.style.display = 'none';
@@ -85,18 +85,20 @@ ResizeTestCase.prototype.testHiddenDivWithResize = function() {
 
   // Setting strokeWidth 3 removes any ambiguitiy from the pixel sampling
   // request, below.
-  var g = new Dygraph(div, ResizeTestCase.data, {strokeWidth: 3});
+  var g = new Dygraph(div, data, {strokeWidth: 3});
   div.style.display = '';
 
   g.resize();
-  area = g.getArea();
-  assertTrue(area.w > 0);
-  assertTrue(area.h > 0);
+  var area = g.getArea();
+  assert.isTrue(area.w > 0);
+  assert.isTrue(area.h > 0);
 
   // Regression test: check that graph remains visible after no-op resize.
   g.resize();
   var x = Math.floor(g.toDomXCoord(2));
   var y = Math.floor(g.toDomYCoord(200));
-  assertEquals("Unexpected grid color found at pixel: x: " + x + " y: " + y,
-               [0, 128, 128, 255], Util.samplePixel(g.hidden_, x, y));
-};
+  assert.deepEqual([0, 128, 128, 255], Util.samplePixel(g.hidden_, x, y),
+                   "Unexpected grid color found at pixel: x: " + x + " y: " + y);
+});
+
+});
index 86b3e3c..4c281b9 100644 (file)
@@ -3,16 +3,16 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var rollingAverageTestCase = TestCase("rolling-average");
+describe("rolling-average", function() {
 
-rollingAverageTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-rollingAverageTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-rollingAverageTestCase.prototype.testRollingAverage = function() {
+it('testRollingAverage', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -29,35 +29,35 @@ rollingAverageTestCase.prototype.testRollingAverage = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  g.setSelection(0); assertEquals("0: Y: 0", Util.getLegend());
-  g.setSelection(1); assertEquals("1: Y: 1", Util.getLegend());
-  g.setSelection(2); assertEquals("2: Y: 2", Util.getLegend());
-  g.setSelection(3); assertEquals("3: Y: 3", Util.getLegend());
-  assertEquals(1, g.rollPeriod());
+  g.setSelection(0); assert.equal("0: Y: 0", Util.getLegend());
+  g.setSelection(1); assert.equal("1: Y: 1", Util.getLegend());
+  g.setSelection(2); assert.equal("2: Y: 2", Util.getLegend());
+  g.setSelection(3); assert.equal("3: Y: 3", Util.getLegend());
+  assert.equal(1, g.rollPeriod());
 
   g.updateOptions({rollPeriod: 2});
-  g.setSelection(0); assertEquals("0: Y: 0", Util.getLegend());
-  g.setSelection(1); assertEquals("1: Y: 0.5", Util.getLegend());
-  g.setSelection(2); assertEquals("2: Y: 1.5", Util.getLegend());
-  g.setSelection(3); assertEquals("3: Y: 2.5", Util.getLegend());
-  assertEquals(2, g.rollPeriod());
+  g.setSelection(0); assert.equal("0: Y: 0", Util.getLegend());
+  g.setSelection(1); assert.equal("1: Y: 0.5", Util.getLegend());
+  g.setSelection(2); assert.equal("2: Y: 1.5", Util.getLegend());
+  g.setSelection(3); assert.equal("3: Y: 2.5", Util.getLegend());
+  assert.equal(2, g.rollPeriod());
 
   g.updateOptions({rollPeriod: 3});
-  g.setSelection(0); assertEquals("0: Y: 0", Util.getLegend());
-  g.setSelection(1); assertEquals("1: Y: 0.5", Util.getLegend());
-  g.setSelection(2); assertEquals("2: Y: 1", Util.getLegend());
-  g.setSelection(3); assertEquals("3: Y: 2", Util.getLegend());
-  assertEquals(3, g.rollPeriod());
+  g.setSelection(0); assert.equal("0: Y: 0", Util.getLegend());
+  g.setSelection(1); assert.equal("1: Y: 0.5", Util.getLegend());
+  g.setSelection(2); assert.equal("2: Y: 1", Util.getLegend());
+  g.setSelection(3); assert.equal("3: Y: 2", Util.getLegend());
+  assert.equal(3, g.rollPeriod());
 
   g.updateOptions({rollPeriod: 4});
-  g.setSelection(0); assertEquals("0: Y: 0", Util.getLegend());
-  g.setSelection(1); assertEquals("1: Y: 0.5", Util.getLegend());
-  g.setSelection(2); assertEquals("2: Y: 1", Util.getLegend());
-  g.setSelection(3); assertEquals("3: Y: 1.5", Util.getLegend());
-  assertEquals(4, g.rollPeriod());
-};
-
-rollingAverageTestCase.prototype.testRollBoxDoesntDisapper = function() {
+  g.setSelection(0); assert.equal("0: Y: 0", Util.getLegend());
+  g.setSelection(1); assert.equal("1: Y: 0.5", Util.getLegend());
+  g.setSelection(2); assert.equal("2: Y: 1", Util.getLegend());
+  g.setSelection(3); assert.equal("3: Y: 1.5", Util.getLegend());
+  assert.equal(4, g.rollPeriod());
+});
+
+it('testRollBoxDoesntDisapper', function() {
   var opts = {
     showRoller: true
   };
@@ -72,17 +72,17 @@ rollingAverageTestCase.prototype.testRollBoxDoesntDisapper = function() {
   var g = new Dygraph(graph, data, opts);
 
   var roll_box = graph.getElementsByTagName("input");
-  assertEquals(1, roll_box.length);
-  assertEquals("1", roll_box[0].value);
+  assert.equal(1, roll_box.length);
+  assert.equal("1", roll_box[0].value);
 
   graph.style.width = "500px";
   g.resize();
-  assertEquals(1, roll_box.length);
-  assertEquals("1", roll_box[0].value);
-};
+  assert.equal(1, roll_box.length);
+  assert.equal("1", roll_box[0].value);
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=426
-rollingAverageTestCase.prototype.testRollShortFractions = function() {
+it('testRollShortFractions', function() {
   var opts = {
     customBars: true,
     labels: ['x', 'A']
@@ -98,10 +98,10 @@ rollingAverageTestCase.prototype.testRollShortFractions = function() {
   var rolled1 = g.dataHandler_.rollingAverage(data1, 1, g);
   var rolled2 = g.dataHandler_.rollingAverage(data2, 1, g);
 
-  assertEquals(rolled1[0], rolled2[0]);
-};
+  assert.deepEqual(rolled1[0], rolled2[0]);
+});
 
-rollingAverageTestCase.prototype.testRollCustomBars = function() {
+it('testRollCustomBars', function() {
   var opts = {
     customBars: true,
     rollPeriod: 2,
@@ -115,14 +115,14 @@ rollingAverageTestCase.prototype.testRollCustomBars = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  var rolled = this.getRolledData(g, data, 1, 2);
-  assertEquals([1, 10, [1, 20]], rolled[0]);
-  assertEquals([2, 15, [1, 25]], rolled[1]);
-  assertEquals([3, 25, [1, 35]], rolled[2]);
-  assertEquals([4, 35, [1, 45]], rolled[3]);
-};
-
-rollingAverageTestCase.prototype.testRollErrorBars = function() {
+  var rolled = getRolledData(g, data, 1, 2);
+  assert.deepEqual([1, 10, [1, 20]], rolled[0]);
+  assert.deepEqual([2, 15, [1, 25]], rolled[1]);
+  assert.deepEqual([3, 25, [1, 35]], rolled[2]);
+  assert.deepEqual([4, 35, [1, 45]], rolled[3]);
+});
+
+it('testRollErrorBars', function() {
   var opts = {
     errorBars: true,
     rollPeriod: 2,
@@ -136,20 +136,20 @@ rollingAverageTestCase.prototype.testRollErrorBars = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  var rolled = this.getRolledData(g, data, 1, 2);
-  assertEquals([1, 10, [8, 12]], rolled[0]);
+  var rolled = getRolledData(g, data, 1, 2);
+  assert.deepEqual([1, 10, [8, 12]], rolled[0]);
  
   // variance = sqrt( pow(error) * rollPeriod)
   var variance = Math.sqrt(2);
   for (var i=1;i<data.length;i++) {
     var value = data[i][1][0] - 5;
-    assertEquals("unexpected rolled average", value, rolled[i][1]);
-    assertEquals("unexpected rolled min", value - variance, rolled[i][2][0]);
-    assertEquals("unexpected rolled max", value + variance, rolled[i][2][1]);
+    assert.equal(value, rolled[i][1], "unexpected rolled average");
+    assert.equal(value - variance, rolled[i][2][0], "unexpected rolled min");
+    assert.equal(value + variance, rolled[i][2][1], "unexpected rolled max");
   }
-};
+});
 
-rollingAverageTestCase.prototype.testRollFractions = function() {
+it('testRollFractions', function() {
   var opts = {
     fractions: true,
     rollPeriod: 2,
@@ -163,14 +163,14 @@ rollingAverageTestCase.prototype.testRollFractions = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  var rolled = this.getRolledData(g, data, 1, 2);
-  assertEquals([1, 10], rolled[0]);
-  assertEquals([2, 15], rolled[1]);
-  assertEquals([3, 25], rolled[2]);
-  assertEquals([4, 35], rolled[3]);
-};
-
-rollingAverageTestCase.prototype.testRollFractionsBars = function() {
+  var rolled = getRolledData(g, data, 1, 2);
+  assert.deepEqual([1, 10], rolled[0]);
+  assert.deepEqual([2, 15], rolled[1]);
+  assert.deepEqual([3, 25], rolled[2]);
+  assert.deepEqual([4, 35], rolled[3]);
+});
+
+it('testRollFractionsBars', function() {
   var opts = {
     fractions: true,
     errorBars: true,
@@ -186,7 +186,7 @@ rollingAverageTestCase.prototype.testRollFractionsBars = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  var rolled = this.getRolledData(g, data, 1, 2);
+  var rolled = getRolledData(g, data, 1, 2);
 
   // precalculated rounded values expected
   var values = [10, 15, 25, 35];
@@ -194,13 +194,13 @@ rollingAverageTestCase.prototype.testRollFractionsBars = function() {
   var highs = [29, 31, 44, 56];
 
   for (var i=0;i<data.length;i++) {
-    assertEquals("unexpected rolled average", values[i], Math.round(rolled[i][1]));
-    assertEquals("unexpected rolled min", lows[i], Math.round(rolled[i][2][0]));
-    assertEquals("unexpected rolled max", highs[i], Math.round(rolled[i][2][1]));
+    assert.equal(values[i], Math.round(rolled[i][1]), "unexpected rolled average");
+    assert.equal(lows[i], Math.round(rolled[i][2][0]), "unexpected rolled min");
+    assert.equal(highs[i], Math.round(rolled[i][2][1]), "unexpected rolled max");
   }
-};
+});
 
-rollingAverageTestCase.prototype.testRollFractionsBarsWilson = function() {
+it('testRollFractionsBarsWilson', function() {
   var opts = {
     fractions: true,
     errorBars: true,
@@ -216,7 +216,7 @@ rollingAverageTestCase.prototype.testRollFractionsBarsWilson = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
-  var rolled = this.getRolledData(g, data, 1, 2);
+  var rolled = getRolledData(g, data, 1, 2);
 
   //precalculated rounded values expected
   var values = [10, 15, 25, 35];
@@ -224,13 +224,15 @@ rollingAverageTestCase.prototype.testRollFractionsBarsWilson = function() {
   var highs = [41, 37, 47, 57];
 
   for (var i=0;i<data.length;i++) {
-    assertEquals("unexpected rolled average", values[i], Math.round(rolled[i][1]));
-    assertEquals("unexpected rolled min", lows[i], Math.round(rolled[i][2][0]));
-    assertEquals("unexpected rolled max", highs[i], Math.round(rolled[i][2][1]));
+    assert.equal(values[i], Math.round(rolled[i][1]), "unexpected rolled average");
+    assert.equal(lows[i], Math.round(rolled[i][2][0]), "unexpected rolled min");
+    assert.equal(highs[i], Math.round(rolled[i][2][1]), "unexpected rolled max");
   }
-};
+});
 
-rollingAverageTestCase.prototype.getRolledData = function(g, data, seriesIdx, rollPeriod){
+var getRolledData = function(g, data, seriesIdx, rollPeriod){
   var options = g.attributes_;
   return g.dataHandler_.rollingAverage(g.dataHandler_.extractSeries(data, seriesIdx, options), rollPeriod, options);
 };
+
+});
index 95a1aa2..9eeb7f3 100644 (file)
 var DEAD_SIMPLE_DATA = [[ 10, 2100 ]];
 var ZERO_TO_FIFTY = [[ 10, 0 ] , [ 20, 50 ]];
 
-var SanityTestCase = TestCase("dygraphs-sanity");
+describe("dygraphs-sanity", function() {
 
-SanityTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
 /**
  * The sanity test of sanity tests.
  */
-SanityTestCase.prototype.testTrue = function() {
-  assertTrue(true);
-};
+it('testTrue', function() {
+  assert.isTrue(true);
+});
 
 /**
  * Sanity test that ensures the graph element exists.
  */
-SanityTestCase.prototype.testGraphExists = function() {
+it('testGraphExists', function() {
   var graph = document.getElementById("graph");
-  assertNotNull(graph);
-};
+  assert.isNotNull(graph);
+});
 
 // TODO(konigsberg): Move the following tests to a new package that
 // tests all kinds of toDomCoords, toDataCoords, toPercent, et cetera.
@@ -55,34 +55,34 @@ SanityTestCase.prototype.testGraphExists = function() {
  * A sanity test of sorts, by ensuring the dygraph is created, and
  * isn't just some piece of junk object.
  */
-SanityTestCase.prototype.testToString = function() {
+it('testToString', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, DEAD_SIMPLE_DATA, {});
-  assertNotNull(g);
-  assertEquals("[Dygraph graph]", g.toString());
-};
+  assert.isNotNull(g);
+  assert.equal("[Dygraph graph]", g.toString());
+});
 
 /**
  * Test that when no valueRange is specified, the y axis range is
  * adjusted by 10% on top.
  */
-SanityTestCase.prototype.testYAxisRange_default = function() {
+it('testYAxisRange_default', function() {
   var graph = document.getElementById("graph");
-  assertEquals(0, graph.style.length);
+  assert.equal(0, graph.style.length);
   var g = new Dygraph(graph, ZERO_TO_FIFTY, {});
-  assertEquals([0, 55], g.yAxisRange(0));
-};
+  assert.deepEqual([0, 55], g.yAxisRange(0));
+});
 
 /**
  * Test that valueRange matches the y-axis range specifically.
  */
-SanityTestCase.prototype.testYAxisRange_custom = function() {
+it('testYAxisRange_custom', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, ZERO_TO_FIFTY, { valueRange: [0,50] });
-  assertEquals([0, 50], g.yAxisRange(0));
+  assert.deepEqual([0, 50], g.yAxisRange(0));
   g.updateOptions({valueRange: null, axes: {y: {valueRange: [10, 40]}}});
-  assertEquals([10, 40], g.yAxisRange(0));
-};
+  assert.deepEqual([10, 40], g.yAxisRange(0));
+});
 
 /**
  * Test that valueRange matches the y-axis range specifically.
@@ -91,48 +91,50 @@ SanityTestCase.prototype.testYAxisRange_custom = function() {
  * axis label and tick marks.
  * TODO(konigsberg): change yAxisLabelWidth to 0 (or 20) and try again.
  */
-SanityTestCase.prototype.testToDomYCoord = function() {
+it('testToDomYCoord', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, ZERO_TO_FIFTY, { height: 70, valueRange: [0,50] });
 
-  assertEquals(50, g.toDomYCoord(0));
-  assertEquals(0, g.toDomYCoord(50));
+  assert.equal(50, g.toDomYCoord(0));
+  assert.equal(0, g.toDomYCoord(50));
 
   for (var x = 0; x <= 50; x++) {
-    assertEqualsDelta(50 - x, g.toDomYCoord(x), 0.00001);
+    assert.closeTo(50 - x, g.toDomYCoord(x), 0.00001);
   }
   g.updateOptions({valueRange: null, axes: {y: {valueRange: [0, 50]}}});
 
-  assertEquals(50, g.toDomYCoord(0));
-  assertEquals(0, g.toDomYCoord(50));
+  assert.equal(50, g.toDomYCoord(0));
+  assert.equal(0, g.toDomYCoord(50));
 
   for (var x = 0; x <= 50; x++) {
-    assertEqualsDelta(50 - x, g.toDomYCoord(x), 0.00001);
+    assert.closeTo(50 - x, g.toDomYCoord(x), 0.00001);
   }
-};
+});
 
 /**
  * Test that the two-argument form of the constructor (no options) works.
  */
-SanityTestCase.prototype.testTwoArgumentConstructor = function() {
+it('testTwoArgumentConstructor', function() {
   var graph = document.getElementById("graph");
   new Dygraph(graph, ZERO_TO_FIFTY);
-};
+});
 
 // Here is the first of a series of tests that just ensure the graph is drawn
 // without exception.
 //TODO(konigsberg): Move to its own test case.
-SanityTestCase.prototype.testFillStack1 = function() {
+it('testFillStack1', function() {
   var graph = document.getElementById("graph");
   new Dygraph(graph, ZERO_TO_FIFTY, { stackedGraph: true });
-}
+});
 
-SanityTestCase.prototype.testFillStack2 = function() {
+it('testFillStack2', function() {
   var graph = document.getElementById("graph");
   new Dygraph(graph, ZERO_TO_FIFTY, { stackedGraph: true, fillGraph: true });
-}
+});
 
-SanityTestCase.prototype.testFillStack3 = function() {
+it('testFillStack3', function() {
   var graph = document.getElementById("graph");
   new Dygraph(graph, ZERO_TO_FIFTY, { fillGraph: true });
-}
+});
+
+});
index bbdeb6a..ebbf2ed 100644 (file)
@@ -5,14 +5,14 @@
  *
  * @author danvk@google.com (Dan Vanderkam)
  */
-var scientificNotationTestCase = TestCase("scientific-notation");
+describe("scientific-notation", function() {
 
-scientificNotationTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-scientificNotationTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 function getXValues(g) {
   var xs = [];
@@ -22,7 +22,7 @@ function getXValues(g) {
   return xs;
 }
 
-scientificNotationTestCase.prototype.testScientificInput = function() {
+it('testScientificInput', function() {
   var data = "X,Y\n" +
       "1.0e1,-1\n" +
       "2.0e1,0\n" +
@@ -32,10 +32,10 @@ scientificNotationTestCase.prototype.testScientificInput = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  assertEqualsDelta([10, 20, 30, 40], getXValues(g), 1e-6);
-};
+  assertDeepCloseTo([10, 20, 30, 40], getXValues(g), 1e-6);
+});
 
-scientificNotationTestCase.prototype.testScientificInputPlus = function() {
+it('testScientificInputPlus', function() {
   var data = "X,Y\n" +
       "1.0e+1,-1\n" +
       "2.0e+1,0\n" +
@@ -45,10 +45,10 @@ scientificNotationTestCase.prototype.testScientificInputPlus = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  assertEqualsDelta([10, 20, 30, 40], getXValues(g), 1e-6);
-};
+  assertDeepCloseTo([10, 20, 30, 40], getXValues(g), 1e-6);
+});
 
-scientificNotationTestCase.prototype.testScientificInputMinus = function() {
+it('testScientificInputMinus', function() {
   var data = "X,Y\n" +
       "1.0e-1,-1\n" +
       "2.0e-1,0\n" +
@@ -58,10 +58,10 @@ scientificNotationTestCase.prototype.testScientificInputMinus = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  assertEqualsDelta([0.1, 0.2, 0.3, 0.4], getXValues(g), 1e-6);
-};
+  assertDeepCloseTo([0.1, 0.2, 0.3, 0.4], getXValues(g), 1e-6);
+});
 
-scientificNotationTestCase.prototype.testScientificInputMinusCap = function() {
+it('testScientificInputMinusCap', function() {
   var data = "X,Y\n" +
       "1.0E-1,-1\n" +
       "2.0E-1,0\n" +
@@ -71,5 +71,7 @@ scientificNotationTestCase.prototype.testScientificInputMinusCap = function() {
 
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, {});
-  assertEqualsDelta([0.1, 0.2, 0.3, 0.4], getXValues(g), 1e-6);
-};
+  assertDeepCloseTo([0.1, 0.2, 0.3, 0.4], getXValues(g), 1e-6);
+});
+
+});
index ceb2a3d..ce95a7e 100644 (file)
@@ -3,9 +3,11 @@
  *
  * @author konigsberg@google.com (Robert Konigsbrg)
  */
-var ScrollingDivTestCase = TestCase("scrolling-div");
+describe("scrolling-div", function() {
 
-ScrollingDivTestCase.prototype.setUp = function() {
+var point, g; 
+
+beforeEach(function() {
 
 var LOREM_IPSUM =
     "<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n" +
@@ -35,26 +37,25 @@ var LOREM_IPSUM =
 
   var graph = document.getElementById("graph");
 
-  this.point = null;
-  var self = this;
-  this.g = new Dygraph(graph, data,
+  point = null;
+  g = new Dygraph(graph, data,
           {
             labels : ['a', 'b'],
             drawPoints : true,
             highlightCircleSize : 6,
-            pointClickCallback : function(evt, point) {
-              self.point = point;
+            pointClickCallback : function(evt, p) {
+              point = p;
             }
           }
       );
   
-};
+});
 
 // This is usually something like 15, but for OS X Lion and its auto-hiding
 // scrollbars, it's 0. This is a large enough difference that we need to
 // consider it when synthesizing clicks.
 // Adapted from http://davidwalsh.name/detect-scrollbar-width
-ScrollingDivTestCase.prototype.detectScrollbarWidth = function() {
+var detectScrollbarWidth = function() {
   // Create the measurement node
   var scrollDiv = document.createElement("div");
   scrollDiv.style.width = "100px";
@@ -73,13 +74,13 @@ ScrollingDivTestCase.prototype.detectScrollbarWidth = function() {
   return scrollbarWidth;
 };
 
-ScrollingDivTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 /**
  * This tests that when the nested div is unscrolled, things work normally.
  */
-ScrollingDivTestCase.prototype.testUnscrolledDiv = function() {
+it('testUnscrolledDiv', function() {
 
   document.getElementById('scroller').scrollTop = 0;
 
@@ -90,31 +91,33 @@ ScrollingDivTestCase.prototype.testUnscrolledDiv = function() {
     screenY: 320
   };
 
-  DygraphOps.dispatchCanvasEvent(this.g, DygraphOps.createEvent(clickOn4_40, { type : 'mousemove' }));
-  DygraphOps.dispatchCanvasEvent(this.g, DygraphOps.createEvent(clickOn4_40, { type : 'mousedown' }));
-  DygraphOps.dispatchCanvasEvent(this.g, DygraphOps.createEvent(clickOn4_40, { type : 'mouseup' }));
+  DygraphOps.dispatchCanvasEvent(g, DygraphOps.createEvent(clickOn4_40, { type : 'mousemove' }));
+  DygraphOps.dispatchCanvasEvent(g, DygraphOps.createEvent(clickOn4_40, { type : 'mousedown' }));
+  DygraphOps.dispatchCanvasEvent(g, DygraphOps.createEvent(clickOn4_40, { type : 'mouseup' }));
 
-  assertEquals(40, this.point.xval);
-  assertEquals(4, this.point.yval);
-};
+  assert.equal(40, point.xval);
+  assert.equal(4, point.yval);
+});
 
 /**
  * This tests that when the nested div is scrolled, things work normally.
  */
-ScrollingDivTestCase.prototype.testScrolledDiv = function() {
+it('testScrolledDiv', function() {
   document.getElementById('scroller').scrollTop = 117;
 
   var clickOn4_40 = {
     clientX: 244,
-    clientY: 30 - this.detectScrollbarWidth(),
+    clientY: 30 - detectScrollbarWidth(),
     screenX: 416,
     screenY: 160
   };
 
-  DygraphOps.dispatchCanvasEvent(this.g, DygraphOps.createEvent(clickOn4_40, { type : 'mousemove' }));
-  DygraphOps.dispatchCanvasEvent(this.g, DygraphOps.createEvent(clickOn4_40, { type : 'mousedown' }));
-  DygraphOps.dispatchCanvasEvent(this.g, DygraphOps.createEvent(clickOn4_40, { type : 'mouseup' }));
+  DygraphOps.dispatchCanvasEvent(g, DygraphOps.createEvent(clickOn4_40, { type : 'mousemove' }));
+  DygraphOps.dispatchCanvasEvent(g, DygraphOps.createEvent(clickOn4_40, { type : 'mousedown' }));
+  DygraphOps.dispatchCanvasEvent(g, DygraphOps.createEvent(clickOn4_40, { type : 'mouseup' }));
 
-  assertEquals(40, this.point.xval);
-  assertEquals(4, this.point.yval);
-};
+  assert.equal(40, point.xval);
+  assert.equal(4, point.yval);
+});
+
+});
index ff220f6..5dfc5ed 100644 (file)
@@ -6,13 +6,13 @@
  * @author danvk@google.com (Dan Vanderkam)
  */
 
-var SelectionTestCase = TestCase("selection");
+describe("selection", function() {
 
-SelectionTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-SelectionTestCase.prototype.testSetGetSelection = function() {
+it('testSetGetSelection', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph,
     "X,Y\n" +
@@ -22,14 +22,14 @@ SelectionTestCase.prototype.testSetGetSelection = function() {
   );
 
   g.setSelection(0);
-  assertEquals(0, g.getSelection());
+  assert.equal(0, g.getSelection());
   g.setSelection(1);
-  assertEquals(1, g.getSelection());
+  assert.equal(1, g.getSelection());
   g.setSelection(2);
-  assertEquals(2, g.getSelection());
-};
+  assert.equal(2, g.getSelection());
+});
 
-SelectionTestCase.prototype.testSetGetSelectionDense = function() {
+it('testSetGetSelectionDense', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph,
     "X,Y\n" +
@@ -40,17 +40,17 @@ SelectionTestCase.prototype.testSetGetSelectionDense = function() {
   );
 
   g.setSelection(0);
-  assertEquals(0, g.getSelection());
+  assert.equal(0, g.getSelection());
   g.setSelection(1);
-  assertEquals(1, g.getSelection());
+  assert.equal(1, g.getSelection());
   g.setSelection(2);
-  assertEquals(2, g.getSelection());
+  assert.equal(2, g.getSelection());
   g.setSelection(3);
-  assertEquals(3, g.getSelection());
-};
+  assert.equal(3, g.getSelection());
+});
 
-SelectionTestCase.prototype.testSetGetSelectionMissingPoints = function() {
-  dataHandler = function() {};
+it('testSetGetSelectionMissingPoints', function() {
+  var dataHandler = function() {};
   dataHandler.prototype = new Dygraph.DataHandlers.DefaultHandler();
   dataHandler.prototype.seriesToPoints = function(series, setName, boundaryIdStart) {
     var val = null;
@@ -82,9 +82,11 @@ SelectionTestCase.prototype.testSetGetSelectionMissingPoints = function() {
   );
 
   g.setSelection(0);
-  assertEquals(0, g.getSelection());
+  assert.equal(0, g.getSelection());
   g.setSelection(1);
-  assertEquals(1, g.getSelection());
+  assert.equal(1, g.getSelection());
   g.setSelection(2);
-  assertEquals(2, g.getSelection());
-};
+  assert.equal(2, g.getSelection());
+});
+
+});
index 5ffe532..5570132 100644 (file)
  */
 var ZERO_TO_FIFTY = [[ 10, 0 ] , [ 20, 50 ]];
 
-var SimpleDrawingTestCase = TestCase("simple-drawing");
+describe("simple-drawing", function() {
 
-SimpleDrawingTestCase._origFunc = Dygraph.getContext;
-SimpleDrawingTestCase.prototype.setUp = function() {
+var _origFunc = Dygraph.getContext;
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(SimpleDrawingTestCase._origFunc(canvas));
+    return new Proxy(_origFunc(canvas));
   }
-};
+});
 
-SimpleDrawingTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = SimpleDrawingTestCase._origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = _origFunc;
+});
 
-SimpleDrawingTestCase.prototype.testDrawSimpleRangePlusOne = function() {
+it('testDrawSimpleRangePlusOne', function() {
   var opts = {
     axes : {
       x : {
@@ -63,10 +63,10 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangePlusOne = function() {
   });
   g.destroy(); // to balance context saves and destroys.
   CanvasAssertions.assertBalancedSaveRestore(htx);
-};
+});
 
 // See http://code.google.com/p/dygraphs/issues/detail?id=185
-SimpleDrawingTestCase.prototype.testDrawSimpleRangeZeroToFifty = function() {
+it('testDrawSimpleRangeZeroToFifty', function() {
   var opts = {
     axes : {
       x : {
@@ -88,25 +88,25 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangeZeroToFifty = function() {
     strokeStyle: "#008080",
     lineWidth: 1
   });
-  assertEquals(1, lines.length);
+  assert.equal(1, lines.length);
   g.destroy(); // to balance context saves and destroys.
   CanvasAssertions.assertBalancedSaveRestore(htx);
-};
+});
 
-SimpleDrawingTestCase.prototype.testDrawWithAxis = function() {
+it('testDrawWithAxis', function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, ZERO_TO_FIFTY);
 
   var htx = g.hidden_ctx_;
   g.destroy(); // to balance context saves and destroys.
   CanvasAssertions.assertBalancedSaveRestore(htx);
-};
+});
 
 /**
  * Tests that it is drawing dashes, and it remember the dash history between
  * points.
  */
-SimpleDrawingTestCase.prototype.testDrawSimpleDash = function() {
+it('testDrawSimpleDash', function() {
   var opts = {
     axes: {
       x: {
@@ -129,19 +129,19 @@ SimpleDrawingTestCase.prototype.testDrawSimpleDash = function() {
   graph.style.width='480px';
   graph.style.height='320px';
   var g = new Dygraph(graph, [[1, 4], [2, 5], [3, 3], [4, 7], [5, 9]], opts);
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   // TODO(danvk): figure out a good way to restore this test.
-  // assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000"));
+  // assert.equal(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000"));
   g.destroy(); // to balance context saves and destroys.
   CanvasAssertions.assertBalancedSaveRestore(htx);
-};
+});
 
 /**
  * Tests that thick lines are drawn continuously.
  * Regression test for http://code.google.com/p/dygraphs/issues/detail?id=328
  */
-SimpleDrawingTestCase.prototype.testDrawThickLine = function() {
+it('testDrawThickLine', function() {
   var opts = {
     axes : {
       x : {
@@ -162,7 +162,7 @@ SimpleDrawingTestCase.prototype.testDrawThickLine = function() {
   graph.style.width='480px';
   graph.style.height='320px';
   var g = new Dygraph(graph, [[1, 2], [2, 5], [3, 2], [4, 7], [5, 0]], opts);
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   // There's a big gap in the line at (2, 5)
   // If the bug is fixed, then there should be some red going up from here.
@@ -170,12 +170,14 @@ SimpleDrawingTestCase.prototype.testDrawThickLine = function() {
   var x = Math.round(xy[0]), y = Math.round(xy[1]);
 
   var sampler = new PixelSampler(g);
-  assertEquals([255,0,0,255], sampler.colorAtPixel(x, y));
-  assertEquals([255,0,0,255], sampler.colorAtPixel(x, y - 1));
-  assertEquals([255,0,0,255], sampler.colorAtPixel(x, y - 2));
+  assert.deepEqual([255,0,0,255], sampler.colorAtPixel(x, y));
+  assert.deepEqual([255,0,0,255], sampler.colorAtPixel(x, y - 1));
+  assert.deepEqual([255,0,0,255], sampler.colorAtPixel(x, y - 2));
 
   // TODO(danvk): figure out a good way to restore this test.
-  // assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000"));
+  // assert.equal(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000"));
   g.destroy(); // to balance context saves and destroys.
   CanvasAssertions.assertBalancedSaveRestore(htx);
-};
+});
+
+});
index 2268bb3..41b0f5d 100644 (file)
@@ -3,43 +3,45 @@
  *
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
-var smoothPlotterTestCase = TestCase("smooth-plotter");
+describe("smooth-plotter", function() {
 
 var getControlPoints = smoothPlotter._getControlPoints;
 
-smoothPlotterTestCase.prototype.setUp = function() {
-};
+beforeEach(function() {
+});
 
-smoothPlotterTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
-smoothPlotterTestCase.prototype.testNoSmoothing = function() {
+it('testNoSmoothing', function() {
   var lastPt = {x: 10, y: 0},
       pt = {x: 11, y: 1},
       nextPt = {x: 12, y: 0},
       alpha = 0;
 
-  assertEquals([11, 1, 11, 1], getControlPoints(lastPt, pt, nextPt, alpha));
-};
+  assert.deepEqual([11, 1, 11, 1], getControlPoints(lastPt, pt, nextPt, alpha));
+});
 
-smoothPlotterTestCase.prototype.testHalfSmoothing = function() {
+it('testHalfSmoothing', function() {
   var lastPt = {x: 10, y: 0},
       pt = {x: 11, y: 1},
       nextPt = {x: 12, y: 0},
       alpha = 0.5;
 
-  assertEquals([10.5, 1, 11.5, 1], getControlPoints(lastPt, pt, nextPt, alpha));
-}
+  assert.deepEqual([10.5, 1, 11.5, 1], getControlPoints(lastPt, pt, nextPt, alpha));
+});
 
-smoothPlotterTestCase.prototype.testExtrema = function() {
+it('testExtrema', function() {
   var lastPt = {x: 10, y: 0},
       pt = {x: 11, y: 1},
       nextPt = {x: 12, y: 1},
       alpha = 0.5;
 
-  assertEquals([10.5, 0.75, 11.5, 1.25],
+  assert.deepEqual([10.5, 0.75, 11.5, 1.25],
                getControlPoints(lastPt, pt, nextPt, alpha, true));
 
-  assertEquals([10.5, 1, 11.5, 1],
+  assert.deepEqual([10.5, 1, 11.5, 1],
                getControlPoints(lastPt, pt, nextPt, alpha, false));
-}
+});
+
+});
index a041d7d..d3add12 100644 (file)
@@ -3,22 +3,22 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var stackedTestCase = TestCase("stacked");
+describe("stacked", function() {
 
-stackedTestCase._origGetContext = Dygraph.getContext;
+var _origGetContext = Dygraph.getContext;
 
-stackedTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(stackedTestCase._origGetContext(canvas));
+    return new Proxy(_origGetContext(canvas));
   }
-};
+});
 
-stackedTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = stackedTestCase._origGetContext;
-};
+afterEach(function() {
+  Dygraph.getContext = _origGetContext;
+});
 
-stackedTestCase.prototype.testCorrectColors = function() {
+it('testCorrectColors', function() {
   var opts = {
     width: 400,
     height: 300,
@@ -54,12 +54,12 @@ stackedTestCase.prototype.testCorrectColors = function() {
   // y pixels 0-99 = nothing (white)
 
   // 38 = round(0.15 * 255)
-  assertEquals([0, 0, 255, 38], Util.samplePixel(g.hidden_, 200, 250));
-  assertEquals([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 150));
-};
+  assert.deepEqual([0, 0, 255, 38], Util.samplePixel(g.hidden_, 200, 250));
+  assert.deepEqual([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 150));
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=358
-stackedTestCase.prototype.testSelectionValues = function() {
+it('testSelectionValues', function() {
   var opts = {
     stackedGraph: true
   };
@@ -71,11 +71,11 @@ stackedTestCase.prototype.testSelectionValues = function() {
   ;
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   g.setSelection(0);
 
-  assertEquals("0: Y1: 1 Y2: 1", Util.getLegend());
+  assert.equal("0: Y1: 1 Y2: 1", Util.getLegend());
 
   // Verify that the behavior is correct with highlightSeriesOpts as well.
   g.updateOptions({
@@ -84,17 +84,17 @@ stackedTestCase.prototype.testSelectionValues = function() {
     }
   });
   g.setSelection(0);
-  assertEquals("0: Y1: 1 Y2: 1", Util.getLegend());
+  assert.equal("0: Y1: 1 Y2: 1", Util.getLegend());
 
   g.setSelection(1);
-  assertEquals("1: Y1: 1 Y2: 1", Util.getLegend());
+  assert.equal("1: Y1: 1 Y2: 1", Util.getLegend());
 
   g.setSelection(0, 'Y2');
-  assertEquals("0: Y1: 1 Y2: 1", Util.getLegend());
-};
+  assert.equal("0: Y1: 1 Y2: 1", Util.getLegend());
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=176
-stackedTestCase.prototype.testDuplicatedXValue = function() {
+it('testDuplicatedXValue', function() {
   var opts = {
     stackedGraph: true,
     fillAlpha: 0.15,
@@ -111,17 +111,17 @@ stackedTestCase.prototype.testDuplicatedXValue = function() {
   ;
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   assert(g.yAxisRange()[1] < 2);
 
-  assertEquals([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 250));
-  assertEquals([0, 255, 0, 38], Util.samplePixel(g.hidden_, 317, 250));
-}
+  assert.deepEqual([0, 255, 0, 38], Util.samplePixel(g.hidden_, 200, 250));
+  assert.deepEqual([0, 255, 0, 38], Util.samplePixel(g.hidden_, 317, 250));
+});
 
 // Validates regression when null values in stacked graphs show up
 // incorrectly in the legend.
-stackedTestCase.prototype.testNullValues = function() {
+it('testNullValues', function() {
   var opts = {
     stackedGraph: true,
     stepPlot:true
@@ -135,26 +135,26 @@ stackedTestCase.prototype.testNullValues = function() {
   ;
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   g.setSelection(0);
-  assertEquals("0: Y1: -5 Y2: -1 Y3: 1", Util.getLegend());
+  assert.equal("0: Y1: -5 Y2: -1 Y3: 1", Util.getLegend());
 
   g.setSelection(1);
-  assertEquals("1: Y1: 1 Y3: 1", Util.getLegend());
+  assert.equal("1: Y1: 1 Y3: 1", Util.getLegend());
 
   g.setSelection(2);
-  assertEquals("2: Y1: 1 Y2: 2 Y3: 3", Util.getLegend());
+  assert.equal("2: Y1: 1 Y2: 2 Y3: 3", Util.getLegend());
 
   g.setSelection(3);
-  assertEquals("3: Y1: 3 Y3: 4", Util.getLegend());
+  assert.equal("3: Y1: 3 Y3: 4", Util.getLegend());
 
   g.setSelection(4);
-  assertEquals("4: Y1: 3 Y2: 2 Y3: 3", Util.getLegend());
-};
+  assert.equal("4: Y1: 3 Y2: 2 Y3: 3", Util.getLegend());
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=438
-stackedTestCase.prototype.testMissingValueAtZero = function() {
+it('testMissingValueAtZero', function() {
   var opts = {
     stackedGraph: true
   };
@@ -165,19 +165,19 @@ stackedTestCase.prototype.testMissingValueAtZero = function() {
   ;
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   g.setSelection(0);
-  assertEquals("0: Y2: 1", Util.getLegend());
+  assert.equal("0: Y2: 1", Util.getLegend());
 
   g.setSelection(1);
-  assertEquals("1: Y1: 1 Y2: 2", Util.getLegend());
+  assert.equal("1: Y1: 1 Y2: 2", Util.getLegend());
 
   g.setSelection(2);
-  assertEquals("2: Y2: 3", Util.getLegend());
-};
+  assert.equal("2: Y2: 3", Util.getLegend());
+});
 
-stackedTestCase.prototype.testInterpolation = function() {
+it('testInterpolation', function() {
   var opts = {
     colors: ['#ff0000', '#00ff00', '#0000ff'],
     stackedGraph: true
@@ -199,7 +199,7 @@ stackedTestCase.prototype.testInterpolation = function() {
     [109, 1, N, N, N]];
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   var htx = g.hidden_ctx_;
   var attrs = {};
@@ -217,23 +217,23 @@ stackedTestCase.prototype.testInterpolation = function() {
 
   // Check that the expected number of line segments gets drawn
   // for each series. Gaps don't get a line.
-  assertEquals(7, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
-  assertEquals(4, CanvasAssertions.numLinesDrawn(htx, '#00ff00'));
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  assert.equal(7, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(4, CanvasAssertions.numLinesDrawn(htx, '#00ff00'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
 
   // Check that the selection returns the original (non-stacked)
   // values and skips gaps.
   g.setSelection(1);
-  assertEquals("101: Y1: 1 Y2: 2 Y3: 2", Util.getLegend());
+  assert.equal("101: Y1: 1 Y2: 2 Y3: 2", Util.getLegend());
 
   g.setSelection(8);
-  assertEquals("108: Y1: 1 Y2: 2 Y3: 9", Util.getLegend());
+  assert.equal("108: Y1: 1 Y2: 2 Y3: 9", Util.getLegend());
 
   g.setSelection(9);
-  assertEquals("109: Y1: 1", Util.getLegend());
-};
+  assert.equal("109: Y1: 1", Util.getLegend());
+});
 
-stackedTestCase.prototype.testInterpolationOptions = function() {
+it('testInterpolationOptions', function() {
   var opts = {
     colors: ['#ff0000', '#00ff00', '#0000ff'],
     stackedGraph: true
@@ -255,7 +255,7 @@ stackedTestCase.prototype.testInterpolationOptions = function() {
   for (var i = 0; i < choices.length; ++i) {
     var graph = document.getElementById("graph");
     opts['stackedGraphNaNFill'] = choices[i];
-    g = new Dygraph(graph, data, opts);
+    var g = new Dygraph(graph, data, opts);
 
     var htx = g.hidden_ctx_;
     var attrs = {};
@@ -269,9 +269,9 @@ stackedTestCase.prototype.testInterpolationOptions = function() {
           {strokeStyle: '#ff0000'});
     }
   }
-};
+});
 
-stackedTestCase.prototype.testMultiAxisInterpolation = function() {
+it('testMultiAxisInterpolation', function() {
   // Setting 2 axes to test that each axis stacks separately 
   var opts = {
     colors: ['#ff0000', '#00ff00', '#0000ff'],
@@ -308,7 +308,7 @@ stackedTestCase.prototype.testMultiAxisInterpolation = function() {
     [109, 1, N, N, N]];
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, data, opts);
+  var g = new Dygraph(graph, data, opts);
 
   var htx = g.hidden_ctx_;
   var attrs = {};
@@ -326,18 +326,20 @@ stackedTestCase.prototype.testMultiAxisInterpolation = function() {
 
   // Check that the expected number of line segments gets drawn
   // for each series. Gaps don't get a line.
-  assertEquals(7, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
-  assertEquals(4, CanvasAssertions.numLinesDrawn(htx, '#00ff00'));
-  assertEquals(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
+  assert.equal(7, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+  assert.equal(4, CanvasAssertions.numLinesDrawn(htx, '#00ff00'));
+  assert.equal(2, CanvasAssertions.numLinesDrawn(htx, '#0000ff'));
 
   // Check that the selection returns the original (non-stacked)
   // values and skips gaps.
   g.setSelection(1);
-  assertEquals("101: Y1: 1 Y2: 2 Y3: 2", Util.getLegend());
+  assert.equal("101: Y1: 1 Y2: 2 Y3: 2", Util.getLegend());
 
   g.setSelection(8);
-  assertEquals("108: Y1: 1 Y2: 2 Y3: 9", Util.getLegend());
+  assert.equal("108: Y1: 1 Y2: 2 Y3: 9", Util.getLegend());
 
   g.setSelection(9);
-  assertEquals("109: Y1: 1", Util.getLegend());
-};
+  assert.equal("109: Y1: 1", Util.getLegend());
+});
+
+});
index 9d278a2..a5ff5ee 100644 (file)
@@ -8,26 +8,26 @@
  *
  * @author julian.eichstaedt@ch.sauter-bc.com (Fr. Sauter AG)
  */
-var StepTestCase = TestCase("step-plot-per-series");
+describe("step-plot-per-series", function() {
 
-StepTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-StepTestCase.origFunc = Dygraph.getContext;
+var origFunc = Dygraph.getContext;
 
-StepTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(StepTestCase.origFunc(canvas));
+    return new Proxy(origFunc(canvas));
   };
-};
+});
 
-StepTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = StepTestCase.origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = origFunc;
+});
 
-StepTestCase.prototype.testMixedModeStepAndLineFilled = function() {
+it('testMixedModeStepAndLineFilled', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -63,7 +63,7 @@ StepTestCase.prototype.testMixedModeStepAndLineFilled = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -94,9 +94,9 @@ StepTestCase.prototype.testMixedModeStepAndLineFilled = function() {
     xy2 = g.toDomCoords(x2, y2);
     CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs);
   }
-};
+});
 
-StepTestCase.prototype.testMixedModeStepAndLineStackedAndFilled = function() {
+it('testMixedModeStepAndLineStackedAndFilled', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -134,7 +134,7 @@ StepTestCase.prototype.testMixedModeStepAndLineStackedAndFilled = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -249,9 +249,9 @@ StepTestCase.prototype.testMixedModeStepAndLineStackedAndFilled = function() {
     // The last edge can not be tested via assertLineDrawn since it wasn't drawn as a line but via clossePath.
     // But a rectangle is completely tested with three of its four edges.
   }
-};
+});
 
-StepTestCase.prototype.testMixedModeStepAndLineErrorBars = function() {
+it('testMixedModeStepAndLineErrorBars', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -284,7 +284,7 @@ StepTestCase.prototype.testMixedModeStepAndLineErrorBars = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -348,9 +348,9 @@ StepTestCase.prototype.testMixedModeStepAndLineErrorBars = function() {
     CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs);
   }
 
-};
+});
 
-StepTestCase.prototype.testMixedModeStepAndLineCustomBars = function() {
+it('testMixedModeStepAndLineCustomBars', function() {
   var opts = {
     width: 480,
     height: 320,
@@ -382,7 +382,7 @@ StepTestCase.prototype.testMixedModeStepAndLineCustomBars = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, data, opts);
 
-  htx = g.hidden_ctx_;
+  var htx = g.hidden_ctx_;
 
   var attrs = {};  
 
@@ -447,4 +447,6 @@ StepTestCase.prototype.testMixedModeStepAndLineCustomBars = function() {
     xy2 = g.toDomCoords(data[i + 1][0], data[i + 1][2][1]);
     CanvasAssertions.assertLineDrawn(htx, xy1, xy2, attrs);
   }
-};
+});
+
+});
index 5bfab1b..abbe332 100644 (file)
@@ -4,35 +4,35 @@
  * @author danvk@google.com (Dan Vanderkam)
  */
 
-var ToDomCoordsTestCase = TestCase("to-dom-coords");
+describe("to-dom-coords", function() {
 
-ToDomCoordsTestCase._origFunc = Dygraph.getContext;
-ToDomCoordsTestCase.prototype.setUp = function() {
+var origFunc = Dygraph.getContext;
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
   Dygraph.getContext = function(canvas) {
-    return new Proxy(ToDomCoordsTestCase._origFunc(canvas));
+    return new Proxy(origFunc(canvas));
   }
-};
+});
 
-ToDomCoordsTestCase.prototype.tearDown = function() {
-  Dygraph.getContext = ToDomCoordsTestCase._origFunc;
-};
+afterEach(function() {
+  Dygraph.getContext = origFunc;
+});
 
 // Checks that toDomCoords and toDataCoords are inverses of one another.
-ToDomCoordsTestCase.prototype.checkForInverses = function(g) {
+var checkForInverses = function(g) {
   var x_range = g.xAxisRange();
   var y_range = g.yAxisRange();
   for (var i = 0; i <= 10; i++) {
     var x = x_range[0] + i / 10.0 * (x_range[1] - x_range[0]);
     for (var j = 0; j <= 10; j++) {
       var y = y_range[0] + j / 10.0 * (y_range[1] - y_range[0]);
-      assertEquals(x, g.toDataXCoord(g.toDomXCoord(x)));
-      assertEquals(y, g.toDataYCoord(g.toDomYCoord(y)));
+      assert.equal(x, g.toDataXCoord(g.toDomXCoord(x)));
+      assert.equal(y, g.toDataYCoord(g.toDomYCoord(y)));
     }
   }
-}
+};
 
-ToDomCoordsTestCase.prototype.testPlainChart = function() {
+it('testPlainChart', function() {
   var opts = {
     axes : {
       x : {
@@ -53,21 +53,21 @@ ToDomCoordsTestCase.prototype.testPlainChart = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
+  var g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
 
-  assertEquals([0, 100], g.toDataCoords(0, 0));
-  assertEquals([0, 0], g.toDataCoords(0, 400));
-  assertEquals([100, 100], g.toDataCoords(400, 0));
-  assertEquals([100, 0], g.toDataCoords(400, 400));
+  assert.deepEqual([0, 100], g.toDataCoords(0, 0));
+  assert.deepEqual([0, 0], g.toDataCoords(0, 400));
+  assert.deepEqual([100, 100], g.toDataCoords(400, 0));
+  assert.deepEqual([100, 0], g.toDataCoords(400, 400));
 
-  this.checkForInverses(g);
+  checkForInverses(g);
 
   // TODO(konigsberg): This doesn't really belong here. Move to its own test.
   var htx = g.hidden_ctx_;
-  assertEquals(1, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
-}
+  assert.equal(1, CanvasAssertions.numLinesDrawn(htx, '#ff0000'));
+});
 
-ToDomCoordsTestCase.prototype.testChartWithAxes = function() {
+it('testChartWithAxes', function() {
   var opts = {
     axes : {
       x : {
@@ -91,17 +91,17 @@ ToDomCoordsTestCase.prototype.testChartWithAxes = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
+  var g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
 
-  assertEquals([0, 100], g.toDataCoords(100, 0));
-  assertEquals([0, 0], g.toDataCoords(100, 400));
-  assertEquals([100, 100], g.toDataCoords(500, 0));
-  assertEquals([100, 0], g.toDataCoords(500, 400));
+  assert.deepEqual([0, 100], g.toDataCoords(100, 0));
+  assert.deepEqual([0, 0], g.toDataCoords(100, 400));
+  assert.deepEqual([100, 100], g.toDataCoords(500, 0));
+  assert.deepEqual([100, 0], g.toDataCoords(500, 400));
 
-  this.checkForInverses(g);
-}
+  checkForInverses(g);
+});
 
-ToDomCoordsTestCase.prototype.testChartWithAxesAndLabels = function() {
+it('testChartWithAxesAndLabels', function() {
   var opts = {
     axes : {
       x : {
@@ -130,17 +130,17 @@ ToDomCoordsTestCase.prototype.testChartWithAxesAndLabels = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
+  var g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
 
-  assertEquals([0, 100], g.toDataCoords(100, 25));
-  assertEquals([0, 0], g.toDataCoords(100, 425));
-  assertEquals([100, 100], g.toDataCoords(500, 25));
-  assertEquals([100, 0], g.toDataCoords(500, 425));
+  assert.deepEqual([0, 100], g.toDataCoords(100, 25));
+  assert.deepEqual([0, 0], g.toDataCoords(100, 425));
+  assert.deepEqual([100, 100], g.toDataCoords(500, 25));
+  assert.deepEqual([100, 0], g.toDataCoords(500, 425));
 
-  this.checkForInverses(g);
-}
+  checkForInverses(g);
+});
 
-ToDomCoordsTestCase.prototype.testYAxisLabelWidth = function() {
+it('testYAxisLabelWidth', function() {
   var opts = {
     axes: { y: { axisLabelWidth: 100 } },
     axisTickSize: 0,
@@ -152,19 +152,19 @@ ToDomCoordsTestCase.prototype.testYAxisLabelWidth = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
+  var g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
 
-  assertEquals([100, 0], g.toDomCoords(0, 100));
-  assertEquals([500, 486], g.toDomCoords(100, 0));
+  assert.deepEqual([100, 0], g.toDomCoords(0, 100));
+  assert.deepEqual([500, 486], g.toDomCoords(100, 0));
 
   g.updateOptions({     
     axes: { y: { axisLabelWidth: 50 }},
   });
-  assertEquals([50, 0], g.toDomCoords(0, 100));
-  assertEquals([500, 486], g.toDomCoords(100, 0));
-}
+  assert.deepEqual([50, 0], g.toDomCoords(0, 100));
+  assert.deepEqual([500, 486], g.toDomCoords(100, 0));
+});
 
-ToDomCoordsTestCase.prototype.testAxisTickSize = function() {
+it('testAxisTickSize', function() {
   var opts = {
     axes: { y: { axisLabelWidth: 100 } },
     axisTickSize: 0,
@@ -176,17 +176,17 @@ ToDomCoordsTestCase.prototype.testAxisTickSize = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
+  var g = new Dygraph(graph, [ [0,0], [100,100] ], opts);
 
-  assertEquals([100, 0], g.toDomCoords(0, 100));
-  assertEquals([500, 486], g.toDomCoords(100, 0));
+  assert.deepEqual([100, 0], g.toDomCoords(0, 100));
+  assert.deepEqual([500, 486], g.toDomCoords(100, 0));
 
   g.updateOptions({ axisTickSize : 50 });
-  assertEquals([200, 0], g.toDomCoords(0, 100));
-  assertEquals([500, 386], g.toDomCoords(100, 0));
-}
+  assert.deepEqual([200, 0], g.toDomCoords(0, 100));
+  assert.deepEqual([500, 386], g.toDomCoords(100, 0));
+});
 
-ToDomCoordsTestCase.prototype.testChartLogarithmic_YAxis = function() {
+it('testChartLogarithmic_YAxis', function() {
   var opts = {
     rightGap: 0,
     valueRange: [1, 4],
@@ -208,23 +208,23 @@ ToDomCoordsTestCase.prototype.testChartLogarithmic_YAxis = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [1,1], [4,4] ], opts);
+  var g = new Dygraph(graph, [ [1,1], [4,4] ], opts);
 
   var epsilon = 1e-8;
-  assertEqualsDelta([0, 4], g.toDataCoords(0, 0), epsilon);
-  assertEqualsDelta([0, 1], g.toDataCoords(0, 400), epsilon);
-  assertEqualsDelta([10, 4], g.toDataCoords(400, 0), epsilon);
-  assertEqualsDelta([10, 1], g.toDataCoords(400, 400), epsilon);
-  assertEqualsDelta([10, 2], g.toDataCoords(400, 200), epsilon);
+  assertDeepCloseTo([0, 4], g.toDataCoords(0, 0), epsilon);
+  assertDeepCloseTo([0, 1], g.toDataCoords(0, 400), epsilon);
+  assertDeepCloseTo([10, 4], g.toDataCoords(400, 0), epsilon);
+  assertDeepCloseTo([10, 1], g.toDataCoords(400, 400), epsilon);
+  assertDeepCloseTo([10, 2], g.toDataCoords(400, 200), epsilon);
   
-  assertEquals([0, 0], g.toDomCoords(0, 4));
-  assertEquals([0, 400], g.toDomCoords(0, 1));
-  assertEquals([400, 0], g.toDomCoords(10, 4));
-  assertEquals([400, 400], g.toDomCoords(10, 1));
-  assertEquals([400, 200], g.toDomCoords(10, 2));
-}
-
-ToDomCoordsTestCase.prototype.testChartLogarithmic_XAxis = function() {
+  assert.deepEqual([0, 0], g.toDomCoords(0, 4));
+  assert.deepEqual([0, 400], g.toDomCoords(0, 1));
+  assert.deepEqual([400, 0], g.toDomCoords(10, 4));
+  assert.deepEqual([400, 400], g.toDomCoords(10, 1));
+  assert.deepEqual([400, 200], g.toDomCoords(10, 2));
+});
+
+it('testChartLogarithmic_XAxis', function() {
   var opts = {
     rightGap: 0,
     valueRange: [1, 1000],
@@ -246,41 +246,43 @@ ToDomCoordsTestCase.prototype.testChartLogarithmic_XAxis = function() {
   }
 
   var graph = document.getElementById("graph");
-  g = new Dygraph(graph, [ [1,1], [10, 10], [100,100], [1000,1000] ], opts);
+  var g = new Dygraph(graph, [ [1,1], [10, 10], [100,100], [1000,1000] ], opts);
 
   var epsilon = 1e-8;
-  assertEqualsDelta(1, g.toDataXCoord(0), epsilon);
-  assertEqualsDelta(5.623413251903489, g.toDataXCoord(100), epsilon);
-  assertEqualsDelta(31.62277660168378, g.toDataXCoord(200), epsilon);
-  assertEqualsDelta(177.8279410038921, g.toDataXCoord(300), epsilon);
-  assertEqualsDelta(1000, g.toDataXCoord(400), epsilon);
-
-  assertEqualsDelta(0, g.toDomXCoord(1), epsilon);
-  assertEqualsDelta(3.6036036036036037, g.toDomXCoord(10), epsilon);
-  assertEqualsDelta(39.63963963963964, g.toDomXCoord(100), epsilon);
-  assertEqualsDelta(400, g.toDomXCoord(1000), epsilon);
-
-  assertEqualsDelta(0, g.toPercentXCoord(1), epsilon);
-  assertEqualsDelta(0.3333333333, g.toPercentXCoord(10), epsilon);
-  assertEqualsDelta(0.6666666666, g.toPercentXCoord(100), epsilon);
-  assertEqualsDelta(1, g.toPercentXCoord(1000), epsilon);
+  assert.closeTo(1, g.toDataXCoord(0), epsilon);
+  assert.closeTo(5.623413251903489, g.toDataXCoord(100), epsilon);
+  assert.closeTo(31.62277660168378, g.toDataXCoord(200), epsilon);
+  assert.closeTo(177.8279410038921, g.toDataXCoord(300), epsilon);
+  assert.closeTo(1000, g.toDataXCoord(400), epsilon);
+
+  assert.closeTo(0, g.toDomXCoord(1), epsilon);
+  assert.closeTo(3.6036036036036037, g.toDomXCoord(10), epsilon);
+  assert.closeTo(39.63963963963964, g.toDomXCoord(100), epsilon);
+  assert.closeTo(400, g.toDomXCoord(1000), epsilon);
+
+  assert.closeTo(0, g.toPercentXCoord(1), epsilon);
+  assert.closeTo(0.3333333333, g.toPercentXCoord(10), epsilon);
+  assert.closeTo(0.6666666666, g.toPercentXCoord(100), epsilon);
+  assert.closeTo(1, g.toPercentXCoord(1000), epsilon);
  
   // Now zoom in and ensure that the methods return reasonable values.
   g.updateOptions({dateWindow: [ 10, 100 ]});
 
-  assertEqualsDelta(10, g.toDataXCoord(0), epsilon);
-  assertEqualsDelta(17.78279410038923, g.toDataXCoord(100), epsilon);
-  assertEqualsDelta(31.62277660168379, g.toDataXCoord(200), epsilon);
-  assertEqualsDelta(56.23413251903491, g.toDataXCoord(300), epsilon);
-  assertEqualsDelta(100, g.toDataXCoord(400), epsilon);
-
-  assertEqualsDelta(-40, g.toDomXCoord(1), epsilon);
-  assertEqualsDelta(0, g.toDomXCoord(10), epsilon);
-  assertEqualsDelta(400, g.toDomXCoord(100), epsilon);
-  assertEqualsDelta(4400, g.toDomXCoord(1000), epsilon);
-
-  assertEqualsDelta(-1, g.toPercentXCoord(1), epsilon);
-  assertEqualsDelta(0, g.toPercentXCoord(10), epsilon);
-  assertEqualsDelta(1, g.toPercentXCoord(100), epsilon);
-  assertEqualsDelta(2, g.toPercentXCoord(1000), epsilon);
-}
\ No newline at end of file
+  assert.closeTo(10, g.toDataXCoord(0), epsilon);
+  assert.closeTo(17.78279410038923, g.toDataXCoord(100), epsilon);
+  assert.closeTo(31.62277660168379, g.toDataXCoord(200), epsilon);
+  assert.closeTo(56.23413251903491, g.toDataXCoord(300), epsilon);
+  assert.closeTo(100, g.toDataXCoord(400), epsilon);
+
+  assert.closeTo(-40, g.toDomXCoord(1), epsilon);
+  assert.closeTo(0, g.toDomXCoord(10), epsilon);
+  assert.closeTo(400, g.toDomXCoord(100), epsilon);
+  assert.closeTo(4400, g.toDomXCoord(1000), epsilon);
+
+  assert.closeTo(-1, g.toPercentXCoord(1), epsilon);
+  assert.closeTo(0, g.toPercentXCoord(10), epsilon);
+  assert.closeTo(1, g.toPercentXCoord(100), epsilon);
+  assert.closeTo(2, g.toPercentXCoord(1000), epsilon);
+});
+
+});
index 88a564e..9e2d490 100644 (file)
@@ -3,9 +3,9 @@
  *
  * @author gmadrid@gmail.com (George Madrid)
  */
-var TwoDigitYearsTestCase = TestCase("two-digit-years");
+describe("two-digit-years", function() {
 
-TwoDigitYearsTestCase.prototype.testTwoDigitYears = function() {
+it('testTwoDigitYears', function() {
   // A date with a one digit year: '9 AD'.
   var start = new Date(9, 2, 3);
   // A date with a two digit year: '11 AD'.
@@ -21,5 +21,7 @@ TwoDigitYearsTestCase.prototype.testTwoDigitYears = function() {
   });
 
   // This breaks in Firefox & Safari:
-  // assertEquals([{"v":-61875345600000,"label":"Apr 9"},{"v":-61867483200000,"label":"Jul 9"},{"v":-61859534400000,"label":"Oct 9"},{"v":-61851582000000,"label":"Jan 10"},{"v":-61843809600000,"label":"Apr 10"},{"v":-61835947200000,"label":"Jul 10"},{"v":-61827998400000,"label":"Oct 10"},{"v":-61820046000000,"label":"Jan 11"},{"v":-61812273600000,"label":"Apr 11"}], ticks);
-};
+  // assert.deepEqual([{"v":-61875345600000,"label":"Apr 9"},{"v":-61867483200000,"label":"Jul 9"},{"v":-61859534400000,"label":"Oct 9"},{"v":-61851582000000,"label":"Jan 10"},{"v":-61843809600000,"label":"Apr 10"},{"v":-61835947200000,"label":"Jul 10"},{"v":-61827998400000,"label":"Oct 10"},{"v":-61820046000000,"label":"Jan 11"},{"v":-61812273600000,"label":"Apr 11"}], ticks);
+});
+
+});
index faf8f25..d2465c9 100644 (file)
@@ -4,65 +4,65 @@
  * @fileoverview Tests for the updateOptions function.
  * @author antrob@google.com (Anthony Robledo)
  */
-var UpdateOptionsTestCase = TestCase("update-options");
+describe("update-options", function() {
   
-UpdateOptionsTestCase.prototype.opts = {
+var opts = {
   width: 480,
   height: 320,
 };
 
-UpdateOptionsTestCase.prototype.data = "X,Y1,Y2\n" +
+var data = "X,Y1,Y2\n" +
   "2011-01-01,2,3\n" +
   "2011-02-02,5,3\n" +
   "2011-03-03,6,1\n" +
   "2011-04-04,9,5\n" +
   "2011-05-05,8,3\n";
 
-UpdateOptionsTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div><div id='labels'></div>";
-};
+});
 
-UpdateOptionsTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 /*
  * Tweaks the dygraph so it sets g._testDrawCalled to true when internal method
  * drawGraph_ is called. Call unWrapDrawGraph when done with this.
  */
-UpdateOptionsTestCase.prototype.wrapDrawGraph = function(g) {
+var wrapDrawGraph = function(g) {
   g._testDrawCalled = false;
   g._oldDrawGraph = g.drawGraph_;
   g.drawGraph_ = function() {
     g._testDrawCalled = true;
     g._oldDrawGraph.call(g);
   }
-}
+};
 
 /*
  * See wrapDrawGraph
  */
-UpdateOptionsTestCase.prototype.unwrapDrawGraph = function(g) {
+var unwrapDrawGraph = function(g) {
   g.drawGraph_ = g._oldDrawGraph;
 }
 
-UpdateOptionsTestCase.prototype.testStrokeAll = function() {
+it('testStrokeAll', function() {
   var graphDiv = document.getElementById("graph");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
   var updatedOptions = { };
 
   updatedOptions['strokeWidth'] = 3;
 
   // These options will allow us to jump to renderGraph_()
   // drawGraph_() will be skipped.
-  this.wrapDrawGraph(graph);
+  wrapDrawGraph(graph);
   graph.updateOptions(updatedOptions);
-  this.unwrapDrawGraph(graph);
-  assertFalse(graph._testDrawCalled);
-};
+  unwrapDrawGraph(graph);
+  assert.isFalse(graph._testDrawCalled);
+});
 
-UpdateOptionsTestCase.prototype.testStrokeSingleSeries = function() {
+it('testStrokeSingleSeries', function() {
   var graphDiv = document.getElementById("graph");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
   var updatedOptions = { };
   var optionsForY1 = { };
 
@@ -71,15 +71,15 @@ UpdateOptionsTestCase.prototype.testStrokeSingleSeries = function() {
 
   // These options will allow us to jump to renderGraph_()
   // drawGraph_() will be skipped.
-  this.wrapDrawGraph(graph);
+  wrapDrawGraph(graph);
   graph.updateOptions(updatedOptions);
-  this.unwrapDrawGraph(graph);
-  assertFalse(graph._testDrawCalled);
-};
+  unwrapDrawGraph(graph);
+  assert.isFalse(graph._testDrawCalled);
+});
  
-UpdateOptionsTestCase.prototype.testSingleSeriesRequiresNewPoints = function() {
+it('testSingleSeriesRequiresNewPoints', function() {
   var graphDiv = document.getElementById("graph");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
   var updatedOptions = {
     series: {
       Y1: {
@@ -93,15 +93,15 @@ UpdateOptionsTestCase.prototype.testSingleSeriesRequiresNewPoints = function() {
 
   // These options will not allow us to jump to renderGraph_()
   // drawGraph_() must be called
-  this.wrapDrawGraph(graph);
+  wrapDrawGraph(graph);
   graph.updateOptions(updatedOptions);
-  this.unwrapDrawGraph(graph);
-  assertTrue(graph._testDrawCalled);
-};
+  unwrapDrawGraph(graph);
+  assert.isTrue(graph._testDrawCalled);
+});
 
-UpdateOptionsTestCase.prototype.testWidthChangeNeedsNewPoints = function() {
+it('testWidthChangeNeedsNewPoints', function() {
   var graphDiv = document.getElementById("graph");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
   var updatedOptions = { };
 
   // This will require new points.
@@ -109,65 +109,67 @@ UpdateOptionsTestCase.prototype.testWidthChangeNeedsNewPoints = function() {
 
   // These options will not allow us to jump to renderGraph_()
   // drawGraph_() must be called
-  this.wrapDrawGraph(graph);
+  wrapDrawGraph(graph);
   graph.updateOptions(updatedOptions);
-  this.unwrapDrawGraph(graph);
-  assertTrue(graph._testDrawCalled);
-};
+  unwrapDrawGraph(graph);
+  assert.isTrue(graph._testDrawCalled);
+});
 
 // Test https://github.com/danvk/dygraphs/issues/87
-UpdateOptionsTestCase.prototype.testUpdateLabelsDivDoesntInfiniteLoop = function() {
+it('testUpdateLabelsDivDoesntInfiniteLoop', function() {
   var graphDiv = document.getElementById("graph");
   var labelsDiv = document.getElementById("labels");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
   graph.updateOptions({labelsDiv : labelsDiv});
-}
+});
 
 // Test https://github.com/danvk/dygraphs/issues/247
-UpdateOptionsTestCase.prototype.testUpdateColors = function() {
+it('testUpdateColors', function() {
   var graphDiv = document.getElementById("graph");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
 
   var defaultColors = ["rgb(0,128,0)", "rgb(0,0,128)"];
-  assertEquals(["rgb(0,128,0)", "rgb(0,0,128)"], graph.getColors());
+  assert.deepEqual(["rgb(0,128,0)", "rgb(0,0,128)"], graph.getColors());
 
   var colors1 = [ "#aaa", "#bbb" ];
   graph.updateOptions({ colors: colors1 });
-  assertEquals(colors1, graph.getColors());
+  assert.deepEqual(colors1, graph.getColors());
 
   // extra colors are ignored until you add additional data series.
   var colors2 = [ "#ddd", "#eee", "#fff" ];
   graph.updateOptions({ colors: colors2 });
-  assertEquals(["#ddd", "#eee"], graph.getColors());
+  assert.deepEqual(["#ddd", "#eee"], graph.getColors());
 
   graph.updateOptions({ file:
       "X,Y1,Y2,Y3\n" +
       "2011-01-01,2,3,4\n" +
       "2011-02-02,5,3,2\n"
   });
-  assertEquals(colors2, graph.getColors());
+  assert.deepEqual(colors2, graph.getColors());
 
-  graph.updateOptions({ colors: null, file: this.data });
-  assertEquals(defaultColors, graph.getColors());
-}
+  graph.updateOptions({ colors: null, file: data });
+  assert.deepEqual(defaultColors, graph.getColors());
+});
 
 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=249
 // Verifies that setting 'legend: always' via update immediately shows the
 // legend.
-UpdateOptionsTestCase.prototype.testUpdateLegendAlways = function() {
+it('testUpdateLegendAlways', function() {
   var graphDiv = document.getElementById("graph");
-  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var graph = new Dygraph(graphDiv, data, opts);
 
   var legend = document.getElementsByClassName("dygraph-legend");
-  assertEquals(1, legend.length);
+  assert.equal(1, legend.length);
   legend = legend[0];
-  assertEquals("", legend.innerHTML);
+  assert.equal("", legend.innerHTML);
 
   graph.updateOptions({legend: 'always'});
 
   legend = document.getElementsByClassName("dygraph-legend");
-  assertEquals(1, legend.length);
+  assert.equal(1, legend.length);
   legend = legend[0];
-  assertNotEquals(-1, legend.textContent.indexOf("Y1"));
-  assertNotEquals(-1, legend.textContent.indexOf("Y2"));
-};
+  assert.notEqual(-1, legend.textContent.indexOf("Y1"));
+  assert.notEqual(-1, legend.textContent.indexOf("Y2"));
+});
+
+});
index 8634bd0..a94f6bd 100644 (file)
@@ -5,20 +5,20 @@
  *
  * @author dan@dygraphs.com (Dan Vanderkam)
  */
-var updateWhilePanningTestCase = TestCase("update-while-panning");
+describe("update-while-panning", function() {
 
-updateWhilePanningTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-updateWhilePanningTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 // This tests the following sequence:
 // 1. Begin dragging a chart (x-panning)
 // 2. Do a data update (updateOptions({file: ...}))
 // 3. Verify that the y-axis is still well-defined.
-updateWhilePanningTestCase.prototype.testUpdateWhilePanning = function() {
+it('testUpdateWhilePanning', function() {
   var sinewave = function(start, limit, step) {
     var data = [];
     for (var x = start; x < limit; x += step) {
@@ -36,23 +36,25 @@ updateWhilePanningTestCase.prototype.testUpdateWhilePanning = function() {
   var graph = document.getElementById("graph");
 
   var g = new Dygraph(graph, sinewave(0, 6, 0.1), opts);
-  assertEquals([-2, 2], g.yAxisRange());
+  assert.deepEqual([-2, 2], g.yAxisRange());
 
   // Start a pan, but don't finish it yet.
   DygraphOps.dispatchMouseDown_Point(g, 200, 100, {shiftKey: true});
   DygraphOps.dispatchMouseMove_Point(g, 100, 100, {shiftKey: true});
-  assertEquals([-2, 2], g.yAxisRange());
+  assert.deepEqual([-2, 2], g.yAxisRange());
 
   // Now do a data update. y-axis should remain the same.
   g.updateOptions({file: sinewave(0, 7, 0.1)});
-  assertEquals([-2, 2], g.yAxisRange());
+  assert.deepEqual([-2, 2], g.yAxisRange());
 
   // Keep the pan going.
   DygraphOps.dispatchMouseMove_Point(g, 50, 100, {shiftKey: true});
-  assertEquals([-2, 2], g.yAxisRange());
+  assert.deepEqual([-2, 2], g.yAxisRange());
 
   // Now finish the pan.
   DygraphOps.dispatchMouseUp_Point(g, 100, 100, {shiftKey: true});
-  assertEquals([-2, 2], g.yAxisRange());
-};
+  assert.deepEqual([-2, 2], g.yAxisRange());
+});
 
+
+});
index 5bd7f8d..765ed1e 100644 (file)
@@ -4,59 +4,59 @@
  * @author danvdk@gmail.com (Dan Vanderkam)
  */
 
-var UtilsTestCase = TestCase("utils-tests");
+describe("utils-tests", function() {
 
-UtilsTestCase.prototype.testUpdate = function() {
+it('testUpdate', function() {
   var a = {
     a: 1,
     b: [1, 2, 3],
     c: { x: 1, y: 2},
     d: { f: 10, g: 20}
   };
-  assertEquals(1, a['a']);
-  assertEquals([1, 2, 3], a['b']);
-  assertEquals({x: 1, y: 2}, a['c']);
-  assertEquals({f: 10, g: 20}, a['d']);
+  assert.equal(1, a['a']);
+  assert.deepEqual([1, 2, 3], a['b']);
+  assert.deepEqual({x: 1, y: 2}, a['c']);
+  assert.deepEqual({f: 10, g: 20}, a['d']);
 
   Dygraph.update(a, { c: { x: 2 } });
-  assertEquals({x: 2}, a['c']);
+  assert.deepEqual({x: 2}, a['c']);
 
   Dygraph.update(a, { d: null });
-  assertEquals(null, a['d']);
+  assert.equal(null, a['d']);
 
   Dygraph.update(a, { a: 10, b: [1, 2] });
-  assertEquals(10, a['a']);
-  assertEquals([1, 2], a['b']);
-  assertEquals({x: 2}, a['c']);
-  assertEquals(null, a['d']);
-};
+  assert.equal(10, a['a']);
+  assert.deepEqual([1, 2], a['b']);
+  assert.deepEqual({x: 2}, a['c']);
+  assert.equal(null, a['d']);
+});
 
-UtilsTestCase.prototype.testUpdateDeep = function() {
+it('testUpdateDeep', function() {
   var a = {
     a: 1,
     b: [1, 2, 3],
     c: { x: 1, y: 2},
     d: { f: 10, g: 20}
   };
-  assertEquals(1, a['a']);
-  assertEquals([1, 2, 3], a['b']);
-  assertEquals({x: 1, y: 2}, a['c']);
-  assertEquals({f: 10, g: 20}, a['d']);
+  assert.equal(1, a['a']);
+  assert.deepEqual([1, 2, 3], a['b']);
+  assert.deepEqual({x: 1, y: 2}, a['c']);
+  assert.deepEqual({f: 10, g: 20}, a['d']);
 
   Dygraph.updateDeep(a, { c: { x: 2 } });
-  assertEquals({x: 2, y: 2}, a['c']);
+  assert.deepEqual({x: 2, y: 2}, a['c']);
 
   Dygraph.updateDeep(a, { d: null });
-  assertEquals(null, a['d']);
+  assert.equal(null, a['d']);
 
   Dygraph.updateDeep(a, { a: 10, b: [1, 2] });
-  assertEquals(10, a['a']);
-  assertEquals([1, 2], a['b']);
-  assertEquals({x: 2, y: 2}, a['c']);
-  assertEquals(null, a['d']);
-};
+  assert.equal(10, a['a']);
+  assert.deepEqual([1, 2], a['b']);
+  assert.deepEqual({x: 2, y: 2}, a['c']);
+  assert.equal(null, a['d']);
+});
 
-UtilsTestCase.prototype.testUpdateDeepDecoupled = function() {
+it('testUpdateDeepDecoupled', function() {
   var a = {
     a: 1,
     b: [1, 2, 3],
@@ -67,78 +67,78 @@ UtilsTestCase.prototype.testUpdateDeepDecoupled = function() {
   Dygraph.updateDeep(b, a);
 
   b.a = 2;
-  assertEquals(1, a.a);
+  assert.equal(1, a.a);
 
   b.b[0] = 2;
-  assertEquals(1, a.b[0]);
+  assert.equal(1, a.b[0]);
 
   b.c.x = "new value";
-  assertEquals("original", a.c.x);
-};
+  assert.equal("original", a.c.x);
+});
 
 
-UtilsTestCase.prototype.testIterator_nopredicate = function() {
+it('testIterator_nopredicate', function() {
   var array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
   var iter = Dygraph.createIterator(array, 1, 4);
-  assertTrue(iter.hasNext);
-  assertEquals('b', iter.peek);
-  assertEquals('b', iter.next());
-  assertTrue(iter.hasNext);
+  assert.isTrue(iter.hasNext);
+  assert.equal('b', iter.peek);
+  assert.equal('b', iter.next());
+  assert.isTrue(iter.hasNext);
 
-  assertEquals('c', iter.peek);
-  assertEquals('c', iter.next());
+  assert.equal('c', iter.peek);
+  assert.equal('c', iter.next());
 
-  assertTrue(iter.hasNext);
-  assertEquals('d', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('d', iter.next());
 
-  assertTrue(iter.hasNext);
-  assertEquals('e', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('e', iter.next());
 
-  assertFalse(iter.hasNext);
-};
+  assert.isFalse(iter.hasNext);
+});
 
-UtilsTestCase.prototype.testIterator_predicate = function() {
+it('testIterator_predicate', function() {
   var array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
   var iter = Dygraph.createIterator(array, 1, 4,
       function(array, idx) { return array[idx] !== 'd' });
-  assertTrue(iter.hasNext);
-  assertEquals('b', iter.peek);
-  assertEquals('b', iter.next());
-  assertTrue(iter.hasNext);
+  assert.isTrue(iter.hasNext);
+  assert.equal('b', iter.peek);
+  assert.equal('b', iter.next());
+  assert.isTrue(iter.hasNext);
 
-  assertEquals('c', iter.peek);
-  assertEquals('c', iter.next());
+  assert.equal('c', iter.peek);
+  assert.equal('c', iter.next());
 
-  assertTrue(iter.hasNext);
-  assertEquals('e', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('e', iter.next());
 
-  assertFalse(iter.hasNext);
-}
+  assert.isFalse(iter.hasNext);
+});
 
-UtilsTestCase.prototype.testIterator_empty = function() {
+it('testIterator_empty', function() {
   var array = [];
   var iter = Dygraph.createIterator([], 0, 0);
-  assertFalse(iter.hasNext);
-};
+  assert.isFalse(iter.hasNext);
+});
 
-UtilsTestCase.prototype.testIterator_outOfRange = function() {
+it('testIterator_outOfRange', function() {
   var array = ['a', 'b', 'c'];
   var iter = Dygraph.createIterator(array, 1, 4,
       function(array, idx) { return array[idx] !== 'd' });
-  assertTrue(iter.hasNext);
-  assertEquals('b', iter.peek);
-  assertEquals('b', iter.next());
-  assertTrue(iter.hasNext);
+  assert.isTrue(iter.hasNext);
+  assert.equal('b', iter.peek);
+  assert.equal('b', iter.next());
+  assert.isTrue(iter.hasNext);
 
-  assertEquals('c', iter.peek);
-  assertEquals('c', iter.next());
+  assert.equal('c', iter.peek);
+  assert.equal('c', iter.next());
 
-  assertFalse(iter.hasNext);
-};
+  assert.isFalse(iter.hasNext);
+});
 
 // Makes sure full array is tested, and that the predicate isn't called
 // with invalid boundaries.
-UtilsTestCase.prototype.testIterator_whole_array = function() {
+it('testIterator_whole_array', function() {
   var array = ['a', 'b', 'c'];
   var iter = Dygraph.createIterator(array, 0, array.length,
       function(array, idx) {
@@ -146,55 +146,57 @@ UtilsTestCase.prototype.testIterator_whole_array = function() {
           throw "err";
         } else {
           return true;
-        };
+        }
       });
-  assertTrue(iter.hasNext);
-  assertEquals('a', iter.next());
-  assertTrue(iter.hasNext);
-  assertEquals('b', iter.next());
-  assertTrue(iter.hasNext);
-  assertEquals('c', iter.next());
-  assertFalse(iter.hasNext);
-  assertNull(iter.next());
-};
-
-UtilsTestCase.prototype.testIterator_no_args = function() {
+  assert.isTrue(iter.hasNext);
+  assert.equal('a', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('b', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('c', iter.next());
+  assert.isFalse(iter.hasNext);
+  assert.isNull(iter.next());
+});
+
+it('testIterator_no_args', function() {
   var array = ['a', 'b', 'c'];
   var iter = Dygraph.createIterator(array);
-  assertTrue(iter.hasNext);
-  assertEquals('a', iter.next());
-  assertTrue(iter.hasNext);
-  assertEquals('b', iter.next());
-  assertTrue(iter.hasNext);
-  assertEquals('c', iter.next());
-  assertFalse(iter.hasNext);
-  assertNull(iter.next());
-};
-
-UtilsTestCase.prototype.testToRGB = function() {
-  assertEquals({r: 255, g: 200, b: 150}, Dygraph.toRGB_('rgb(255,200,150)'));
-  assertEquals({r: 255, g: 200, b: 150}, Dygraph.toRGB_('#FFC896'));
-  assertEquals({r: 255, g: 0, b: 0}, Dygraph.toRGB_('red'));
-};
-
-UtilsTestCase.prototype.testIsPixelChangingOptionList = function() {
+  assert.isTrue(iter.hasNext);
+  assert.equal('a', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('b', iter.next());
+  assert.isTrue(iter.hasNext);
+  assert.equal('c', iter.next());
+  assert.isFalse(iter.hasNext);
+  assert.isNull(iter.next());
+});
+
+it('testToRGB', function() {
+  assert.deepEqual({r: 255, g: 200, b: 150}, Dygraph.toRGB_('rgb(255,200,150)'));
+  assert.deepEqual({r: 255, g: 200, b: 150}, Dygraph.toRGB_('#FFC896'));
+  assert.deepEqual({r: 255, g: 0, b: 0}, Dygraph.toRGB_('red'));
+});
+
+it('testIsPixelChangingOptionList', function() {
   var isPx = Dygraph.isPixelChangingOptionList;
-  assertTrue(isPx([], { axes: { y: { digitsAfterDecimal: 3 }}}));
-  assertFalse(isPx([], { axes: { y: { axisLineColor: 'blue' }}}));
-};
+  assert.isTrue(isPx([], { axes: { y: { digitsAfterDecimal: 3 }}}));
+  assert.isFalse(isPx([], { axes: { y: { axisLineColor: 'blue' }}}));
+});
 
 /*
-UtilsTestCase.prototype.testDateSet = function() {
+it('testDateSet', function() {
   var base = new Date(1383455100000);
   var d = new Date(base);
 
   // A one hour shift -- this is surprising behavior!
   d.setMilliseconds(10);
-  assertEquals(3600010, d.getTime() - base.getTime());
+  assert.equal(3600010, d.getTime() - base.getTime());
 
   // setDateSameTZ compensates for this surprise.
   d = new Date(base);
   Dygraph.setDateSameTZ(d, {ms: 10});
-  assertEquals(10, d.getTime() - base.getTime());
-};
+  assert.equal(10, d.getTime() - base.getTime());
+});
 */
+
+});
index 77f2ea6..e8860c6 100644 (file)
@@ -3,14 +3,14 @@
  * @author sergeyslepian@gmail.com
  */
 
-var VisibilityTestCase = TestCase("visibility");
+describe("visibility", function() {
 
-VisibilityTestCase.prototype.setUp = function() {
+beforeEach(function() {
   document.body.innerHTML = "<div id='graph'></div>";
-};
+});
 
-VisibilityTestCase.prototype.tearDown = function() {
-};
+afterEach(function() {
+});
 
 /**
  * Does a bunch of the shared busywork of setting up a graph and changing its visibility.
@@ -18,7 +18,7 @@ VisibilityTestCase.prototype.tearDown = function() {
  * @param {*[]} setVisibilityArgs An array of arguments to be passed directly to setVisibility()
  * @returns {string} The output of Util.getLegend() called after the visibility is set
  */
-VisibilityTestCase.prototype.getVisibleSeries = function(startingVisibility, setVisibilityArgs) {
+var getVisibleSeries = function(startingVisibility, setVisibilityArgs) {
   var opts = {
     width: 480,
     height: 320,
@@ -46,23 +46,25 @@ VisibilityTestCase.prototype.getVisibleSeries = function(startingVisibility, set
   return Util.getLegend();
 };
 
-VisibilityTestCase.prototype.testDefaultCases = function() {
-  assertEquals(' A  B  C  D  E', this.getVisibleSeries(true, [[], true]));
-  assertEquals('', this.getVisibleSeries(false, [[], true]));
-};
+it('testDefaultCases', function() {
+  assert.equal(' A  B  C  D  E', getVisibleSeries(true, [[], true]));
+  assert.equal('', getVisibleSeries(false, [[], true]));
+});
 
-VisibilityTestCase.prototype.testSingleSeriesHide = function() {
-  assertEquals(' A  C  D  E', this.getVisibleSeries(true, [1, false]));
-};
+it('testSingleSeriesHide', function() {
+  assert.equal(' A  C  D  E', getVisibleSeries(true, [1, false]));
+});
 
-VisibilityTestCase.prototype.testSingleSeriesShow = function() {
-  assertEquals(' E', this.getVisibleSeries(false, [4, true]));
-};
+it('testSingleSeriesShow', function() {
+  assert.equal(' E', getVisibleSeries(false, [4, true]));
+});
 
-VisibilityTestCase.prototype.testMultiSeriesHide = function() {
-  assertEquals(' A  E', this.getVisibleSeries(true, [[1,2,3], false]));
-};
+it('testMultiSeriesHide', function() {
+  assert.equal(' A  E', getVisibleSeries(true, [[1,2,3], false]));
+});
+
+it('testMultiSeriesShow', function() {
+  assert.equal(' B  D', getVisibleSeries(false, [[1,3], true]));
+});
 
-VisibilityTestCase.prototype.testMultiSeriesShow = function() {
-  assertEquals(' B  D', this.getVisibleSeries(false, [[1,3], true]));
-};
\ No newline at end of file
+});
diff --git a/check-combined-unaffected.sh b/check-combined-unaffected.sh
deleted file mode 100755 (executable)
index de1721e..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash
-# Ensures that dygraph-combined.js is unaffected.
-# Helpful for pull requests, where this is a common mistake.
-
-grep 'var' dygraph-combined.js > /dev/null
-if [ $? -eq 0 ]; then
-  echo 'Please revert changes to dygraph-combined.js' >&2
-  echo 'You can do this by running:  ' >& 2
-  echo '' >& 2
-  echo '    git checkout dygraph-combined.js' >&2
-  echo '' >& 2
-  exit 1
-fi
-
-exit 0
diff --git a/closure-todo.txt b/closure-todo.txt
deleted file mode 100644 (file)
index ae77622..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-Many of the functions and methods in the dygraphs source have some form of
-Closure annotations on them ("@param", "@return", "@private", etc.).
-
-These provide some documentation value, but they were largely written by
-developers who had never used the Closure Compiler and so the syntax is
-often not quite correct.
-
-This file tracks which files have been fully "closurized", i.e. compile
-under the Closure Compiler without any errors or warnings.
-
-Core:
-- dygraph-canvas.js
-- dygraph-interaction-model.js
-- dygraph-layout.js
-x dygraph-options.js
-- dygraph.js
-x dygraph-gviz.js
-x dygraph-tickers.js
-x dygraph-options-reference.js
-x dygraph-utils.js
-x dashed-canvas.js
-- dygraph-plugin-base.js
-- dygraph-plugin-install.js
-
-Plugins:
-- plugins/annotations.js
-- plugins/axes.js
-- plugins/chart-labels.js
-- plugins/grid.js
-- plugins/legend.js
-- plugins/range-selector.js
-
-Datahandler:
-- datahandler/bars-custom.js
-- datahandler/bars-error.js
-- datahandler/bars-fractions.js
-- datahandler/bars.js
-- datahandler/datahandler.js
-- datahandler/default-fractions.js
-- datahandler/default.js
-
-Here's a command that can be used to build dygraphs using the closure
-compiler:
-java -jar ../../closure-compiler-read-only/build/compiler.jar --js=dygraph-utils.js --js=dashed-canvas.js --js=dygraph-options-reference.js --js=dygraph-tickers.js --js=dygraph-gviz.js --js=dygraph-options.js --js_output_file=/tmp/out.js --compilation_level ADVANCED_OPTIMIZATIONS --warning_level VERBOSE --externs dygraph-externs.js
-
-As each file is closurized, it can be added as a "--js" parameter.
diff --git a/compile-with-closure.sh b/compile-with-closure.sh
deleted file mode 100755 (executable)
index 90d9d9a..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-
-# This script runs dygraphs through the Closure Compiler. This checks for
-# errors (both in the JS and in the jsdoc) and flags type errors as WARNINGS.
-
-# It outputs minified JS to a temp file. This should be ignored for now, until
-# it's fully functional.
-
-CLOSURE_COMPILER=node_modules/closure-compiler/lib/vendor/compiler.jar
-BASE_JS=node_modules/obvious-closure-library/closure/goog/base.js
-if [[ (! -f $CLOSURE_COMPILER) || (! -f $BASE_JS) ]]; then
-  echo "Missing compiler.jar or base.js. Try running 'npm install'." 1>&2
-  exit 1
-fi
-
-java -jar $CLOSURE_COMPILER \
- --compilation_level ADVANCED_OPTIMIZATIONS  \
- --warning_level VERBOSE  \
- --output_wrapper='(function() {%output%})();'  \
- --js $BASE_JS \
- --js=dashed-canvas.js \
- --js=dygraph-options.js \
- --js=dygraph-layout.js \
- --js=dygraph-canvas.js \
- --js=dygraph.js \
- --js=dygraph-utils.js \
- --js=dygraph-gviz.js \
- --js=dygraph-interaction-model.js \
- --js=dygraph-tickers.js \
- --js=dygraph-plugin-base.js \
- --js=plugins/annotations.js \
- --js=plugins/axes.js \
- --js=plugins/chart-labels.js \
- --js=plugins/grid.js \
- --js=plugins/legend.js \
- --js=plugins/range-selector.js \
- --js=dygraph-plugin-install.js \
- --js=dygraph-options-reference.js \
- --js=datahandler/datahandler.js \
- --js=datahandler/default.js \
- --js=datahandler/default-fractions.js \
- --js=datahandler/bars.js \
- --js=datahandler/bars-custom.js \
- --js=datahandler/bars-error.js \
- --js=datahandler/bars-fractions.js \
- --js=dygraph-exports.js \
- --externs dygraph-internal.externs.js  \
- --externs gviz-api.js  \
- --js_output_file=/tmp/out.js
diff --git a/dashed-canvas.js b/dashed-canvas.js
deleted file mode 100644 (file)
index 0fc6874..0000000
+++ /dev/null
@@ -1,180 +0,0 @@
-/**
- * @license
- * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-(function() {
-'use strict';
-
-/**
- * @fileoverview Adds support for dashed lines to the HTML5 canvas.
- *
- * Usage:
- *   var ctx = canvas.getContext("2d");
- *   ctx.installPattern([10, 5])  // draw 10 pixels, skip 5 pixels, repeat.
- *   ctx.beginPath();
- *   ctx.moveTo(100, 100);  // start the first line segment.
- *   ctx.lineTo(150, 200);
- *   ctx.lineTo(200, 100);
- *   ctx.moveTo(300, 150);  // start a second, unconnected line
- *   ctx.lineTo(400, 250);
- *   ...
- *   ctx.stroke();          // draw the dashed line.
- *   ctx.uninstallPattern();
- *
- * This is designed to leave the canvas untouched when it's not used.
- * If you never install a pattern, or call uninstallPattern(), then the canvas
- * will be exactly as it would have if you'd never used this library. The only
- * difference from the standard canvas will be the "installPattern" method of
- * the drawing context.
- */
-
-/**
- * Change the stroking style of the canvas drawing context from a solid line to
- * a pattern (e.g. dashes, dash-dot-dash, etc.)
- *
- * Once you've installed the pattern, you can draw with it by using the
- * beginPath(), moveTo(), lineTo() and stroke() method calls. Note that some
- * more advanced methods (e.g. quadraticCurveTo() and bezierCurveTo()) are not
- * supported. See file overview for a working example.
- *
- * Side effects of calling this method include adding an "isPatternInstalled"
- * property and "uninstallPattern" method to this particular canvas context.
- * You must call uninstallPattern() before calling installPattern() again.
- *
- * @param {Array.<number>} pattern A description of the stroke pattern. Even
- * indices indicate a draw and odd indices indicate a gap (in pixels). The
- * array should have a even length as any odd lengthed array could be expressed
- * as a smaller even length array.
- */
-CanvasRenderingContext2D.prototype.installPattern = function(pattern) {
-  if (typeof(this.isPatternInstalled) !== 'undefined') {
-    throw "Must un-install old line pattern before installing a new one.";
-  }
-  this.isPatternInstalled = true;
-
-  var dashedLineToHistory = [0, 0];
-
-  // list of connected line segements:
-  // [ [x1, y1], ..., [xn, yn] ], [ [x1, y1], ..., [xn, yn] ]
-  var segments = [];
-
-  // Stash away copies of the unmodified line-drawing functions.
-  var realBeginPath = this.beginPath;
-  var realLineTo = this.lineTo;
-  var realMoveTo = this.moveTo;
-  var realStroke = this.stroke;
-
-  /** @type {function()|undefined} */
-  this.uninstallPattern = function() {
-    this.beginPath = realBeginPath;
-    this.lineTo = realLineTo;
-    this.moveTo = realMoveTo;
-    this.stroke = realStroke;
-    this.uninstallPattern = undefined;
-    this.isPatternInstalled = undefined;
-  };
-
-  // Keep our own copies of the line segments as they're drawn.
-  this.beginPath = function() {
-    segments = [];
-    realBeginPath.call(this);
-  };
-  this.moveTo = function(x, y) {
-    segments.push([[x, y]]);
-    realMoveTo.call(this, x, y);
-  };
-  this.lineTo = function(x, y) {
-    var last = segments[segments.length - 1];
-    last.push([x, y]);
-  };
-
-  this.stroke = function() {
-    if (segments.length === 0) {
-      // Maybe the user is drawing something other than a line.
-      // TODO(danvk): test this case.
-      realStroke.call(this);
-      return;
-    }
-
-    for (var i = 0; i < segments.length; i++) {
-      var seg = segments[i];
-      var x1 = seg[0][0], y1 = seg[0][1];
-      for (var j = 1; j < seg.length; j++) {
-        // Draw a dashed line from (x1, y1) - (x2, y2)
-        var x2 = seg[j][0], y2 = seg[j][1];
-        this.save();
-
-        // Calculate transformation parameters
-        var dx = (x2-x1);
-        var dy = (y2-y1);
-        var len = Math.sqrt(dx*dx + dy*dy);
-        var rot = Math.atan2(dy, dx);
-
-        // Set transformation
-        this.translate(x1, y1);
-        realMoveTo.call(this, 0, 0);
-        this.rotate(rot);
-
-        // Set last pattern index we used for this pattern.
-        var patternIndex = dashedLineToHistory[0];
-        var x = 0;
-        while (len > x) {
-          // Get the length of the pattern segment we are dealing with.
-          var segment = pattern[patternIndex];
-          // If our last draw didn't complete the pattern segment all the way
-          // we will try to finish it. Otherwise we will try to do the whole
-          // segment.
-          if (dashedLineToHistory[1]) {
-            x += dashedLineToHistory[1];
-          } else {
-            x += segment;
-          }
-
-          if (x > len) {
-            // We were unable to complete this pattern index all the way, keep
-            // where we are the history so our next draw continues where we
-            // left off in the pattern.
-            dashedLineToHistory = [patternIndex, x-len];
-            x = len;
-          } else {
-            // We completed this patternIndex, we put in the history that we
-            // are on the beginning of the next segment.
-            dashedLineToHistory = [(patternIndex+1)%pattern.length, 0];
-          }
-
-          // We do a line on a even pattern index and just move on a odd
-          // pattern index.  The move is the empty space in the dash.
-          if (patternIndex % 2 === 0) {
-            realLineTo.call(this, x, 0);
-          } else {
-            realMoveTo.call(this, x, 0);
-          }
-
-          // If we are not done, next loop process the next pattern segment, or
-          // the first segment again if we are at the end of the pattern.
-          patternIndex = (patternIndex+1) % pattern.length;
-        }
-
-        this.restore();
-        x1 = x2;
-        y1 = y2;
-      }
-    }
-    realStroke.call(this);
-    segments = [];
-  };
-};
-
-/**
- * Removes the previously-installed pattern.
- * You must call installPattern() before calling this. You can install at most
- * one pattern at a time--there is no pattern stack.
- */
-CanvasRenderingContext2D.prototype.uninstallPattern = function() {
-  // This will be replaced by a non-error version when a pattern is installed.
-  throw "Must install a line pattern before uninstalling it.";
-};
-
-})();
diff --git a/data.js b/data.js
deleted file mode 100644 (file)
index d92a633..0000000
--- a/data.js
+++ /dev/null
@@ -1,63 +0,0 @@
-function StubbedData() {
-return "" +
-"Date,A,B\n" +
-"20061001,3.01953818828,0.7212041046,2.18487394958,0.599318549691\n" +
-"20061002,3.63321799308,0.778297234566,1.69491525424,0.531417655826\n" +
-"20061003,2.44328097731,0.644967734352,2.51256281407,0.640539070386\n" +
-"20061004,3.52733686067,0.774700921683,2.68456375839,0.66207105053\n" +
-"20061005,3.28719723183,0.741636245748,2.35294117647,0.621407707226\n" +
-"20061006,1.58450704225,0.523967868159,3.78657487091,0.791868460623\n" +
-"20061007,5.32859680284,0.946589405904,4.0404040404,0.807910739509\n" +
-"20061008,2.64084507042,0.672799548916,2.37288135593,0.626609885481\n" +
-"20061009,2.26480836237,0.620990945917,3.5413153457,0.75897176848\n" +
-"20061010,3.29289428076,0.74289969528,2.02702702703,0.579191340004\n" +
-"20061011,2.7633851468,0.681234043829,1.1744966443,0.4413034044\n" +
-"20061012,3.28719723183,0.741636245748,3.37268128162,0.741327769578\n" +
-"20061013,1.77304964539,0.55569466381,1.85810810811,0.555011329732\n" +
-"20061014,3.39892665474,0.7664008338,1.67224080268,0.524368852929\n" +
-"20061015,2.65017667845,0.675144574777,3.35570469799,0.737661045752\n" +
-"20061016,3.63951473137,0.779620631266,2.34899328859,0.620377617453\n" +
-"20061017,2.25694444444,0.618859623032,1.68067226891,0.526990133716\n" +
-"20061018,4.47504302926,0.857766274964,2.51677852349,0.641599927369\n" +
-"20061019,2.44755244755,0.646081155692,1.68067226891,0.526990133716\n" +
-"20061020,3.67775831874,0.787656442774,3.066439523,0.711598843969\n" +
-"20061021,3.94265232975,0.823839169829,3.85906040268,0.788990618726\n" +
-"20061022,2.59067357513,0.660187558973,3.71621621622,0.777438794254\n" +
-"20061023,4.33275563258,0.847570482324,3.85906040268,0.788990618726\n" +
-"20061024,3.10344827586,0.720049610821,2.84280936455,0.679611549697\n" +
-"20061025,1.40350877193,0.492720767725,2.7027027027,0.666482380968\n" +
-"20061026,1.95035460993,0.582291234145,2.36486486486,0.624518599275\n" +
-"20061027,2.30905861456,0.632980642182,2.03045685279,0.580161203819\n" +
-"20061028,4.09252669039,0.835706590809,2.87648054146,0.68754192469\n" +
-"20061029,2.66903914591,0.679883997626,2.02360876897,0.578224712918\n" +
-"20061030,4.74516695958,0.89127787497,4.36241610738,0.836670992529\n" +
-"20061031,2.78260869565,0.685905251933,3.20945945946,0.724388507178\n" +
-"20061101,1.5873015873,0.524884521441,1.51260504202,0.500373860545\n" +
-"20061102,2.78745644599,0.687083077461,2.0202020202,0.57726130639\n" +
-"20061103,5.11463844797,0.925157232782,2.68907563025,0.663168401088\n" +
-"20061104,4.9001814882,0.919644816432,3.07692307692,0.713993047527\n" +
-"20061105,5.13274336283,0.928343545136,3.55329949239,0.761492892041\n" +
-"20061106,1.92644483363,0.575222935029,2.35294117647,0.621407707226\n" +
-"20061107,2.46478873239,0.650573541306,1.52027027027,0.502889967904\n" +
-"20061108,2.13523131673,0.609772022763,2.6981450253,0.665374048085\n" +
-"20061109,3.88007054674,0.811026422222,2.72572402044,0.672079879106\n" +
-"20061110,2.63620386643,0.671633132526,3.71621621622,0.777438794254\n" +
-"20061111,3.69718309859,0.791736755355,3.0303030303,0.703344064467\n" +
-"20061112,3.83944153578,0.802703592906,4.05405405405,0.81058250986\n" +
-"20061113,2.47787610619,0.653984033555,2.20338983051,0.604340313133\n" +
-"20061114,1.77304964539,0.55569466381,2.22222222222,0.60944692682\n" +
-"20061115,2.30088495575,0.630766388737,0.843170320405,0.375484163785\n" +
-"20061116,1.57894736842,0.522144132232,2.19594594595,0.602321544724\n" +
-"20061118,2.45183887916,0.647198426991,1.69491525424,0.531417655826\n" +
-"20061119,3.52733686067,0.774700921683,1.85185185185,0.55316023504\n" +
-"20061120,2.97723292469,0.711254751484,2.6981450253,0.665374048085\n" +
-"20061121,2.29681978799,0.629665059963,2.01680672269,0.576301104352\n" +
-"20061122,3.01418439716,0.719945245328,2.5466893039,0.649125445325\n" +
-"20061123,3.78378378378,0.809917534069,2.6936026936,0.664269394219\n" +
-"20061124,3.18584070796,0.738851643987,2.01005025126,0.57439025002\n" +
-"20061125,2.83185840708,0.697868332879,3.066439523,0.711598843969\n" +
-"20061126,3.01953818828,0.7212041046,2.53378378378,0.645878720149\n" +
-"20061127,2.81195079086,0.693033387099,1.51006711409,0.499540743312\n" +
-"20061128,2.97723292469,0.711254751484,2.54237288136,0.648039583782\n" +
-"20061129,1.41093474427,0.495309102312,3.02013422819,0.701020603129";
-}
diff --git a/datahandler/bars-custom.js b/datahandler/bars-custom.js
deleted file mode 100644 (file)
index 7313ca2..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DataHandler implementation for the custom bars option.
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- */
-
-(function() {
-
-/*global Dygraph:false */
-"use strict";
-
-/**
- * @constructor
- * @extends Dygraph.DataHandlers.BarsHandler
- */
-Dygraph.DataHandlers.CustomBarsHandler = function() {
-};
-
-var CustomBarsHandler = Dygraph.DataHandlers.CustomBarsHandler;
-CustomBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler();
-
-/** @inheritDoc */
-CustomBarsHandler.prototype.extractSeries = function(rawData, i, options) {
-  // TODO(danvk): pre-allocate series here.
-  var series = [];
-  var x, y, point;
-  var logScale = options.get('logscale');
-  for ( var j = 0; j < rawData.length; j++) {
-    x = rawData[j][0];
-    point = rawData[j][i];
-    if (logScale && point !== null) {
-      // On the log scale, points less than zero do not exist.
-      // This will create a gap in the chart.
-      if (point[0] <= 0 || point[1] <= 0 || point[2] <= 0) {
-        point = null;
-      }
-    }
-    // Extract to the unified data format.
-    if (point !== null) {
-      y = point[1];
-      if (y !== null && !isNaN(y)) {
-        series.push([ x, y, [ point[0], point[2] ] ]);
-      } else {
-        series.push([ x, y, [ y, y ] ]);
-      }
-    } else {
-      series.push([ x, null, [ null, null ] ]);
-    }
-  }
-  return series;
-};
-
-/** @inheritDoc */
-CustomBarsHandler.prototype.rollingAverage =
-    function(originalData, rollPeriod, options) {
-  rollPeriod = Math.min(rollPeriod, originalData.length);
-  var rollingData = [];
-  var y, low, high, mid,count, i, extremes;
-
-  low = 0;
-  mid = 0;
-  high = 0;
-  count = 0;
-  for (i = 0; i < originalData.length; i++) {
-    y = originalData[i][1];
-    extremes = originalData[i][2];
-    rollingData[i] = originalData[i];
-
-    if (y !== null && !isNaN(y)) {
-      low += extremes[0];
-      mid += y;
-      high += extremes[1];
-      count += 1;
-    }
-    if (i - rollPeriod >= 0) {
-      var prev = originalData[i - rollPeriod];
-      if (prev[1] !== null && !isNaN(prev[1])) {
-        low -= prev[2][0];
-        mid -= prev[1];
-        high -= prev[2][1];
-        count -= 1;
-      }
-    }
-    if (count) {
-      rollingData[i] = [
-          originalData[i][0],
-          1.0 * mid / count, 
-          [ 1.0 * low / count,
-            1.0 * high / count ] ];
-    } else {
-      rollingData[i] = [ originalData[i][0], null, [ null, null ] ];
-    }
-  }
-
-  return rollingData;
-};
-
-})();
diff --git a/datahandler/bars-error.js b/datahandler/bars-error.js
deleted file mode 100644 (file)
index 71dbe34..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DataHandler implementation for the error bars option.
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- */
-
-(function() {
-
-/*global Dygraph:false */
-"use strict";
-
-/**
- * @constructor
- * @extends Dygraph.DataHandlers.BarsHandler
- */
-Dygraph.DataHandlers.ErrorBarsHandler = function() {
-};
-
-var ErrorBarsHandler = Dygraph.DataHandlers.ErrorBarsHandler;
-ErrorBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler();
-
-/** @inheritDoc */
-ErrorBarsHandler.prototype.extractSeries = function(rawData, i, options) {
-  // TODO(danvk): pre-allocate series here.
-  var series = [];
-  var x, y, variance, point;
-  var sigma = options.get("sigma");
-  var logScale = options.get('logscale');
-  for ( var j = 0; j < rawData.length; j++) {
-    x = rawData[j][0];
-    point = rawData[j][i];
-    if (logScale && point !== null) {
-      // On the log scale, points less than zero do not exist.
-      // This will create a gap in the chart.
-      if (point[0] <= 0 || point[0] - sigma * point[1] <= 0) {
-        point = null;
-      }
-    }
-    // Extract to the unified data format.
-    if (point !== null) {
-      y = point[0];
-      if (y !== null && !isNaN(y)) {
-        variance = sigma * point[1];
-        // preserve original error value in extras for further
-        // filtering
-        series.push([ x, y, [ y - variance, y + variance, point[1] ] ]);
-      } else {
-        series.push([ x, y, [ y, y, y ] ]);
-      }
-    } else {
-      series.push([ x, null, [ null, null, null ] ]);
-    }
-  }
-  return series;
-};
-
-/** @inheritDoc */
-ErrorBarsHandler.prototype.rollingAverage =
-    function(originalData, rollPeriod, options) {
-  rollPeriod = Math.min(rollPeriod, originalData.length);
-  var rollingData = [];
-  var sigma = options.get("sigma");
-
-  var i, j, y, v, sum, num_ok, stddev, variance, value;
-
-  // Calculate the rolling average for the first rollPeriod - 1 points
-  // where there is not enough data to roll over the full number of points
-  for (i = 0; i < originalData.length; i++) {
-    sum = 0;
-    variance = 0;
-    num_ok = 0;
-    for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
-      y = originalData[j][1];
-      if (y === null || isNaN(y))
-        continue;
-      num_ok++;
-      sum += y;
-      variance += Math.pow(originalData[j][2][2], 2);
-    }
-    if (num_ok) {
-      stddev = Math.sqrt(variance) / num_ok;
-      value = sum / num_ok;
-      rollingData[i] = [ originalData[i][0], value,
-          [value - sigma * stddev, value + sigma * stddev] ];
-    } else {
-      // This explicitly preserves NaNs to aid with "independent
-      // series".
-      // See testRollingAveragePreservesNaNs.
-      v = (rollPeriod == 1) ? originalData[i][1] : null;
-      rollingData[i] = [ originalData[i][0], v, [ v, v ] ];
-    }
-  }
-
-  return rollingData;
-};
-
-})();
diff --git a/datahandler/bars-fractions.js b/datahandler/bars-fractions.js
deleted file mode 100644 (file)
index 8594c07..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DataHandler implementation for the combination 
- * of error bars and fractions options.
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- */
-
-(function() {
-
-/*global Dygraph:false */
-"use strict";
-
-/**
- * @constructor
- * @extends Dygraph.DataHandlers.BarsHandler
- */
-Dygraph.DataHandlers.FractionsBarsHandler = function() {
-};
-
-var FractionsBarsHandler = Dygraph.DataHandlers.FractionsBarsHandler;
-FractionsBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler();
-
-/** @inheritDoc */
-FractionsBarsHandler.prototype.extractSeries = function(rawData, i, options) {
-  // TODO(danvk): pre-allocate series here.
-  var series = [];
-  var x, y, point, num, den, value, stddev, variance;
-  var mult = 100.0;
-  var sigma = options.get("sigma");
-  var logScale = options.get('logscale');
-  for ( var j = 0; j < rawData.length; j++) {
-    x = rawData[j][0];
-    point = rawData[j][i];
-    if (logScale && point !== null) {
-      // On the log scale, points less than zero do not exist.
-      // This will create a gap in the chart.
-      if (point[0] <= 0 || point[1] <= 0) {
-        point = null;
-      }
-    }
-    // Extract to the unified data format.
-    if (point !== null) {
-      num = point[0];
-      den = point[1];
-      if (num !== null && !isNaN(num)) {
-        value = den ? num / den : 0.0;
-        stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
-        variance = mult * stddev;
-        y = mult * value;
-        // preserve original values in extras for further filtering
-        series.push([ x, y, [ y - variance, y + variance, num, den ] ]);
-      } else {
-        series.push([ x, num, [ num, num, num, den ] ]);
-      }
-    } else {
-      series.push([ x, null, [ null, null, null, null ] ]);
-    }
-  }
-  return series;
-};
-
-/** @inheritDoc */
-FractionsBarsHandler.prototype.rollingAverage =
-    function(originalData, rollPeriod, options) {
-  rollPeriod = Math.min(rollPeriod, originalData.length);
-  var rollingData = [];
-  var sigma = options.get("sigma");
-  var wilsonInterval = options.get("wilsonInterval");
-
-  var low, high, i, stddev;
-  var num = 0;
-  var den = 0; // numerator/denominator
-  var mult = 100.0;
-  for (i = 0; i < originalData.length; i++) {
-    num += originalData[i][2][2];
-    den += originalData[i][2][3];
-    if (i - rollPeriod >= 0) {
-      num -= originalData[i - rollPeriod][2][2];
-      den -= originalData[i - rollPeriod][2][3];
-    }
-
-    var date = originalData[i][0];
-    var value = den ? num / den : 0.0;
-    if (wilsonInterval) {
-      // For more details on this confidence interval, see:
-      // http://en.wikipedia.org/wiki/Binomial_confidence_interval
-      if (den) {
-        var p = value < 0 ? 0 : value, n = den;
-        var pm = sigma * Math.sqrt(p * (1 - p) / n + sigma * sigma / (4 * n * n));
-        var denom = 1 + sigma * sigma / den;
-        low = (p + sigma * sigma / (2 * den) - pm) / denom;
-        high = (p + sigma * sigma / (2 * den) + pm) / denom;
-        rollingData[i] = [ date, p * mult,
-            [ low * mult, high * mult ] ];
-      } else {
-        rollingData[i] = [ date, 0, [ 0, 0 ] ];
-      }
-    } else {
-      stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
-      rollingData[i] = [ date, mult * value, 
-                         [ mult * (value - stddev), mult * (value + stddev) ] ];
-    }
-  }
-
-  return rollingData;
-};
-
-})();
diff --git a/datahandler/bars.js b/datahandler/bars.js
deleted file mode 100644 (file)
index 7100148..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DataHandler base implementation for the "bar" 
- * data formats. This implementation must be extended and the
- * extractSeries and rollingAverage must be implemented.
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- */
-
-(function() {
-
-/*global Dygraph:false */
-/*global DygraphLayout:false */
-"use strict";
-
-/**
- * @constructor
- * @extends {Dygraph.DataHandler}
- */
-Dygraph.DataHandlers.BarsHandler = function() {
-  Dygraph.DataHandler.call(this);
-};
-Dygraph.DataHandlers.BarsHandler.prototype = new Dygraph.DataHandler();
-
-// alias for the rest of the implementation
-var BarsHandler = Dygraph.DataHandlers.BarsHandler;
-
-// TODO(danvk): figure out why the jsdoc has to be copy/pasted from superclass.
-//   (I get closure compiler errors if this isn't here.)
-/**
- * @override
- * @param {!Array.<Array>} rawData The raw data passed into dygraphs where 
- *     rawData[i] = [x,ySeries1,...,ySeriesN].
- * @param {!number} seriesIndex Index of the series to extract. All other
- *     series should be ignored.
- * @param {!DygraphOptions} options Dygraph options.
- * @return {Array.<[!number,?number,?]>} The series in the unified data format
- *     where series[i] = [x,y,{extras}]. 
- */
-BarsHandler.prototype.extractSeries = function(rawData, seriesIndex, options) {
-  // Not implemented here must be extended
-};
-
-/**
- * @override
- * @param {!Array.<[!number,?number,?]>} series The series in the unified 
- *          data format where series[i] = [x,y,{extras}].
- * @param {!number} rollPeriod The number of points over which to average the data
- * @param {!DygraphOptions} options The dygraph options.
- * TODO(danvk): be more specific than "Array" here.
- * @return {!Array.<[!number,?number,?]>} the rolled series.
- */
-BarsHandler.prototype.rollingAverage =
-    function(series, rollPeriod, options) {
-  // Not implemented here, must be extended.
-};
-
-/** @inheritDoc */
-BarsHandler.prototype.onPointsCreated_ = function(series, points) {
-  for (var i = 0; i < series.length; ++i) {
-    var item = series[i];
-    var point = points[i];
-    point.y_top = NaN;
-    point.y_bottom = NaN;
-    point.yval_minus = Dygraph.DataHandler.parseFloat(item[2][0]);
-    point.yval_plus = Dygraph.DataHandler.parseFloat(item[2][1]);
-  }
-};
-
-/** @inheritDoc */
-BarsHandler.prototype.getExtremeYValues = function(series, dateWindow, options) {
-  var minY = null, maxY = null, y;
-
-  var firstIdx = 0;
-  var lastIdx = series.length - 1;
-
-  for ( var j = firstIdx; j <= lastIdx; j++) {
-    y = series[j][1];
-    if (y === null || isNaN(y)) continue;
-
-    var low = series[j][2][0];
-    var high = series[j][2][1];
-
-    if (low > y) low = y; // this can happen with custom bars,
-    if (high < y) high = y; // e.g. in tests/custom-bars.html
-
-    if (maxY === null || high > maxY) maxY = high;
-    if (minY === null || low < minY) minY = low;
-  }
-
-  return [ minY, maxY ];
-};
-
-/** @inheritDoc */
-BarsHandler.prototype.onLineEvaluated = function(points, axis, logscale) {
-  var point;
-  for (var j = 0; j < points.length; j++) {
-    // Copy over the error terms
-    point = points[j];
-    point.y_top = DygraphLayout.calcYNormal_(axis, point.yval_minus, logscale);
-    point.y_bottom = DygraphLayout.calcYNormal_(axis, point.yval_plus, logscale);
-  }
-};
-
-})();
diff --git a/datahandler/datahandler.js b/datahandler/datahandler.js
deleted file mode 100644 (file)
index b3eae91..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview This file contains the managment of data handlers
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- * 
- * The idea is to define a common, generic data format that works for all data
- * structures supported by dygraphs. To make this possible, the DataHandler
- * interface is introduced. This makes it possible, that dygraph itself can work
- * with the same logic for every data type independent of the actual format and
- * the DataHandler takes care of the data format specific jobs. 
- * DataHandlers are implemented for all data types supported by Dygraphs and
- * return Dygraphs compliant formats.
- * By default the correct DataHandler is chosen based on the options set.
- * Optionally the user may use his own DataHandler (similar to the plugin
- * system).
- * 
- * 
- * The unified data format returend by each handler is defined as so: 
- * series[n][point] = [x,y,(extras)] 
- * 
- * This format contains the common basis that is needed to draw a simple line
- * series extended by optional extras for more complex graphing types. It
- * contains a primitive x value as first array entry, a primitive y value as
- * second array entry and an optional extras object for additional data needed.
- * 
- * x must always be a number.
- * y must always be a number, NaN of type number or null.
- * extras is optional and must be interpreted by the DataHandler. It may be of
- * any type. 
- * 
- * In practice this might look something like this:
- * default: [x, yVal]
- * errorBar / customBar: [x, yVal, [yTopVariance, yBottomVariance] ]
- * 
- */
-/*global Dygraph:false */
-/*global DygraphLayout:false */
-
-/**
- * 
- * The data handler is responsible for all data specific operations. All of the
- * series data it receives and returns is always in the unified data format.
- * Initially the unified data is created by the extractSeries method
- * @constructor
- */
-Dygraph.DataHandler = function () {
-};
-
-/**
- * A collection of functions to create and retrieve data handlers.
- * @type {Object.<!Dygraph.DataHandler>}
- */
-Dygraph.DataHandlers = {};
-
-(function() {
-
-"use strict";
-
-var handler = Dygraph.DataHandler;
-
-/**
- * X-value array index constant for unified data samples.
- * @const
- * @type {number}
- */
-handler.X = 0;
-
-/**
- * Y-value array index constant for unified data samples.
- * @const
- * @type {number}
- */
-handler.Y = 1;
-
-/**
- * Extras-value array index constant for unified data samples.
- * @const
- * @type {number}
- */
-handler.EXTRAS = 2;
-
-/**
- * Extracts one series from the raw data (a 2D array) into an array of the
- * unified data format.
- * This is where undesirable points (i.e. negative values on log scales and
- * missing values through which we wish to connect lines) are dropped.
- * TODO(danvk): the "missing values" bit above doesn't seem right.
- * 
- * @param {!Array.<Array>} rawData The raw data passed into dygraphs where 
- *     rawData[i] = [x,ySeries1,...,ySeriesN].
- * @param {!number} seriesIndex Index of the series to extract. All other
- *     series should be ignored.
- * @param {!DygraphOptions} options Dygraph options.
- * @return {Array.<[!number,?number,?]>} The series in the unified data format
- *     where series[i] = [x,y,{extras}]. 
- */
-handler.prototype.extractSeries = function(rawData, seriesIndex, options) {
-};
-
-/**
- * Converts a series to a Point array.  The resulting point array must be
- * returned in increasing order of idx property.
- * 
- * @param {!Array.<[!number,?number,?]>} series The series in the unified 
- *          data format where series[i] = [x,y,{extras}].
- * @param {!string} setName Name of the series.
- * @param {!number} boundaryIdStart Index offset of the first point, equal to the
- *          number of skipped points left of the date window minimum (if any).
- * @return {!Array.<Dygraph.PointType>} List of points for this series.
- */
-handler.prototype.seriesToPoints = function(series, setName, boundaryIdStart) {
-  // TODO(bhs): these loops are a hot-spot for high-point-count charts. In
-  // fact,
-  // on chrome+linux, they are 6 times more expensive than iterating through
-  // the
-  // points and drawing the lines. The brunt of the cost comes from allocating
-  // the |point| structures.
-  var points = [];
-  for ( var i = 0; i < series.length; ++i) {
-    var item = series[i];
-    var yraw = item[1];
-    var yval = yraw === null ? null : handler.parseFloat(yraw);
-    var point = {
-      x : NaN,
-      y : NaN,
-      xval : handler.parseFloat(item[0]),
-      yval : yval,
-      name : setName, // TODO(danvk): is this really necessary?
-      idx : i + boundaryIdStart
-    };
-    points.push(point);
-  }
-  this.onPointsCreated_(series, points);
-  return points;
-};
-
-/**
- * Callback called for each series after the series points have been generated
- * which will later be used by the plotters to draw the graph.
- * Here data may be added to the seriesPoints which is needed by the plotters.
- * The indexes of series and points are in sync meaning the original data
- * sample for series[i] is points[i].
- * 
- * @param {!Array.<[!number,?number,?]>} series The series in the unified 
- *     data format where series[i] = [x,y,{extras}].
- * @param {!Array.<Dygraph.PointType>} points The corresponding points passed 
- *     to the plotter.
- * @protected
- */
-handler.prototype.onPointsCreated_ = function(series, points) {
-};
-
-/**
- * Calculates the rolling average of a data set.
- * 
- * @param {!Array.<[!number,?number,?]>} series The series in the unified 
- *          data format where series[i] = [x,y,{extras}].
- * @param {!number} rollPeriod The number of points over which to average the data
- * @param {!DygraphOptions} options The dygraph options.
- * @return {!Array.<[!number,?number,?]>} the rolled series.
- */
-handler.prototype.rollingAverage = function(series, rollPeriod, options) {
-};
-
-/**
- * Computes the range of the data series (including confidence intervals).
- * 
- * @param {!Array.<[!number,?number,?]>} series The series in the unified 
- *     data format where series[i] = [x, y, {extras}].
- * @param {!Array.<number>} dateWindow The x-value range to display with 
- *     the format: [min, max].
- * @param {!DygraphOptions} options The dygraph options.
- * @return {Array.<number>} The low and high extremes of the series in the
- *     given window with the format: [low, high].
- */
-handler.prototype.getExtremeYValues = function(series, dateWindow, options) {
-};
-
-/**
- * Callback called for each series after the layouting data has been
- * calculated before the series is drawn. Here normalized positioning data
- * should be calculated for the extras of each point.
- * 
- * @param {!Array.<Dygraph.PointType>} points The points passed to 
- *          the plotter.
- * @param {!Object} axis The axis on which the series will be plotted.
- * @param {!boolean} logscale Weather or not to use a logscale.
- */
-handler.prototype.onLineEvaluated = function(points, axis, logscale) {
-};
-
-/**
- * Helper method that computes the y value of a line defined by the points p1
- * and p2 and a given x value.
- * 
- * @param {!Array.<number>} p1 left point ([x,y]).
- * @param {!Array.<number>} p2 right point ([x,y]).
- * @param {!number} xValue The x value to compute the y-intersection for.
- * @return {number} corresponding y value to x on the line defined by p1 and p2.
- * @private
- */
-handler.prototype.computeYInterpolation_ = function(p1, p2, xValue) {
-  var deltaY = p2[1] - p1[1];
-  var deltaX = p2[0] - p1[0];
-  var gradient = deltaY / deltaX;
-  var growth = (xValue - p1[0]) * gradient;
-  return p1[1] + growth;
-};
-
-/**
- * Helper method that returns the first and the last index of the given series
- * that lie inside the given dateWindow.
- * 
- * @param {!Array.<[!number,?number,?]>} series The series in the unified 
- *     data format where series[i] = [x,y,{extras}].
- * @param {!Array.<number>} dateWindow The x-value range to display with 
- *     the format: [min,max].
- * @return {!Array.<[!number,?number,?]>} The samples of the series that 
- *     are in the given date window.
- * @private
- */
-handler.prototype.getIndexesInWindow_ = function(series, dateWindow) {
-  var firstIdx = 0, lastIdx = series.length - 1;
-  if (dateWindow) {
-    var idx = 0;
-    var low = dateWindow[0];
-    var high = dateWindow[1];
-
-    // Start from each side of the array to minimize the performance
-    // needed.
-    while (idx < series.length - 1 && series[idx][0] < low) {
-      firstIdx++;
-      idx++;
-    }
-    idx = series.length - 1;
-    while (idx > 0 && series[idx][0] > high) {
-      lastIdx--;
-      idx--;
-    }
-  }
-  if (firstIdx <= lastIdx) {
-    return [ firstIdx, lastIdx ];
-  } else {
-    return [ 0, series.length - 1 ];
-  }
-};
-
-/**
- * Optimized replacement for parseFloat, which was way too slow when almost
- * all values were type number, with few edge cases, none of which were strings.
- * @param {?number} val
- * @return {number}
- * @protected
- */
-handler.parseFloat = function(val) {
-  // parseFloat(null) is NaN
-  if (val === null) {
-    return NaN;
-  }
-
-  // Assume it's a number or NaN. If it's something else, I'll be shocked.
-  return val;
-};
-
-})();
diff --git a/datahandler/default-fractions.js b/datahandler/default-fractions.js
deleted file mode 100644 (file)
index 35d36eb..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DataHandler implementation for the fractions option.
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- */
-
-(function() {
-
-/*global Dygraph:false */
-"use strict";
-
-/**
- * @extends Dygraph.DataHandlers.DefaultHandler
- * @constructor
- */
-Dygraph.DataHandlers.DefaultFractionHandler = function() {
-};
-  
-var DefaultFractionHandler = Dygraph.DataHandlers.DefaultFractionHandler;
-DefaultFractionHandler.prototype = new Dygraph.DataHandlers.DefaultHandler();
-
-DefaultFractionHandler.prototype.extractSeries = function(rawData, i, options) {
-  // TODO(danvk): pre-allocate series here.
-  var series = [];
-  var x, y, point, num, den, value;
-  var mult = 100.0;
-  var logScale = options.get('logscale');
-  for ( var j = 0; j < rawData.length; j++) {
-    x = rawData[j][0];
-    point = rawData[j][i];
-    if (logScale && point !== null) {
-      // On the log scale, points less than zero do not exist.
-      // This will create a gap in the chart.
-      if (point[0] <= 0 || point[1] <= 0) {
-        point = null;
-      }
-    }
-    // Extract to the unified data format.
-    if (point !== null) {
-      num = point[0];
-      den = point[1];
-      if (num !== null && !isNaN(num)) {
-        value = den ? num / den : 0.0;
-        y = mult * value;
-        // preserve original values in extras for further filtering
-        series.push([ x, y, [ num, den ] ]);
-      } else {
-        series.push([ x, num, [ num, den ] ]);
-      }
-    } else {
-      series.push([ x, null, [ null, null ] ]);
-    }
-  }
-  return series;
-};
-
-DefaultFractionHandler.prototype.rollingAverage = function(originalData, rollPeriod,
-    options) {
-  rollPeriod = Math.min(rollPeriod, originalData.length);
-  var rollingData = [];
-
-  var i;
-  var num = 0;
-  var den = 0; // numerator/denominator
-  var mult = 100.0;
-  for (i = 0; i < originalData.length; i++) {
-    num += originalData[i][2][0];
-    den += originalData[i][2][1];
-    if (i - rollPeriod >= 0) {
-      num -= originalData[i - rollPeriod][2][0];
-      den -= originalData[i - rollPeriod][2][1];
-    }
-
-    var date = originalData[i][0];
-    var value = den ? num / den : 0.0;
-    rollingData[i] = [ date, mult * value ];
-  }
-
-  return rollingData;
-};
-
-})();
diff --git a/datahandler/default.js b/datahandler/default.js
deleted file mode 100644 (file)
index e42b92b..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * @license
- * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DataHandler default implementation used for simple line charts.
- * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
- */
-
-(function() {
-
-/*global Dygraph:false */
-"use strict";
-
-/**
- * @constructor
- * @extends Dygraph.DataHandler
- */
-Dygraph.DataHandlers.DefaultHandler = function() {
-};
-
-var DefaultHandler = Dygraph.DataHandlers.DefaultHandler;
-DefaultHandler.prototype = new Dygraph.DataHandler();
-
-/** @inheritDoc */
-DefaultHandler.prototype.extractSeries = function(rawData, i, options) {
-  // TODO(danvk): pre-allocate series here.
-  var series = [];
-  var logScale = options.get('logscale');
-  for ( var j = 0; j < rawData.length; j++) {
-    var x = rawData[j][0];
-    var point = rawData[j][i];
-    if (logScale) {
-      // On the log scale, points less than zero do not exist.
-      // This will create a gap in the chart.
-      if (point <= 0) {
-        point = null;
-      }
-    }
-    series.push([ x, point ]);
-  }
-  return series;
-};
-
-/** @inheritDoc */
-DefaultHandler.prototype.rollingAverage = function(originalData, rollPeriod,
-    options) {
-  rollPeriod = Math.min(rollPeriod, originalData.length);
-  var rollingData = [];
-
-  var i, j, y, sum, num_ok;
-  // Calculate the rolling average for the first rollPeriod - 1 points
-  // where
-  // there is not enough data to roll over the full number of points
-  if (rollPeriod == 1) {
-    return originalData;
-  }
-  for (i = 0; i < originalData.length; i++) {
-    sum = 0;
-    num_ok = 0;
-    for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
-      y = originalData[j][1];
-      if (y === null || isNaN(y))
-        continue;
-      num_ok++;
-      sum += originalData[j][1];
-    }
-    if (num_ok) {
-      rollingData[i] = [ originalData[i][0], sum / num_ok ];
-    } else {
-      rollingData[i] = [ originalData[i][0], null ];
-    }
-  }
-
-  return rollingData;
-};
-
-/** @inheritDoc */
-DefaultHandler.prototype.getExtremeYValues = function(series, dateWindow,
-    options) {
-  var minY = null, maxY = null, y;
-  var firstIdx = 0, lastIdx = series.length - 1;
-
-  for ( var j = firstIdx; j <= lastIdx; j++) {
-    y = series[j][1];
-    if (y === null || isNaN(y))
-      continue;
-    if (maxY === null || y > maxY) {
-      maxY = y;
-    }
-    if (minY === null || y < minY) {
-      minY = y;
-    }
-  }
-  return [ minY, maxY ];
-};
-
-})();
diff --git a/dygraph-canvas.js b/dygraph-canvas.js
deleted file mode 100644 (file)
index a688c43..0000000
+++ /dev/null
@@ -1,862 +0,0 @@
-/**
- * @license
- * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
- * needs of dygraphs.
- *
- * In particular, support for:
- * - grid overlays
- * - error bars
- * - dygraphs attribute system
- */
-
-/**
- * The DygraphCanvasRenderer class does the actual rendering of the chart onto
- * a canvas. It's based on PlotKit.CanvasRenderer.
- * @param {Object} element The canvas to attach to
- * @param {Object} elementContext The 2d context of the canvas (injected so it
- * can be mocked for testing.)
- * @param {Layout} layout The DygraphLayout object for this graph.
- * @constructor
- */
-
-var DygraphCanvasRenderer = (function() {
-/*global Dygraph:false */
-"use strict";
-
-
-/**
- * @constructor
- *
- * This gets called when there are "new points" to chart. This is generally the
- * case when the underlying data being charted has changed. It is _not_ called
- * in the common case that the user has zoomed or is panning the view.
- *
- * The chart canvas has already been created by the Dygraph object. The
- * renderer simply gets a drawing context.
- *
- * @param {Dygraph} dygraph The chart to which this renderer belongs.
- * @param {HTMLCanvasElement} element The &lt;canvas&gt; DOM element on which to draw.
- * @param {CanvasRenderingContext2D} elementContext The drawing context.
- * @param {DygraphLayout} layout The chart's DygraphLayout object.
- *
- * TODO(danvk): remove the elementContext property.
- */
-var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
-  this.dygraph_ = dygraph;
-
-  this.layout = layout;
-  this.element = element;
-  this.elementContext = elementContext;
-
-  this.height = dygraph.height_;
-  this.width = dygraph.width_;
-
-  // --- check whether everything is ok before we return
-  if (!Dygraph.isCanvasSupported(this.element)) {
-    throw "Canvas is not supported.";
-  }
-
-  // internal state
-  this.area = layout.getPlotArea();
-
-  // Set up a clipping area for the canvas (and the interaction canvas).
-  // This ensures that we don't overdraw.
-  // on Android 3 and 4, setting a clipping area on a canvas prevents it from
-  // displaying anything.
-  if (!Dygraph.isAndroid()) {
-    var ctx = this.dygraph_.canvas_ctx_;
-    ctx.beginPath();
-    ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
-    ctx.clip();
-
-    ctx = this.dygraph_.hidden_ctx_;
-    ctx.beginPath();
-    ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
-    ctx.clip();
-  }
-};
-
-/**
- * Clears out all chart content and DOM elements.
- * This is called immediately before render() on every frame, including
- * during zooms and pans.
- * @private
- */
-DygraphCanvasRenderer.prototype.clear = function() {
-  this.elementContext.clearRect(0, 0, this.width, this.height);
-};
-
-/**
- * This method is responsible for drawing everything on the chart, including
- * lines, error bars, fills and axes.
- * It is called immediately after clear() on every frame, including during pans
- * and zooms.
- * @private
- */
-DygraphCanvasRenderer.prototype.render = function() {
-  // attaches point.canvas{x,y}
-  this._updatePoints();
-
-  // actually draws the chart.
-  this._renderLineChart();
-};
-
-/**
- * Returns a predicate to be used with an iterator, which will
- * iterate over points appropriately, depending on whether
- * connectSeparatedPoints is true. When it's false, the predicate will
- * skip over points with missing yVals.
- */
-DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
-  return connectSeparatedPoints ?
-      DygraphCanvasRenderer._predicateThatSkipsEmptyPoints :
-      null;
-};
-
-DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
-    function(array, idx) {
-  return array[idx].yval !== null;
-};
-
-/**
- * Draws a line with the styles passed in and calls all the drawPointCallbacks.
- * @param {Object} e The dictionary passed to the plotter function.
- * @private
- */
-DygraphCanvasRenderer._drawStyledLine = function(e,
-    color, strokeWidth, strokePattern, drawPoints,
-    drawPointCallback, pointSize) {
-  var g = e.dygraph;
-  // TODO(konigsberg): Compute attributes outside this method call.
-  var stepPlot = g.getBooleanOption("stepPlot", e.setName);
-
-  if (!Dygraph.isArrayLike(strokePattern)) {
-    strokePattern = null;
-  }
-
-  var drawGapPoints = g.getBooleanOption('drawGapEdgePoints', e.setName);
-
-  var points = e.points;
-  var setName = e.setName;
-  var iter = Dygraph.createIterator(points, 0, points.length,
-      DygraphCanvasRenderer._getIteratorPredicate(
-          g.getBooleanOption("connectSeparatedPoints", setName)));
-
-  var stroking = strokePattern && (strokePattern.length >= 2);
-
-  var ctx = e.drawingContext;
-  ctx.save();
-  if (stroking) {
-    ctx.installPattern(strokePattern);
-  }
-
-  var pointsOnLine = DygraphCanvasRenderer._drawSeries(
-      e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color);
-  DygraphCanvasRenderer._drawPointsOnLine(
-      e, pointsOnLine, drawPointCallback, color, pointSize);
-
-  if (stroking) {
-    ctx.uninstallPattern();
-  }
-
-  ctx.restore();
-};
-
-/**
- * This does the actual drawing of lines on the canvas, for just one series.
- * Returns a list of [canvasx, canvasy] pairs for points for which a
- * drawPointCallback should be fired.  These include isolated points, or all
- * points if drawPoints=true.
- * @param {Object} e The dictionary passed to the plotter function.
- * @private
- */
-DygraphCanvasRenderer._drawSeries = function(e,
-    iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color) {
-
-  var prevCanvasX = null;
-  var prevCanvasY = null;
-  var nextCanvasY = null;
-  var isIsolated; // true if this point is isolated (no line segments)
-  var point; // the point being processed in the while loop
-  var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
-  var first = true; // the first cycle through the while loop
-
-  var ctx = e.drawingContext;
-  ctx.beginPath();
-  ctx.strokeStyle = color;
-  ctx.lineWidth = strokeWidth;
-
-  // NOTE: we break the iterator's encapsulation here for about a 25% speedup.
-  var arr = iter.array_;
-  var limit = iter.end_;
-  var predicate = iter.predicate_;
-
-  for (var i = iter.start_; i < limit; i++) {
-    point = arr[i];
-    if (predicate) {
-      while (i < limit && !predicate(arr, i)) {
-        i++;
-      }
-      if (i == limit) break;
-      point = arr[i];
-    }
-
-    // FIXME: The 'canvasy != canvasy' test here catches NaN values but the test
-    // doesn't catch Infinity values. Could change this to
-    // !isFinite(point.canvasy), but I assume it avoids isNaN for performance?
-    if (point.canvasy === null || point.canvasy != point.canvasy) {
-      if (stepPlot && prevCanvasX !== null) {
-        // Draw a horizontal line to the start of the missing data
-        ctx.moveTo(prevCanvasX, prevCanvasY);
-        ctx.lineTo(point.canvasx, prevCanvasY);
-      }
-      prevCanvasX = prevCanvasY = null;
-    } else {
-      isIsolated = false;
-      if (drawGapPoints || !prevCanvasX) {
-        iter.nextIdx_ = i;
-        iter.next();
-        nextCanvasY = iter.hasNext ? iter.peek.canvasy : null;
-
-        var isNextCanvasYNullOrNaN = nextCanvasY === null ||
-            nextCanvasY != nextCanvasY;
-        isIsolated = (!prevCanvasX && isNextCanvasYNullOrNaN);
-        if (drawGapPoints) {
-          // Also consider a point to be "isolated" if it's adjacent to a
-          // null point, excluding the graph edges.
-          if ((!first && !prevCanvasX) ||
-              (iter.hasNext && isNextCanvasYNullOrNaN)) {
-            isIsolated = true;
-          }
-        }
-      }
-
-      if (prevCanvasX !== null) {
-        if (strokeWidth) {
-          if (stepPlot) {
-            ctx.moveTo(prevCanvasX, prevCanvasY);
-            ctx.lineTo(point.canvasx, prevCanvasY);
-          }
-
-          ctx.lineTo(point.canvasx, point.canvasy);
-        }
-      } else {
-        ctx.moveTo(point.canvasx, point.canvasy);
-      }
-      if (drawPoints || isIsolated) {
-        pointsOnLine.push([point.canvasx, point.canvasy, point.idx]);
-      }
-      prevCanvasX = point.canvasx;
-      prevCanvasY = point.canvasy;
-    }
-    first = false;
-  }
-  ctx.stroke();
-  return pointsOnLine;
-};
-
-/**
- * This fires the drawPointCallback functions, which draw dots on the points by
- * default. This gets used when the "drawPoints" option is set, or when there
- * are isolated points.
- * @param {Object} e The dictionary passed to the plotter function.
- * @private
- */
-DygraphCanvasRenderer._drawPointsOnLine = function(
-    e, pointsOnLine, drawPointCallback, color, pointSize) {
-  var ctx = e.drawingContext;
-  for (var idx = 0; idx < pointsOnLine.length; idx++) {
-    var cb = pointsOnLine[idx];
-    ctx.save();
-    drawPointCallback.call(e.dygraph,
-        e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize, cb[2]);
-    ctx.restore();
-  }
-};
-
-/**
- * Attaches canvas coordinates to the points array.
- * @private
- */
-DygraphCanvasRenderer.prototype._updatePoints = function() {
-  // Update Points
-  // TODO(danvk): here
-  //
-  // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
-  // transformations can be pushed into the canvas via linear transformation
-  // matrices.
-  // NOTE(danvk): this is trickier than it sounds at first. The transformation
-  // needs to be done before the .moveTo() and .lineTo() calls, but must be
-  // undone before the .stroke() call to ensure that the stroke width is
-  // unaffected.  An alternative is to reduce the stroke width in the
-  // transformed coordinate space, but you can't specify different values for
-  // each dimension (as you can with .scale()). The speedup here is ~12%.
-  var sets = this.layout.points;
-  for (var i = sets.length; i--;) {
-    var points = sets[i];
-    for (var j = points.length; j--;) {
-      var point = points[j];
-      point.canvasx = this.area.w * point.x + this.area.x;
-      point.canvasy = this.area.h * point.y + this.area.y;
-    }
-  }
-};
-
-/**
- * Add canvas Actually draw the lines chart, including error bars.
- *
- * This function can only be called if DygraphLayout's points array has been
- * updated with canvas{x,y} attributes, i.e. by
- * DygraphCanvasRenderer._updatePoints.
- *
- * @param {string=} opt_seriesName when specified, only that series will
- *     be drawn. (This is used for expedited redrawing with highlightSeriesOpts)
- * @param {CanvasRenderingContext2D} opt_ctx when specified, the drawing
- *     context.  However, lines are typically drawn on the object's
- *     elementContext.
- * @private
- */
-DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) {
-  var ctx = opt_ctx || this.elementContext;
-  var i;
-
-  var sets = this.layout.points;
-  var setNames = this.layout.setNames;
-  var setName;
-
-  this.colors = this.dygraph_.colorsMap_;
-
-  // Determine which series have specialized plotters.
-  var plotter_attr = this.dygraph_.getOption("plotter");
-  var plotters = plotter_attr;
-  if (!Dygraph.isArrayLike(plotters)) {
-    plotters = [plotters];
-  }
-
-  var setPlotters = {};  // series name -> plotter fn.
-  for (i = 0; i < setNames.length; i++) {
-    setName = setNames[i];
-    var setPlotter = this.dygraph_.getOption("plotter", setName);
-    if (setPlotter == plotter_attr) continue;  // not specialized.
-
-    setPlotters[setName] = setPlotter;
-  }
-
-  for (i = 0; i < plotters.length; i++) {
-    var plotter = plotters[i];
-    var is_last = (i == plotters.length - 1);
-
-    for (var j = 0; j < sets.length; j++) {
-      setName = setNames[j];
-      if (opt_seriesName && setName != opt_seriesName) continue;
-
-      var points = sets[j];
-
-      // Only throw in the specialized plotters on the last iteration.
-      var p = plotter;
-      if (setName in setPlotters) {
-        if (is_last) {
-          p = setPlotters[setName];
-        } else {
-          // Don't use the standard plotters in this case.
-          continue;
-        }
-      }
-
-      var color = this.colors[setName];
-      var strokeWidth = this.dygraph_.getOption("strokeWidth", setName);
-
-      ctx.save();
-      ctx.strokeStyle = color;
-      ctx.lineWidth = strokeWidth;
-      p({
-        points: points,
-        setName: setName,
-        drawingContext: ctx,
-        color: color,
-        strokeWidth: strokeWidth,
-        dygraph: this.dygraph_,
-        axis: this.dygraph_.axisPropertiesForSeries(setName),
-        plotArea: this.area,
-        seriesIndex: j,
-        seriesCount: sets.length,
-        singleSeriesName: opt_seriesName,
-        allSeriesPoints: sets
-      });
-      ctx.restore();
-    }
-  }
-};
-
-/**
- * Standard plotters. These may be used by clients via Dygraph.Plotters.
- * See comments there for more details.
- */
-DygraphCanvasRenderer._Plotters = {
-  linePlotter: function(e) {
-    DygraphCanvasRenderer._linePlotter(e);
-  },
-
-  fillPlotter: function(e) {
-    DygraphCanvasRenderer._fillPlotter(e);
-  },
-
-  errorPlotter: function(e) {
-    DygraphCanvasRenderer._errorPlotter(e);
-  }
-};
-
-/**
- * Plotter which draws the central lines for a series.
- * @private
- */
-DygraphCanvasRenderer._linePlotter = function(e) {
-  var g = e.dygraph;
-  var setName = e.setName;
-  var strokeWidth = e.strokeWidth;
-
-  // TODO(danvk): Check if there's any performance impact of just calling
-  // getOption() inside of _drawStyledLine. Passing in so many parameters makes
-  // this code a bit nasty.
-  var borderWidth = g.getNumericOption("strokeBorderWidth", setName);
-  var drawPointCallback = g.getOption("drawPointCallback", setName) ||
-      Dygraph.Circles.DEFAULT;
-  var strokePattern = g.getOption("strokePattern", setName);
-  var drawPoints = g.getBooleanOption("drawPoints", setName);
-  var pointSize = g.getNumericOption("pointSize", setName);
-
-  if (borderWidth && strokeWidth) {
-    DygraphCanvasRenderer._drawStyledLine(e,
-        g.getOption("strokeBorderColor", setName),
-        strokeWidth + 2 * borderWidth,
-        strokePattern,
-        drawPoints,
-        drawPointCallback,
-        pointSize
-        );
-  }
-
-  DygraphCanvasRenderer._drawStyledLine(e,
-      e.color,
-      strokeWidth,
-      strokePattern,
-      drawPoints,
-      drawPointCallback,
-      pointSize
-  );
-};
-
-/**
- * Draws the shaded error bars/confidence intervals for each series.
- * This happens before the center lines are drawn, since the center lines
- * need to be drawn on top of the error bars for all series.
- * @private
- */
-DygraphCanvasRenderer._errorPlotter = function(e) {
-  var g = e.dygraph;
-  var setName = e.setName;
-  var errorBars = g.getBooleanOption("errorBars") ||
-      g.getBooleanOption("customBars");
-  if (!errorBars) return;
-
-  var fillGraph = g.getBooleanOption("fillGraph", setName);
-  if (fillGraph) {
-    console.warn("Can't use fillGraph option with error bars");
-  }
-
-  var ctx = e.drawingContext;
-  var color = e.color;
-  var fillAlpha = g.getNumericOption('fillAlpha', setName);
-  var stepPlot = g.getBooleanOption("stepPlot", setName);
-  var points = e.points;
-
-  var iter = Dygraph.createIterator(points, 0, points.length,
-      DygraphCanvasRenderer._getIteratorPredicate(
-          g.getBooleanOption("connectSeparatedPoints", setName)));
-
-  var newYs;
-
-  // setup graphics context
-  var prevX = NaN;
-  var prevY = NaN;
-  var prevYs = [-1, -1];
-  // should be same color as the lines but only 15% opaque.
-  var rgb = Dygraph.toRGB_(color);
-  var err_color =
-      'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
-  ctx.fillStyle = err_color;
-  ctx.beginPath();
-
-  var isNullUndefinedOrNaN = function(x) {
-    return (x === null ||
-            x === undefined ||
-            isNaN(x));
-  };
-
-  while (iter.hasNext) {
-    var point = iter.next();
-    if ((!stepPlot && isNullUndefinedOrNaN(point.y)) ||
-        (stepPlot && !isNaN(prevY) && isNullUndefinedOrNaN(prevY))) {
-      prevX = NaN;
-      continue;
-    }
-
-    newYs = [ point.y_bottom, point.y_top ];
-    if (stepPlot) {
-      prevY = point.y;
-    }
-
-    // The documentation specifically disallows nulls inside the point arrays,
-    // but in case it happens we should do something sensible.
-    if (isNaN(newYs[0])) newYs[0] = point.y;
-    if (isNaN(newYs[1])) newYs[1] = point.y;
-
-    newYs[0] = e.plotArea.h * newYs[0] + e.plotArea.y;
-    newYs[1] = e.plotArea.h * newYs[1] + e.plotArea.y;
-    if (!isNaN(prevX)) {
-      if (stepPlot) {
-        ctx.moveTo(prevX, prevYs[0]);
-        ctx.lineTo(point.canvasx, prevYs[0]);
-        ctx.lineTo(point.canvasx, prevYs[1]);
-      } else {
-        ctx.moveTo(prevX, prevYs[0]);
-        ctx.lineTo(point.canvasx, newYs[0]);
-        ctx.lineTo(point.canvasx, newYs[1]);
-      }
-      ctx.lineTo(prevX, prevYs[1]);
-      ctx.closePath();
-    }
-    prevYs = newYs;
-    prevX = point.canvasx;
-  }
-  ctx.fill();
-};
-
-
-/**
- * Proxy for CanvasRenderingContext2D which drops moveTo/lineTo calls which are
- * superfluous. It accumulates all movements which haven't changed the x-value
- * and only applies the two with the most extreme y-values.
- * 
- * Calls to lineTo/moveTo must have non-decreasing x-values.
- */
-DygraphCanvasRenderer._fastCanvasProxy = function(context) {
-  var pendingActions = [];  // array of [type, x, y] tuples
-  var lastRoundedX = null;
-
-  var LINE_TO = 1,
-      MOVE_TO = 2;
-
-  var actionCount = 0;  // number of moveTos and lineTos passed to context.
-
-  // Drop superfluous motions
-  // Assumes all pendingActions have the same (rounded) x-value.
-  var compressActions = function(opt_losslessOnly) {
-    if (pendingActions.length <= 1) return;
-
-    // Lossless compression: drop inconsequential moveTos.
-    for (var i = pendingActions.length - 1; i > 0; i--) {
-      var action = pendingActions[i];
-      if (action[0] == MOVE_TO) {
-        var prevAction = pendingActions[i - 1];
-        if (prevAction[1] == action[1] && prevAction[2] == action[2]) {
-          pendingActions.splice(i, 1);
-        }
-      }
-    }
-
-    // Lossless compression: ... drop consecutive moveTos ...
-    for (var i = 0; i < pendingActions.length - 1; /* incremented internally */) {
-      var action = pendingActions[i];
-      if (action[0] == MOVE_TO && pendingActions[i + 1][0] == MOVE_TO) {
-        pendingActions.splice(i, 1);
-      } else {
-        i++;
-      }
-    }
-
-    // Lossy compression: ... drop all but the extreme y-values ...
-    if (pendingActions.length > 2 && !opt_losslessOnly) {
-      // keep an initial moveTo, but drop all others.
-      var startIdx = 0;
-      if (pendingActions[0][0] == MOVE_TO) startIdx++;
-      var minIdx = null, maxIdx = null;
-      for (var i = startIdx; i < pendingActions.length; i++) {
-        var action = pendingActions[i];
-        if (action[0] != LINE_TO) continue;
-        if (minIdx === null && maxIdx === null) {
-          minIdx = i;
-          maxIdx = i;
-        } else {
-          var y = action[2];
-          if (y < pendingActions[minIdx][2]) {
-            minIdx = i;
-          } else if (y > pendingActions[maxIdx][2]) {
-            maxIdx = i;
-          }
-        }
-      }
-      var minAction = pendingActions[minIdx],
-          maxAction = pendingActions[maxIdx];
-      pendingActions.splice(startIdx, pendingActions.length - startIdx);
-      if (minIdx < maxIdx) {
-        pendingActions.push(minAction);
-        pendingActions.push(maxAction);
-      } else if (minIdx > maxIdx) {
-        pendingActions.push(maxAction);
-        pendingActions.push(minAction);
-      } else {
-        pendingActions.push(minAction);
-      }
-    }
-  };
-
-  var flushActions = function(opt_noLossyCompression) {
-    compressActions(opt_noLossyCompression);
-    for (var i = 0, len = pendingActions.length; i < len; i++) {
-      var action = pendingActions[i];
-      if (action[0] == LINE_TO) {
-        context.lineTo(action[1], action[2]);
-      } else if (action[0] == MOVE_TO) {
-        context.moveTo(action[1], action[2]);
-      }
-    }
-    actionCount += pendingActions.length;
-    pendingActions = [];
-  };
-
-  var addAction = function(action, x, y) {
-    var rx = Math.round(x);
-    if (lastRoundedX === null || rx != lastRoundedX) {
-      flushActions();
-      lastRoundedX = rx;
-    }
-    pendingActions.push([action, x, y]);
-  };
-
-  return {
-    moveTo: function(x, y) {
-      addAction(MOVE_TO, x, y);
-    },
-    lineTo: function(x, y) {
-      addAction(LINE_TO, x, y);
-    },
-
-    // for major operations like stroke/fill, we skip compression to ensure
-    // that there are no artifacts at the right edge.
-    stroke:    function() { flushActions(true); context.stroke(); },
-    fill:      function() { flushActions(true); context.fill(); },
-    beginPath: function() { flushActions(true); context.beginPath(); },
-    closePath: function() { flushActions(true); context.closePath(); },
-
-    _count: function() { return actionCount; }
-  };
-};
-
-/**
- * Draws the shaded regions when "fillGraph" is set. Not to be confused with
- * error bars.
- *
- * For stacked charts, it's more convenient to handle all the series
- * simultaneously. So this plotter plots all the points on the first series
- * it's asked to draw, then ignores all the other series.
- *
- * @private
- */
-DygraphCanvasRenderer._fillPlotter = function(e) {
-  // Skip if we're drawing a single series for interactive highlight overlay.
-  if (e.singleSeriesName) return;
-
-  // We'll handle all the series at once, not one-by-one.
-  if (e.seriesIndex !== 0) return;
-
-  var g = e.dygraph;
-  var setNames = g.getLabels().slice(1);  // remove x-axis
-
-  // getLabels() includes names for invisible series, which are not included in
-  // allSeriesPoints. We remove those to make the two match.
-  // TODO(danvk): provide a simpler way to get this information.
-  for (var i = setNames.length; i >= 0; i--) {
-    if (!g.visibility()[i]) setNames.splice(i, 1);
-  }
-
-  var anySeriesFilled = (function() {
-    for (var i = 0; i < setNames.length; i++) {
-      if (g.getBooleanOption("fillGraph", setNames[i])) return true;
-    }
-    return false;
-  })();
-
-  if (!anySeriesFilled) return;
-
-  var area = e.plotArea;
-  var sets = e.allSeriesPoints;
-  var setCount = sets.length;
-
-  var fillAlpha = g.getNumericOption('fillAlpha');
-  var stackedGraph = g.getBooleanOption("stackedGraph");
-  var colors = g.getColors();
-
-  // For stacked graphs, track the baseline for filling.
-  //
-  // The filled areas below graph lines are trapezoids with two
-  // vertical edges. The top edge is the line segment being drawn, and
-  // the baseline is the bottom edge. Each baseline corresponds to the
-  // top line segment from the previous stacked line. In the case of
-  // step plots, the trapezoids are rectangles.
-  var baseline = {};
-  var currBaseline;
-  var prevStepPlot;  // for different line drawing modes (line/step) per series
-
-  // Helper function to trace a line back along the baseline.
-  var traceBackPath = function(ctx, baselineX, baselineY, pathBack) {
-    ctx.lineTo(baselineX, baselineY);
-    if (stackedGraph) {
-      for (var i = pathBack.length - 1; i >= 0; i--) {
-        var pt = pathBack[i];
-        ctx.lineTo(pt[0], pt[1]);
-      }
-    }
-  };
-
-  // process sets in reverse order (needed for stacked graphs)
-  for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) {
-    var ctx = e.drawingContext;
-    var setName = setNames[setIdx];
-    if (!g.getBooleanOption('fillGraph', setName)) continue;
-
-    var stepPlot = g.getBooleanOption('stepPlot', setName);
-    var color = colors[setIdx];
-    var axis = g.axisPropertiesForSeries(setName);
-    var axisY = 1.0 + axis.minyval * axis.yscale;
-    if (axisY < 0.0) axisY = 0.0;
-    else if (axisY > 1.0) axisY = 1.0;
-    axisY = area.h * axisY + area.y;
-
-    var points = sets[setIdx];
-    var iter = Dygraph.createIterator(points, 0, points.length,
-        DygraphCanvasRenderer._getIteratorPredicate(
-            g.getBooleanOption("connectSeparatedPoints", setName)));
-
-    // setup graphics context
-    var prevX = NaN;
-    var prevYs = [-1, -1];
-    var newYs;
-    // should be same color as the lines but only 15% opaque.
-    var rgb = Dygraph.toRGB_(color);
-    var err_color =
-        'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
-    ctx.fillStyle = err_color;
-    ctx.beginPath();
-    var last_x, is_first = true;
-
-    // If the point density is high enough, dropping segments on their way to
-    // the canvas justifies the overhead of doing so.
-    if (points.length > 2 * g.width_) {
-      ctx = DygraphCanvasRenderer._fastCanvasProxy(ctx);
-    }
-
-    // For filled charts, we draw points from left to right, then back along
-    // the x-axis to complete a shape for filling.
-    // For stacked plots, this "back path" is a more complex shape. This array
-    // stores the [x, y] values needed to trace that shape.
-    var pathBack = [];
-
-    // TODO(danvk): there are a lot of options at play in this loop.
-    //     The logic would be much clearer if some (e.g. stackGraph and
-    //     stepPlot) were split off into separate sub-plotters.
-    var point;
-    while (iter.hasNext) {
-      point = iter.next();
-      if (!Dygraph.isOK(point.y) && !stepPlot) {
-        traceBackPath(ctx, prevX, prevYs[1], pathBack);
-        pathBack = [];
-        prevX = NaN;
-        if (point.y_stacked !== null && !isNaN(point.y_stacked)) {
-          baseline[point.canvasx] = area.h * point.y_stacked + area.y;
-        }
-        continue;
-      }
-      if (stackedGraph) {
-        if (!is_first && last_x == point.xval) {
-          continue;
-        } else {
-          is_first = false;
-          last_x = point.xval;
-        }
-
-        currBaseline = baseline[point.canvasx];
-        var lastY;
-        if (currBaseline === undefined) {
-          lastY = axisY;
-        } else {
-          if(prevStepPlot) {
-            lastY = currBaseline[0];
-          } else {
-            lastY = currBaseline;
-          }
-        }
-        newYs = [ point.canvasy, lastY ];
-
-        if (stepPlot) {
-          // Step plots must keep track of the top and bottom of
-          // the baseline at each point.
-          if (prevYs[0] === -1) {
-            baseline[point.canvasx] = [ point.canvasy, axisY ];
-          } else {
-            baseline[point.canvasx] = [ point.canvasy, prevYs[0] ];
-          }
-        } else {
-          baseline[point.canvasx] = point.canvasy;
-        }
-
-      } else {
-        if (isNaN(point.canvasy) && stepPlot) {
-          newYs = [ area.y + area.h, axisY ];
-        } else {
-          newYs = [ point.canvasy, axisY ];
-        }
-      }
-      if (!isNaN(prevX)) {
-        // Move to top fill point
-        if (stepPlot) {
-          ctx.lineTo(point.canvasx, prevYs[0]);
-          ctx.lineTo(point.canvasx, newYs[0]);
-        } else {
-          ctx.lineTo(point.canvasx, newYs[0]);
-        }
-
-        // Record the baseline for the reverse path.
-        if (stackedGraph) {
-          pathBack.push([prevX, prevYs[1]]);
-          if (prevStepPlot && currBaseline) {
-            // Draw to the bottom of the baseline
-            pathBack.push([point.canvasx, currBaseline[1]]);
-          } else {
-            pathBack.push([point.canvasx, newYs[1]]);
-          }
-        }
-      } else {
-        ctx.moveTo(point.canvasx, newYs[1]);
-        ctx.lineTo(point.canvasx, newYs[0]);
-      }
-      prevYs = newYs;
-      prevX = point.canvasx;
-    }
-    prevStepPlot = stepPlot;
-    if (newYs && point) {
-      traceBackPath(ctx, point.canvasx, newYs[1], pathBack);
-      pathBack = [];
-    }
-    ctx.fill();
-  }
-};
-
-return DygraphCanvasRenderer;
-
-})();
diff --git a/dygraph-combined.js b/dygraph-combined.js
deleted file mode 100644 (file)
index e6380de..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-This is not the file you are looking for.
-A reasonably up-to-date version can be found at http://dygraphs.com/dygraph-combined.js
-
-dygraph-combined.js is a "packed" version of the larger dygraphs JS files. It is
-smaller and loads more quickly, but is harder to debug.
-
-To generate this file, run "make" or generate-combined.sh.
index 26a96df..506312e 100644 (file)
   // This list needs to be kept in sync w/ the one in generate-combined.sh
   // and the one in jsTestDriver.conf.
   var source_files = [
-    "polyfills/console.js",
-    "dashed-canvas.js",
-    "dygraph-options.js",
-    "dygraph-layout.js",
-    "dygraph-canvas.js",
-    "dygraph.js",
-    "dygraph-utils.js",
-    "dygraph-gviz.js",
-    "dygraph-interaction-model.js",
-    "dygraph-tickers.js",
-    "dygraph-plugin-base.js",
-    "plugins/annotations.js",
-    "plugins/axes.js",
-    "plugins/chart-labels.js",
-    "plugins/grid.js",
-    "plugins/legend.js",
-    "plugins/range-selector.js",
-    "dygraph-plugin-install.js",
-    "dygraph-options-reference.js",  // Shouldn't be included in generate-combined.sh
-    "datahandler/datahandler.js",
-    "datahandler/default.js",
-    "datahandler/default-fractions.js",
-    "datahandler/bars.js",
-    "datahandler/bars-error.js",
-    "datahandler/bars-custom.js",
-    "datahandler/bars-fractions.js"
+    "src/polyfills/console.js",
+    "src/polyfills/dashed-canvas.js",
+    "src/dygraph-options.js",
+    "src/dygraph-layout.js",
+    "src/dygraph-canvas.js",
+    "src/dygraph.js",
+    "src/dygraph-utils.js",
+    "src/dygraph-gviz.js",
+    "src/dygraph-interaction-model.js",
+    "src/dygraph-tickers.js",
+    "src/dygraph-plugin-base.js",
+    "src/plugins/annotations.js",
+    "src/plugins/axes.js",
+    "src/plugins/chart-labels.js",
+    "src/plugins/grid.js",
+    "src/plugins/legend.js",
+    "src/plugins/range-selector.js",
+    "src/dygraph-plugin-install.js",
+    "src/dygraph-options-reference.js",  // Shouldn't be included in generate-combined.sh
+    "src/datahandler/datahandler.js",
+    "src/datahandler/default.js",
+    "src/datahandler/default-fractions.js",
+    "src/datahandler/bars.js",
+    "src/datahandler/bars-error.js",
+    "src/datahandler/bars-custom.js",
+    "src/datahandler/bars-fractions.js"
   ];
 
   for (var i = 0; i < source_files.length; i++) {
diff --git a/dygraph-gviz.js b/dygraph-gviz.js
deleted file mode 100644 (file)
index d2d7a0d..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview A wrapper around the Dygraph class which implements the
- * interface for a GViz (aka Google Visualization API) visualization.
- * It is designed to be a drop-in replacement for Google's AnnotatedTimeline,
- * so the documentation at
- * http://code.google.com/apis/chart/interactive/docs/gallery/annotatedtimeline.html
- * translates over directly.
- *
- * For a full demo, see:
- * - http://dygraphs.com/tests/gviz.html
- * - http://dygraphs.com/tests/annotation-gviz.html
- */
-
-(function() {
-/*global Dygraph:false */
-"use strict";
-
-/**
- * A wrapper around Dygraph that implements the gviz API.
- * @param {!HTMLDivElement} container The DOM object the visualization should
- *     live in.
- * @constructor
- */
-Dygraph.GVizChart = function(container) {
-  this.container = container;
-};
-
-/**
- * @param {GVizDataTable} data
- * @param {Object.<*>} options
- */
-Dygraph.GVizChart.prototype.draw = function(data, options) {
-  // Clear out any existing dygraph.
-  // TODO(danvk): would it make more sense to simply redraw using the current
-  // date_graph object?
-  this.container.innerHTML = '';
-  if (typeof(this.date_graph) != 'undefined') {
-    this.date_graph.destroy();
-  }
-
-  this.date_graph = new Dygraph(this.container, data, options);
-};
-
-/**
- * Google charts compatible setSelection
- * Only row selection is supported, all points in the row will be highlighted
- * @param {Array.<{row:number}>} selection_array array of the selected cells
- * @public
- */
-Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
-  var row = false;
-  if (selection_array.length) {
-    row = selection_array[0].row;
-  }
-  this.date_graph.setSelection(row);
-};
-
-/**
- * Google charts compatible getSelection implementation
- * @return {Array.<{row:number,column:number}>} array of the selected cells
- * @public
- */
-Dygraph.GVizChart.prototype.getSelection = function() {
-  var selection = [];
-
-  var row = this.date_graph.getSelection();
-
-  if (row < 0) return selection;
-
-  var points = this.date_graph.layout_.points;
-  for (var setIdx = 0; setIdx < points.length; ++setIdx) {
-    selection.push({row: row, column: setIdx + 1});
-  }
-
-  return selection;
-};
-
-})();
diff --git a/dygraph-interaction-model.js b/dygraph-interaction-model.js
deleted file mode 100644 (file)
index 53e2f44..0000000
+++ /dev/null
@@ -1,757 +0,0 @@
-/**
- * @license
- * Copyright 2011 Robert Konigsberg (konigsberg@google.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview The default interaction model for Dygraphs. This is kept out
- * of dygraph.js for better navigability.
- * @author Robert Konigsberg (konigsberg@google.com)
- */
-
-(function() {
-/*global Dygraph:false */
-"use strict";
-
-/**
- * You can drag this many pixels past the edge of the chart and still have it
- * be considered a zoom. This makes it easier to zoom to the exact edge of the
- * chart, a fairly common operation.
- */
-var DRAG_EDGE_MARGIN = 100;
-
-/**
- * A collection of functions to facilitate build custom interaction models.
- * @class
- */
-Dygraph.Interaction = {};
-
-/**
- * Checks whether the beginning & ending of an event were close enough that it
- * should be considered a click. If it should, dispatch appropriate events.
- * Returns true if the event was treated as a click.
- *
- * @param {Event} event
- * @param {Dygraph} g
- * @param {Object} context
- */
-Dygraph.Interaction.maybeTreatMouseOpAsClick = function(event, g, context) {
-  context.dragEndX = Dygraph.dragGetX_(event, context);
-  context.dragEndY = Dygraph.dragGetY_(event, context);
-  var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
-  var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
-
-  if (regionWidth < 2 && regionHeight < 2 &&
-      g.lastx_ !== undefined && g.lastx_ != -1) {
-    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
-  }
-
-  context.regionWidth = regionWidth;
-  context.regionHeight = regionHeight;
-};
-
-/**
- * Called in response to an interaction model operation that
- * should start the default panning behavior.
- *
- * It's used in the default callback for "mousedown" operations.
- * Custom interaction model builders can use it to provide the default
- * panning behavior.
- *
- * @param {Event} event the event object which led to the startPan call.
- * @param {Dygraph} g The dygraph on which to act.
- * @param {Object} context The dragging context object (with
- *     dragStartX/dragStartY/etc. properties). This function modifies the
- *     context.
- */
-Dygraph.Interaction.startPan = function(event, g, context) {
-  var i, axis;
-  context.isPanning = true;
-  var xRange = g.xAxisRange();
-
-  if (g.getOptionForAxis("logscale", "x")) {
-    context.initialLeftmostDate = Dygraph.log10(xRange[0]);
-    context.dateRange = Dygraph.log10(xRange[1]) - Dygraph.log10(xRange[0]);
-  } else {
-    context.initialLeftmostDate = xRange[0];    
-    context.dateRange = xRange[1] - xRange[0];
-  }
-  context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
-
-  if (g.getNumericOption("panEdgeFraction")) {
-    var maxXPixelsToDraw = g.width_ * g.getNumericOption("panEdgeFraction");
-    var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
-
-    var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
-    var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
-
-    var boundedLeftDate = g.toDataXCoord(boundedLeftX);
-    var boundedRightDate = g.toDataXCoord(boundedRightX);
-    context.boundedDates = [boundedLeftDate, boundedRightDate];
-
-    var boundedValues = [];
-    var maxYPixelsToDraw = g.height_ * g.getNumericOption("panEdgeFraction");
-
-    for (i = 0; i < g.axes_.length; i++) {
-      axis = g.axes_[i];
-      var yExtremes = axis.extremeRange;
-
-      var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
-      var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
-
-      var boundedTopValue = g.toDataYCoord(boundedTopY, i);
-      var boundedBottomValue = g.toDataYCoord(boundedBottomY, i);
-
-      boundedValues[i] = [boundedTopValue, boundedBottomValue];
-    }
-    context.boundedValues = boundedValues;
-  }
-
-  // Record the range of each y-axis at the start of the drag.
-  // If any axis has a valueRange or valueWindow, then we want a 2D pan.
-  // We can't store data directly in g.axes_, because it does not belong to us
-  // and could change out from under us during a pan (say if there's a data
-  // update).
-  context.is2DPan = false;
-  context.axes = [];
-  for (i = 0; i < g.axes_.length; i++) {
-    axis = g.axes_[i];
-    var axis_data = {};
-    var yRange = g.yAxisRange(i);
-    // TODO(konigsberg): These values should be in |context|.
-    // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
-    var logscale = g.attributes_.getForAxis("logscale", i);
-    if (logscale) {
-      axis_data.initialTopValue = Dygraph.log10(yRange[1]);
-      axis_data.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
-    } else {
-      axis_data.initialTopValue = yRange[1];
-      axis_data.dragValueRange = yRange[1] - yRange[0];
-    }
-    axis_data.unitsPerPixel = axis_data.dragValueRange / (g.plotter_.area.h - 1);
-    context.axes.push(axis_data);
-
-    // While calculating axes, set 2dpan.
-    if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
-  }
-};
-
-/**
- * Called in response to an interaction model operation that
- * responds to an event that pans the view.
- *
- * It's used in the default callback for "mousemove" operations.
- * Custom interaction model builders can use it to provide the default
- * panning behavior.
- *
- * @param {Event} event the event object which led to the movePan call.
- * @param {Dygraph} g The dygraph on which to act.
- * @param {Object} context The dragging context object (with
- *     dragStartX/dragStartY/etc. properties). This function modifies the
- *     context.
- */
-Dygraph.Interaction.movePan = function(event, g, context) {
-  context.dragEndX = Dygraph.dragGetX_(event, context);
-  context.dragEndY = Dygraph.dragGetY_(event, context);
-
-  var minDate = context.initialLeftmostDate -
-    (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
-  if (context.boundedDates) {
-    minDate = Math.max(minDate, context.boundedDates[0]);
-  }
-  var maxDate = minDate + context.dateRange;
-  if (context.boundedDates) {
-    if (maxDate > context.boundedDates[1]) {
-      // Adjust minDate, and recompute maxDate.
-      minDate = minDate - (maxDate - context.boundedDates[1]);
-      maxDate = minDate + context.dateRange;
-    }
-  }
-
-  if (g.getOptionForAxis("logscale", "x")) {
-    g.dateWindow_ = [ Math.pow(Dygraph.LOG_SCALE, minDate),
-                      Math.pow(Dygraph.LOG_SCALE, maxDate) ];
-  } else {
-    g.dateWindow_ = [minDate, maxDate];    
-  }
-
-  // y-axis scaling is automatic unless this is a full 2D pan.
-  if (context.is2DPan) {
-
-    var pixelsDragged = context.dragEndY - context.dragStartY;
-
-    // Adjust each axis appropriately.
-    for (var i = 0; i < g.axes_.length; i++) {
-      var axis = g.axes_[i];
-      var axis_data = context.axes[i];
-      var unitsDragged = pixelsDragged * axis_data.unitsPerPixel;
-
-      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
-
-      // In log scale, maxValue and minValue are the logs of those values.
-      var maxValue = axis_data.initialTopValue + unitsDragged;
-      if (boundedValue) {
-        maxValue = Math.min(maxValue, boundedValue[1]);
-      }
-      var minValue = maxValue - axis_data.dragValueRange;
-      if (boundedValue) {
-        if (minValue < boundedValue[0]) {
-          // Adjust maxValue, and recompute minValue.
-          maxValue = maxValue - (minValue - boundedValue[0]);
-          minValue = maxValue - axis_data.dragValueRange;
-        }
-      }
-      if (g.attributes_.getForAxis("logscale", i)) {
-        axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
-                             Math.pow(Dygraph.LOG_SCALE, maxValue) ];
-      } else {
-        axis.valueWindow = [ minValue, maxValue ];
-      }
-    }
-  }
-
-  g.drawGraph_(false);
-};
-
-/**
- * Called in response to an interaction model operation that
- * responds to an event that ends panning.
- *
- * It's used in the default callback for "mouseup" operations.
- * Custom interaction model builders can use it to provide the default
- * panning behavior.
- *
- * @param {Event} event the event object which led to the endPan call.
- * @param {Dygraph} g The dygraph on which to act.
- * @param {Object} context The dragging context object (with
- *     dragStartX/dragStartY/etc. properties). This function modifies the
- *     context.
- */
-Dygraph.Interaction.endPan = Dygraph.Interaction.maybeTreatMouseOpAsClick;
-
-/**
- * Called in response to an interaction model operation that
- * responds to an event that starts zooming.
- *
- * It's used in the default callback for "mousedown" operations.
- * Custom interaction model builders can use it to provide the default
- * zooming behavior.
- *
- * @param {Event} event the event object which led to the startZoom call.
- * @param {Dygraph} g The dygraph on which to act.
- * @param {Object} context The dragging context object (with
- *     dragStartX/dragStartY/etc. properties). This function modifies the
- *     context.
- */
-Dygraph.Interaction.startZoom = function(event, g, context) {
-  context.isZooming = true;
-  context.zoomMoved = false;
-};
-
-/**
- * Called in response to an interaction model operation that
- * responds to an event that defines zoom boundaries.
- *
- * It's used in the default callback for "mousemove" operations.
- * Custom interaction model builders can use it to provide the default
- * zooming behavior.
- *
- * @param {Event} event the event object which led to the moveZoom call.
- * @param {Dygraph} g The dygraph on which to act.
- * @param {Object} context The dragging context object (with
- *     dragStartX/dragStartY/etc. properties). This function modifies the
- *     context.
- */
-Dygraph.Interaction.moveZoom = function(event, g, context) {
-  context.zoomMoved = true;
-  context.dragEndX = Dygraph.dragGetX_(event, context);
-  context.dragEndY = Dygraph.dragGetY_(event, context);
-
-  var xDelta = Math.abs(context.dragStartX - context.dragEndX);
-  var yDelta = Math.abs(context.dragStartY - context.dragEndY);
-
-  // drag direction threshold for y axis is twice as large as x axis
-  context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
-
-  g.drawZoomRect_(
-      context.dragDirection,
-      context.dragStartX,
-      context.dragEndX,
-      context.dragStartY,
-      context.dragEndY,
-      context.prevDragDirection,
-      context.prevEndX,
-      context.prevEndY);
-
-  context.prevEndX = context.dragEndX;
-  context.prevEndY = context.dragEndY;
-  context.prevDragDirection = context.dragDirection;
-};
-
-/**
- * TODO(danvk): move this logic into dygraph.js
- * @param {Dygraph} g
- * @param {Event} event
- * @param {Object} context
- */
-Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
-  var clickCallback = g.getFunctionOption('clickCallback');
-  var pointClickCallback = g.getFunctionOption('pointClickCallback');
-
-  var selectedPoint = null;
-
-  // Find out if the click occurs on a point.
-  var closestIdx = -1;
-  var closestDistance = Number.MAX_VALUE;
-
-  // check if the click was on a particular point.
-  for (var i = 0; i < g.selPoints_.length; i++) {
-    var p = g.selPoints_[i];
-    var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
-                   Math.pow(p.canvasy - context.dragEndY, 2);
-    if (!isNaN(distance) &&
-        (closestIdx == -1 || distance < closestDistance)) {
-      closestDistance = distance;
-      closestIdx = i;
-    }
-  }
-
-  // Allow any click within two pixels of the dot.
-  var radius = g.getNumericOption('highlightCircleSize') + 2;
-  if (closestDistance <= radius * radius) {
-    selectedPoint = g.selPoints_[closestIdx];
-  }
-
-  if (selectedPoint) {
-    var e = {
-      cancelable: true,
-      point: selectedPoint,
-      canvasx: context.dragEndX,
-      canvasy: context.dragEndY
-    };
-    var defaultPrevented = g.cascadeEvents_('pointClick', e);
-    if (defaultPrevented) {
-      // Note: this also prevents click / clickCallback from firing.
-      return;
-    }
-    if (pointClickCallback) {
-      pointClickCallback.call(g, event, selectedPoint);
-    }
-  }
-
-  var e = {
-    cancelable: true,
-    xval: g.lastx_,  // closest point by x value
-    pts: g.selPoints_,
-    canvasx: context.dragEndX,
-    canvasy: context.dragEndY
-  };
-  if (!g.cascadeEvents_('click', e)) {
-    if (clickCallback) {
-      // TODO(danvk): pass along more info about the points, e.g. 'x'
-      clickCallback.call(g, event, g.lastx_, g.selPoints_);
-    }
-  }
-};
-
-/**
- * Called in response to an interaction model operation that
- * responds to an event that performs a zoom based on previously defined
- * bounds..
- *
- * It's used in the default callback for "mouseup" operations.
- * Custom interaction model builders can use it to provide the default
- * zooming behavior.
- *
- * @param {Event} event the event object which led to the endZoom call.
- * @param {Dygraph} g The dygraph on which to end the zoom.
- * @param {Object} context The dragging context object (with
- *     dragStartX/dragStartY/etc. properties). This function modifies the
- *     context.
- */
-Dygraph.Interaction.endZoom = function(event, g, context) {
-  g.clearZoomRect_();
-  context.isZooming = false;
-  Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
-
-  // The zoom rectangle is visibly clipped to the plot area, so its behavior
-  // should be as well.
-  // See http://code.google.com/p/dygraphs/issues/detail?id=280
-  var plotArea = g.getArea();
-  if (context.regionWidth >= 10 &&
-      context.dragDirection == Dygraph.HORIZONTAL) {
-    var left = Math.min(context.dragStartX, context.dragEndX),
-        right = Math.max(context.dragStartX, context.dragEndX);
-    left = Math.max(left, plotArea.x);
-    right = Math.min(right, plotArea.x + plotArea.w);
-    if (left < right) {
-      g.doZoomX_(left, right);
-    }
-    context.cancelNextDblclick = true;
-  } else if (context.regionHeight >= 10 &&
-             context.dragDirection == Dygraph.VERTICAL) {
-    var top = Math.min(context.dragStartY, context.dragEndY),
-        bottom = Math.max(context.dragStartY, context.dragEndY);
-    top = Math.max(top, plotArea.y);
-    bottom = Math.min(bottom, plotArea.y + plotArea.h);
-    if (top < bottom) {
-      g.doZoomY_(top, bottom);
-    }
-    context.cancelNextDblclick = true;
-  }
-  context.dragStartX = null;
-  context.dragStartY = null;
-};
-
-/**
- * @private
- */
-Dygraph.Interaction.startTouch = function(event, g, context) {
-  event.preventDefault();  // touch browsers are all nice.
-  if (event.touches.length > 1) {
-    // If the user ever puts two fingers down, it's not a double tap.
-    context.startTimeForDoubleTapMs = null;
-  }
-
-  var touches = [];
-  for (var i = 0; i < event.touches.length; i++) {
-    var t = event.touches[i];
-    // we dispense with 'dragGetX_' because all touchBrowsers support pageX
-    touches.push({
-      pageX: t.pageX,
-      pageY: t.pageY,
-      dataX: g.toDataXCoord(t.pageX),
-      dataY: g.toDataYCoord(t.pageY)
-      // identifier: t.identifier
-    });
-  }
-  context.initialTouches = touches;
-
-  if (touches.length == 1) {
-    // This is just a swipe.
-    context.initialPinchCenter = touches[0];
-    context.touchDirections = { x: true, y: true };
-  } else if (touches.length >= 2) {
-    // It's become a pinch!
-    // In case there are 3+ touches, we ignore all but the "first" two.
-
-    // only screen coordinates can be averaged (data coords could be log scale).
-    context.initialPinchCenter = {
-      pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
-      pageY: 0.5 * (touches[0].pageY + touches[1].pageY),
-
-      // TODO(danvk): remove
-      dataX: 0.5 * (touches[0].dataX + touches[1].dataX),
-      dataY: 0.5 * (touches[0].dataY + touches[1].dataY)
-    };
-
-    // Make pinches in a 45-degree swath around either axis 1-dimensional zooms.
-    var initialAngle = 180 / Math.PI * Math.atan2(
-        context.initialPinchCenter.pageY - touches[0].pageY,
-        touches[0].pageX - context.initialPinchCenter.pageX);
-
-    // use symmetry to get it into the first quadrant.
-    initialAngle = Math.abs(initialAngle);
-    if (initialAngle > 90) initialAngle = 90 - initialAngle;
-
-    context.touchDirections = {
-      x: (initialAngle < (90 - 45/2)),
-      y: (initialAngle > 45/2)
-    };
-  }
-
-  // save the full x & y ranges.
-  context.initialRange = {
-    x: g.xAxisRange(),
-    y: g.yAxisRange()
-  };
-};
-
-/**
- * @private
- */
-Dygraph.Interaction.moveTouch = function(event, g, context) {
-  // If the tap moves, then it's definitely not part of a double-tap.
-  context.startTimeForDoubleTapMs = null;
-
-  var i, touches = [];
-  for (i = 0; i < event.touches.length; i++) {
-    var t = event.touches[i];
-    touches.push({
-      pageX: t.pageX,
-      pageY: t.pageY
-    });
-  }
-  var initialTouches = context.initialTouches;
-
-  var c_now;
-
-  // old and new centers.
-  var c_init = context.initialPinchCenter;
-  if (touches.length == 1) {
-    c_now = touches[0];
-  } else {
-    c_now = {
-      pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
-      pageY: 0.5 * (touches[0].pageY + touches[1].pageY)
-    };
-  }
-
-  // this is the "swipe" component
-  // we toss it out for now, but could use it in the future.
-  var swipe = {
-    pageX: c_now.pageX - c_init.pageX,
-    pageY: c_now.pageY - c_init.pageY
-  };
-  var dataWidth = context.initialRange.x[1] - context.initialRange.x[0];
-  var dataHeight = context.initialRange.y[0] - context.initialRange.y[1];
-  swipe.dataX = (swipe.pageX / g.plotter_.area.w) * dataWidth;
-  swipe.dataY = (swipe.pageY / g.plotter_.area.h) * dataHeight;
-  var xScale, yScale;
-
-  // The residual bits are usually split into scale & rotate bits, but we split
-  // them into x-scale and y-scale bits.
-  if (touches.length == 1) {
-    xScale = 1.0;
-    yScale = 1.0;
-  } else if (touches.length >= 2) {
-    var initHalfWidth = (initialTouches[1].pageX - c_init.pageX);
-    xScale = (touches[1].pageX - c_now.pageX) / initHalfWidth;
-
-    var initHalfHeight = (initialTouches[1].pageY - c_init.pageY);
-    yScale = (touches[1].pageY - c_now.pageY) / initHalfHeight;
-  }
-
-  // Clip scaling to [1/8, 8] to prevent too much blowup.
-  xScale = Math.min(8, Math.max(0.125, xScale));
-  yScale = Math.min(8, Math.max(0.125, yScale));
-
-  var didZoom = false;
-  if (context.touchDirections.x) {
-    g.dateWindow_ = [
-      c_init.dataX - swipe.dataX + (context.initialRange.x[0] - c_init.dataX) / xScale,
-      c_init.dataX - swipe.dataX + (context.initialRange.x[1] - c_init.dataX) / xScale
-    ];
-    didZoom = true;
-  }
-  
-  if (context.touchDirections.y) {
-    for (i = 0; i < 1  /*g.axes_.length*/; i++) {
-      var axis = g.axes_[i];
-      var logscale = g.attributes_.getForAxis("logscale", i);
-      if (logscale) {
-        // TODO(danvk): implement
-      } else {
-        axis.valueWindow = [
-          c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale,
-          c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale
-        ];
-        didZoom = true;
-      }
-    }
-  }
-
-  g.drawGraph_(false);
-
-  // We only call zoomCallback on zooms, not pans, to mirror desktop behavior.
-  if (didZoom && touches.length > 1 && g.getFunctionOption('zoomCallback')) {
-    var viewWindow = g.xAxisRange();
-    g.getFunctionOption("zoomCallback").call(g, viewWindow[0], viewWindow[1], g.yAxisRanges());
-  }
-};
-
-/**
- * @private
- */
-Dygraph.Interaction.endTouch = function(event, g, context) {
-  if (event.touches.length !== 0) {
-    // this is effectively a "reset"
-    Dygraph.Interaction.startTouch(event, g, context);
-  } else if (event.changedTouches.length == 1) {
-    // Could be part of a "double tap"
-    // The heuristic here is that it's a double-tap if the two touchend events
-    // occur within 500ms and within a 50x50 pixel box.
-    var now = new Date().getTime();
-    var t = event.changedTouches[0];
-    if (context.startTimeForDoubleTapMs &&
-        now - context.startTimeForDoubleTapMs < 500 &&
-        context.doubleTapX && Math.abs(context.doubleTapX - t.screenX) < 50 &&
-        context.doubleTapY && Math.abs(context.doubleTapY - t.screenY) < 50) {
-      g.resetZoom();
-    } else {
-      context.startTimeForDoubleTapMs = now;
-      context.doubleTapX = t.screenX;
-      context.doubleTapY = t.screenY;
-    }
-  }
-};
-
-// Determine the distance from x to [left, right].
-var distanceFromInterval = function(x, left, right) {
-  if (x < left) {
-    return left - x;
-  } else if (x > right) {
-    return x - right;
-  } else {
-    return 0;
-  }
-};
-
-/**
- * Returns the number of pixels by which the event happens from the nearest
- * edge of the chart. For events in the interior of the chart, this returns zero.
- */
-var distanceFromChart = function(event, g) {
-  var chartPos = Dygraph.findPos(g.canvas_);
-  var box = {
-    left: chartPos.x,
-    right: chartPos.x + g.canvas_.offsetWidth,
-    top: chartPos.y,
-    bottom: chartPos.y + g.canvas_.offsetHeight
-  };
-
-  var pt = {
-    x: Dygraph.pageX(event),
-    y: Dygraph.pageY(event)
-  };
-
-  var dx = distanceFromInterval(pt.x, box.left, box.right),
-      dy = distanceFromInterval(pt.y, box.top, box.bottom);
-  return Math.max(dx, dy);
-};
-
-/**
- * Default interation model for dygraphs. You can refer to specific elements of
- * this when constructing your own interaction model, e.g.:
- * g.updateOptions( {
- *   interactionModel: {
- *     mousedown: Dygraph.defaultInteractionModel.mousedown
- *   }
- * } );
- */
-Dygraph.Interaction.defaultModel = {
-  // Track the beginning of drag events
-  mousedown: function(event, g, context) {
-    // Right-click should not initiate a zoom.
-    if (event.button && event.button == 2) return;
-
-    context.initializeMouseDown(event, g, context);
-
-    if (event.altKey || event.shiftKey) {
-      Dygraph.startPan(event, g, context);
-    } else {
-      Dygraph.startZoom(event, g, context);
-    }
-
-    // Note: we register mousemove/mouseup on document to allow some leeway for
-    // events to move outside of the chart. Interaction model events get
-    // registered on the canvas, which is too small to allow this.
-    var mousemove = function(event) {
-      if (context.isZooming) {
-        // When the mouse moves >200px from the chart edge, cancel the zoom.
-        var d = distanceFromChart(event, g);
-        if (d < DRAG_EDGE_MARGIN) {
-          Dygraph.moveZoom(event, g, context);
-        } else {
-          if (context.dragEndX !== null) {
-            context.dragEndX = null;
-            context.dragEndY = null;
-            g.clearZoomRect_();
-          }
-        }
-      } else if (context.isPanning) {
-        Dygraph.movePan(event, g, context);
-      }
-    };
-    var mouseup = function(event) {
-      if (context.isZooming) {
-        if (context.dragEndX !== null) {
-          Dygraph.endZoom(event, g, context);
-        } else {
-          Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
-        }
-      } else if (context.isPanning) {
-        Dygraph.endPan(event, g, context);
-      }
-
-      Dygraph.removeEvent(document, 'mousemove', mousemove);
-      Dygraph.removeEvent(document, 'mouseup', mouseup);
-      context.destroy();
-    };
-
-    g.addAndTrackEvent(document, 'mousemove', mousemove);
-    g.addAndTrackEvent(document, 'mouseup', mouseup);
-  },
-  willDestroyContextMyself: true,
-
-  touchstart: function(event, g, context) {
-    Dygraph.Interaction.startTouch(event, g, context);
-  },
-  touchmove: function(event, g, context) {
-    Dygraph.Interaction.moveTouch(event, g, context);
-  },
-  touchend: function(event, g, context) {
-    Dygraph.Interaction.endTouch(event, g, context);
-  },
-
-  // Disable zooming out if panning.
-  dblclick: function(event, g, context) {
-    if (context.cancelNextDblclick) {
-      context.cancelNextDblclick = false;
-      return;
-    }
-
-    // Give plugins a chance to grab this event.
-    var e = {
-      canvasx: context.dragEndX,
-      canvasy: context.dragEndY
-    };
-    if (g.cascadeEvents_('dblclick', e)) {
-      return;
-    }
-
-    if (event.altKey || event.shiftKey) {
-      return;
-    }
-    g.resetZoom();
-  }
-};
-
-Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
-
-// old ways of accessing these methods/properties
-Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
-Dygraph.endZoom = Dygraph.Interaction.endZoom;
-Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
-Dygraph.startZoom = Dygraph.Interaction.startZoom;
-Dygraph.endPan = Dygraph.Interaction.endPan;
-Dygraph.movePan = Dygraph.Interaction.movePan;
-Dygraph.startPan = Dygraph.Interaction.startPan;
-
-Dygraph.Interaction.nonInteractiveModel_ = {
-  mousedown: function(event, g, context) {
-    context.initializeMouseDown(event, g, context);
-  },
-  mouseup: Dygraph.Interaction.maybeTreatMouseOpAsClick
-};
-
-// Default interaction model when using the range selector.
-Dygraph.Interaction.dragIsPanInteractionModel = {
-  mousedown: function(event, g, context) {
-    context.initializeMouseDown(event, g, context);
-    Dygraph.startPan(event, g, context);
-  },
-  mousemove: function(event, g, context) {
-    if (context.isPanning) {
-      Dygraph.movePan(event, g, context);
-    }
-  },
-  mouseup: function(event, g, context) {
-    if (context.isPanning) {
-      Dygraph.endPan(event, g, context);
-    }
-  }
-};
-
-})();
diff --git a/dygraph-internal.externs.js b/dygraph-internal.externs.js
deleted file mode 100644 (file)
index 2108fce..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// This file:
-// - declares symbols that are provided outisde of dygraphs
-// - defines custom types used internally
-
-
-/**
- * @typedef {function(
- *   (number|Date),
- *   number,
- *   function(string):*,
- *   (Dygraph|undefined)
- * ):string}
- */
-var AxisLabelFormatter;
-
-
-/**
- * @typedef {function(number,function(string),Dygraph):string}
- */
-var ValueFormatter;
-
-
-/**
- * @typedef {Array.<Array.<string|number|Array.<number>>>}
- */
-var DygraphDataArray;
-
-/**
- * @constructor
- */
-function GVizDataTable() {}
diff --git a/dygraph-layout.js b/dygraph-layout.js
deleted file mode 100644 (file)
index ef1df91..0000000
+++ /dev/null
@@ -1,351 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview Based on PlotKitLayout, but modified to meet the needs of
- * dygraphs.
- */
-
-var DygraphLayout = (function() {
-
-/*global Dygraph:false */
-"use strict";
-
-/**
- * Creates a new DygraphLayout object.
- *
- * This class contains all the data to be charted.
- * It uses data coordinates, but also records the chart range (in data
- * coordinates) and hence is able to calculate percentage positions ('In this
- * view, Point A lies 25% down the x-axis.')
- *
- * Two things that it does not do are:
- * 1. Record pixel coordinates for anything.
- * 2. (oddly) determine anything about the layout of chart elements.
- *
- * The naming is a vestige of Dygraph's original PlotKit roots.
- *
- * @constructor
- */
-var DygraphLayout = function(dygraph) {
-  this.dygraph_ = dygraph;
-  /**
-   * Array of points for each series.
-   *
-   * [series index][row index in series] = |Point| structure,
-   * where series index refers to visible series only, and the
-   * point index is for the reduced set of points for the current
-   * zoom region (including one point just outside the window).
-   * All points in the same row index share the same X value.
-   *
-   * @type {Array.<Array.<Dygraph.PointType>>}
-   */
-  this.points = [];
-  this.setNames = [];
-  this.annotations = [];
-  this.yAxes_ = null;
-
-  // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and
-  // yticks are outputs. Clean this up.
-  this.xTicks_ = null;
-  this.yTicks_ = null;
-};
-
-/**
- * Add points for a single series.
- *
- * @param {string} setname Name of the series.
- * @param {Array.<Dygraph.PointType>} set_xy Points for the series.
- */
-DygraphLayout.prototype.addDataset = function(setname, set_xy) {
-  this.points.push(set_xy);
-  this.setNames.push(setname);
-};
-
-/**
- * Returns the box which the chart should be drawn in. This is the canvas's
- * box, less space needed for the axis and chart labels.
- *
- * @return {{x: number, y: number, w: number, h: number}}
- */
-DygraphLayout.prototype.getPlotArea = function() {
-  return this.area_;
-};
-
-// Compute the box which the chart should be drawn in. This is the canvas's
-// box, less space needed for axis, chart labels, and other plug-ins.
-// NOTE: This should only be called by Dygraph.predraw_().
-DygraphLayout.prototype.computePlotArea = function() {
-  var area = {
-    // TODO(danvk): per-axis setting.
-    x: 0,
-    y: 0
-  };
-
-  area.w = this.dygraph_.width_ - area.x - this.dygraph_.getOption('rightGap');
-  area.h = this.dygraph_.height_;
-
-  // Let plugins reserve space.
-  var e = {
-    chart_div: this.dygraph_.graphDiv,
-    reserveSpaceLeft: function(px) {
-      var r = {
-        x: area.x,
-        y: area.y,
-        w: px,
-        h: area.h
-      };
-      area.x += px;
-      area.w -= px;
-      return r;
-    },
-    reserveSpaceRight: function(px) {
-      var r = {
-        x: area.x + area.w - px,
-        y: area.y,
-        w: px,
-        h: area.h
-      };
-      area.w -= px;
-      return r;
-    },
-    reserveSpaceTop: function(px) {
-      var r = {
-        x: area.x,
-        y: area.y,
-        w: area.w,
-        h: px
-      };
-      area.y += px;
-      area.h -= px;
-      return r;
-    },
-    reserveSpaceBottom: function(px) {
-      var r = {
-        x: area.x,
-        y: area.y + area.h - px,
-        w: area.w,
-        h: px
-      };
-      area.h -= px;
-      return r;
-    },
-    chartRect: function() {
-      return {x:area.x, y:area.y, w:area.w, h:area.h};
-    }
-  };
-  this.dygraph_.cascadeEvents_('layout', e);
-
-  this.area_ = area;
-};
-
-DygraphLayout.prototype.setAnnotations = function(ann) {
-  // The Dygraph object's annotations aren't parsed. We parse them here and
-  // save a copy. If there is no parser, then the user must be using raw format.
-  this.annotations = [];
-  var parse = this.dygraph_.getOption('xValueParser') || function(x) { return x; };
-  for (var i = 0; i < ann.length; i++) {
-    var a = {};
-    if (!ann[i].xval && ann[i].x === undefined) {
-      console.error("Annotations must have an 'x' property");
-      return;
-    }
-    if (ann[i].icon &&
-        !(ann[i].hasOwnProperty('width') &&
-          ann[i].hasOwnProperty('height'))) {
-      console.error("Must set width and height when setting " +
-                    "annotation.icon property");
-      return;
-    }
-    Dygraph.update(a, ann[i]);
-    if (!a.xval) a.xval = parse(a.x);
-    this.annotations.push(a);
-  }
-};
-
-DygraphLayout.prototype.setXTicks = function(xTicks) {
-  this.xTicks_ = xTicks;
-};
-
-// TODO(danvk): add this to the Dygraph object's API or move it into Layout.
-DygraphLayout.prototype.setYAxes = function (yAxes) {
-  this.yAxes_ = yAxes;
-};
-
-DygraphLayout.prototype.evaluate = function() {
-  this._xAxis = {};
-  this._evaluateLimits();
-  this._evaluateLineCharts();
-  this._evaluateLineTicks();
-  this._evaluateAnnotations();
-};
-
-DygraphLayout.prototype._evaluateLimits = function() {
-  var xlimits = this.dygraph_.xAxisRange();
-  this._xAxis.minval = xlimits[0];
-  this._xAxis.maxval = xlimits[1];
-  var xrange = xlimits[1] - xlimits[0];
-  this._xAxis.scale = (xrange !== 0 ? 1 / xrange : 1.0);
-
-  if (this.dygraph_.getOptionForAxis("logscale", 'x')) {
-    this._xAxis.xlogrange = Dygraph.log10(this._xAxis.maxval) - Dygraph.log10(this._xAxis.minval);
-    this._xAxis.xlogscale = (this._xAxis.xlogrange !== 0 ? 1.0 / this._xAxis.xlogrange : 1.0);
-  }
-  for (var i = 0; i < this.yAxes_.length; i++) {
-    var axis = this.yAxes_[i];
-    axis.minyval = axis.computedValueRange[0];
-    axis.maxyval = axis.computedValueRange[1];
-    axis.yrange = axis.maxyval - axis.minyval;
-    axis.yscale = (axis.yrange !== 0 ? 1.0 / axis.yrange : 1.0);
-
-    if (this.dygraph_.getOption("logscale")) {
-      axis.ylogrange = Dygraph.log10(axis.maxyval) - Dygraph.log10(axis.minyval);
-      axis.ylogscale = (axis.ylogrange !== 0 ? 1.0 / axis.ylogrange : 1.0);
-      if (!isFinite(axis.ylogrange) || isNaN(axis.ylogrange)) {
-        console.error('axis ' + i + ' of graph at ' + axis.g +
-                      ' can\'t be displayed in log scale for range [' +
-                      axis.minyval + ' - ' + axis.maxyval + ']');
-      }
-    }
-  }
-};
-
-DygraphLayout.calcXNormal_ = function(value, xAxis, logscale) {
-  if (logscale) {
-    return ((Dygraph.log10(value) - Dygraph.log10(xAxis.minval)) * xAxis.xlogscale);
-  } else {
-    return (value - xAxis.minval) * xAxis.scale;
-  }
-};
-
-/**
- * @param {DygraphAxisType} axis
- * @param {number} value
- * @param {boolean} logscale
- * @return {number}
- */
-DygraphLayout.calcYNormal_ = function(axis, value, logscale) {
-  if (logscale) {
-    var x = 1.0 - ((Dygraph.log10(value) - Dygraph.log10(axis.minyval)) * axis.ylogscale);
-    return isFinite(x) ? x : NaN;  // shim for v8 issue; see pull request 276
-  } else {
-    return 1.0 - ((value - axis.minyval) * axis.yscale);
-  }
-};
-
-DygraphLayout.prototype._evaluateLineCharts = function() {
-  var isStacked = this.dygraph_.getOption("stackedGraph");
-  var isLogscaleForX = this.dygraph_.getOptionForAxis("logscale", 'x');
-
-  for (var setIdx = 0; setIdx < this.points.length; setIdx++) {
-    var points = this.points[setIdx];
-    var setName = this.setNames[setIdx];
-    var connectSeparated = this.dygraph_.getOption('connectSeparatedPoints', setName);
-    var axis = this.dygraph_.axisPropertiesForSeries(setName);
-    // TODO (konigsberg): use optionsForAxis instead.
-    var logscale = this.dygraph_.attributes_.getForSeries("logscale", setName);
-
-    for (var j = 0; j < points.length; j++) {
-      var point = points[j];
-
-      // Range from 0-1 where 0 represents left and 1 represents right.
-      point.x = DygraphLayout.calcXNormal_(point.xval, this._xAxis, isLogscaleForX);
-      // Range from 0-1 where 0 represents top and 1 represents bottom
-      var yval = point.yval;
-      if (isStacked) {
-        point.y_stacked = DygraphLayout.calcYNormal_(
-            axis, point.yval_stacked, logscale);
-        if (yval !== null && !isNaN(yval)) {
-          yval = point.yval_stacked;
-        }
-      }
-      if (yval === null) {
-        yval = NaN;
-        if (!connectSeparated) {
-          point.yval = NaN;
-        }
-      }
-      point.y = DygraphLayout.calcYNormal_(axis, yval, logscale);
-    }
-
-    this.dygraph_.dataHandler_.onLineEvaluated(points, axis, logscale);
-  }
-};
-
-DygraphLayout.prototype._evaluateLineTicks = function() {
-  var i, tick, label, pos;
-  this.xticks = [];
-  for (i = 0; i < this.xTicks_.length; i++) {
-    tick = this.xTicks_[i];
-    label = tick.label;
-    pos = this.dygraph_.toPercentXCoord(tick.v);
-    if ((pos >= 0.0) && (pos < 1.0)) {
-      this.xticks.push([pos, label]);
-    }
-  }
-
-  this.yticks = [];
-  for (i = 0; i < this.yAxes_.length; i++ ) {
-    var axis = this.yAxes_[i];
-    for (var j = 0; j < axis.ticks.length; j++) {
-      tick = axis.ticks[j];
-      label = tick.label;
-      pos = this.dygraph_.toPercentYCoord(tick.v, i);
-      if ((pos > 0.0) && (pos <= 1.0)) {
-        this.yticks.push([i, pos, label]);
-      }
-    }
-  }
-};
-
-DygraphLayout.prototype._evaluateAnnotations = function() {
-  // Add the annotations to the point to which they belong.
-  // Make a map from (setName, xval) to annotation for quick lookups.
-  var i;
-  var annotations = {};
-  for (i = 0; i < this.annotations.length; i++) {
-    var a = this.annotations[i];
-    annotations[a.xval + "," + a.series] = a;
-  }
-
-  this.annotated_points = [];
-
-  // Exit the function early if there are no annotations.
-  if (!this.annotations || !this.annotations.length) {
-    return;
-  }
-
-  // TODO(antrob): loop through annotations not points.
-  for (var setIdx = 0; setIdx < this.points.length; setIdx++) {
-    var points = this.points[setIdx];
-    for (i = 0; i < points.length; i++) {
-      var p = points[i];
-      var k = p.xval + "," + p.name;
-      if (k in annotations) {
-        p.annotation = annotations[k];
-        this.annotated_points.push(p);
-      }
-    }
-  }
-};
-
-/**
- * Convenience function to remove all the data sets from a graph
- */
-DygraphLayout.prototype.removeAllDatasets = function() {
-  delete this.points;
-  delete this.setNames;
-  delete this.setPointsLengths;
-  delete this.setPointsOffsets;
-  this.points = [];
-  this.setNames = [];
-  this.setPointsLengths = [];
-  this.setPointsOffsets = [];
-};
-
-return DygraphLayout;
-
-})();
diff --git a/dygraph-options-reference.js b/dygraph-options-reference.js
deleted file mode 100644 (file)
index ca94dc5..0000000
+++ /dev/null
@@ -1,887 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-// NOTE: in addition to parsing as JS, this snippet is expected to be valid
-// JSON. This assumption cannot be checked in JS, but it will be checked when
-// documentation is generated by the generate-documentation.py script. For the
-// most part, this just means that you should always use double quotes.
-Dygraph.OPTIONS_REFERENCE =  // <JSON>
-{
-  "xValueParser": {
-    "default": "parseFloat() or Date.parse()*",
-    "labels": ["CSV parsing"],
-    "type": "function(str) -> number",
-    "description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
-  },
-  "stackedGraph": {
-    "default": "false",
-    "labels": ["Data Line display"],
-    "type": "boolean",
-    "description": "If set, stack series on top of one another rather than drawing them independently. The first series specified in the input data will wind up on top of the chart and the last will be on bottom. NaN values are drawn as white areas without a line on top, see stackedGraphNaNFill for details."
-  },
-  "stackedGraphNaNFill": {
-    "default": "all",
-    "labels": ["Data Line display"],
-    "type": "string",
-    "description": "Controls handling of NaN values inside a stacked graph. NaN values are interpolated/extended for stacking purposes, but the actual point value remains NaN in the legend display. Valid option values are \"all\" (interpolate internally, repeat leftmost and rightmost value as needed), \"inside\" (interpolate internally only, use zero outside leftmost and rightmost value), and \"none\" (treat NaN as zero everywhere)."
-  },
-  "pointSize": {
-    "default": "1",
-    "labels": ["Data Line display"],
-    "type": "integer",
-    "description": "The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is \"isolated\", i.e. there is a missing point on either side of it. This also controls the size of those dots."
-  },
-  "labelsDivStyles": {
-    "default": "null",
-    "labels": ["Legend"],
-    "type": "{}",
-    "description": "Additional styles to apply to the currently-highlighted points div. For example, { 'fontWeight': 'bold' } will make the labels bold. In general, it is better to use CSS to style the .dygraph-legend class than to use this property."
-  },
-  "drawPoints": {
-    "default": "false",
-    "labels": ["Data Line display"],
-    "type": "boolean",
-    "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart. The small dot can be replaced with a custom rendering by supplying a <a href='#drawPointCallback'>drawPointCallback</a>."
-  },
-  "drawGapEdgePoints": {
-    "default": "false",
-    "labels": ["Data Line display"],
-    "type": "boolean",
-    "description": "Draw points at the edges of gaps in the data. This improves visibility of small data segments or other data irregularities."
-  },
-  "drawPointCallback": {
-    "default": "null",
-    "labels": ["Data Line display"],
-    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
-    "parameters": [
-      [ "g" , "the reference graph" ],
-      [ "seriesName" , "the name of the series" ],
-      [ "canvasContext" , "the canvas to draw on" ],
-      [ "cx" , "center x coordinate" ],
-      [ "cy" , "center y coordinate" ],
-      [ "color" , "series color" ],
-      [ "pointSize" , "the radius of the image." ],
-      [ "idx" , "the row-index of the point in the data."]
-    ],
-    "description": "Draw a custom item when drawPoints is enabled. Default is a small dot matching the series color. This method should constrain drawing to within pointSize pixels from (cx, cy).  Also see <a href='#drawHighlightPointCallback'>drawHighlightPointCallback</a>"
-  },
-  "height": {
-    "default": "320",
-    "labels": ["Overall display"],
-    "type": "integer",
-    "description": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
-  },
-  "zoomCallback": {
-    "default": "null",
-    "labels": ["Callbacks"],
-    "type": "function(minDate, maxDate, yRanges)",
-    "parameters": [
-      [ "minDate" , "milliseconds since epoch" ],
-      [ "maxDate" , "milliseconds since epoch." ],
-      [ "yRanges" , "is an array of [bottom, top] pairs, one for each y-axis." ]
-    ],
-    "description": "A function to call when the zoom window is changed (either by zooming in or out)."
-  },
-  "pointClickCallback": {
-    "snippet": "function(e, point){<br>&nbsp;&nbsp;alert(point);<br>}",
-    "default": "null",
-    "labels": ["Callbacks", "Interactive Elements"],
-    "type": "function(e, point)",
-    "parameters": [
-      [ "e" , "the event object for the click" ],
-      [ "point" , "the point that was clicked See <a href='#point_properties'>Point properties</a> for details" ]
-    ],
-    "description": "A function to call when a data point is clicked. and the point that was clicked."
-  },
-  "color": {
-    "default": "(see description)",
-    "labels": ["Data Series Colors"],
-    "type": "string",
-    "example": "red",
-    "description": "A per-series color definition. Used in conjunction with, and overrides, the colors option."
-  },
-  "colors": {
-    "default": "(see description)",
-    "labels": ["Data Series Colors"],
-    "type": "array<string>",
-    "example": "['red', '#00FF00']",
-    "description": "List of colors for the data series. These can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\", etc. If not specified, equally-spaced points around a color wheel are used. Overridden by the 'color' option."
-  },
-  "connectSeparatedPoints": {
-    "default": "false",
-    "labels": ["Data Line display"],
-    "type": "boolean",
-    "description": "Usually, when Dygraphs encounters a missing value in a data series, it interprets this as a gap and draws it as such. If, instead, the missing values represents an x-value for which only a different series has data, then you'll want to connect the dots by setting this to true. To explicitly include a gap with this option set, use a value of NaN."
-  },
-  "highlightCallback": {
-    "default": "null",
-    "labels": ["Callbacks"],
-    "type": "function(event, x, points, row, seriesName)",
-    "description": "When set, this callback gets called every time a new point is highlighted.",
-    "parameters": [
-      ["event", "the JavaScript mousemove event"],
-      ["x", "the x-coordinate of the highlighted points"],
-      ["points", "an array of highlighted points: <code>[ {name: 'series', yval: y-value}, &hellip; ]</code>"],
-      ["row", "integer index of the highlighted row in the data table, starting from 0"],
-      ["seriesName", "name of the highlighted series, only present if highlightSeriesOpts is set."]
-    ]
-  },
-  "drawHighlightPointCallback": {
-    "default": "null",
-    "labels": ["Data Line display"],
-    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
-    "parameters": [
-      [ "g" , "the reference graph" ],
-      [ "seriesName" , "the name of the series" ],
-      [ "canvasContext" , "the canvas to draw on" ],
-      [ "cx" , "center x coordinate" ],
-      [ "cy" , "center y coordinate" ],
-      [ "color" , "series color" ],
-      [ "pointSize" , "the radius of the image." ],
-      [ "idx" , "the row-index of the point in the data."]
-    ],
-    "description": "Draw a custom item when a point is highlighted.  Default is a small dot matching the series color. This method should constrain drawing to within pointSize pixels from (cx, cy) Also see <a href='#drawPointCallback'>drawPointCallback</a>"
-  },
-  "highlightSeriesOpts": {
-    "default": "null",
-    "labels": ["Interactive Elements"],
-    "type": "Object",
-    "description": "When set, the options from this object are applied to the timeseries closest to the mouse pointer for interactive highlighting. See also 'highlightCallback'. Example: highlightSeriesOpts: { strokeWidth: 3 }."
-  },
-  "highlightSeriesBackgroundAlpha": {
-    "default": "0.5",
-    "labels": ["Interactive Elements"],
-    "type": "float",
-    "description": "Fade the background while highlighting series. 1=fully visible background (disable fading), 0=hiddden background (show highlighted series only)."
-  },
-  "includeZero": {
-    "default": "false",
-    "labels": ["Axis display"],
-    "type": "boolean",
-    "description": "Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data"
-  },
-  "rollPeriod": {
-    "default": "1",
-    "labels": ["Error Bars", "Rolling Averages"],
-    "type": "integer &gt;= 1",
-    "description": "Number of days over which to average data. Discussed extensively above."
-  },
-  "unhighlightCallback": {
-    "default": "null",
-    "labels": ["Callbacks"],
-    "type": "function(event)",
-    "parameters": [
-      [ "event" , "the mouse event" ]
-    ],
-    "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph."
-  },
-  "axisTickSize": {
-    "default": "3.0",
-    "labels": ["Axis display"],
-    "type": "number",
-    "description": "The size of the line to display next to each tick mark on x- or y-axes."
-  },
-  "labelsSeparateLines": {
-    "default": "false",
-    "labels": ["Legend"],
-    "type": "boolean",
-    "description": "Put <code>&lt;br/&gt;</code> between lines in the label string. Often used in conjunction with <strong>labelsDiv</strong>."
-  },
-  "valueFormatter": {
-    "default": "Depends on the type of your data.",
-    "labels": ["Legend", "Value display/formatting"],
-    "type": "function(num or millis, opts, seriesName, dygraph, row, col)",
-    "description": "Function to provide a custom display format for the values displayed on mouseover. This does not affect the values that appear on tick marks next to the axes. To format those, see axisLabelFormatter. This is usually set on a <a href='per-axis.html'>per-axis</a> basis. .",
-    "parameters": [
-      ["num_or_millis", "The value to be formatted. This is always a number. For date axes, it's millis since epoch. You can call new Date(millis) to get a Date object."],
-      ["opts", "This is a function you can call to access various options (e.g. opts('labelsKMB')). It returns per-axis values for the option when available."],
-      ["seriesName", "The name of the series from which the point came, e.g. 'X', 'Y', 'A', etc."],
-      ["dygraph", "The dygraph object for which the formatting is being done"],
-      ["row", "The row of the data from which this point comes. g.getValue(row, 0) will return the x-value for this point."],
-      ["col", "The column of the data from which this point comes. g.getValue(row, col) will return the original y-value for this point. This can be used to get the full confidence interval for the point, or access un-rolled values for the point."]
-    ]
-  },
-  "annotationMouseOverHandler": {
-    "default": "null",
-    "labels": ["Annotations"],
-    "type": "function(annotation, point, dygraph, event)",
-    "description": "If provided, this function is called whenever the user mouses over an annotation."
-  },
-  "annotationMouseOutHandler": {
-    "default": "null",
-    "labels": ["Annotations"],
-    "type": "function(annotation, point, dygraph, event)",
-    "parameters": [
-      [ "annotation" , "the annotation left" ],
-      [ "point" , "the point associated with the annotation" ],
-      [ "dygraph" , "the reference graph" ],
-      [ "event" , "the mouse event" ]
-    ],
-    "description": "If provided, this function is called whenever the user mouses out of an annotation."
-  },
-  "annotationClickHandler": {
-    "default": "null",
-    "labels": ["Annotations"],
-    "type": "function(annotation, point, dygraph, event)",
-    "parameters": [
-      [ "annotation" , "the annotation left" ],
-      [ "point" , "the point associated with the annotation" ],
-      [ "dygraph" , "the reference graph" ],
-      [ "event" , "the mouse event" ]
-    ],
-    "description": "If provided, this function is called whenever the user clicks on an annotation."
-  },
-  "annotationDblClickHandler": {
-    "default": "null",
-    "labels": ["Annotations"],
-    "type": "function(annotation, point, dygraph, event)",
-    "parameters": [
-      [ "annotation" , "the annotation left" ],
-      [ "point" , "the point associated with the annotation" ],
-      [ "dygraph" , "the reference graph" ],
-      [ "event" , "the mouse event" ]
-    ],
-    "description": "If provided, this function is called whenever the user double-clicks on an annotation."
-  },
-  "drawCallback": {
-    "default": "null",
-    "labels": ["Callbacks"],
-    "type": "function(dygraph, is_initial)",
-    "parameters": [
-      [ "dygraph" , "The graph being drawn" ],
-      [ "is_initial" , "True if this is the initial draw, false for subsequent draws." ]
-    ],
-    "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning."
-  },
-  "labelsKMG2": {
-    "default": "false",
-    "labels": ["Value display/formatting"],
-    "type": "boolean",
-    "description": "Show k/M/G for kilo/Mega/Giga on y-axis. This is different than <code>labelsKMB</code> in that it uses base 2, not 10."
-  },
-  "delimiter": {
-    "default": ",",
-    "labels": ["CSV parsing"],
-    "type": "string",
-    "description": "The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."
-  },
-  "axisLabelFontSize": {
-    "default": "14",
-    "labels": ["Axis display"],
-    "type": "integer",
-    "description": "Size of the font (in pixels) to use in the axis labels, both x- and y-axis."
-  },
-  "underlayCallback": {
-    "default": "null",
-    "labels": ["Callbacks"],
-    "type": "function(context, area, dygraph)",
-    "parameters": [
-      [ "context" , "the canvas drawing context on which to draw" ],
-      [ "area" , "An object with {x,y,w,h} properties describing the drawing area." ],
-      [ "dygraph" , "the reference graph" ]
-    ],
-    "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
-  },
-  "width": {
-    "default": "480",
-    "labels": ["Overall display"],
-    "type": "integer",
-    "description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
-  },
-  "interactionModel": {
-    "default": "...",
-    "labels": ["Interactive Elements"],
-    "type": "Object",
-    "description": "TODO(konigsberg): document this"
-  },
-  "ticker": {
-    "default": "Dygraph.dateTicker or Dygraph.numericTicks",
-    "labels": ["Axis display"],
-    "type": "function(min, max, pixels, opts, dygraph, vals) -> [{v: ..., label: ...}, ...]",
-    "parameters": [
-      [ "min" , "" ],
-      [ "max" , "" ],
-      [ "pixels" , "" ],
-      [ "opts" , "" ],
-      [ "dygraph" , "the reference graph" ],
-      [ "vals" , "" ]
-    ],
-    "description": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result. See dygraph-tickers.js for an extensive discussion. This is set on a <a href='per-axis.html'>per-axis</a> basis."
-  },
-  "xAxisHeight": {
-    "default": "(null)",
-    "labels": ["Axis display"],
-    "type": "integer",
-    "description": "Height, in pixels, of the x-axis. If not set explicitly, this is computed based on axisLabelFontSize and axisTickSize."
-  },
-  "showLabelsOnHighlight": {
-    "default": "true",
-    "labels": ["Interactive Elements", "Legend"],
-    "type": "boolean",
-    "description": "Whether to show the legend upon mouseover."
-  },
-  "axis": {
-    "default": "(none)",
-    "labels": ["Axis display"],
-    "type": "string",
-    "description": "Set to either 'y1' or 'y2' to assign a series to a y-axis (primary or secondary). Must be set per-series."
-  },
-  "pixelsPerLabel": {
-    "default": "70 (x-axis) or 30 (y-axes)",
-    "labels": ["Axis display", "Grid"],
-    "type": "integer",
-    "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks. This is set on a <a href='per-axis.html'>per-axis</a> basis."
-  },
-  "labelsDiv": {
-    "default": "null",
-    "labels": ["Legend"],
-    "type": "DOM element or string",
-    "example": "<code style='font-size: small'>document.getElementById('foo')</code>or<code>'foo'",
-    "description": "Show data labels in an external div, rather than on the graph.  This value can either be a div element or a div id."
-  },
-  "fractions": {
-    "default": "false",
-    "labels": ["CSV parsing", "Error Bars"],
-    "type": "boolean",
-    "description": "When set, attempt to parse each cell in the CSV file as \"a/b\", where a and b are integers. The ratio will be plotted. This allows computation of Wilson confidence intervals (see below)."
-  },
-  "logscale": {
-    "default": "false",
-    "labels": ["Axis display"],
-    "type": "boolean",
-    "description": "When set for the y-axis or x-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed. Showing log scale with ranges that go below zero will result in an unviewable graph.\n\n Not compatible with showZero. connectSeparatedPoints is ignored. This is ignored for date-based x-axes."
-  },
-  "strokeWidth": {
-    "default": "1.0",
-    "labels": ["Data Line display"],
-    "type": "float",
-    "example": "0.5, 2.0",
-    "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
-  },
-  "strokePattern": {
-    "default": "null",
-    "labels": ["Data Line display"],
-    "type": "array<integer>",
-    "example": "[10, 2, 5, 2]",
-    "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array. This is used to create dashed lines."
-  },
-  "strokeBorderWidth": {
-    "default": "null",
-    "labels": ["Data Line display"],
-    "type": "float",
-    "example": "1.0",
-    "description": "Draw a border around graph lines to make crossing lines more easily distinguishable. Useful for graphs with many lines."
-  },
-  "strokeBorderColor": {
-    "default": "white",
-    "labels": ["Data Line display"],
-    "type": "string",
-    "example": "red, #ccffdd",
-    "description": "Color for the line border used if strokeBorderWidth is set."
-  },
-  "wilsonInterval": {
-    "default": "true",
-    "labels": ["Error Bars"],
-    "type": "boolean",
-    "description": "Use in conjunction with the \"fractions\" option. Instead of plotting +/- N standard deviations, dygraphs will compute a Wilson confidence interval and plot that. This has more reasonable behavior for ratios close to 0 or 1."
-  },
-  "fillGraph": {
-    "default": "false",
-    "labels": ["Data Line display"],
-    "type": "boolean",
-    "description": "Should the area underneath the graph be filled? This option is not compatible with error bars. This may be set on a <a href='per-axis.html'>per-series</a> basis."
-  },
-  "highlightCircleSize": {
-    "default": "3",
-    "labels": ["Interactive Elements"],
-    "type": "integer",
-    "description": "The size in pixels of the dot drawn over highlighted points."
-  },
-  "gridLineColor": {
-    "default": "rgb(128,128,128)",
-    "labels": ["Grid"],
-    "type": "red, blue",
-    "description": "The color of the gridlines. This may be set on a per-axis basis to define each axis' grid separately."
-  },
-  "gridLinePattern": {
-    "default": "null",
-    "labels": ["Grid"],
-    "type": "array<integer>",
-    "example": "[10, 2, 5, 2]",
-    "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array. This is used to create dashed gridlines."
-  },
-  "visibility": {
-    "default": "[true, true, ...]",
-    "labels": ["Data Line display"],
-    "type": "Array of booleans",
-    "description": "Which series should initially be visible? Once the Dygraph has been constructed, you can access and modify the visibility of each series using the <code>visibility</code> and <code>setVisibility</code> methods."
-  },
-  "valueRange": {
-    "default": "Full range of the input is shown",
-    "labels": ["Axis display"],
-    "type": "Array of two numbers",
-    "example": "[10, 110]",
-    "description": "Explicitly set the vertical range of the graph to [low, high]. This may be set on a per-axis basis to define each y-axis separately. If either limit is unspecified, it will be calculated automatically (e.g. [null, 30] to automatically calculate just the lower bound)"
-  },
-  "labelsDivWidth": {
-    "default": "250",
-    "labels": ["Legend"],
-    "type": "integer",
-    "description": "Width (in pixels) of the div which shows information on the currently-highlighted points."
-  },
-  "colorSaturation": {
-    "default": "1.0",
-    "labels": ["Data Series Colors"],
-    "type": "float (0.0 - 1.0)",
-    "description": "If <strong>colors</strong> is not specified, saturation of the automatically-generated data series colors."
-  },
-  "hideOverlayOnMouseOut": {
-    "default": "true",
-    "labels": ["Interactive Elements", "Legend"],
-    "type": "boolean",
-    "description": "Whether to hide the legend when the mouse leaves the chart area."
-  },
-  "legend": {
-    "default": "onmouseover",
-    "labels": ["Legend"],
-    "type": "string",
-    "description": "When to display the legend. By default, it only appears when a user mouses over the chart. Set it to \"always\" to always display a legend of some sort. When set to \"follow\", legend follows highlighted points."
-  },
-  "labelsShowZeroValues": {
-    "default": "true",
-    "labels": ["Legend"],
-    "type": "boolean",
-    "description": "Show zero value labels in the labelsDiv."
-  },
-  "stepPlot": {
-    "default": "false",
-    "labels": ["Data Line display"],
-    "type": "boolean",
-    "description": "When set, display the graph as a step plot instead of a line plot. This option may either be set for the whole graph or for single series."
-  },
-  "labelsUTC": {
-    "default": "false",
-    "labels": ["Value display/formatting", "Axis display"],
-    "type": "boolean",
-    "description": "Show date/time labels according to UTC (instead of local time)."
-  },
-  "labelsKMB": {
-    "default": "false",
-    "labels": ["Value display/formatting"],
-    "type": "boolean",
-    "description": "Show K/M/B for thousands/millions/billions on y-axis."
-  },
-  "rightGap": {
-    "default": "5",
-    "labels": ["Overall display"],
-    "type": "integer",
-    "description": "Number of pixels to leave blank at the right edge of the Dygraph. This makes it easier to highlight the right-most data point."
-  },
-  "avoidMinZero": {
-    "default": "false",
-    "labels": ["Deprecated"],
-    "type": "boolean",
-    "description": "Deprecated, please use yRangePad instead. When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
-  },
-  "drawAxesAtZero": {
-    "default": "false",
-    "labels": ["Axis display"],
-    "type": "boolean",
-    "description": "When set, draw the X axis at the Y=0 position and the Y axis at the X=0 position if those positions are inside the graph's visible area. Otherwise, draw the axes at the bottom or left graph edge as usual."
-  },
-  "xRangePad": {
-    "default": "0",
-    "labels": ["Axis display"],
-    "type": "float",
-    "description": "Add the specified amount of extra space (in pixels) around the X-axis value range to ensure points at the edges remain visible."
-  },
-  "yRangePad": {
-    "default": "null",
-    "labels": ["Axis display"],
-    "type": "float",
-    "description": "If set, add the specified amount of extra space (in pixels) around the Y-axis value range to ensure points at the edges remain visible. If unset, use the traditional Y padding algorithm."
-  },
-  "axisLabelFormatter": {
-    "default": "Depends on the data type",
-    "labels": ["Axis display"],
-    "type": "function(number or Date, granularity, opts, dygraph)",
-    "parameters": [
-      [ "number or date" , "Either a number (for a numeric axis) or a Date object (for a date axis)" ],
-      [ "granularity" , "specifies how fine-grained the axis is. For date axes, this is a reference to the time granularity enumeration, defined in dygraph-tickers.js, e.g. Dygraph.WEEKLY." ],
-      [ "opts" , "a function which provides access to various options on the dygraph, e.g. opts('labelsKMB')." ],
-      [ "dygraph" , "the referenced graph" ]
-    ],
-    "description": "Function to call to format the tick values that appear along an axis. This is usually set on a <a href='per-axis.html'>per-axis</a> basis."
-  },
-  "clickCallback": {
-    "snippet": "function(e, date_millis){<br>&nbsp;&nbsp;alert(new Date(date_millis));<br>}",
-    "default": "null",
-    "labels": ["Callbacks"],
-    "type": "function(e, x, points)",
-    "parameters": [
-      [ "e" , "The event object for the click" ],
-      [ "x" , "The x value that was clicked (for dates, this is milliseconds since epoch)" ],
-      [ "points" , "The closest points along that date. See <a href='#point_properties'>Point properties</a> for details." ]
-    ],
-    "description": "A function to call when the canvas is clicked."
-  },
-  "labels": {
-    "default": "[\"X\", \"Y1\", \"Y2\", ...]*",
-    "labels": ["Legend"],
-    "type": "array<string>",
-    "description": "A name for each data series, including the independent (X) series. For CSV files and DataTable objections, this is determined by context. For raw data, this must be specified. If it is not, default values are supplied and a warning is logged."
-  },
-  "dateWindow": {
-    "default": "Full range of the input is shown",
-    "labels": ["Axis display"],
-    "type": "Array of two numbers",
-    "example": "[<br>&nbsp;&nbsp;Date.parse('2006-01-01'),<br>&nbsp;&nbsp;(new Date()).valueOf()<br>]",
-    "description": "Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers."
-  },
-  "showRoller": {
-    "default": "false",
-    "labels": ["Interactive Elements", "Rolling Averages"],
-    "type": "boolean",
-    "description": "If the rolling average period text box should be shown."
-  },
-  "sigma": {
-    "default": "2.0",
-    "labels": ["Error Bars"],
-    "type": "float",
-    "description": "When errorBars is set, shade this many standard deviations above/below each point."
-  },
-  "customBars": {
-    "default": "false",
-    "labels": ["CSV parsing", "Error Bars"],
-    "type": "boolean",
-    "description": "When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle."
-  },
-  "colorValue": {
-    "default": "1.0",
-    "labels": ["Data Series Colors"],
-    "type": "float (0.0 - 1.0)",
-    "description": "If colors is not specified, value of the data series colors, as in hue/saturation/value. (0.0-1.0, default 0.5)"
-  },
-  "errorBars": {
-    "default": "false",
-    "labels": ["CSV parsing", "Error Bars"],
-    "type": "boolean",
-    "description": "Does the data contain standard deviations? Setting this to true alters the input format (see above)."
-  },
-  "displayAnnotations": {
-    "default": "false",
-    "labels": ["Annotations"],
-    "type": "boolean",
-    "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
-  },
-  "panEdgeFraction": {
-    "default": "null",
-    "labels": ["Axis display", "Interactive Elements"],
-    "type": "float",
-    "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
-  },
-  "title": {
-    "labels": ["Chart labels"],
-    "type": "string",
-    "default": "null",
-    "description": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."
-  },
-  "titleHeight": {
-    "default": "18",
-    "labels": ["Chart labels"],
-    "type": "integer",
-    "description": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."
-  },
-  "xlabel": {
-    "labels": ["Chart labels"],
-    "type": "string",
-    "default": "null",
-    "description": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."
-  },
-  "xLabelHeight": {
-    "labels": ["Chart labels"],
-    "type": "integer",
-    "default": "18",
-    "description": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."
-  },
-  "ylabel": {
-    "labels": ["Chart labels"],
-    "type": "string",
-    "default": "null",
-    "description": "Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option."
-  },
-  "y2label": {
-    "labels": ["Chart labels"],
-    "type": "string",
-    "default": "null",
-    "description": "Text to display to the right of the chart's secondary y-axis. This label is only displayed if a secondary y-axis is present. See <a href='http://dygraphs.com/tests/two-axes.html'>this test</a> for an example of how to do this. The comments for the 'ylabel' option generally apply here as well. This label gets a 'dygraph-y2label' instead of a 'dygraph-ylabel' class."
-  },
-  "yLabelWidth": {
-    "labels": ["Chart labels"],
-    "type": "integer",
-    "default": "18",
-    "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
-  },
-  "isZoomedIgnoreProgrammaticZoom" : {
-    "default": "false",
-    "labels": ["Zooming"],
-    "type": "boolean",
-    "description" : "When this option is passed to updateOptions() along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
-  },
-  "drawGrid": {
-    "default": "true for x and y, false for y2",
-    "labels": ["Grid"],
-    "type": "boolean",
-    "description" : "Whether to display gridlines in the chart. This may be set on a per-axis basis to define the visibility of each axis' grid separately."
-  },
-  "independentTicks": {
-    "default": "true for y, false for y2",
-    "labels": ["Axis display", "Grid"],
-    "type": "boolean",
-    "description" : "Only valid for y and y2, has no effect on x: This option defines whether the y axes should align their ticks or if they should be independent. Possible combinations: 1.) y=true, y2=false (default): y is the primary axis and the y2 ticks are aligned to the the ones of y. (only 1 grid) 2.) y=false, y2=true: y2 is the primary axis and the y ticks are aligned to the the ones of y2. (only 1 grid) 3.) y=true, y2=true: Both axis are independent and have their own ticks. (2 grids) 4.) y=false, y2=false: Invalid configuration causes an error."
-  },
-  "drawAxis": {
-    "default": "true for x and y, false for y2",
-    "labels": ["Axis display"],
-    "type": "boolean",
-    "description" : "Whether to draw the specified axis. This may be set on a per-axis basis to define the visibility of each axis separately. Setting this to false also prevents axis ticks from being drawn and reclaims the space for the chart grid/lines."
-  },
-  "gridLineWidth": {
-    "default": "0.3",
-    "labels": ["Grid"],
-    "type": "float",
-    "description" : "Thickness (in pixels) of the gridlines drawn under the chart. The vertical/horizontal gridlines can be turned off entirely by using the drawGrid option. This may be set on a per-axis basis to define each axis' grid separately."
-  },
-  "axisLineWidth": {
-    "default": "0.3",
-    "labels": ["Axis display"],
-    "type": "float",
-    "description" : "Thickness (in pixels) of the x- and y-axis lines."
-  },
-  "axisLineColor": {
-    "default": "black",
-    "labels": ["Axis display"],
-    "type": "string",
-    "description" : "Color of the x- and y-axis lines. Accepts any value which the HTML canvas strokeStyle attribute understands, e.g. 'black' or 'rgb(0, 100, 255)'."
-  },
-  "fillAlpha": {
-    "default": "0.15",
-    "labels": ["Error Bars", "Data Series Colors"],
-    "type": "float (0.0 - 1.0)",
-    "description" : "Error bars (or custom bars) for each series are drawn in the same color as the series, but with partial transparency. This sets the transparency. A value of 0.0 means that the error bars will not be drawn, whereas a value of 1.0 means that the error bars will be as dark as the line for the series itself. This can be used to produce chart lines whose thickness varies at each point."
-  },
-  "axisLabelColor": {
-    "default": "black",
-    "labels": ["Axis display"],
-    "type": "string",
-    "description" : "Color for x- and y-axis labels. This is a CSS color string."
-  },
-  "axisLabelWidth": {
-    "default": "50 (y-axis), 60 (x-axis)",
-    "labels": ["Axis display", "Chart labels"],
-    "type": "integer",
-    "description" : "Width (in pixels) of the containing divs for x- and y-axis labels. For the y-axis, this also controls the width of the y-axis. Note that for the x-axis, this is independent from pixelsPerLabel, which controls the spacing between labels."
-  },
-  "sigFigs" : {
-    "default": "null",
-    "labels": ["Value display/formatting"],
-    "type": "integer",
-    "description": "By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3."
-  },
-  "digitsAfterDecimal" : {
-    "default": "2",
-    "labels": ["Value display/formatting"],
-    "type": "integer",
-    "description": "Unless it's run in scientific mode (see the <code>sigFigs</code> option), dygraphs displays numbers with <code>digitsAfterDecimal</code> digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation."
-  },
-  "maxNumberWidth" : {
-    "default": "6",
-    "labels": ["Value display/formatting"],
-    "type": "integer",
-    "description": "When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than <code>maxNumberWidth</code> digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30."
-  },
-  "file": {
-    "default": "(set when constructed)",
-    "labels": ["Data"],
-    "type": "string (URL of CSV or CSV), GViz DataTable or 2D Array",
-    "description": "Sets the data being displayed in the chart. This can only be set when calling updateOptions; it cannot be set from the constructor. For a full description of valid data formats, see the <a href='http://dygraphs.com/data.html'>Data Formats</a> page."
-  },
-  "timingName": {
-    "default": "null",
-    "labels": [ "Debugging" ],
-    "type": "string",
-    "description": "Set this option to log timing information. The value of the option will be logged along with the timimg, so that you can distinguish multiple dygraphs on the same page."
-  },
-  "showRangeSelector": {
-    "default": "false",
-    "labels": ["Range Selector"],
-    "type": "boolean",
-    "description": "Show or hide the range selector widget."
-  },
-  "rangeSelectorHeight": {
-    "default": "40",
-    "labels": ["Range Selector"],
-    "type": "integer",
-    "description": "Height, in pixels, of the range selector widget. This option can only be specified at Dygraph creation time."
-  },
-  "rangeSelectorPlotStrokeColor": {
-    "default": "#808FAB",
-    "labels": ["Range Selector"],
-    "type": "string",
-    "description": "The range selector mini plot stroke color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off stroke."
-  },
-  "rangeSelectorPlotFillColor": {
-    "default": "#A7B1C4",
-    "labels": ["Range Selector"],
-    "type": "string",
-    "description": "The range selector mini plot fill color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off fill."
-  },
-  "rangeSelectorPlotFillGradientColor": {
-    "default": "white",
-    "labels": ["Range Selector"],
-    "type": "string",
-    "description": "The top color for the range selector mini plot fill color gradient. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"rgba(255,100,200,42)\" or \"yellow\". You can also specify null or \"\" to disable the gradient and fill with one single color."
-  },
-  "rangeSelectorBackgroundStrokeColor": {
-    "default": "gray",
-    "labels": ["Range Selector"],
-    "type": "string",
-    "description": "The color of the lines below and on both sides of the range selector mini plot. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\"."
-  },
-  "rangeSelectorBackgroundLineWidth": {
-    "default": "1",
-    "labels": ["Range Selector"],
-    "type": "float",
-    "description": "The width of the lines below and on both sides of the range selector mini plot."
-  },
-  "rangeSelectorPlotLineWidth": {
-    "default": "1.5",
-    "labels": ["Range Selector"],
-    "type": "float",
-    "description": "The width of the range selector mini plot line."
-  },
-  "rangeSelectorForegroundStrokeColor": {
-    "default": "black",
-    "labels": ["Range Selector"],
-    "type": "string",
-    "description": "The color of the lines in the interactive layer of the range selector. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\"."
-  },
-  "rangeSelectorForegroundLineWidth": {
-    "default": "1",
-    "labels": ["Range Selector"],
-    "type": "float",
-    "description": "The width the lines in the interactive layer of the range selector."
-  },
-  "rangeSelectorAlpha": {
-    "default": "0.6",
-    "labels": ["Range Selector"],
-    "type": "float (0.0 - 1.0)",
-    "description": "The transparency of the veil that is drawn over the unselected portions of the range selector mini plot. A value of 0 represents full transparency and the unselected portions of the mini plot will appear as normal. A value of 1 represents full opacity and the unselected portions of the mini plot will be hidden."
-  },
-  "showInRangeSelector": {
-    "default": "null",
-    "labels": ["Range Selector"],
-    "type": "boolean",
-    "description": "Mark this series for inclusion in the range selector. The mini plot curve will be an average of all such series. If this is not specified for any series, the default behavior is to average all the series. Setting it for one series will result in that series being charted alone in the range selector."
-  },
-  "animatedZooms": {
-    "default": "false",
-    "labels": ["Interactive Elements"],
-    "type": "boolean",
-    "description": "Set this option to animate the transition between zoom windows. Applies to programmatic and interactive zooms. Note that if you also set a drawCallback, it will be called several times on each zoom. If you set a zoomCallback, it will only be called after the animation is complete."
-  },
-  "plotter": {
-    "default": "[DygraphCanvasRenderer.Plotters.fillPlotter, DygraphCanvasRenderer.Plotters.errorPlotter, DygraphCanvasRenderer.Plotters.linePlotter]",
-    "labels": ["Data Line display"],
-    "type": "array or function",
-    "description": "A function (or array of functions) which plot each data series on the chart. TODO(danvk): more details! May be set per-series."
-  },
-  "axes": {
-    "default": "null",
-    "labels": ["Configuration"],
-    "type": "Object",
-    "description": "Defines per-axis options. Valid keys are 'x', 'y' and 'y2'. Only some options may be set on a per-axis basis. If an option may be set in this way, it will be noted on this page. See also documentation on <a href='http://dygraphs.com/per-axis.html'>per-series and per-axis options</a>."
-  },
-  "series": {
-    "default": "null",
-    "labels": ["Series"],
-    "type": "Object",
-    "description": "Defines per-series options. Its keys match the y-axis label names, and the values are dictionaries themselves that contain options specific to that series."
-  },
-  "plugins": {
-    "default": "[]",
-    "labels": ["Configuration"],
-    "type": "Array<plugin>",
-    "description": "Defines per-graph plugins. Useful for per-graph customization"
-  },
-  "dataHandler": {
-    "default": "(depends on data)",
-    "labels": ["Data"],
-    "type": "Dygraph.DataHandler",
-    "description": "Custom DataHandler. This is an advanced customization. See http://bit.ly/151E7Aq."
-  }
-}
-;  // </JSON>
-// NOTE: in addition to parsing as JS, this snippet is expected to be valid
-// JSON. This assumption cannot be checked in JS, but it will be checked when
-// documentation is generated by the generate-documentation.py script. For the
-// most part, this just means that you should always use double quotes.
-
-// Do a quick sanity check on the options reference.
-(function() {
-  "use strict";
-  var warn = function(msg) { if (window.console) window.console.warn(msg); };
-  var flds = ['type', 'default', 'description'];
-  var valid_cats = [
-   'Annotations',
-   'Axis display',
-   'Chart labels',
-   'CSV parsing',
-   'Callbacks',
-   'Data',
-   'Data Line display',
-   'Data Series Colors',
-   'Error Bars',
-   'Grid',
-   'Interactive Elements',
-   'Range Selector',
-   'Legend',
-   'Overall display',
-   'Rolling Averages',
-   'Series',
-   'Value display/formatting',
-   'Zooming',
-   'Debugging',
-   'Configuration',
-   'Deprecated'
-  ];
-  var i;
-  var cats = {};
-  for (i = 0; i < valid_cats.length; i++) cats[valid_cats[i]] = true;
-
-  for (var k in Dygraph.OPTIONS_REFERENCE) {
-    if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(k)) continue;
-    var op = Dygraph.OPTIONS_REFERENCE[k];
-    for (i = 0; i < flds.length; i++) {
-      if (!op.hasOwnProperty(flds[i])) {
-        warn('Option ' + k + ' missing "' + flds[i] + '" property');
-      } else if (typeof(op[flds[i]]) != 'string') {
-        warn(k + '.' + flds[i] + ' must be of type string');
-      }
-    }
-    var labels = op.labels;
-    if (typeof(labels) !== 'object') {
-      warn('Option "' + k + '" is missing a "labels": [...] option');
-    } else {
-      for (i = 0; i < labels.length; i++) {
-        if (!cats.hasOwnProperty(labels[i])) {
-          warn('Option "' + k + '" has label "' + labels[i] +
-               '", which is invalid.');
-        }
-      }
-    }
-  }
-})();
diff --git a/dygraph-options.js b/dygraph-options.js
deleted file mode 100644 (file)
index d21ccc1..0000000
+++ /dev/null
@@ -1,405 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview DygraphOptions is responsible for parsing and returning information about options.
- *
- * Still tightly coupled to Dygraphs, we could remove some of that, you know.
- */
-
-var DygraphOptions = (function() {
-/*jshint strict:false */
-
-// For "production" code, this gets set to false by uglifyjs.
-// Need to define it outside of "use strict", hence the nested IIFEs.
-if (typeof(DEBUG) === 'undefined') DEBUG=true;
-
-return (function() {
-
-// TODO: remove this jshint directive & fix the warnings.
-/*jshint sub:true */
-/*global Dygraph:false */
-"use strict";
-
-/*
- * Interesting member variables: (REMOVING THIS LIST AS I CLOSURIZE)
- * global_ - global attributes (common among all graphs, AIUI)
- * user - attributes set by the user
- * series_ - { seriesName -> { idx, yAxis, options }}
- */
-
-/**
- * This parses attributes into an object that can be easily queried.
- *
- * It doesn't necessarily mean that all options are available, specifically
- * if labels are not yet available, since those drive details of the per-series
- * and per-axis options.
- *
- * @param {Dygraph} dygraph The chart to which these options belong.
- * @constructor
- */
-var DygraphOptions = function(dygraph) {
-  /**
-   * The dygraph.
-   * @type {!Dygraph}
-   */
-  this.dygraph_ = dygraph;
-
-  /**
-   * Array of axis index to { series : [ series names ] , options : { axis-specific options. }
-   * @type {Array.<{series : Array.<string>, options : Object}>} @private
-   */
-  this.yAxes_ = [];
-
-  /**
-   * Contains x-axis specific options, which are stored in the options key.
-   * This matches the yAxes_ object structure (by being a dictionary with an
-   * options element) allowing for shared code.
-   * @type {options: Object} @private
-   */
-  this.xAxis_ = {};
-  this.series_ = {};
-
-  // Once these two objects are initialized, you can call get();
-  this.global_ = this.dygraph_.attrs_;
-  this.user_ = this.dygraph_.user_attrs_ || {};
-
-  /**
-   * A list of series in columnar order.
-   * @type {Array.<string>}
-   */
-  this.labels_ = [];
-
-  this.highlightSeries_ = this.get("highlightSeriesOpts") || {};
-  this.reparseSeries();
-};
-
-/**
- * Not optimal, but does the trick when you're only using two axes.
- * If we move to more axes, this can just become a function.
- *
- * @type {Object.<number>}
- * @private
- */
-DygraphOptions.AXIS_STRING_MAPPINGS_ = {
-  'y' : 0,
-  'Y' : 0,
-  'y1' : 0,
-  'Y1' : 0,
-  'y2' : 1,
-  'Y2' : 1
-};
-
-/**
- * @param {string|number} axis
- * @private
- */
-DygraphOptions.axisToIndex_ = function(axis) {
-  if (typeof(axis) == "string") {
-    if (DygraphOptions.AXIS_STRING_MAPPINGS_.hasOwnProperty(axis)) {
-      return DygraphOptions.AXIS_STRING_MAPPINGS_[axis];
-    }
-    throw "Unknown axis : " + axis;
-  }
-  if (typeof(axis) == "number") {
-    if (axis === 0 || axis === 1) {
-      return axis;
-    }
-    throw "Dygraphs only supports two y-axes, indexed from 0-1.";
-  }
-  if (axis) {
-    throw "Unknown axis : " + axis;
-  }
-  // No axis specification means axis 0.
-  return 0;
-};
-
-/**
- * Reparses options that are all related to series. This typically occurs when
- * options are either updated, or source data has been made available.
- *
- * TODO(konigsberg): The method name is kind of weak; fix.
- */
-DygraphOptions.prototype.reparseSeries = function() {
-  var labels = this.get("labels");
-  if (!labels) {
-    return; // -- can't do more for now, will parse after getting the labels.
-  }
-
-  this.labels_ = labels.slice(1);
-
-  this.yAxes_ = [ { series : [], options : {}} ]; // Always one axis at least.
-  this.xAxis_ = { options : {} };
-  this.series_ = {};
-
-  // Series are specified in the series element:
-  //
-  // {
-  //   labels: [ "X", "foo", "bar" ],
-  //   pointSize: 3,
-  //   series : {
-  //     foo : {}, // options for foo
-  //     bar : {} // options for bar
-  //   }
-  // }
-  //
-  // So, if series is found, it's expected to contain per-series data, otherwise set a
-  // default.
-  var seriesDict = this.user_.series || {};
-  for (var idx = 0; idx < this.labels_.length; idx++) {
-    var seriesName = this.labels_[idx];
-    var optionsForSeries = seriesDict[seriesName] || {};
-    var yAxis = DygraphOptions.axisToIndex_(optionsForSeries["axis"]);
-
-    this.series_[seriesName] = {
-      idx: idx,
-      yAxis: yAxis,
-      options : optionsForSeries };
-
-    if (!this.yAxes_[yAxis]) {
-      this.yAxes_[yAxis] =  { series : [ seriesName ], options : {} };
-    } else {
-      this.yAxes_[yAxis].series.push(seriesName);
-    }
-  }
-
-  var axis_opts = this.user_["axes"] || {};
-  Dygraph.update(this.yAxes_[0].options, axis_opts["y"] || {});
-  if (this.yAxes_.length > 1) {
-    Dygraph.update(this.yAxes_[1].options, axis_opts["y2"] || {});
-  }
-  Dygraph.update(this.xAxis_.options, axis_opts["x"] || {});
-
-  if (DEBUG) this.validateOptions_();
-};
-
-/**
- * Get a global value.
- *
- * @param {string} name the name of the option.
- */
-DygraphOptions.prototype.get = function(name) {
-  var result = this.getGlobalUser_(name);
-  if (result !== null) {
-    return result;
-  }
-  return this.getGlobalDefault_(name);
-};
-
-DygraphOptions.prototype.getGlobalUser_ = function(name) {
-  if (this.user_.hasOwnProperty(name)) {
-    return this.user_[name];
-  }
-  return null;
-};
-
-DygraphOptions.prototype.getGlobalDefault_ = function(name) {
-  if (this.global_.hasOwnProperty(name)) {
-    return this.global_[name];
-  }
-  if (Dygraph.DEFAULT_ATTRS.hasOwnProperty(name)) {
-    return Dygraph.DEFAULT_ATTRS[name];
-  }
-  return null;
-};
-
-/**
- * Get a value for a specific axis. If there is no specific value for the axis,
- * the global value is returned.
- *
- * @param {string} name the name of the option.
- * @param {string|number} axis the axis to search. Can be the string representation
- * ("y", "y2") or the axis number (0, 1).
- */
-DygraphOptions.prototype.getForAxis = function(name, axis) {
-  var axisIdx;
-  var axisString;
-
-  // Since axis can be a number or a string, straighten everything out here.
-  if (typeof(axis) == 'number') {
-    axisIdx = axis;
-    axisString = axisIdx === 0 ? "y" : "y2";
-  } else {
-    if (axis == "y1") { axis = "y"; } // Standardize on 'y'. Is this bad? I think so.
-    if (axis == "y") {
-      axisIdx = 0;
-    } else if (axis == "y2") {
-      axisIdx = 1;
-    } else if (axis == "x") {
-      axisIdx = -1; // simply a placeholder for below.
-    } else {
-      throw "Unknown axis " + axis;
-    }
-    axisString = axis;
-  }
-
-  var userAxis = (axisIdx == -1) ? this.xAxis_ : this.yAxes_[axisIdx];
-
-  // Search the user-specified axis option first.
-  if (userAxis) { // This condition could be removed if we always set up this.yAxes_ for y2.
-    var axisOptions = userAxis.options;
-    if (axisOptions.hasOwnProperty(name)) {
-      return axisOptions[name];
-    }
-  }
-
-  // User-specified global options second.
-  // But, hack, ignore globally-specified 'logscale' for 'x' axis declaration.
-  if (!(axis === 'x' && name === 'logscale')) {
-    var result = this.getGlobalUser_(name);
-    if (result !== null) {
-      return result;
-    }
-  }
-  // Default axis options third.
-  var defaultAxisOptions = Dygraph.DEFAULT_ATTRS.axes[axisString];
-  if (defaultAxisOptions.hasOwnProperty(name)) {
-    return defaultAxisOptions[name];
-  }
-
-  // Default global options last.
-  return this.getGlobalDefault_(name);
-};
-
-/**
- * Get a value for a specific series. If there is no specific value for the series,
- * the value for the axis is returned (and afterwards, the global value.)
- *
- * @param {string} name the name of the option.
- * @param {string} series the series to search.
- */
-DygraphOptions.prototype.getForSeries = function(name, series) {
-  // Honors indexes as series.
-  if (series === this.dygraph_.getHighlightSeries()) {
-    if (this.highlightSeries_.hasOwnProperty(name)) {
-      return this.highlightSeries_[name];
-    }
-  }
-
-  if (!this.series_.hasOwnProperty(series)) {
-    throw "Unknown series: " + series;
-  }
-
-  var seriesObj = this.series_[series];
-  var seriesOptions = seriesObj["options"];
-  if (seriesOptions.hasOwnProperty(name)) {
-    return seriesOptions[name];
-  }
-
-  return this.getForAxis(name, seriesObj["yAxis"]);
-};
-
-/**
- * Returns the number of y-axes on the chart.
- * @return {number} the number of axes.
- */
-DygraphOptions.prototype.numAxes = function() {
-  return this.yAxes_.length;
-};
-
-/**
- * Return the y-axis for a given series, specified by name.
- */
-DygraphOptions.prototype.axisForSeries = function(series) {
-  return this.series_[series].yAxis;
-};
-
-/**
- * Returns the options for the specified axis.
- */
-// TODO(konigsberg): this is y-axis specific. Support the x axis.
-DygraphOptions.prototype.axisOptions = function(yAxis) {
-  return this.yAxes_[yAxis].options;
-};
-
-/**
- * Return the series associated with an axis.
- */
-DygraphOptions.prototype.seriesForAxis = function(yAxis) {
-  return this.yAxes_[yAxis].series;
-};
-
-/**
- * Return the list of all series, in their columnar order.
- */
-DygraphOptions.prototype.seriesNames = function() {
-  return this.labels_;
-};
-
-if (DEBUG) {
-
-/**
- * Validate all options.
- * This requires Dygraph.OPTIONS_REFERENCE, which is only available in debug builds.
- * @private
- */
-DygraphOptions.prototype.validateOptions_ = function() {
-  if (typeof Dygraph.OPTIONS_REFERENCE === 'undefined') {
-    throw 'Called validateOptions_ in prod build.';
-  }
-
-  var that = this;
-  var validateOption = function(optionName) {
-    if (!Dygraph.OPTIONS_REFERENCE[optionName]) {
-      that.warnInvalidOption_(optionName);
-    }
-  };
-
-  var optionsDicts = [this.xAxis_.options,
-                      this.yAxes_[0].options,
-                      this.yAxes_[1] && this.yAxes_[1].options,
-                      this.global_,
-                      this.user_,
-                      this.highlightSeries_];
-  var names = this.seriesNames();
-  for (var i = 0; i < names.length; i++) {
-    var name = names[i];
-    if (this.series_.hasOwnProperty(name)) {
-      optionsDicts.push(this.series_[name].options);
-    }
-  }
-  for (var i = 0; i < optionsDicts.length; i++) {
-    var dict = optionsDicts[i];
-    if (!dict) continue;
-    for (var optionName in dict) {
-      if (dict.hasOwnProperty(optionName)) {
-        validateOption(optionName);
-      }
-    }
-  }
-};
-
-var WARNINGS = {};  // Only show any particular warning once.
-
-/**
- * Logs a warning about invalid options.
- * TODO: make this throw for testing
- * @private
- */
-DygraphOptions.prototype.warnInvalidOption_ = function(optionName) {
-  if (!WARNINGS[optionName]) {
-    WARNINGS[optionName] = true;
-    var isSeries = (this.labels_.indexOf(optionName) >= 0);
-    if (isSeries) {
-      console.warn('Use new-style per-series options (saw ' + optionName + ' as top-level options key). See http://bit.ly/1tceaJs');
-    } else {
-      console.warn('Unknown option ' + optionName + ' (full list of options at dygraphs.com/options.html');
-      throw "invalid option " + optionName;
-    }
-  }
-};
-
-// Reset list of previously-shown warnings. Used for testing.
-DygraphOptions.resetWarnings_ = function() {
-  WARNINGS = {};
-};
-
-}
-
-return DygraphOptions;
-
-})();
-})();
diff --git a/dygraph-plugin-base.js b/dygraph-plugin-base.js
deleted file mode 100644 (file)
index 7f758a9..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-/*global Dygraph:false */
-
-// Namespace for plugins. Load this before plugins/*.js files.
-Dygraph.Plugins = {};
diff --git a/dygraph-plugin-install.js b/dygraph-plugin-install.js
deleted file mode 100644 (file)
index 806a927..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-/*global Dygraph:false */
-
-// This file defines the ordering of the plugins.
-//
-// The ordering is from most-general to most-specific.
-// This means that, in an event cascade, plugins which have registered for that
-// event will be called in reverse order.
-//
-// This is most relevant for plugins which register a layout event, e.g.
-// Axes, Legend and ChartLabels.
-
-Dygraph.PLUGINS.push(
-  Dygraph.Plugins.Legend,
-  Dygraph.Plugins.Axes,
-  Dygraph.Plugins.RangeSelector, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
-  Dygraph.Plugins.ChartLabels,
-  Dygraph.Plugins.Annotations,
-  Dygraph.Plugins.Grid
-);
diff --git a/dygraph-tickers.js b/dygraph-tickers.js
deleted file mode 100644 (file)
index 0f6b1ab..0000000
+++ /dev/null
@@ -1,456 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview Description of this file.
- * @author danvk@google.com (Dan Vanderkam)
- *
- * A ticker is a function with the following interface:
- *
- * function(a, b, pixels, options_view, dygraph, forced_values);
- * -> [ { v: tick1_v, label: tick1_label[, label_v: label_v1] },
- *      { v: tick2_v, label: tick2_label[, label_v: label_v2] },
- *      ...
- *    ]
- *
- * The returned value is called a "tick list".
- *
- * Arguments
- * ---------
- *
- * [a, b] is the range of the axis for which ticks are being generated. For a
- * numeric axis, these will simply be numbers. For a date axis, these will be
- * millis since epoch (convertable to Date objects using "new Date(a)" and "new
- * Date(b)").
- *
- * opts provides access to chart- and axis-specific options. It can be used to
- * access number/date formatting code/options, check for a log scale, etc.
- *
- * pixels is the length of the axis in pixels. opts('pixelsPerLabel') is the
- * minimum amount of space to be allotted to each label. For instance, if
- * pixels=400 and opts('pixelsPerLabel')=40 then the ticker should return
- * between zero and ten (400/40) ticks.
- *
- * dygraph is the Dygraph object for which an axis is being constructed.
- *
- * forced_values is used for secondary y-axes. The tick positions are typically
- * set by the primary y-axis, so the secondary y-axis has no choice in where to
- * put these. It simply has to generate labels for these data values.
- *
- * Tick lists
- * ----------
- * Typically a tick will have both a grid/tick line and a label at one end of
- * that line (at the bottom for an x-axis, at left or right for the y-axis).
- *
- * A tick may be missing one of these two components:
- * - If "label_v" is specified instead of "v", then there will be no tick or
- *   gridline, just a label.
- * - Similarly, if "label" is not specified, then there will be a gridline
- *   without a label.
- *
- * This flexibility is useful in a few situations:
- * - For log scales, some of the tick lines may be too close to all have labels.
- * - For date scales where years are being displayed, it is desirable to display
- *   tick marks at the beginnings of years but labels (e.g. "2006") in the
- *   middle of the years.
- */
-
-/*jshint sub:true */
-/*global Dygraph:false */
-(function() {
-"use strict";
-
-/** @typedef {Array.<{v:number, label:string, label_v:(string|undefined)}>} */
-Dygraph.TickList = undefined;  // the ' = undefined' keeps jshint happy.
-
-/** @typedef {function(
- *    number,
- *    number,
- *    number,
- *    function(string):*,
- *    Dygraph=,
- *    Array.<number>=
- *  ): Dygraph.TickList}
- */
-Dygraph.Ticker = undefined;  // the ' = undefined' keeps jshint happy.
-
-/** @type {Dygraph.Ticker} */
-Dygraph.numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) {
-  var nonLogscaleOpts = function(opt) {
-    if (opt === 'logscale') return false;
-    return opts(opt);
-  };
-  return Dygraph.numericTicks(a, b, pixels, nonLogscaleOpts, dygraph, vals);
-};
-
-/** @type {Dygraph.Ticker} */
-Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) {
-  var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
-  var ticks = [];
-  var i, j, tickV, nTicks;
-  if (vals) {
-    for (i = 0; i < vals.length; i++) {
-      ticks.push({v: vals[i]});
-    }
-  } else {
-    // TODO(danvk): factor this log-scale block out into a separate function.
-    if (opts("logscale")) {
-      nTicks  = Math.floor(pixels / pixels_per_tick);
-      var minIdx = Dygraph.binarySearch(a, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
-      var maxIdx = Dygraph.binarySearch(b, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
-      if (minIdx == -1) {
-        minIdx = 0;
-      }
-      if (maxIdx == -1) {
-        maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
-      }
-      // Count the number of tick values would appear, if we can get at least
-      // nTicks / 4 accept them.
-      var lastDisplayed = null;
-      if (maxIdx - minIdx >= nTicks / 4) {
-        for (var idx = maxIdx; idx >= minIdx; idx--) {
-          var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
-          var pixel_coord = Math.log(tickValue / a) / Math.log(b / a) * pixels;
-          var tick = { v: tickValue };
-          if (lastDisplayed === null) {
-            lastDisplayed = {
-              tickValue : tickValue,
-              pixel_coord : pixel_coord
-            };
-          } else {
-            if (Math.abs(pixel_coord - lastDisplayed.pixel_coord) >= pixels_per_tick) {
-              lastDisplayed = {
-                tickValue : tickValue,
-                pixel_coord : pixel_coord
-              };
-            } else {
-              tick.label = "";
-            }
-          }
-          ticks.push(tick);
-        }
-        // Since we went in backwards order.
-        ticks.reverse();
-      }
-    }
-
-    // ticks.length won't be 0 if the log scale function finds values to insert.
-    if (ticks.length === 0) {
-      // Basic idea:
-      // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
-      // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
-      // The first spacing greater than pixelsPerYLabel is what we use.
-      // TODO(danvk): version that works on a log scale.
-      var kmg2 = opts("labelsKMG2");
-      var mults, base;
-      if (kmg2) {
-        mults = [1, 2, 4, 8, 16, 32, 64, 128, 256];
-        base = 16;
-      } else {
-        mults = [1, 2, 5, 10, 20, 50, 100];
-        base = 10;
-      }
-
-      // Get the maximum number of permitted ticks based on the
-      // graph's pixel size and pixels_per_tick setting.
-      var max_ticks = Math.ceil(pixels / pixels_per_tick);
-
-      // Now calculate the data unit equivalent of this tick spacing.
-      // Use abs() since graphs may have a reversed Y axis.
-      var units_per_tick = Math.abs(b - a) / max_ticks;
-
-      // Based on this, get a starting scale which is the largest
-      // integer power of the chosen base (10 or 16) that still remains
-      // below the requested pixels_per_tick spacing.
-      var base_power = Math.floor(Math.log(units_per_tick) / Math.log(base));
-      var base_scale = Math.pow(base, base_power);
-
-      // Now try multiples of the starting scale until we find one
-      // that results in tick marks spaced sufficiently far apart.
-      // The "mults" array should cover the range 1 .. base^2 to
-      // adjust for rounding and edge effects.
-      var scale, low_val, high_val, spacing;
-      for (j = 0; j < mults.length; j++) {
-        scale = base_scale * mults[j];
-        low_val = Math.floor(a / scale) * scale;
-        high_val = Math.ceil(b / scale) * scale;
-        nTicks = Math.abs(high_val - low_val) / scale;
-        spacing = pixels / nTicks;
-        if (spacing > pixels_per_tick) break;
-      }
-
-      // Construct the set of ticks.
-      // Allow reverse y-axis if it's explicitly requested.
-      if (low_val > high_val) scale *= -1;
-      for (i = 0; i <= nTicks; i++) {
-        tickV = low_val + i * scale;
-        ticks.push( {v: tickV} );
-      }
-    }
-  }
-
-  var formatter = /**@type{AxisLabelFormatter}*/(opts('axisLabelFormatter'));
-
-  // Add labels to the ticks.
-  for (i = 0; i < ticks.length; i++) {
-    if (ticks[i].label !== undefined) continue;  // Use current label.
-    // TODO(danvk): set granularity to something appropriate here.
-    ticks[i].label = formatter.call(dygraph, ticks[i].v, 0, opts, dygraph);
-  }
-
-  return ticks;
-};
-
-
-/** @type {Dygraph.Ticker} */
-Dygraph.dateTicker = function(a, b, pixels, opts, dygraph, vals) {
-  var chosen = Dygraph.pickDateTickGranularity(a, b, pixels, opts);
-
-  if (chosen >= 0) {
-    return Dygraph.getDateAxis(a, b, chosen, opts, dygraph);
-  } else {
-    // this can happen if self.width_ is zero.
-    return [];
-  }
-};
-
-// Time granularity enumeration
-// TODO(danvk): make this an @enum
-Dygraph.SECONDLY = 0;
-Dygraph.TWO_SECONDLY = 1;
-Dygraph.FIVE_SECONDLY = 2;
-Dygraph.TEN_SECONDLY = 3;
-Dygraph.THIRTY_SECONDLY  = 4;
-Dygraph.MINUTELY = 5;
-Dygraph.TWO_MINUTELY = 6;
-Dygraph.FIVE_MINUTELY = 7;
-Dygraph.TEN_MINUTELY = 8;
-Dygraph.THIRTY_MINUTELY = 9;
-Dygraph.HOURLY = 10;
-Dygraph.TWO_HOURLY = 11;
-Dygraph.SIX_HOURLY = 12;
-Dygraph.DAILY = 13;
-Dygraph.TWO_DAILY = 14;
-Dygraph.WEEKLY = 15;
-Dygraph.MONTHLY = 16;
-Dygraph.QUARTERLY = 17;
-Dygraph.BIANNUAL = 18;
-Dygraph.ANNUAL = 19;
-Dygraph.DECADAL = 20;
-Dygraph.CENTENNIAL = 21;
-Dygraph.NUM_GRANULARITIES = 22;
-
-// Date components enumeration (in the order of the arguments in Date)
-// TODO: make this an @enum
-Dygraph.DATEFIELD_Y = 0;
-Dygraph.DATEFIELD_M = 1;
-Dygraph.DATEFIELD_D = 2;
-Dygraph.DATEFIELD_HH = 3;
-Dygraph.DATEFIELD_MM = 4;
-Dygraph.DATEFIELD_SS = 5;
-Dygraph.DATEFIELD_MS = 6;
-Dygraph.NUM_DATEFIELDS = 7;
-
-
-/**
- * The value of datefield will start at an even multiple of "step", i.e.
- *   if datefield=SS and step=5 then the first tick will be on a multiple of 5s.
- *
- * For granularities <= HOURLY, ticks are generated every `spacing` ms.
- *
- * At coarser granularities, ticks are generated by incrementing `datefield` by
- *   `step`. In this case, the `spacing` value is only used to estimate the
- *   number of ticks. It should roughly correspond to the spacing between
- *   adjacent ticks.
- *
- * @type {Array.<{datefield:number, step:number, spacing:number}>}
- */
-Dygraph.TICK_PLACEMENT = [];
-Dygraph.TICK_PLACEMENT[Dygraph.SECONDLY]        = {datefield: Dygraph.DATEFIELD_SS, step:   1, spacing: 1000 * 1};
-Dygraph.TICK_PLACEMENT[Dygraph.TWO_SECONDLY]    = {datefield: Dygraph.DATEFIELD_SS, step:   2, spacing: 1000 * 2};
-Dygraph.TICK_PLACEMENT[Dygraph.FIVE_SECONDLY]   = {datefield: Dygraph.DATEFIELD_SS, step:   5, spacing: 1000 * 5};
-Dygraph.TICK_PLACEMENT[Dygraph.TEN_SECONDLY]    = {datefield: Dygraph.DATEFIELD_SS, step:  10, spacing: 1000 * 10};
-Dygraph.TICK_PLACEMENT[Dygraph.THIRTY_SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step:  30, spacing: 1000 * 30};
-Dygraph.TICK_PLACEMENT[Dygraph.MINUTELY]        = {datefield: Dygraph.DATEFIELD_MM, step:   1, spacing: 1000 * 60};
-Dygraph.TICK_PLACEMENT[Dygraph.TWO_MINUTELY]    = {datefield: Dygraph.DATEFIELD_MM, step:   2, spacing: 1000 * 60 * 2};
-Dygraph.TICK_PLACEMENT[Dygraph.FIVE_MINUTELY]   = {datefield: Dygraph.DATEFIELD_MM, step:   5, spacing: 1000 * 60 * 5};
-Dygraph.TICK_PLACEMENT[Dygraph.TEN_MINUTELY]    = {datefield: Dygraph.DATEFIELD_MM, step:  10, spacing: 1000 * 60 * 10};
-Dygraph.TICK_PLACEMENT[Dygraph.THIRTY_MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step:  30, spacing: 1000 * 60 * 30};
-Dygraph.TICK_PLACEMENT[Dygraph.HOURLY]          = {datefield: Dygraph.DATEFIELD_HH, step:   1, spacing: 1000 * 3600};
-Dygraph.TICK_PLACEMENT[Dygraph.TWO_HOURLY]      = {datefield: Dygraph.DATEFIELD_HH, step:   2, spacing: 1000 * 3600 * 2};
-Dygraph.TICK_PLACEMENT[Dygraph.SIX_HOURLY]      = {datefield: Dygraph.DATEFIELD_HH, step:   6, spacing: 1000 * 3600 * 6};
-Dygraph.TICK_PLACEMENT[Dygraph.DAILY]           = {datefield: Dygraph.DATEFIELD_D,  step:   1, spacing: 1000 * 86400};
-Dygraph.TICK_PLACEMENT[Dygraph.TWO_DAILY]       = {datefield: Dygraph.DATEFIELD_D,  step:   2, spacing: 1000 * 86400 * 2};
-Dygraph.TICK_PLACEMENT[Dygraph.WEEKLY]          = {datefield: Dygraph.DATEFIELD_D,  step:   7, spacing: 1000 * 604800};
-Dygraph.TICK_PLACEMENT[Dygraph.MONTHLY]         = {datefield: Dygraph.DATEFIELD_M,  step:   1, spacing: 1000 * 7200  * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 12
-Dygraph.TICK_PLACEMENT[Dygraph.QUARTERLY]       = {datefield: Dygraph.DATEFIELD_M,  step:   3, spacing: 1000 * 21600 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 4
-Dygraph.TICK_PLACEMENT[Dygraph.BIANNUAL]        = {datefield: Dygraph.DATEFIELD_M,  step:   6, spacing: 1000 * 43200 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 2
-Dygraph.TICK_PLACEMENT[Dygraph.ANNUAL]          = {datefield: Dygraph.DATEFIELD_Y,  step:   1, spacing: 1000 * 86400   * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 1
-Dygraph.TICK_PLACEMENT[Dygraph.DECADAL]         = {datefield: Dygraph.DATEFIELD_Y,  step:  10, spacing: 1000 * 864000  * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 10
-Dygraph.TICK_PLACEMENT[Dygraph.CENTENNIAL]      = {datefield: Dygraph.DATEFIELD_Y,  step: 100, spacing: 1000 * 8640000 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 100
-
-
-/**
- * This is a list of human-friendly values at which to show tick marks on a log
- * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
- * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
- * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
- * @type {Array.<number>}
- */
-Dygraph.PREFERRED_LOG_TICK_VALUES = (function() {
-  var vals = [];
-  for (var power = -39; power <= 39; power++) {
-    var range = Math.pow(10, power);
-    for (var mult = 1; mult <= 9; mult++) {
-      var val = range * mult;
-      vals.push(val);
-    }
-  }
-  return vals;
-})();
-
-/**
- * Determine the correct granularity of ticks on a date axis.
- *
- * @param {number} a Left edge of the chart (ms)
- * @param {number} b Right edge of the chart (ms)
- * @param {number} pixels Size of the chart in the relevant dimension (width).
- * @param {function(string):*} opts Function mapping from option name -&gt; value.
- * @return {number} The appropriate axis granularity for this chart. See the
- *     enumeration of possible values in dygraph-tickers.js.
- */
-Dygraph.pickDateTickGranularity = function(a, b, pixels, opts) {
-  var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
-  for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
-    var num_ticks = Dygraph.numDateTicks(a, b, i);
-    if (pixels / num_ticks >= pixels_per_tick) {
-      return i;
-    }
-  }
-  return -1;
-};
-
-/**
- * Compute the number of ticks on a date axis for a given granularity.
- * @param {number} start_time
- * @param {number} end_time
- * @param {number} granularity (one of the granularities enumerated above)
- * @return {number} (Approximate) number of ticks that would result.
- */
-Dygraph.numDateTicks = function(start_time, end_time, granularity) {
-  var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing;
-  return Math.round(1.0 * (end_time - start_time) / spacing);
-};
-
-/**
- * Compute the positions and labels of ticks on a date axis for a given granularity.
- * @param {number} start_time
- * @param {number} end_time
- * @param {number} granularity (one of the granularities enumerated above)
- * @param {function(string):*} opts Function mapping from option name -&gt; value.
- * @param {Dygraph=} dg
- * @return {!Dygraph.TickList}
- */
-Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
-  var formatter = /** @type{AxisLabelFormatter} */(
-      opts("axisLabelFormatter"));
-  var utc = opts("labelsUTC");
-  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
-
-  var datefield = Dygraph.TICK_PLACEMENT[granularity].datefield;
-  var step = Dygraph.TICK_PLACEMENT[granularity].step;
-  var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing;
-
-  // Choose a nice tick position before the initial instant.
-  // Currently, this code deals properly with the existent daily granularities:
-  // DAILY (with step of 1) and WEEKLY (with step of 7 but specially handled).
-  // Other daily granularities (say TWO_DAILY) should also be handled specially
-  // by setting the start_date_offset to 0.
-  var start_date = new Date(start_time);
-  var date_array = [];
-  date_array[Dygraph.DATEFIELD_Y]  = accessors.getFullYear(start_date);
-  date_array[Dygraph.DATEFIELD_M]  = accessors.getMonth(start_date);
-  date_array[Dygraph.DATEFIELD_D]  = accessors.getDate(start_date);
-  date_array[Dygraph.DATEFIELD_HH] = accessors.getHours(start_date);
-  date_array[Dygraph.DATEFIELD_MM] = accessors.getMinutes(start_date);
-  date_array[Dygraph.DATEFIELD_SS] = accessors.getSeconds(start_date);
-  date_array[Dygraph.DATEFIELD_MS] = accessors.getMilliseconds(start_date);
-
-  var start_date_offset = date_array[datefield] % step;
-  if (granularity == Dygraph.WEEKLY) {
-    // This will put the ticks on Sundays.
-    start_date_offset = accessors.getDay(start_date);
-  }
-  
-  date_array[datefield] -= start_date_offset;
-  for (var df = datefield + 1; df < Dygraph.NUM_DATEFIELDS; df++) {
-    // The minimum value is 1 for the day of month, and 0 for all other fields.
-    date_array[df] = (df === Dygraph.DATEFIELD_D) ? 1 : 0;
-  }
-
-  // Generate the ticks.
-  // For granularities not coarser than HOURLY we use the fact that:
-  //   the number of milliseconds between ticks is constant
-  //   and equal to the defined spacing.
-  // Otherwise we rely on the 'roll over' property of the Date functions:
-  //   when some date field is set to a value outside of its logical range,
-  //   the excess 'rolls over' the next (more significant) field.
-  // However, when using local time with DST transitions,
-  // there are dates that do not represent any time value at all
-  // (those in the hour skipped at the 'spring forward'),
-  // and the JavaScript engines usually return an equivalent value.
-  // Hence we have to check that the date is properly increased at each step,
-  // returning a date at a nice tick position.
-  var ticks = [];
-  var tick_date = accessors.makeDate.apply(null, date_array);
-  var tick_time = tick_date.getTime();
-  if (granularity <= Dygraph.HOURLY) {
-    if (tick_time < start_time) {
-      tick_time += spacing;
-      tick_date = new Date(tick_time);
-    }
-    while (tick_time <= end_time) {
-      ticks.push({ v: tick_time,
-                   label: formatter.call(dg, tick_date, granularity, opts, dg)
-                 });
-      tick_time += spacing;
-      tick_date = new Date(tick_time);
-    }
-  } else {
-    if (tick_time < start_time) {
-      date_array[datefield] += step;
-      tick_date = accessors.makeDate.apply(null, date_array);
-      tick_time = tick_date.getTime();
-    }
-    while (tick_time <= end_time) {
-      if (granularity >= Dygraph.DAILY ||
-          accessors.getHours(tick_date) % step === 0) {
-        ticks.push({ v: tick_time,
-                     label: formatter.call(dg, tick_date, granularity, opts, dg)
-                   });
-      }
-      date_array[datefield] += step;
-      tick_date = accessors.makeDate.apply(null, date_array);
-      tick_time = tick_date.getTime();
-    }
-  }
-  return ticks;
-};
-
-// These are set here so that this file can be included after dygraph.js
-// or independently.
-if (Dygraph &&
-    Dygraph.DEFAULT_ATTRS &&
-    Dygraph.DEFAULT_ATTRS['axes'] &&
-    Dygraph.DEFAULT_ATTRS['axes']['x'] &&
-    Dygraph.DEFAULT_ATTRS['axes']['y'] &&
-    Dygraph.DEFAULT_ATTRS['axes']['y2']) {
-  Dygraph.DEFAULT_ATTRS['axes']['x']['ticker'] = Dygraph.dateTicker;
-  Dygraph.DEFAULT_ATTRS['axes']['y']['ticker'] = Dygraph.numericTicks;
-  Dygraph.DEFAULT_ATTRS['axes']['y2']['ticker'] = Dygraph.numericTicks;
-}
-
-})();
diff --git a/dygraph-types.js b/dygraph-types.js
deleted file mode 100644 (file)
index 4434758..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @license
- * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-// This file contains typedefs and externs that are needed by the Closure Compiler.
-
-/**
- * @typedef {{
- *   px: number,
- *   py: number,
- *   isZooming: boolean,
- *   isPanning: boolean,
- *   is2DPan: boolean,
- *   cancelNextDblclick: boolean,
- *   initializeMouseDown:
- *       function(!Event, !Dygraph, !DygraphInteractionContext)
- * }}
- */
-var DygraphInteractionContext;
-
-/**
- * Point structure.
- *
- * xval_* and yval_* are the original unscaled data values,
- * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
- * yval_stacked is the cumulative Y value used for stacking graphs,
- * and bottom/top/minus/plus are used for error bar graphs.
- *
- * @typedef {{
- *     idx: number,
- *     name: string,
- *     x: ?number,
- *     xval: ?number,
- *     y_bottom: ?number,
- *     y: ?number,
- *     y_stacked: ?number,
- *     y_top: ?number,
- *     yval_minus: ?number,
- *     yval: ?number,
- *     yval_plus: ?number,
- *     yval_stacked
- * }}
- */
-Dygraph.PointType;
diff --git a/dygraph-utils.js b/dygraph-utils.js
deleted file mode 100644 (file)
index eb08fef..0000000
+++ /dev/null
@@ -1,1185 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview This file contains utility functions used by dygraphs. These
- * are typically static (i.e. not related to any particular dygraph). Examples
- * include date/time formatting functions, basic algorithms (e.g. binary
- * search) and generic DOM-manipulation functions.
- */
-
-(function() {
-
-/*global Dygraph:false, Node:false */
-"use strict";
-
-Dygraph.LOG_SCALE = 10;
-Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
-
-/**
- * @private
- * @param {number} x
- * @return {number}
- */
-Dygraph.log10 = function(x) {
-  return Math.log(x) / Dygraph.LN_TEN;
-};
-
-/** A dotted line stroke pattern. */
-Dygraph.DOTTED_LINE = [2, 2];
-/** A dashed line stroke pattern. */
-Dygraph.DASHED_LINE = [7, 3];
-/** A dot dash stroke pattern. */
-Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
-
-/**
- * Return the 2d context for a dygraph canvas.
- *
- * This method is only exposed for the sake of replacing the function in
- * automated tests, e.g.
- *
- * var oldFunc = Dygraph.getContext();
- * Dygraph.getContext = function(canvas) {
- *   var realContext = oldFunc(canvas);
- *   return new Proxy(realContext);
- * };
- * @param {!HTMLCanvasElement} canvas
- * @return {!CanvasRenderingContext2D}
- * @private
- */
-Dygraph.getContext = function(canvas) {
-  return /** @type{!CanvasRenderingContext2D}*/(canvas.getContext("2d"));
-};
-
-/**
- * Add an event handler.
- * @param {!Node} elem The element to add the event to.
- * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
- * @param {function(Event):(boolean|undefined)} fn The function to call
- *     on the event. The function takes one parameter: the event object.
- * @private
- */
-Dygraph.addEvent = function addEvent(elem, type, fn) {
-  elem.addEventListener(type, fn, false);
-};
-
-/**
- * Add an event handler. This event handler is kept until the graph is
- * destroyed with a call to graph.destroy().
- *
- * @param {!Node} elem The element to add the event to.
- * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
- * @param {function(Event):(boolean|undefined)} fn The function to call
- *     on the event. The function takes one parameter: the event object.
- * @private
- */
-Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) {
-  Dygraph.addEvent(elem, type, fn);
-  this.registeredEvents_.push({ elem : elem, type : type, fn : fn });
-};
-
-/**
- * Remove an event handler.
- * @param {!Node} elem The element to remove the event from.
- * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
- * @param {function(Event):(boolean|undefined)} fn The function to call
- *     on the event. The function takes one parameter: the event object.
- * @private
- */
-Dygraph.removeEvent = function(elem, type, fn) {
-  elem.removeEventListener(type, fn, false);
-};
-
-Dygraph.prototype.removeTrackedEvents_ = function() {
-  if (this.registeredEvents_) {
-    for (var idx = 0; idx < this.registeredEvents_.length; idx++) {
-      var reg = this.registeredEvents_[idx];
-      Dygraph.removeEvent(reg.elem, reg.type, reg.fn);
-    }
-  }
-
-  this.registeredEvents_ = [];
-};
-
-/**
- * Cancels further processing of an event. This is useful to prevent default
- * browser actions, e.g. highlighting text on a double-click.
- * Based on the article at
- * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
- * @param {!Event} e The event whose normal behavior should be canceled.
- * @private
- */
-Dygraph.cancelEvent = function(e) {
-  e = e ? e : window.event;
-  if (e.stopPropagation) {
-    e.stopPropagation();
-  }
-  if (e.preventDefault) {
-    e.preventDefault();
-  }
-  e.cancelBubble = true;
-  e.cancel = true;
-  e.returnValue = false;
-  return false;
-};
-
-/**
- * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
- * is used to generate default series colors which are evenly spaced on the
- * color wheel.
- * @param { number } hue Range is 0.0-1.0.
- * @param { number } saturation Range is 0.0-1.0.
- * @param { number } value Range is 0.0-1.0.
- * @return { string } "rgb(r,g,b)" where r, g and b range from 0-255.
- * @private
- */
-Dygraph.hsvToRGB = function (hue, saturation, value) {
-  var red;
-  var green;
-  var blue;
-  if (saturation === 0) {
-    red = value;
-    green = value;
-    blue = value;
-  } else {
-    var i = Math.floor(hue * 6);
-    var f = (hue * 6) - i;
-    var p = value * (1 - saturation);
-    var q = value * (1 - (saturation * f));
-    var t = value * (1 - (saturation * (1 - f)));
-    switch (i) {
-      case 1: red = q; green = value; blue = p; break;
-      case 2: red = p; green = value; blue = t; break;
-      case 3: red = p; green = q; blue = value; break;
-      case 4: red = t; green = p; blue = value; break;
-      case 5: red = value; green = p; blue = q; break;
-      case 6: // fall through
-      case 0: red = value; green = t; blue = p; break;
-    }
-  }
-  red = Math.floor(255 * red + 0.5);
-  green = Math.floor(255 * green + 0.5);
-  blue = Math.floor(255 * blue + 0.5);
-  return 'rgb(' + red + ',' + green + ',' + blue + ')';
-};
-
-// The following functions are from quirksmode.org with a modification for Safari from
-// http://blog.firetree.net/2005/07/04/javascript-find-position/
-// http://www.quirksmode.org/js/findpos.html
-// ... and modifications to support scrolling divs.
-
-/**
- * Find the coordinates of an object relative to the top left of the page.
- *
- * TODO(danvk): change obj type from Node -&gt; !Node
- * @param {Node} obj
- * @return {{x:number,y:number}}
- * @private
- */
-Dygraph.findPos = function(obj) {
-  var curleft = 0, curtop = 0;
-  if (obj.offsetParent) {
-    var copyObj = obj;
-    while (1) {
-      var borderLeft = "0", borderTop = "0";
-      var computedStyle = window.getComputedStyle(copyObj, null);
-      borderLeft = computedStyle.borderLeft || "0";
-      borderTop = computedStyle.borderTop || "0";
-      curleft += parseInt(borderLeft, 10) ;
-      curtop += parseInt(borderTop, 10) ;
-      curleft += copyObj.offsetLeft;
-      curtop += copyObj.offsetTop;
-      if (!copyObj.offsetParent) {
-        break;
-      }
-      copyObj = copyObj.offsetParent;
-    }
-  } else {
-    // TODO(danvk): why would obj ever have these properties?
-    if (obj.x) curleft += obj.x;
-    if (obj.y) curtop += obj.y;
-  }
-
-  // This handles the case where the object is inside a scrolled div.
-  while (obj && obj != document.body) {
-    curleft -= obj.scrollLeft;
-    curtop -= obj.scrollTop;
-    obj = obj.parentNode;
-  }
-  return {x: curleft, y: curtop};
-};
-
-/**
- * Returns the x-coordinate of the event in a coordinate system where the
- * top-left corner of the page (not the window) is (0,0).
- * Taken from MochiKit.Signal
- * @param {!Event} e
- * @return {number}
- * @private
- */
-Dygraph.pageX = function(e) {
-  if (e.pageX) {
-    return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
-  } else {
-    var de = document.documentElement;
-    var b = document.body;
-    return e.clientX +
-        (de.scrollLeft || b.scrollLeft) -
-        (de.clientLeft || 0);
-  }
-};
-
-/**
- * Returns the y-coordinate of the event in a coordinate system where the
- * top-left corner of the page (not the window) is (0,0).
- * Taken from MochiKit.Signal
- * @param {!Event} e
- * @return {number}
- * @private
- */
-Dygraph.pageY = function(e) {
-  if (e.pageY) {
-    return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
-  } else {
-    var de = document.documentElement;
-    var b = document.body;
-    return e.clientY +
-        (de.scrollTop || b.scrollTop) -
-        (de.clientTop || 0);
-  }
-};
-
-/**
- * Converts page the x-coordinate of the event to pixel x-coordinates on the
- * canvas (i.e. DOM Coords).
- * @param {!Event} e Drag event.
- * @param {!DygraphInteractionContext} context Interaction context object.
- * @return {number} The amount by which the drag has moved to the right.
- */
-Dygraph.dragGetX_ = function(e, context) {
-  return Dygraph.pageX(e) - context.px;
-};
-
-/**
- * Converts page the y-coordinate of the event to pixel y-coordinates on the
- * canvas (i.e. DOM Coords).
- * @param {!Event} e Drag event.
- * @param {!DygraphInteractionContext} context Interaction context object.
- * @return {number} The amount by which the drag has moved down.
- */
-Dygraph.dragGetY_ = function(e, context) {
-  return Dygraph.pageY(e) - context.py;
-};
-
-/**
- * This returns true unless the parameter is 0, null, undefined or NaN.
- * TODO(danvk): rename this function to something like 'isNonZeroNan'.
- *
- * @param {number} x The number to consider.
- * @return {boolean} Whether the number is zero or NaN.
- * @private
- */
-Dygraph.isOK = function(x) {
-  return !!x && !isNaN(x);
-};
-
-/**
- * @param {{x:?number,y:?number,yval:?number}} p The point to consider, valid
- *     points are {x, y} objects
- * @param {boolean=} opt_allowNaNY Treat point with y=NaN as valid
- * @return {boolean} Whether the point has numeric x and y.
- * @private
- */
-Dygraph.isValidPoint = function(p, opt_allowNaNY) {
-  if (!p) return false;  // null or undefined object
-  if (p.yval === null) return false;  // missing point
-  if (p.x === null || p.x === undefined) return false;
-  if (p.y === null || p.y === undefined) return false;
-  if (isNaN(p.x) || (!opt_allowNaNY && isNaN(p.y))) return false;
-  return true;
-};
-
-/**
- * Number formatting function which mimicks the behavior of %g in printf, i.e.
- * either exponential or fixed format (without trailing 0s) is used depending on
- * the length of the generated string.  The advantage of this format is that
- * there is a predictable upper bound on the resulting string length,
- * significant figures are not dropped, and normal numbers are not displayed in
- * exponential notation.
- *
- * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
- * It creates strings which are too long for absolute values between 10^-4 and
- * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
- * output examples.
- *
- * @param {number} x The number to format
- * @param {number=} opt_precision The precision to use, default 2.
- * @return {string} A string formatted like %g in printf.  The max generated
- *                  string length should be precision + 6 (e.g 1.123e+300).
- */
-Dygraph.floatFormat = function(x, opt_precision) {
-  // Avoid invalid precision values; [1, 21] is the valid range.
-  var p = Math.min(Math.max(1, opt_precision || 2), 21);
-
-  // This is deceptively simple.  The actual algorithm comes from:
-  //
-  // Max allowed length = p + 4
-  // where 4 comes from 'e+n' and '.'.
-  //
-  // Length of fixed format = 2 + y + p
-  // where 2 comes from '0.' and y = # of leading zeroes.
-  //
-  // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
-  // 1.0e-3.
-  //
-  // Since the behavior of toPrecision() is identical for larger numbers, we
-  // don't have to worry about the other bound.
-  //
-  // Finally, the argument for toExponential() is the number of trailing digits,
-  // so we take off 1 for the value before the '.'.
-  return (Math.abs(x) < 1.0e-3 && x !== 0.0) ?
-      x.toExponential(p - 1) : x.toPrecision(p);
-};
-
-/**
- * Converts '9' to '09' (useful for dates)
- * @param {number} x
- * @return {string}
- * @private
- */
-Dygraph.zeropad = function(x) {
-  if (x < 10) return "0" + x; else return "" + x;
-};
-
-/**
- * Date accessors to get the parts of a calendar date (year, month,
- * day, hour, minute, second and millisecond) according to local time,
- * and factory method to call the Date constructor with an array of arguments.
- */
-Dygraph.DateAccessorsLocal = {
-  getFullYear:     function(d) {return d.getFullYear();},
-  getMonth:        function(d) {return d.getMonth();},
-  getDate:         function(d) {return d.getDate();},
-  getHours:        function(d) {return d.getHours();},
-  getMinutes:      function(d) {return d.getMinutes();},
-  getSeconds:      function(d) {return d.getSeconds();},
-  getMilliseconds: function(d) {return d.getMilliseconds();},
-  getDay:          function(d) {return d.getDay();},
-  makeDate:        function(y, m, d, hh, mm, ss, ms) {
-    return new Date(y, m, d, hh, mm, ss, ms);
-  }
-};
-
-/**
- * Date accessors to get the parts of a calendar date (year, month,
- * day of month, hour, minute, second and millisecond) according to UTC time,
- * and factory method to call the Date constructor with an array of arguments.
- */
-Dygraph.DateAccessorsUTC = {
-  getFullYear:     function(d) {return d.getUTCFullYear();},
-  getMonth:        function(d) {return d.getUTCMonth();},
-  getDate:         function(d) {return d.getUTCDate();},
-  getHours:        function(d) {return d.getUTCHours();},
-  getMinutes:      function(d) {return d.getUTCMinutes();},
-  getSeconds:      function(d) {return d.getUTCSeconds();},
-  getMilliseconds: function(d) {return d.getUTCMilliseconds();},
-  getDay:          function(d) {return d.getUTCDay();},
-  makeDate:        function(y, m, d, hh, mm, ss, ms) {
-    return new Date(Date.UTC(y, m, d, hh, mm, ss, ms));
-  }
-};
-
-/**
- * Return a string version of the hours, minutes and seconds portion of a date.
- * @param {number} hh The hours (from 0-23)
- * @param {number} mm The minutes (from 0-59)
- * @param {number} ss The seconds (from 0-59)
- * @return {string} A time of the form "HH:MM" or "HH:MM:SS"
- * @private
- */
-Dygraph.hmsString_ = function(hh, mm, ss) {
-  var zeropad = Dygraph.zeropad;
-  var ret = zeropad(hh) + ":" + zeropad(mm);
-  if (ss) {
-    ret += ":" + zeropad(ss);
-  }
-  return ret;
-};
-
-/**
- * Convert a JS date (millis since epoch) to a formatted string.
- * @param {number} time The JavaScript time value (ms since epoch)
- * @param {boolean} utc Wether output UTC or local time
- * @return {string} A date of one of these forms:
- *     "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS"
- * @private
- */
-Dygraph.dateString_ = function(time, utc) {
-  var zeropad = Dygraph.zeropad;
-  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
-  var date = new Date(time);
-  var y = accessors.getFullYear(date);
-  var m = accessors.getMonth(date);
-  var d = accessors.getDate(date);
-  var hh = accessors.getHours(date);
-  var mm = accessors.getMinutes(date);
-  var ss = accessors.getSeconds(date);
-  // Get a year string:
-  var year = "" + y;
-  // Get a 0 padded month string
-  var month = zeropad(m + 1);  //months are 0-offset, sigh
-  // Get a 0 padded day string
-  var day = zeropad(d);
-  var frac = hh * 3600 + mm * 60 + ss;
-  var ret = year + "/" + month + "/" + day;
-  if (frac) {
-    ret += " " + Dygraph.hmsString_(hh, mm, ss);
-  }
-  return ret;
-};
-
-/**
- * Round a number to the specified number of digits past the decimal point.
- * @param {number} num The number to round
- * @param {number} places The number of decimals to which to round
- * @return {number} The rounded number
- * @private
- */
-Dygraph.round_ = function(num, places) {
-  var shift = Math.pow(10, places);
-  return Math.round(num * shift)/shift;
-};
-
-/**
- * Implementation of binary search over an array.
- * Currently does not work when val is outside the range of arry's values.
- * @param {number} val the value to search for
- * @param {Array.<number>} arry is the value over which to search
- * @param {number} abs If abs > 0, find the lowest entry greater than val
- *     If abs < 0, find the highest entry less than val.
- *     If abs == 0, find the entry that equals val.
- * @param {number=} low The first index in arry to consider (optional)
- * @param {number=} high The last index in arry to consider (optional)
- * @return {number} Index of the element, or -1 if it isn't found.
- * @private
- */
-Dygraph.binarySearch = function(val, arry, abs, low, high) {
-  if (low === null || low === undefined ||
-      high === null || high === undefined) {
-    low = 0;
-    high = arry.length - 1;
-  }
-  if (low > high) {
-    return -1;
-  }
-  if (abs === null || abs === undefined) {
-    abs = 0;
-  }
-  var validIndex = function(idx) {
-    return idx >= 0 && idx < arry.length;
-  };
-  var mid = parseInt((low + high) / 2, 10);
-  var element = arry[mid];
-  var idx;
-  if (element == val) {
-    return mid;
-  } else if (element > val) {
-    if (abs > 0) {
-      // Accept if element > val, but also if prior element < val.
-      idx = mid - 1;
-      if (validIndex(idx) && arry[idx] < val) {
-        return mid;
-      }
-    }
-    return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
-  } else if (element < val) {
-    if (abs < 0) {
-      // Accept if element < val, but also if prior element > val.
-      idx = mid + 1;
-      if (validIndex(idx) && arry[idx] > val) {
-        return mid;
-      }
-    }
-    return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
-  }
-  return -1;  // can't actually happen, but makes closure compiler happy
-};
-
-/**
- * Parses a date, returning the number of milliseconds since epoch. This can be
- * passed in as an xValueParser in the Dygraph constructor.
- * TODO(danvk): enumerate formats that this understands.
- *
- * @param {string} dateStr A date in a variety of possible string formats.
- * @return {number} Milliseconds since epoch.
- * @private
- */
-Dygraph.dateParser = function(dateStr) {
-  var dateStrSlashed;
-  var d;
-
-  // Let the system try the format first, with one caveat:
-  // YYYY-MM-DD[ HH:MM:SS] is interpreted as UTC by a variety of browsers.
-  // dygraphs displays dates in local time, so this will result in surprising
-  // inconsistencies. But if you specify "T" or "Z" (i.e. YYYY-MM-DDTHH:MM:SS),
-  // then you probably know what you're doing, so we'll let you go ahead.
-  // Issue: http://code.google.com/p/dygraphs/issues/detail?id=255
-  if (dateStr.search("-") == -1 ||
-      dateStr.search("T") != -1 || dateStr.search("Z") != -1) {
-    d = Dygraph.dateStrToMillis(dateStr);
-    if (d && !isNaN(d)) return d;
-  }
-
-  if (dateStr.search("-") != -1) {  // e.g. '2009-7-12' or '2009-07-12'
-    dateStrSlashed = dateStr.replace("-", "/", "g");
-    while (dateStrSlashed.search("-") != -1) {
-      dateStrSlashed = dateStrSlashed.replace("-", "/");
-    }
-    d = Dygraph.dateStrToMillis(dateStrSlashed);
-  } else if (dateStr.length == 8) {  // e.g. '20090712'
-    // TODO(danvk): remove support for this format. It's confusing.
-    dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) + "/" +
-        dateStr.substr(6,2);
-    d = Dygraph.dateStrToMillis(dateStrSlashed);
-  } else {
-    // Any format that Date.parse will accept, e.g. "2009/07/12" or
-    // "2009/07/12 12:34:56"
-    d = Dygraph.dateStrToMillis(dateStr);
-  }
-
-  if (!d || isNaN(d)) {
-    console.error("Couldn't parse " + dateStr + " as a date");
-  }
-  return d;
-};
-
-/**
- * This is identical to JavaScript's built-in Date.parse() method, except that
- * it doesn't get replaced with an incompatible method by aggressive JS
- * libraries like MooTools or Joomla.
- * @param {string} str The date string, e.g. "2011/05/06"
- * @return {number} millis since epoch
- * @private
- */
-Dygraph.dateStrToMillis = function(str) {
-  return new Date(str).getTime();
-};
-
-// These functions are all based on MochiKit.
-/**
- * Copies all the properties from o to self.
- *
- * @param {!Object} self
- * @param {!Object} o
- * @return {!Object}
- */
-Dygraph.update = function(self, o) {
-  if (typeof(o) != 'undefined' && o !== null) {
-    for (var k in o) {
-      if (o.hasOwnProperty(k)) {
-        self[k] = o[k];
-      }
-    }
-  }
-  return self;
-};
-
-/**
- * Copies all the properties from o to self.
- *
- * @param {!Object} self
- * @param {!Object} o
- * @return {!Object}
- * @private
- */
-Dygraph.updateDeep = function (self, o) {
-  // Taken from http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
-  function isNode(o) {
-    return (
-      typeof Node === "object" ? o instanceof Node :
-      typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
-    );
-  }
-
-  if (typeof(o) != 'undefined' && o !== null) {
-    for (var k in o) {
-      if (o.hasOwnProperty(k)) {
-        if (o[k] === null) {
-          self[k] = null;
-        } else if (Dygraph.isArrayLike(o[k])) {
-          self[k] = o[k].slice();
-        } else if (isNode(o[k])) {
-          // DOM objects are shallowly-copied.
-          self[k] = o[k];
-        } else if (typeof(o[k]) == 'object') {
-          if (typeof(self[k]) != 'object' || self[k] === null) {
-            self[k] = {};
-          }
-          Dygraph.updateDeep(self[k], o[k]);
-        } else {
-          self[k] = o[k];
-        }
-      }
-    }
-  }
-  return self;
-};
-
-/**
- * @param {*} o
- * @return {boolean}
- * @private
- */
-Dygraph.isArrayLike = function(o) {
-  var typ = typeof(o);
-  if (
-      (typ != 'object' && !(typ == 'function' &&
-        typeof(o.item) == 'function')) ||
-      o === null ||
-      typeof(o.length) != 'number' ||
-      o.nodeType === 3
-     ) {
-    return false;
-  }
-  return true;
-};
-
-/**
- * @param {Object} o
- * @return {boolean}
- * @private
- */
-Dygraph.isDateLike = function (o) {
-  if (typeof(o) != "object" || o === null ||
-      typeof(o.getTime) != 'function') {
-    return false;
-  }
-  return true;
-};
-
-/**
- * Note: this only seems to work for arrays.
- * @param {!Array} o
- * @return {!Array}
- * @private
- */
-Dygraph.clone = function(o) {
-  // TODO(danvk): figure out how MochiKit's version works
-  var r = [];
-  for (var i = 0; i < o.length; i++) {
-    if (Dygraph.isArrayLike(o[i])) {
-      r.push(Dygraph.clone(o[i]));
-    } else {
-      r.push(o[i]);
-    }
-  }
-  return r;
-};
-
-/**
- * Create a new canvas element.
- *
- * @return {!HTMLCanvasElement}
- * @private
- */
-Dygraph.createCanvas = function() {
-  return document.createElement('canvas');
-};
-
-/**
- * Returns the context's pixel ratio, which is the ratio between the device
- * pixel ratio and the backing store ratio. Typically this is 1 for conventional
- * displays, and > 1 for HiDPI displays (such as the Retina MBP).
- * See http://www.html5rocks.com/en/tutorials/canvas/hidpi/ for more details.
- *
- * @param {!CanvasRenderingContext2D} context The canvas's 2d context.
- * @return {number} The ratio of the device pixel ratio and the backing store
- * ratio for the specified context.
- */
-Dygraph.getContextPixelRatio = function(context) {
-  try {
-    var devicePixelRatio = window.devicePixelRatio;
-    var backingStoreRatio = context.webkitBackingStorePixelRatio ||
-                            context.mozBackingStorePixelRatio ||
-                            context.msBackingStorePixelRatio ||
-                            context.oBackingStorePixelRatio ||
-                            context.backingStorePixelRatio || 1;
-    if (devicePixelRatio !== undefined) {
-      return devicePixelRatio / backingStoreRatio;
-    } else {
-      // At least devicePixelRatio must be defined for this ratio to make sense.
-      // We default backingStoreRatio to 1: this does not exist on some browsers
-      // (i.e. desktop Chrome).
-      return 1;
-    }
-  } catch (e) {
-    return 1;
-  }
-};
-
-/**
- * Checks whether the user is on an Android browser.
- * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
- * @return {boolean}
- * @private
- */
-Dygraph.isAndroid = function() {
-  return (/Android/).test(navigator.userAgent);
-};
-
-
-/**
- * TODO(danvk): use @template here when it's better supported for classes.
- * @param {!Array} array
- * @param {number} start
- * @param {number} length
- * @param {function(!Array,?):boolean=} predicate
- * @constructor
- */
-Dygraph.Iterator = function(array, start, length, predicate) {
-  start = start || 0;
-  length = length || array.length;
-  this.hasNext = true; // Use to identify if there's another element.
-  this.peek = null; // Use for look-ahead
-  this.start_ = start;
-  this.array_ = array;
-  this.predicate_ = predicate;
-  this.end_ = Math.min(array.length, start + length);
-  this.nextIdx_ = start - 1; // use -1 so initial advance works.
-  this.next(); // ignoring result.
-};
-
-/**
- * @return {Object}
- */
-Dygraph.Iterator.prototype.next = function() {
-  if (!this.hasNext) {
-    return null;
-  }
-  var obj = this.peek;
-
-  var nextIdx = this.nextIdx_ + 1;
-  var found = false;
-  while (nextIdx < this.end_) {
-    if (!this.predicate_ || this.predicate_(this.array_, nextIdx)) {
-      this.peek = this.array_[nextIdx];
-      found = true;
-      break;
-    }
-    nextIdx++;
-  }
-  this.nextIdx_ = nextIdx;
-  if (!found) {
-    this.hasNext = false;
-    this.peek = null;
-  }
-  return obj;
-};
-
-/**
- * Returns a new iterator over array, between indexes start and
- * start + length, and only returns entries that pass the accept function
- *
- * @param {!Array} array the array to iterate over.
- * @param {number} start the first index to iterate over, 0 if absent.
- * @param {number} length the number of elements in the array to iterate over.
- *     This, along with start, defines a slice of the array, and so length
- *     doesn't imply the number of elements in the iterator when accept doesn't
- *     always accept all values. array.length when absent.
- * @param {function(?):boolean=} opt_predicate a function that takes
- *     parameters array and idx, which returns true when the element should be
- *     returned.  If omitted, all elements are accepted.
- * @private
- */
-Dygraph.createIterator = function(array, start, length, opt_predicate) {
-  return new Dygraph.Iterator(array, start, length, opt_predicate);
-};
-
-// Shim layer with setTimeout fallback.
-// From: http://paulirish.com/2011/requestanimationframe-for-smart-animating/
-// Should be called with the window context:
-//   Dygraph.requestAnimFrame.call(window, function() {})
-Dygraph.requestAnimFrame = (function() {
-  return window.requestAnimationFrame       ||
-          window.webkitRequestAnimationFrame ||
-          window.mozRequestAnimationFrame    ||
-          window.oRequestAnimationFrame      ||
-          window.msRequestAnimationFrame     ||
-          function (callback) {
-            window.setTimeout(callback, 1000 / 60);
-          };
-})();
-
-/**
- * Call a function at most maxFrames times at an attempted interval of
- * framePeriodInMillis, then call a cleanup function once. repeatFn is called
- * once immediately, then at most (maxFrames - 1) times asynchronously. If
- * maxFrames==1, then cleanup_fn() is also called synchronously.  This function
- * is used to sequence animation.
- * @param {function(number)} repeatFn Called repeatedly -- takes the frame
- *     number (from 0 to maxFrames-1) as an argument.
- * @param {number} maxFrames The max number of times to call repeatFn
- * @param {number} framePeriodInMillis Max requested time between frames.
- * @param {function()} cleanupFn A function to call after all repeatFn calls.
- * @private
- */
-Dygraph.repeatAndCleanup = function(repeatFn, maxFrames, framePeriodInMillis,
-    cleanupFn) {
-  var frameNumber = 0;
-  var previousFrameNumber;
-  var startTime = new Date().getTime();
-  repeatFn(frameNumber);
-  if (maxFrames == 1) {
-    cleanupFn();
-    return;
-  }
-  var maxFrameArg = maxFrames - 1;
-
-  (function loop() {
-    if (frameNumber >= maxFrames) return;
-    Dygraph.requestAnimFrame.call(window, function() {
-      // Determine which frame to draw based on the delay so far.  Will skip
-      // frames if necessary.
-      var currentTime = new Date().getTime();
-      var delayInMillis = currentTime - startTime;
-      previousFrameNumber = frameNumber;
-      frameNumber = Math.floor(delayInMillis / framePeriodInMillis);
-      var frameDelta = frameNumber - previousFrameNumber;
-      // If we predict that the subsequent repeatFn call will overshoot our
-      // total frame target, so our last call will cause a stutter, then jump to
-      // the last call immediately.  If we're going to cause a stutter, better
-      // to do it faster than slower.
-      var predictOvershootStutter = (frameNumber + frameDelta) > maxFrameArg;
-      if (predictOvershootStutter || (frameNumber >= maxFrameArg)) {
-        repeatFn(maxFrameArg);  // Ensure final call with maxFrameArg.
-        cleanupFn();
-      } else {
-        if (frameDelta !== 0) {  // Don't call repeatFn with duplicate frames.
-          repeatFn(frameNumber);
-        }
-        loop();
-      }
-    });
-  })();
-};
-
-// A whitelist of options that do not change pixel positions.
-var pixelSafeOptions = {
-  'annotationClickHandler': true,
-  'annotationDblClickHandler': true,
-  'annotationMouseOutHandler': true,
-  'annotationMouseOverHandler': true,
-  'axisLabelColor': true,
-  'axisLineColor': true,
-  'axisLineWidth': true,
-  'clickCallback': true,
-  'drawCallback': true,
-  'drawHighlightPointCallback': true,
-  'drawPoints': true,
-  'drawPointCallback': true,
-  'drawGrid': true,
-  'fillAlpha': true,
-  'gridLineColor': true,
-  'gridLineWidth': true,
-  'hideOverlayOnMouseOut': true,
-  'highlightCallback': true,
-  'highlightCircleSize': true,
-  'interactionModel': true,
-  'isZoomedIgnoreProgrammaticZoom': true,
-  'labelsDiv': true,
-  'labelsDivStyles': true,
-  'labelsDivWidth': true,
-  'labelsKMB': true,
-  'labelsKMG2': true,
-  'labelsSeparateLines': true,
-  'labelsShowZeroValues': true,
-  'legend': true,
-  'panEdgeFraction': true,
-  'pixelsPerYLabel': true,
-  'pointClickCallback': true,
-  'pointSize': true,
-  'rangeSelectorPlotFillColor': true,
-  'rangeSelectorPlotFillGradientColor': true,
-  'rangeSelectorPlotStrokeColor': true,
-  'rangeSelectorBackgroundStrokeColor': true,
-  'rangeSelectorBackgroundLineWidth': true,
-  'rangeSelectorPlotLineWidth': true,
-  'rangeSelectorForegroundStrokeColor': true,
-  'rangeSelectorForegroundLineWidth': true,
-  'rangeSelectorAlpha': true,
-  'showLabelsOnHighlight': true,
-  'showRoller': true,
-  'strokeWidth': true,
-  'underlayCallback': true,
-  'unhighlightCallback': true,
-  'zoomCallback': true
-};
-
-/**
- * This function will scan the option list and determine if they
- * require us to recalculate the pixel positions of each point.
- * TODO: move this into dygraph-options.js
- * @param {!Array.<string>} labels a list of options to check.
- * @param {!Object} attrs
- * @return {boolean} true if the graph needs new points else false.
- * @private
- */
-Dygraph.isPixelChangingOptionList = function(labels, attrs) {
-  // Assume that we do not require new points.
-  // This will change to true if we actually do need new points.
-
-  // Create a dictionary of series names for faster lookup.
-  // If there are no labels, then the dictionary stays empty.
-  var seriesNamesDictionary = { };
-  if (labels) {
-    for (var i = 1; i < labels.length; i++) {
-      seriesNamesDictionary[labels[i]] = true;
-    }
-  }
-
-  // Scan through a flat (i.e. non-nested) object of options.
-  // Returns true/false depending on whether new points are needed.
-  var scanFlatOptions = function(options) {
-    for (var property in options) {
-      if (options.hasOwnProperty(property) &&
-          !pixelSafeOptions[property]) {
-        return true;
-      }
-    }
-    return false;
-  };
-
-  // Iterate through the list of updated options.
-  for (var property in attrs) {
-    if (!attrs.hasOwnProperty(property)) continue;
-
-    // Find out of this field is actually a series specific options list.
-    if (property == 'highlightSeriesOpts' ||
-        (seriesNamesDictionary[property] && !attrs.series)) {
-      // This property value is a list of options for this series.
-      if (scanFlatOptions(attrs[property])) return true;
-    } else if (property == 'series' || property == 'axes') {
-      // This is twice-nested options list.
-      var perSeries = attrs[property];
-      for (var series in perSeries) {
-        if (perSeries.hasOwnProperty(series) &&
-            scanFlatOptions(perSeries[series])) {
-          return true;
-        }
-      }
-    } else {
-      // If this was not a series specific option list, check if it's a pixel
-      // changing property.
-      if (!pixelSafeOptions[property]) return true;
-    }
-  }
-
-  return false;
-};
-
-Dygraph.Circles = {
-  DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
-    ctx.beginPath();
-    ctx.fillStyle = color;
-    ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false);
-    ctx.fill();
-  }
-  // For more shapes, include extras/shapes.js
-};
-
-/**
- * To create a "drag" interaction, you typically register a mousedown event
- * handler on the element where the drag begins. In that handler, you register a
- * mouseup handler on the window to determine when the mouse is released,
- * wherever that release happens. This works well, except when the user releases
- * the mouse over an off-domain iframe. In that case, the mouseup event is
- * handled by the iframe and never bubbles up to the window handler.
- *
- * To deal with this issue, we cover iframes with high z-index divs to make sure
- * they don't capture mouseup.
- *
- * Usage:
- * element.addEventListener('mousedown', function() {
- *   var tarper = new Dygraph.IFrameTarp();
- *   tarper.cover();
- *   var mouseUpHandler = function() {
- *     ...
- *     window.removeEventListener(mouseUpHandler);
- *     tarper.uncover();
- *   };
- *   window.addEventListener('mouseup', mouseUpHandler);
- * };
- *
- * @constructor
- */
-Dygraph.IFrameTarp = function() {
-  /** @type {Array.<!HTMLDivElement>} */
-  this.tarps = [];
-};
-
-/**
- * Find all the iframes in the document and cover them with high z-index
- * transparent divs.
- */
-Dygraph.IFrameTarp.prototype.cover = function() {
-  var iframes = document.getElementsByTagName("iframe");
-  for (var i = 0; i < iframes.length; i++) {
-    var iframe = iframes[i];
-    var pos = Dygraph.findPos(iframe),
-        x = pos.x,
-        y = pos.y,
-        width = iframe.offsetWidth,
-        height = iframe.offsetHeight;
-
-    var div = document.createElement("div");
-    div.style.position = "absolute";
-    div.style.left = x + 'px';
-    div.style.top = y + 'px';
-    div.style.width = width + 'px';
-    div.style.height = height + 'px';
-    div.style.zIndex = 999;
-    document.body.appendChild(div);
-    this.tarps.push(div);
-  }
-};
-
-/**
- * Remove all the iframe covers. You should call this in a mouseup handler.
- */
-Dygraph.IFrameTarp.prototype.uncover = function() {
-  for (var i = 0; i < this.tarps.length; i++) {
-    this.tarps[i].parentNode.removeChild(this.tarps[i]);
-  }
-  this.tarps = [];
-};
-
-/**
- * Determine whether |data| is delimited by CR, CRLF, LF, LFCR.
- * @param {string} data
- * @return {?string} the delimiter that was detected (or null on failure).
- */
-Dygraph.detectLineDelimiter = function(data) {
-  for (var i = 0; i < data.length; i++) {
-    var code = data.charAt(i);
-    if (code === '\r') {
-      // Might actually be "\r\n".
-      if (((i + 1) < data.length) && (data.charAt(i + 1) === '\n')) {
-        return '\r\n';
-      }
-      return code;
-    }
-    if (code === '\n') {
-      // Might actually be "\n\r".
-      if (((i + 1) < data.length) && (data.charAt(i + 1) === '\r')) {
-        return '\n\r';
-      }
-      return code;
-    }
-  }
-
-  return null;
-};
-
-/**
- * Is one node contained by another?
- * @param {Node} containee The contained node.
- * @param {Node} container The container node.
- * @return {boolean} Whether containee is inside (or equal to) container.
- * @private
- */
-Dygraph.isNodeContainedBy = function(containee, container) {
-  if (container === null || containee === null) {
-    return false;
-  }
-  var containeeNode = /** @type {Node} */ (containee);
-  while (containeeNode && containeeNode !== container) {
-    containeeNode = containeeNode.parentNode;
-  }
-  return (containeeNode === container);
-};
-
-
-// This masks some numeric issues in older versions of Firefox,
-// where 1.0/Math.pow(10,2) != Math.pow(10,-2).
-/** @type {function(number,number):number} */
-Dygraph.pow = function(base, exp) {
-  if (exp < 0) {
-    return 1.0 / Math.pow(base, -exp);
-  }
-  return Math.pow(base, exp);
-};
-
-/**
- * Converts any valid CSS color (hex, rgb(), named color) to an RGB tuple.
- *
- * @param {!string} colorStr Any valid CSS color string.
- * @return {{r:number,g:number,b:number}} Parsed RGB tuple.
- * @private
- */
-Dygraph.toRGB_ = function(colorStr) {
-  // TODO(danvk): cache color parses to avoid repeated DOM manipulation.
-  var div = document.createElement('div');
-  div.style.backgroundColor = colorStr;
-  div.style.visibility = 'hidden';
-  document.body.appendChild(div);
-  var rgbStr = window.getComputedStyle(div, null).backgroundColor;
-  document.body.removeChild(div);
-  var bits = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(rgbStr);
-  return {
-    r: parseInt(bits[1], 10),
-    g: parseInt(bits[2], 10),
-    b: parseInt(bits[3], 10)
-  };
-};
-
-/**
- * Checks whether the browser supports the &lt;canvas&gt; tag.
- * @param {HTMLCanvasElement=} opt_canvasElement Pass a canvas element as an
- *     optimization if you have one.
- * @return {boolean} Whether the browser supports canvas.
- */
-Dygraph.isCanvasSupported = function(opt_canvasElement) {
-  try {
-    var canvas = opt_canvasElement || document.createElement("canvas");
-    canvas.getContext("2d");
-  } catch (e) {
-    return false;
-  }
-  return true;
-};
-
-/**
- * Parses the value as a floating point number. This is like the parseFloat()
- * built-in, but with a few differences:
- * - the empty string is parsed as null, rather than NaN.
- * - if the string cannot be parsed at all, an error is logged.
- * If the string can't be parsed, this method returns null.
- * @param {string} x The string to be parsed
- * @param {number=} opt_line_no The line number from which the string comes.
- * @param {string=} opt_line The text of the line from which the string comes.
- */
-Dygraph.parseFloat_ = function(x, opt_line_no, opt_line) {
-  var val = parseFloat(x);
-  if (!isNaN(val)) return val;
-
-  // Try to figure out what happeend.
-  // If the value is the empty string, parse it as null.
-  if (/^ *$/.test(x)) return null;
-
-  // If it was actually "NaN", return it as NaN.
-  if (/^ *nan *$/i.test(x)) return NaN;
-
-  // Looks like a parsing error.
-  var msg = "Unable to parse '" + x + "' as a number";
-  if (opt_line !== undefined && opt_line_no !== undefined) {
-    msg += " on line " + (1+(opt_line_no||0)) + " ('" + opt_line + "') of CSV.";
-  }
-  console.error(msg);
-
-  return null;
-};
-
-})();
diff --git a/dygraph.js b/dygraph.js
deleted file mode 100644 (file)
index aaf4b39..0000000
+++ /dev/null
@@ -1,3785 +0,0 @@
-/**
- * @license
- * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
- * string. Dygraph can handle multiple series with or without error bars. The
- * date/value ranges will be automatically set. Dygraph uses the
- * &lt;canvas&gt; tag, so it only works in FF1.5+.
- * @author danvdk@gmail.com (Dan Vanderkam)
-
-  Usage:
-   <div id="graphdiv" style="width:800px; height:500px;"></div>
-   <script type="text/javascript">
-     new Dygraph(document.getElementById("graphdiv"),
-                 "datafile.csv",  // CSV file with headers
-                 { }); // options
-   </script>
-
- The CSV file is of the form
-
-   Date,SeriesA,SeriesB,SeriesC
-   YYYYMMDD,A1,B1,C1
-   YYYYMMDD,A2,B2,C2
-
- If the 'errorBars' option is set in the constructor, the input should be of
- the form
-   Date,SeriesA,SeriesB,...
-   YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
-   YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
-
- If the 'fractions' option is set, the input should be of the form:
-
-   Date,SeriesA,SeriesB,...
-   YYYYMMDD,A1/B1,A2/B2,...
-   YYYYMMDD,A1/B1,A2/B2,...
-
- And error bars will be calculated automatically using a binomial distribution.
-
- For further documentation and examples, see http://dygraphs.com/
-
- */
-
-// For "production" code, this gets set to false by uglifyjs.
-if (typeof(DEBUG) === 'undefined') DEBUG=true;
-
-var Dygraph = (function() {
-/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */
-"use strict";
-
-/**
- * Creates an interactive, zoomable chart.
- *
- * @constructor
- * @param {div | String} div A div or the id of a div into which to construct
- * the chart.
- * @param {String | Function} file A file containing CSV data or a function
- * that returns this data. The most basic expected format for each line is
- * "YYYY/MM/DD,val1,val2,...". For more information, see
- * http://dygraphs.com/data.html.
- * @param {Object} attrs Various other attributes, e.g. errorBars determines
- * whether the input data contains error ranges. For a complete list of
- * options, see http://dygraphs.com/options.html.
- */
-var Dygraph = function(div, data, opts, opt_fourth_param) {
-  // These have to go above the "Hack for IE" in __init__ since .ready() can be
-  // called as soon as the constructor returns. Once support for OldIE is
-  // dropped, this can go down with the rest of the initializers.
-  this.is_initial_draw_ = true;
-  this.readyFns_ = [];
-
-  if (opt_fourth_param !== undefined) {
-    // Old versions of dygraphs took in the series labels as a constructor
-    // parameter. This doesn't make sense anymore, but it's easy to continue
-    // to support this usage.
-    console.warn("Using deprecated four-argument dygraph constructor");
-    this.__old_init__(div, data, opts, opt_fourth_param);
-  } else {
-    this.__init__(div, data, opts);
-  }
-};
-
-Dygraph.NAME = "Dygraph";
-Dygraph.VERSION = "1.1.0";
-Dygraph.__repr__ = function() {
-  return "[" + Dygraph.NAME + " " + Dygraph.VERSION + "]";
-};
-
-/**
- * Returns information about the Dygraph class.
- */
-Dygraph.toString = function() {
-  return Dygraph.__repr__();
-};
-
-// Various default values
-Dygraph.DEFAULT_ROLL_PERIOD = 1;
-Dygraph.DEFAULT_WIDTH = 480;
-Dygraph.DEFAULT_HEIGHT = 320;
-
-// For max 60 Hz. animation:
-Dygraph.ANIMATION_STEPS = 12;
-Dygraph.ANIMATION_DURATION = 200;
-
-// Label constants for the labelsKMB and labelsKMG2 options.
-// (i.e. '100000' -> '100K')
-Dygraph.KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ];
-Dygraph.KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ];
-Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ];
-
-// These are defined before DEFAULT_ATTRS so that it can refer to them.
-/**
- * @private
- * Return a string version of a number. This respects the digitsAfterDecimal
- * and maxNumberWidth options.
- * @param {number} x The number to be formatted
- * @param {Dygraph} opts An options view
- */
-Dygraph.numberValueFormatter = function(x, opts) {
-  var sigFigs = opts('sigFigs');
-
-  if (sigFigs !== null) {
-    // User has opted for a fixed number of significant figures.
-    return Dygraph.floatFormat(x, sigFigs);
-  }
-
-  var digits = opts('digitsAfterDecimal');
-  var maxNumberWidth = opts('maxNumberWidth');
-
-  var kmb = opts('labelsKMB');
-  var kmg2 = opts('labelsKMG2');
-
-  var label;
-
-  // switch to scientific notation if we underflow or overflow fixed display.
-  if (x !== 0.0 &&
-      (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
-       Math.abs(x) < Math.pow(10, -digits))) {
-    label = x.toExponential(digits);
-  } else {
-    label = '' + Dygraph.round_(x, digits);
-  }
-
-  if (kmb || kmg2) {
-    var k;
-    var k_labels = [];
-    var m_labels = [];
-    if (kmb) {
-      k = 1000;
-      k_labels = Dygraph.KMB_LABELS;
-    }
-    if (kmg2) {
-      if (kmb) console.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
-      k = 1024;
-      k_labels = Dygraph.KMG2_BIG_LABELS;
-      m_labels = Dygraph.KMG2_SMALL_LABELS;
-    }
-
-    var absx = Math.abs(x);
-    var n = Dygraph.pow(k, k_labels.length);
-    for (var j = k_labels.length - 1; j >= 0; j--, n /= k) {
-      if (absx >= n) {
-        label = Dygraph.round_(x / n, digits) + k_labels[j];
-        break;
-      }
-    }
-    if (kmg2) {
-      // TODO(danvk): clean up this logic. Why so different than kmb?
-      var x_parts = String(x.toExponential()).split('e-');
-      if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) {
-        if (x_parts[1] % 3 > 0) {
-          label = Dygraph.round_(x_parts[0] /
-              Dygraph.pow(10, (x_parts[1] % 3)),
-              digits);
-        } else {
-          label = Number(x_parts[0]).toFixed(2);
-        }
-        label += m_labels[Math.floor(x_parts[1] / 3) - 1];
-      }
-    }
-  }
-
-  return label;
-};
-
-/**
- * variant for use as an axisLabelFormatter.
- * @private
- */
-Dygraph.numberAxisLabelFormatter = function(x, granularity, opts) {
-  return Dygraph.numberValueFormatter.call(this, x, opts);
-};
-
-/**
- * @type {!Array.<string>}
- * @private
- * @constant
- */
-Dygraph.SHORT_MONTH_NAMES_ = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
-
-
-/**
- * Convert a JS date to a string appropriate to display on an axis that
- * is displaying values at the stated granularity. This respects the
- * labelsUTC option.
- * @param {Date} date The date to format
- * @param {number} granularity One of the Dygraph granularity constants
- * @param {Dygraph} opts An options view
- * @return {string} The date formatted as local time
- * @private
- */
-Dygraph.dateAxisLabelFormatter = function(date, granularity, opts) {
-  var utc = opts('labelsUTC');
-  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
-
-  var year = accessors.getFullYear(date),
-      month = accessors.getMonth(date),
-      day = accessors.getDate(date),
-      hours = accessors.getHours(date),
-      mins = accessors.getMinutes(date),
-      secs = accessors.getSeconds(date),
-      millis = accessors.getSeconds(date);
-
-  if (granularity >= Dygraph.DECADAL) {
-    return '' + year;
-  } else if (granularity >= Dygraph.MONTHLY) {
-    return Dygraph.SHORT_MONTH_NAMES_[month] + '&#160;' + year;
-  } else {
-    var frac = hours * 3600 + mins * 60 + secs + 1e-3 * millis;
-    if (frac === 0 || granularity >= Dygraph.DAILY) {
-      // e.g. '21 Jan' (%d%b)
-      return Dygraph.zeropad(day) + '&#160;' + Dygraph.SHORT_MONTH_NAMES_[month];
-    } else {
-      return Dygraph.hmsString_(hours, mins, secs);
-    }
-  }
-};
-// alias in case anyone is referencing the old method.
-Dygraph.dateAxisFormatter = Dygraph.dateAxisLabelFormatter;
-
-/**
- * Return a string version of a JS date for a value label. This respects the
- * labelsUTC option.
- * @param {Date} date The date to be formatted
- * @param {Dygraph} opts An options view
- * @private
- */
-Dygraph.dateValueFormatter = function(d, opts) {
-  return Dygraph.dateString_(d, opts('labelsUTC'));
-};
-
-/**
- * Standard plotters. These may be used by clients.
- * Available plotters are:
- * - Dygraph.Plotters.linePlotter: draws central lines (most common)
- * - Dygraph.Plotters.errorPlotter: draws error bars
- * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
- *
- * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
- * This causes all the lines to be drawn over all the fills/error bars.
- */
-Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
-
-
-// Default attribute values.
-Dygraph.DEFAULT_ATTRS = {
-  highlightCircleSize: 3,
-  highlightSeriesOpts: null,
-  highlightSeriesBackgroundAlpha: 0.5,
-
-  labelsDivWidth: 250,
-  labelsDivStyles: {
-    // TODO(danvk): move defaults from createStatusMessage_ here.
-  },
-  labelsSeparateLines: false,
-  labelsShowZeroValues: true,
-  labelsKMB: false,
-  labelsKMG2: false,
-  showLabelsOnHighlight: true,
-
-  digitsAfterDecimal: 2,
-  maxNumberWidth: 6,
-  sigFigs: null,
-
-  strokeWidth: 1.0,
-  strokeBorderWidth: 0,
-  strokeBorderColor: "white",
-
-  axisTickSize: 3,
-  axisLabelFontSize: 14,
-  rightGap: 5,
-
-  showRoller: false,
-  xValueParser: Dygraph.dateParser,
-
-  delimiter: ',',
-
-  sigma: 2.0,
-  errorBars: false,
-  fractions: false,
-  wilsonInterval: true,  // only relevant if fractions is true
-  customBars: false,
-  fillGraph: false,
-  fillAlpha: 0.15,
-  connectSeparatedPoints: false,
-
-  stackedGraph: false,
-  stackedGraphNaNFill: 'all',
-  hideOverlayOnMouseOut: true,
-
-  legend: 'onmouseover',
-  stepPlot: false,
-  avoidMinZero: false,
-  xRangePad: 0,
-  yRangePad: null,
-  drawAxesAtZero: false,
-
-  // Sizes of the various chart labels.
-  titleHeight: 28,
-  xLabelHeight: 18,
-  yLabelWidth: 18,
-
-  axisLineColor: "black",
-  axisLineWidth: 0.3,
-  gridLineWidth: 0.3,
-  axisLabelColor: "black",
-  axisLabelWidth: 50,
-  gridLineColor: "rgb(128,128,128)",
-
-  interactionModel: null,  // will be set to Dygraph.Interaction.defaultModel
-  animatedZooms: false,  // (for now)
-
-  // Range selector options
-  showRangeSelector: false,
-  rangeSelectorHeight: 40,
-  rangeSelectorPlotStrokeColor: "#808FAB",
-  rangeSelectorPlotFillGradientColor: "white",
-  rangeSelectorPlotFillColor: "#A7B1C4",
-  rangeSelectorBackgroundStrokeColor: "gray",
-  rangeSelectorBackgroundLineWidth: 1,
-  rangeSelectorPlotLineWidth:1.5,
-  rangeSelectorForegroundStrokeColor: "black",
-  rangeSelectorForegroundLineWidth: 1,
-  rangeSelectorAlpha: 0.6,
-  showInRangeSelector: null,
-
-  // The ordering here ensures that central lines always appear above any
-  // fill bars/error bars.
-  plotter: [
-    Dygraph.Plotters.fillPlotter,
-    Dygraph.Plotters.errorPlotter,
-    Dygraph.Plotters.linePlotter
-  ],
-
-  plugins: [ ],
-
-  // per-axis options
-  axes: {
-    x: {
-      pixelsPerLabel: 70,
-      axisLabelWidth: 60,
-      axisLabelFormatter: Dygraph.dateAxisLabelFormatter,
-      valueFormatter: Dygraph.dateValueFormatter,
-      drawGrid: true,
-      drawAxis: true,
-      independentTicks: true,
-      ticker: null  // will be set in dygraph-tickers.js
-    },
-    y: {
-      axisLabelWidth: 50,
-      pixelsPerLabel: 30,
-      valueFormatter: Dygraph.numberValueFormatter,
-      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
-      drawGrid: true,
-      drawAxis: true,
-      independentTicks: true,
-      ticker: null  // will be set in dygraph-tickers.js
-    },
-    y2: {
-      axisLabelWidth: 50,
-      pixelsPerLabel: 30,
-      valueFormatter: Dygraph.numberValueFormatter,
-      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
-      drawAxis: true,  // only applies when there are two axes of data.
-      drawGrid: false,
-      independentTicks: false,
-      ticker: null  // will be set in dygraph-tickers.js
-    }
-  }
-};
-
-// Directions for panning and zooming. Use bit operations when combined
-// values are possible.
-Dygraph.HORIZONTAL = 1;
-Dygraph.VERTICAL = 2;
-
-// Installed plugins, in order of precedence (most-general to most-specific).
-// Plugins are installed after they are defined, in plugins/install.js.
-Dygraph.PLUGINS = [
-];
-
-// Used for initializing annotation CSS rules only once.
-Dygraph.addedAnnotationCSS = false;
-
-Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
-  // Labels is no longer a constructor parameter, since it's typically set
-  // directly from the data source. It also conains a name for the x-axis,
-  // which the previous constructor form did not.
-  if (labels !== null) {
-    var new_labels = ["Date"];
-    for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
-    Dygraph.update(attrs, { 'labels': new_labels });
-  }
-  this.__init__(div, file, attrs);
-};
-
-/**
- * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
- * and context &lt;canvas&gt; inside of it. See the constructor for details.
- * on the parameters.
- * @param {Element} div the Element to render the graph into.
- * @param {string | Function} file Source data
- * @param {Object} attrs Miscellaneous other options
- * @private
- */
-Dygraph.prototype.__init__ = function(div, file, attrs) {
-  // Support two-argument constructor
-  if (attrs === null || attrs === undefined) { attrs = {}; }
-
-  attrs = Dygraph.copyUserAttrs_(attrs);
-
-  if (typeof(div) == 'string') {
-    div = document.getElementById(div);
-  }
-
-  if (!div) {
-    console.error("Constructing dygraph with a non-existent div!");
-    return;
-  }
-
-  // Copy the important bits into the object
-  // TODO(danvk): most of these should just stay in the attrs_ dictionary.
-  this.maindiv_ = div;
-  this.file_ = file;
-  this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
-  this.previousVerticalX_ = -1;
-  this.fractions_ = attrs.fractions || false;
-  this.dateWindow_ = attrs.dateWindow || null;
-
-  this.annotations_ = [];
-
-  // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
-  this.zoomed_x_ = false;
-  this.zoomed_y_ = false;
-
-  // Clear the div. This ensure that, if multiple dygraphs are passed the same
-  // div, then only one will be drawn.
-  div.innerHTML = "";
-
-  // For historical reasons, the 'width' and 'height' options trump all CSS
-  // rules _except_ for an explicit 'width' or 'height' on the div.
-  // As an added convenience, if the div has zero height (like <div></div> does
-  // without any styles), then we use a default height/width.
-  if (div.style.width === '' && attrs.width) {
-    div.style.width = attrs.width + "px";
-  }
-  if (div.style.height === '' && attrs.height) {
-    div.style.height = attrs.height + "px";
-  }
-  if (div.style.height === '' && div.clientHeight === 0) {
-    div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
-    if (div.style.width === '') {
-      div.style.width = Dygraph.DEFAULT_WIDTH + "px";
-    }
-  }
-  // These will be zero if the dygraph's div is hidden. In that case,
-  // use the user-specified attributes if present. If not, use zero
-  // and assume the user will call resize to fix things later.
-  this.width_ = div.clientWidth || attrs.width || 0;
-  this.height_ = div.clientHeight || attrs.height || 0;
-
-  // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
-  if (attrs.stackedGraph) {
-    attrs.fillGraph = true;
-    // TODO(nikhilk): Add any other stackedGraph checks here.
-  }
-
-  // DEPRECATION WARNING: All option processing should be moved from
-  // attrs_ and user_attrs_ to options_, which holds all this information.
-  //
-  // Dygraphs has many options, some of which interact with one another.
-  // To keep track of everything, we maintain two sets of options:
-  //
-  //  this.user_attrs_   only options explicitly set by the user.
-  //  this.attrs_        defaults, options derived from user_attrs_, data.
-  //
-  // Options are then accessed this.attr_('attr'), which first looks at
-  // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
-  // defaults without overriding behavior that the user specifically asks for.
-  this.user_attrs_ = {};
-  Dygraph.update(this.user_attrs_, attrs);
-
-  // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
-  this.attrs_ = {};
-  Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
-
-  this.boundaryIds_ = [];
-  this.setIndexByName_ = {};
-  this.datasetIndex_ = [];
-
-  this.registeredEvents_ = [];
-  this.eventListeners_ = {};
-
-  this.attributes_ = new DygraphOptions(this);
-
-  // Create the containing DIV and other interactive elements
-  this.createInterface_();
-
-  // Activate plugins.
-  this.plugins_ = [];
-  var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
-  for (var i = 0; i < plugins.length; i++) {
-    // the plugins option may contain either plugin classes or instances.
-    // Plugin instances contain an activate method.
-    var Plugin = plugins[i];  // either a constructor or an instance.
-    var pluginInstance;
-    if (typeof(Plugin.activate) !== 'undefined') {
-      pluginInstance = Plugin;
-    } else {
-      pluginInstance = new Plugin();
-    }
-
-    var pluginDict = {
-      plugin: pluginInstance,
-      events: {},
-      options: {},
-      pluginOptions: {}
-    };
-
-    var handlers = pluginInstance.activate(this);
-    for (var eventName in handlers) {
-      if (!handlers.hasOwnProperty(eventName)) continue;
-      // TODO(danvk): validate eventName.
-      pluginDict.events[eventName] = handlers[eventName];
-    }
-
-    this.plugins_.push(pluginDict);
-  }
-
-  // At this point, plugins can no longer register event handlers.
-  // Construct a map from event -> ordered list of [callback, plugin].
-  for (var i = 0; i < this.plugins_.length; i++) {
-    var plugin_dict = this.plugins_[i];
-    for (var eventName in plugin_dict.events) {
-      if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
-      var callback = plugin_dict.events[eventName];
-
-      var pair = [plugin_dict.plugin, callback];
-      if (!(eventName in this.eventListeners_)) {
-        this.eventListeners_[eventName] = [pair];
-      } else {
-        this.eventListeners_[eventName].push(pair);
-      }
-    }
-  }
-
-  this.createDragInterface_();
-
-  this.start_();
-};
-
-/**
- * Triggers a cascade of events to the various plugins which are interested in them.
- * Returns true if the "default behavior" should be prevented, i.e. if one
- * of the event listeners called event.preventDefault().
- * @private
- */
-Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
-  if (!(name in this.eventListeners_)) return false;
-
-  // QUESTION: can we use objects & prototypes to speed this up?
-  var e = {
-    dygraph: this,
-    cancelable: false,
-    defaultPrevented: false,
-    preventDefault: function() {
-      if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
-      e.defaultPrevented = true;
-    },
-    propagationStopped: false,
-    stopPropagation: function() {
-      e.propagationStopped = true;
-    }
-  };
-  Dygraph.update(e, extra_props);
-
-  var callback_plugin_pairs = this.eventListeners_[name];
-  if (callback_plugin_pairs) {
-    for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
-      var plugin = callback_plugin_pairs[i][0];
-      var callback = callback_plugin_pairs[i][1];
-      callback.call(plugin, e);
-      if (e.propagationStopped) break;
-    }
-  }
-  return e.defaultPrevented;
-};
-
-/**
- * Fetch a plugin instance of a particular class. Only for testing.
- * @private
- * @param {!Class} type The type of the plugin.
- * @return {Object} Instance of the plugin, or null if there is none.
- */
-Dygraph.prototype.getPluginInstance_ = function(type) {
-  for (var i = 0; i < this.plugins_.length; i++) {
-    var p = this.plugins_[i];
-    if (p.plugin instanceof type) {
-      return p.plugin;
-    }
-  }
-  return null;
-};
-
-/**
- * Returns the zoomed status of the chart for one or both axes.
- *
- * Axis is an optional parameter. Can be set to 'x' or 'y'.
- *
- * The zoomed status for an axis is set whenever a user zooms using the mouse
- * or when the dateWindow or valueRange are updated (unless the
- * isZoomedIgnoreProgrammaticZoom option is also specified).
- */
-Dygraph.prototype.isZoomed = function(axis) {
-  if (axis === null || axis === undefined) {
-    return this.zoomed_x_ || this.zoomed_y_;
-  }
-  if (axis === 'x') return this.zoomed_x_;
-  if (axis === 'y') return this.zoomed_y_;
-  throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'.";
-};
-
-/**
- * Returns information about the Dygraph object, including its containing ID.
- */
-Dygraph.prototype.toString = function() {
-  var maindiv = this.maindiv_;
-  var id = (maindiv && maindiv.id) ? maindiv.id : maindiv;
-  return "[Dygraph " + id + "]";
-};
-
-/**
- * @private
- * Returns the value of an option. This may be set by the user (either in the
- * constructor or by calling updateOptions) or by dygraphs, and may be set to a
- * per-series value.
- * @param {string} name The name of the option, e.g. 'rollPeriod'.
- * @param {string} [seriesName] The name of the series to which the option
- * will be applied. If no per-series value of this option is available, then
- * the global value is returned. This is optional.
- * @return { ... } The value of the option.
- */
-Dygraph.prototype.attr_ = function(name, seriesName) {
-  if (DEBUG) {
-    if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
-      console.error('Must include options reference JS for testing');
-    } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
-      console.error('Dygraphs is using property ' + name + ', which has no ' +
-                    'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
-      // Only log this error once.
-      Dygraph.OPTIONS_REFERENCE[name] = true;
-    }
-  }
-  return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name);
-};
-
-/**
- * Returns the current value for an option, as set in the constructor or via
- * updateOptions. You may pass in an (optional) series name to get per-series
- * values for the option.
- *
- * All values returned by this method should be considered immutable. If you
- * modify them, there is no guarantee that the changes will be honored or that
- * dygraphs will remain in a consistent state. If you want to modify an option,
- * use updateOptions() instead.
- *
- * @param {string} name The name of the option (e.g. 'strokeWidth')
- * @param {string=} opt_seriesName Series name to get per-series values.
- * @return {*} The value of the option.
- */
-Dygraph.prototype.getOption = function(name, opt_seriesName) {
-  return this.attr_(name, opt_seriesName);
-};
-
-/**
- * Like getOption(), but specifically returns a number.
- * This is a convenience function for working with the Closure Compiler.
- * @param {string} name The name of the option (e.g. 'strokeWidth')
- * @param {string=} opt_seriesName Series name to get per-series values.
- * @return {number} The value of the option.
- * @private
- */
-Dygraph.prototype.getNumericOption = function(name, opt_seriesName) {
-  return /** @type{number} */(this.getOption(name, opt_seriesName));
-};
-
-/**
- * Like getOption(), but specifically returns a string.
- * This is a convenience function for working with the Closure Compiler.
- * @param {string} name The name of the option (e.g. 'strokeWidth')
- * @param {string=} opt_seriesName Series name to get per-series values.
- * @return {string} The value of the option.
- * @private
- */
-Dygraph.prototype.getStringOption = function(name, opt_seriesName) {
-  return /** @type{string} */(this.getOption(name, opt_seriesName));
-};
-
-/**
- * Like getOption(), but specifically returns a boolean.
- * This is a convenience function for working with the Closure Compiler.
- * @param {string} name The name of the option (e.g. 'strokeWidth')
- * @param {string=} opt_seriesName Series name to get per-series values.
- * @return {boolean} The value of the option.
- * @private
- */
-Dygraph.prototype.getBooleanOption = function(name, opt_seriesName) {
-  return /** @type{boolean} */(this.getOption(name, opt_seriesName));
-};
-
-/**
- * Like getOption(), but specifically returns a function.
- * This is a convenience function for working with the Closure Compiler.
- * @param {string} name The name of the option (e.g. 'strokeWidth')
- * @param {string=} opt_seriesName Series name to get per-series values.
- * @return {function(...)} The value of the option.
- * @private
- */
-Dygraph.prototype.getFunctionOption = function(name, opt_seriesName) {
-  return /** @type{function(...)} */(this.getOption(name, opt_seriesName));
-};
-
-Dygraph.prototype.getOptionForAxis = function(name, axis) {
-  return this.attributes_.getForAxis(name, axis);
-};
-
-/**
- * @private
- * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2')
- * @return { ... } A function mapping string -> option value
- */
-Dygraph.prototype.optionsViewForAxis_ = function(axis) {
-  var self = this;
-  return function(opt) {
-    var axis_opts = self.user_attrs_.axes;
-    if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
-      return axis_opts[axis][opt];
-    }
-
-    // I don't like that this is in a second spot.
-    if (axis === 'x' && opt === 'logscale') {
-      // return the default value.
-      // TODO(konigsberg): pull the default from a global default.
-      return false;
-    }
-
-    // user-specified attributes always trump defaults, even if they're less
-    // specific.
-    if (typeof(self.user_attrs_[opt]) != 'undefined') {
-      return self.user_attrs_[opt];
-    }
-
-    axis_opts = self.attrs_.axes;
-    if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
-      return axis_opts[axis][opt];
-    }
-    // check old-style axis options
-    // TODO(danvk): add a deprecation warning if either of these match.
-    if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) {
-      return self.axes_[0][opt];
-    } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) {
-      return self.axes_[1][opt];
-    }
-    return self.attr_(opt);
-  };
-};
-
-/**
- * Returns the current rolling period, as set by the user or an option.
- * @return {number} The number of points in the rolling window
- */
-Dygraph.prototype.rollPeriod = function() {
-  return this.rollPeriod_;
-};
-
-/**
- * Returns the currently-visible x-range. This can be affected by zooming,
- * panning or a call to updateOptions.
- * Returns a two-element array: [left, right].
- * If the Dygraph has dates on the x-axis, these will be millis since epoch.
- */
-Dygraph.prototype.xAxisRange = function() {
-  return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
-};
-
-/**
- * Returns the lower- and upper-bound x-axis values of the
- * data set.
- */
-Dygraph.prototype.xAxisExtremes = function() {
-  var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w;
-  if (this.numRows() === 0) {
-    return [0 - pad, 1 + pad];
-  }
-  var left = this.rawData_[0][0];
-  var right = this.rawData_[this.rawData_.length - 1][0];
-  if (pad) {
-    // Must keep this in sync with dygraph-layout _evaluateLimits()
-    var range = right - left;
-    left -= range * pad;
-    right += range * pad;
-  }
-  return [left, right];
-};
-
-/**
- * Returns the currently-visible y-range for an axis. This can be affected by
- * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
- * called with no arguments, returns the range of the first axis.
- * Returns a two-element array: [bottom, top].
- */
-Dygraph.prototype.yAxisRange = function(idx) {
-  if (typeof(idx) == "undefined") idx = 0;
-  if (idx < 0 || idx >= this.axes_.length) {
-    return null;
-  }
-  var axis = this.axes_[idx];
-  return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
-};
-
-/**
- * Returns the currently-visible y-ranges for each axis. This can be affected by
- * zooming, panning, calls to updateOptions, etc.
- * Returns an array of [bottom, top] pairs, one for each y-axis.
- */
-Dygraph.prototype.yAxisRanges = function() {
-  var ret = [];
-  for (var i = 0; i < this.axes_.length; i++) {
-    ret.push(this.yAxisRange(i));
-  }
-  return ret;
-};
-
-// TODO(danvk): use these functions throughout dygraphs.
-/**
- * Convert from data coordinates to canvas/div X/Y coordinates.
- * If specified, do this conversion for the coordinate system of a particular
- * axis. Uses the first axis by default.
- * Returns a two-element array: [X, Y]
- *
- * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
- * instead of toDomCoords(null, y, axis).
- */
-Dygraph.prototype.toDomCoords = function(x, y, axis) {
-  return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
-};
-
-/**
- * Convert from data x coordinates to canvas/div X coordinate.
- * If specified, do this conversion for the coordinate system of a particular
- * axis.
- * Returns a single value or null if x is null.
- */
-Dygraph.prototype.toDomXCoord = function(x) {
-  if (x === null) {
-    return null;
-  }
-
-  var area = this.plotter_.area;
-  var xRange = this.xAxisRange();
-  return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
-};
-
-/**
- * Convert from data x coordinates to canvas/div Y coordinate and optional
- * axis. Uses the first axis by default.
- *
- * returns a single value or null if y is null.
- */
-Dygraph.prototype.toDomYCoord = function(y, axis) {
-  var pct = this.toPercentYCoord(y, axis);
-
-  if (pct === null) {
-    return null;
-  }
-  var area = this.plotter_.area;
-  return area.y + pct * area.h;
-};
-
-/**
- * Convert from canvas/div coords to data coordinates.
- * If specified, do this conversion for the coordinate system of a particular
- * axis. Uses the first axis by default.
- * Returns a two-element array: [X, Y].
- *
- * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
- * instead of toDataCoords(null, y, axis).
- */
-Dygraph.prototype.toDataCoords = function(x, y, axis) {
-  return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
-};
-
-/**
- * Convert from canvas/div x coordinate to data coordinate.
- *
- * If x is null, this returns null.
- */
-Dygraph.prototype.toDataXCoord = function(x) {
-  if (x === null) {
-    return null;
-  }
-
-  var area = this.plotter_.area;
-  var xRange = this.xAxisRange();
-
-  if (!this.attributes_.getForAxis("logscale", 'x')) {
-    return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
-  } else {
-    // TODO: remove duplicate code?
-    // Computing the inverse of toDomCoord.
-    var pct = (x - area.x) / area.w;
-
-    // Computing the inverse of toPercentXCoord. The function was arrived at with
-    // the following steps:
-    //
-    // Original calcuation:
-    // pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0])));
-    //
-    // Multiply both sides by the right-side demoninator.
-    // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
-    //
-    // add log(xRange[0]) to both sides
-    // log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) = log(x);
-    //
-    // Swap both sides of the equation,
-    // log(x) = log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0]))
-    //
-    // Use both sides as the exponent in 10^exp and we're done.
-    // x = 10 ^ (log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])))
-    var logr0 = Dygraph.log10(xRange[0]);
-    var logr1 = Dygraph.log10(xRange[1]);
-    var exponent = logr0 + (pct * (logr1 - logr0));
-    var value = Math.pow(Dygraph.LOG_SCALE, exponent);
-    return value;
-  }
-};
-
-/**
- * Convert from canvas/div y coord to value.
- *
- * If y is null, this returns null.
- * if axis is null, this uses the first axis.
- */
-Dygraph.prototype.toDataYCoord = function(y, axis) {
-  if (y === null) {
-    return null;
-  }
-
-  var area = this.plotter_.area;
-  var yRange = this.yAxisRange(axis);
-
-  if (typeof(axis) == "undefined") axis = 0;
-  if (!this.attributes_.getForAxis("logscale", axis)) {
-    return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]);
-  } else {
-    // Computing the inverse of toDomCoord.
-    var pct = (y - area.y) / area.h;
-
-    // Computing the inverse of toPercentYCoord. The function was arrived at with
-    // the following steps:
-    //
-    // Original calcuation:
-    // pct = (log(yRange[1]) - log(y)) / (log(yRange[1]) - log(yRange[0]));
-    //
-    // Multiply both sides by the right-side demoninator.
-    // pct * (log(yRange[1]) - log(yRange[0])) = log(yRange[1]) - log(y);
-    //
-    // subtract log(yRange[1]) from both sides.
-    // (pct * (log(yRange[1]) - log(yRange[0]))) - log(yRange[1]) = -log(y);
-    //
-    // and multiply both sides by -1.
-    // log(yRange[1]) - (pct * (logr1 - log(yRange[0])) = log(y);
-    //
-    // Swap both sides of the equation,
-    // log(y) = log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0])));
-    //
-    // Use both sides as the exponent in 10^exp and we're done.
-    // y = 10 ^ (log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0]))));
-    var logr0 = Dygraph.log10(yRange[0]);
-    var logr1 = Dygraph.log10(yRange[1]);
-    var exponent = logr1 - (pct * (logr1 - logr0));
-    var value = Math.pow(Dygraph.LOG_SCALE, exponent);
-    return value;
-  }
-};
-
-/**
- * Converts a y for an axis to a percentage from the top to the
- * bottom of the drawing area.
- *
- * If the coordinate represents a value visible on the canvas, then
- * the value will be between 0 and 1, where 0 is the top of the canvas.
- * However, this method will return values outside the range, as
- * values can fall outside the canvas.
- *
- * If y is null, this returns null.
- * if axis is null, this uses the first axis.
- *
- * @param {number} y The data y-coordinate.
- * @param {number} [axis] The axis number on which the data coordinate lives.
- * @return {number} A fraction in [0, 1] where 0 = the top edge.
- */
-Dygraph.prototype.toPercentYCoord = function(y, axis) {
-  if (y === null) {
-    return null;
-  }
-  if (typeof(axis) == "undefined") axis = 0;
-
-  var yRange = this.yAxisRange(axis);
-
-  var pct;
-  var logscale = this.attributes_.getForAxis("logscale", axis);
-  if (logscale) {
-    var logr0 = Dygraph.log10(yRange[0]);
-    var logr1 = Dygraph.log10(yRange[1]);
-    pct = (logr1 - Dygraph.log10(y)) / (logr1 - logr0);
-  } else {
-    // yRange[1] - y is unit distance from the bottom.
-    // yRange[1] - yRange[0] is the scale of the range.
-    // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
-    pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
-  }
-  return pct;
-};
-
-/**
- * Converts an x value to a percentage from the left to the right of
- * the drawing area.
- *
- * If the coordinate represents a value visible on the canvas, then
- * the value will be between 0 and 1, where 0 is the left of the canvas.
- * However, this method will return values outside the range, as
- * values can fall outside the canvas.
- *
- * If x is null, this returns null.
- * @param {number} x The data x-coordinate.
- * @return {number} A fraction in [0, 1] where 0 = the left edge.
- */
-Dygraph.prototype.toPercentXCoord = function(x) {
-  if (x === null) {
-    return null;
-  }
-
-  var xRange = this.xAxisRange();
-  var pct;
-  var logscale = this.attributes_.getForAxis("logscale", 'x') ;
-  if (logscale === true) {  // logscale can be null so we test for true explicitly.
-    var logr0 = Dygraph.log10(xRange[0]);
-    var logr1 = Dygraph.log10(xRange[1]);
-    pct = (Dygraph.log10(x) - logr0) / (logr1 - logr0);
-  } else {
-    // x - xRange[0] is unit distance from the left.
-    // xRange[1] - xRange[0] is the scale of the range.
-    // The full expression below is the % from the left.
-    pct = (x - xRange[0]) / (xRange[1] - xRange[0]);
-  }
-  return pct;
-};
-
-/**
- * Returns the number of columns (including the independent variable).
- * @return {number} The number of columns.
- */
-Dygraph.prototype.numColumns = function() {
-  if (!this.rawData_) return 0;
-  return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
-};
-
-/**
- * Returns the number of rows (excluding any header/label row).
- * @return {number} The number of rows, less any header.
- */
-Dygraph.prototype.numRows = function() {
-  if (!this.rawData_) return 0;
-  return this.rawData_.length;
-};
-
-/**
- * Returns the value in the given row and column. If the row and column exceed
- * the bounds on the data, returns null. Also returns null if the value is
- * missing.
- * @param {number} row The row number of the data (0-based). Row 0 is the
- *     first row of data, not a header row.
- * @param {number} col The column number of the data (0-based)
- * @return {number} The value in the specified cell or null if the row/col
- *     were out of range.
- */
-Dygraph.prototype.getValue = function(row, col) {
-  if (row < 0 || row > this.rawData_.length) return null;
-  if (col < 0 || col > this.rawData_[row].length) return null;
-
-  return this.rawData_[row][col];
-};
-
-/**
- * Generates interface elements for the Dygraph: a containing div, a div to
- * display the current point, and a textbox to adjust the rolling average
- * period. Also creates the Renderer/Layout elements.
- * @private
- */
-Dygraph.prototype.createInterface_ = function() {
-  // Create the all-enclosing graph div
-  var enclosing = this.maindiv_;
-
-  this.graphDiv = document.createElement("div");
-
-  // TODO(danvk): any other styles that are useful to set here?
-  this.graphDiv.style.textAlign = 'left';  // This is a CSS "reset"
-  this.graphDiv.style.position = 'relative';
-  enclosing.appendChild(this.graphDiv);
-
-  // Create the canvas for interactive parts of the chart.
-  this.canvas_ = Dygraph.createCanvas();
-  this.canvas_.style.position = "absolute";
-
-  // ... and for static parts of the chart.
-  this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
-
-  this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
-  this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
-
-  this.resizeElements_();
-
-  // The interactive parts of the graph are drawn on top of the chart.
-  this.graphDiv.appendChild(this.hidden_);
-  this.graphDiv.appendChild(this.canvas_);
-  this.mouseEventElement_ = this.createMouseEventElement_();
-
-  // Create the grapher
-  this.layout_ = new DygraphLayout(this);
-
-  var dygraph = this;
-
-  this.mouseMoveHandler_ = function(e) {
-    dygraph.mouseMove_(e);
-  };
-
-  this.mouseOutHandler_ = function(e) {
-    // The mouse has left the chart if:
-    // 1. e.target is inside the chart
-    // 2. e.relatedTarget is outside the chart
-    var target = e.target || e.fromElement;
-    var relatedTarget = e.relatedTarget || e.toElement;
-    if (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) &&
-        !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) {
-      dygraph.mouseOut_(e);
-    }
-  };
-
-  this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_);
-  this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
-
-  // Don't recreate and register the resize handler on subsequent calls.
-  // This happens when the graph is resized.
-  if (!this.resizeHandler_) {
-    this.resizeHandler_ = function(e) {
-      dygraph.resize();
-    };
-
-    // Update when the window is resized.
-    // TODO(danvk): drop frames depending on complexity of the chart.
-    this.addAndTrackEvent(window, 'resize', this.resizeHandler_);
-  }
-};
-
-Dygraph.prototype.resizeElements_ = function() {
-  this.graphDiv.style.width = this.width_ + "px";
-  this.graphDiv.style.height = this.height_ + "px";
-
-  var canvasScale = Dygraph.getContextPixelRatio(this.canvas_ctx_);
-  this.canvas_.width = this.width_ * canvasScale;
-  this.canvas_.height = this.height_ * canvasScale;
-  this.canvas_.style.width = this.width_ + "px";    // for IE
-  this.canvas_.style.height = this.height_ + "px";  // for IE
-  if (canvasScale !== 1) {
-    this.canvas_ctx_.scale(canvasScale, canvasScale);
-  }
-
-  var hiddenScale = Dygraph.getContextPixelRatio(this.hidden_ctx_);
-  this.hidden_.width = this.width_ * hiddenScale;
-  this.hidden_.height = this.height_ * hiddenScale;
-  this.hidden_.style.width = this.width_ + "px";    // for IE
-  this.hidden_.style.height = this.height_ + "px";  // for IE
-  if (hiddenScale !== 1) {
-    this.hidden_ctx_.scale(hiddenScale, hiddenScale);
-  }
-};
-
-/**
- * Detach DOM elements in the dygraph and null out all data references.
- * Calling this when you're done with a dygraph can dramatically reduce memory
- * usage. See, e.g., the tests/perf.html example.
- */
-Dygraph.prototype.destroy = function() {
-  this.canvas_ctx_.restore();
-  this.hidden_ctx_.restore();
-
-  // Destroy any plugins, in the reverse order that they were registered.
-  for (var i = this.plugins_.length - 1; i >= 0; i--) {
-    var p = this.plugins_.pop();
-    if (p.plugin.destroy) p.plugin.destroy();
-  }
-
-  var removeRecursive = function(node) {
-    while (node.hasChildNodes()) {
-      removeRecursive(node.firstChild);
-      node.removeChild(node.firstChild);
-    }
-  };
-
-  this.removeTrackedEvents_();
-
-  // remove mouse event handlers (This may not be necessary anymore)
-  Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_);
-  Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
-
-  // remove window handlers
-  Dygraph.removeEvent(window,'resize', this.resizeHandler_);
-  this.resizeHandler_ = null;
-
-  removeRecursive(this.maindiv_);
-
-  var nullOut = function(obj) {
-    for (var n in obj) {
-      if (typeof(obj[n]) === 'object') {
-        obj[n] = null;
-      }
-    }
-  };
-  // These may not all be necessary, but it can't hurt...
-  nullOut(this.layout_);
-  nullOut(this.plotter_);
-  nullOut(this);
-};
-
-/**
- * Creates the canvas on which the chart will be drawn. Only the Renderer ever
- * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
- * or the zoom rectangles) is done on this.canvas_.
- * @param {Object} canvas The Dygraph canvas over which to overlay the plot
- * @return {Object} The newly-created canvas
- * @private
- */
-Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
-  var h = Dygraph.createCanvas();
-  h.style.position = "absolute";
-  // TODO(danvk): h should be offset from canvas. canvas needs to include
-  // some extra area to make it easier to zoom in on the far left and far
-  // right. h needs to be precisely the plot area, so that clipping occurs.
-  h.style.top = canvas.style.top;
-  h.style.left = canvas.style.left;
-  h.width = this.width_;
-  h.height = this.height_;
-  h.style.width = this.width_ + "px";    // for IE
-  h.style.height = this.height_ + "px";  // for IE
-  return h;
-};
-
-/**
- * Creates an overlay element used to handle mouse events.
- * @return {Object} The mouse event element.
- * @private
- */
-Dygraph.prototype.createMouseEventElement_ = function() {
-  return this.canvas_;
-};
-
-/**
- * Generate a set of distinct colors for the data series. This is done with a
- * color wheel. Saturation/Value are customizable, and the hue is
- * equally-spaced around the color wheel. If a custom set of colors is
- * specified, that is used instead.
- * @private
- */
-Dygraph.prototype.setColors_ = function() {
-  var labels = this.getLabels();
-  var num = labels.length - 1;
-  this.colors_ = [];
-  this.colorsMap_ = {};
-
-  // These are used for when no custom colors are specified.
-  var sat = this.getNumericOption('colorSaturation') || 1.0;
-  var val = this.getNumericOption('colorValue') || 0.5;
-  var half = Math.ceil(num / 2);
-
-  var colors = this.getOption('colors');
-  var visibility = this.visibility();
-  for (var i = 0; i < num; i++) {
-    if (!visibility[i]) {
-      continue;
-    }
-    var label = labels[i + 1];
-    var colorStr = this.attributes_.getForSeries('color', label);
-    if (!colorStr) {
-      if (colors) {
-        colorStr = colors[i % colors.length];
-      } else {
-        // alternate colors for high contrast.
-        var idx = i % 2 ? (half + (i + 1)/ 2) : Math.ceil((i + 1) / 2);
-        var hue = (1.0 * idx / (1 + num));
-        colorStr = Dygraph.hsvToRGB(hue, sat, val);
-      }
-    }
-    this.colors_.push(colorStr);
-    this.colorsMap_[label] = colorStr;
-  }
-};
-
-/**
- * Return the list of colors. This is either the list of colors passed in the
- * attributes or the autogenerated list of rgb(r,g,b) strings.
- * This does not return colors for invisible series.
- * @return {Array.<string>} The list of colors.
- */
-Dygraph.prototype.getColors = function() {
-  return this.colors_;
-};
-
-/**
- * Returns a few attributes of a series, i.e. its color, its visibility, which
- * axis it's assigned to, and its column in the original data.
- * Returns null if the series does not exist.
- * Otherwise, returns an object with column, visibility, color and axis properties.
- * The "axis" property will be set to 1 for y1 and 2 for y2.
- * The "column" property can be fed back into getValue(row, column) to get
- * values for this series.
- */
-Dygraph.prototype.getPropertiesForSeries = function(series_name) {
-  var idx = -1;
-  var labels = this.getLabels();
-  for (var i = 1; i < labels.length; i++) {
-    if (labels[i] == series_name) {
-      idx = i;
-      break;
-    }
-  }
-  if (idx == -1) return null;
-
-  return {
-    name: series_name,
-    column: idx,
-    visible: this.visibility()[idx - 1],
-    color: this.colorsMap_[series_name],
-    axis: 1 + this.attributes_.axisForSeries(series_name)
-  };
-};
-
-/**
- * Create the text box to adjust the averaging period
- * @private
- */
-Dygraph.prototype.createRollInterface_ = function() {
-  // Create a roller if one doesn't exist already.
-  if (!this.roller_) {
-    this.roller_ = document.createElement("input");
-    this.roller_.type = "text";
-    this.roller_.style.display = "none";
-    this.graphDiv.appendChild(this.roller_);
-  }
-
-  var display = this.getBooleanOption('showRoller') ? 'block' : 'none';
-
-  var area = this.plotter_.area;
-  var textAttr = { "position": "absolute",
-                   "zIndex": 10,
-                   "top": (area.y + area.h - 25) + "px",
-                   "left": (area.x + 1) + "px",
-                   "display": display
-                  };
-  this.roller_.size = "2";
-  this.roller_.value = this.rollPeriod_;
-  for (var name in textAttr) {
-    if (textAttr.hasOwnProperty(name)) {
-      this.roller_.style[name] = textAttr[name];
-    }
-  }
-
-  var dygraph = this;
-  this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
-};
-
-/**
- * Set up all the mouse handlers needed to capture dragging behavior for zoom
- * events.
- * @private
- */
-Dygraph.prototype.createDragInterface_ = function() {
-  var context = {
-    // Tracks whether the mouse is down right now
-    isZooming: false,
-    isPanning: false,  // is this drag part of a pan?
-    is2DPan: false,    // if so, is that pan 1- or 2-dimensional?
-    dragStartX: null, // pixel coordinates
-    dragStartY: null, // pixel coordinates
-    dragEndX: null, // pixel coordinates
-    dragEndY: null, // pixel coordinates
-    dragDirection: null,
-    prevEndX: null, // pixel coordinates
-    prevEndY: null, // pixel coordinates
-    prevDragDirection: null,
-    cancelNextDblclick: false,  // see comment in dygraph-interaction-model.js
-
-    // The value on the left side of the graph when a pan operation starts.
-    initialLeftmostDate: null,
-
-    // The number of units each pixel spans. (This won't be valid for log
-    // scales)
-    xUnitsPerPixel: null,
-
-    // TODO(danvk): update this comment
-    // The range in second/value units that the viewport encompasses during a
-    // panning operation.
-    dateRange: null,
-
-    // Top-left corner of the canvas, in DOM coords
-    // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
-    px: 0,
-    py: 0,
-
-    // Values for use with panEdgeFraction, which limit how far outside the
-    // graph's data boundaries it can be panned.
-    boundedDates: null, // [minDate, maxDate]
-    boundedValues: null, // [[minValue, maxValue] ...]
-
-    // We cover iframes during mouse interactions. See comments in
-    // dygraph-utils.js for more info on why this is a good idea.
-    tarp: new Dygraph.IFrameTarp(),
-
-    // contextB is the same thing as this context object but renamed.
-    initializeMouseDown: function(event, g, contextB) {
-      // prevents mouse drags from selecting page text.
-      if (event.preventDefault) {
-        event.preventDefault();  // Firefox, Chrome, etc.
-      } else {
-        event.returnValue = false;  // IE
-        event.cancelBubble = true;
-      }
-
-      var canvasPos = Dygraph.findPos(g.canvas_);
-      contextB.px = canvasPos.x;
-      contextB.py = canvasPos.y;
-      contextB.dragStartX = Dygraph.dragGetX_(event, contextB);
-      contextB.dragStartY = Dygraph.dragGetY_(event, contextB);
-      contextB.cancelNextDblclick = false;
-      contextB.tarp.cover();
-    },
-    destroy: function() {
-      var context = this;
-      if (context.isZooming || context.isPanning) {
-        context.isZooming = false;
-        context.dragStartX = null;
-        context.dragStartY = null;
-      }
-
-      if (context.isPanning) {
-        context.isPanning = false;
-        context.draggingDate = null;
-        context.dateRange = null;
-        for (var i = 0; i < self.axes_.length; i++) {
-          delete self.axes_[i].draggingValue;
-          delete self.axes_[i].dragValueRange;
-        }
-      }
-
-      context.tarp.uncover();
-    }
-  };
-
-  var interactionModel = this.getOption("interactionModel");
-
-  // Self is the graph.
-  var self = this;
-
-  // Function that binds the graph and context to the handler.
-  var bindHandler = function(handler) {
-    return function(event) {
-      handler(event, self, context);
-    };
-  };
-
-  for (var eventName in interactionModel) {
-    if (!interactionModel.hasOwnProperty(eventName)) continue;
-    this.addAndTrackEvent(this.mouseEventElement_, eventName,
-        bindHandler(interactionModel[eventName]));
-  }
-
-  // If the user releases the mouse button during a drag, but not over the
-  // canvas, then it doesn't count as a zooming action.
-  if (!interactionModel.willDestroyContextMyself) {
-    var mouseUpHandler = function(event) {
-      context.destroy();
-    };
-
-    this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
-  }
-};
-
-/**
- * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
- * up any previous zoom rectangles that were drawn. This could be optimized to
- * avoid extra redrawing, but it's tricky to avoid interactions with the status
- * dots.
- *
- * @param {number} direction the direction of the zoom rectangle. Acceptable
- *     values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
- * @param {number} startX The X position where the drag started, in canvas
- *     coordinates.
- * @param {number} endX The current X position of the drag, in canvas coords.
- * @param {number} startY The Y position where the drag started, in canvas
- *     coordinates.
- * @param {number} endY The current Y position of the drag, in canvas coords.
- * @param {number} prevDirection the value of direction on the previous call to
- *     this function. Used to avoid excess redrawing
- * @param {number} prevEndX The value of endX on the previous call to this
- *     function. Used to avoid excess redrawing
- * @param {number} prevEndY The value of endY on the previous call to this
- *     function. Used to avoid excess redrawing
- * @private
- */
-Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
-                                           endY, prevDirection, prevEndX,
-                                           prevEndY) {
-  var ctx = this.canvas_ctx_;
-
-  // Clean up from the previous rect if necessary
-  if (prevDirection == Dygraph.HORIZONTAL) {
-    ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y,
-                  Math.abs(startX - prevEndX), this.layout_.getPlotArea().h);
-  } else if (prevDirection == Dygraph.VERTICAL) {
-    ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY),
-                  this.layout_.getPlotArea().w, Math.abs(startY - prevEndY));
-  }
-
-  // Draw a light-grey rectangle to show the new viewing area
-  if (direction == Dygraph.HORIZONTAL) {
-    if (endX && startX) {
-      ctx.fillStyle = "rgba(128,128,128,0.33)";
-      ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y,
-                   Math.abs(endX - startX), this.layout_.getPlotArea().h);
-    }
-  } else if (direction == Dygraph.VERTICAL) {
-    if (endY && startY) {
-      ctx.fillStyle = "rgba(128,128,128,0.33)";
-      ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY),
-                   this.layout_.getPlotArea().w, Math.abs(endY - startY));
-    }
-  }
-};
-
-/**
- * Clear the zoom rectangle (and perform no zoom).
- * @private
- */
-Dygraph.prototype.clearZoomRect_ = function() {
-  this.currentZoomRectArgs_ = null;
-  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
-};
-
-/**
- * Zoom to something containing [lowX, highX]. These are pixel coordinates in
- * the canvas. The exact zoom window may be slightly larger if there are no data
- * points near lowX or highX. Don't confuse this function with doZoomXDates,
- * which accepts dates that match the raw data. This function redraws the graph.
- *
- * @param {number} lowX The leftmost pixel value that should be visible.
- * @param {number} highX The rightmost pixel value that should be visible.
- * @private
- */
-Dygraph.prototype.doZoomX_ = function(lowX, highX) {
-  this.currentZoomRectArgs_ = null;
-  // Find the earliest and latest dates contained in this canvasx range.
-  // Convert the call to date ranges of the raw data.
-  var minDate = this.toDataXCoord(lowX);
-  var maxDate = this.toDataXCoord(highX);
-  this.doZoomXDates_(minDate, maxDate);
-};
-
-/**
- * Zoom to something containing [minDate, maxDate] values. Don't confuse this
- * method with doZoomX which accepts pixel coordinates. This function redraws
- * the graph.
- *
- * @param {number} minDate The minimum date that should be visible.
- * @param {number} maxDate The maximum date that should be visible.
- * @private
- */
-Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
-  // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation
-  // can produce strange effects. Rather than the x-axis transitioning slowly
-  // between values, it can jerk around.)
-  var old_window = this.xAxisRange();
-  var new_window = [minDate, maxDate];
-  this.zoomed_x_ = true;
-  var that = this;
-  this.doAnimatedZoom(old_window, new_window, null, null, function() {
-    if (that.getFunctionOption("zoomCallback")) {
-      that.getFunctionOption("zoomCallback").call(that,
-          minDate, maxDate, that.yAxisRanges());
-    }
-  });
-};
-
-/**
- * Zoom to something containing [lowY, highY]. These are pixel coordinates in
- * the canvas. This function redraws the graph.
- *
- * @param {number} lowY The topmost pixel value that should be visible.
- * @param {number} highY The lowest pixel value that should be visible.
- * @private
- */
-Dygraph.prototype.doZoomY_ = function(lowY, highY) {
-  this.currentZoomRectArgs_ = null;
-  // Find the highest and lowest values in pixel range for each axis.
-  // Note that lowY (in pixels) corresponds to the max Value (in data coords).
-  // This is because pixels increase as you go down on the screen, whereas data
-  // coordinates increase as you go up the screen.
-  var oldValueRanges = this.yAxisRanges();
-  var newValueRanges = [];
-  for (var i = 0; i < this.axes_.length; i++) {
-    var hi = this.toDataYCoord(lowY, i);
-    var low = this.toDataYCoord(highY, i);
-    newValueRanges.push([low, hi]);
-  }
-
-  this.zoomed_y_ = true;
-  var that = this;
-  this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() {
-    if (that.getFunctionOption("zoomCallback")) {
-      var xRange = that.xAxisRange();
-      that.getFunctionOption("zoomCallback").call(that,
-          xRange[0], xRange[1], that.yAxisRanges());
-    }
-  });
-};
-
-/**
- * Transition function to use in animations. Returns values between 0.0
- * (totally old values) and 1.0 (totally new values) for each frame.
- * @private
- */
-Dygraph.zoomAnimationFunction = function(frame, numFrames) {
-  var k = 1.5;
-  return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames));
-};
-
-/**
- * Reset the zoom to the original view coordinates. This is the same as
- * double-clicking on the graph.
- */
-Dygraph.prototype.resetZoom = function() {
-  var dirty = false, dirtyX = false, dirtyY = false;
-  if (this.dateWindow_ !== null) {
-    dirty = true;
-    dirtyX = true;
-  }
-
-  for (var i = 0; i < this.axes_.length; i++) {
-    if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) {
-      dirty = true;
-      dirtyY = true;
-    }
-  }
-
-  // Clear any selection, since it's likely to be drawn in the wrong place.
-  this.clearSelection();
-
-  if (dirty) {
-    this.zoomed_x_ = false;
-    this.zoomed_y_ = false;
-
-    var minDate = this.rawData_[0][0];
-    var maxDate = this.rawData_[this.rawData_.length - 1][0];
-
-    // With only one frame, don't bother calculating extreme ranges.
-    // TODO(danvk): merge this block w/ the code below.
-    if (!this.getBooleanOption("animatedZooms")) {
-      this.dateWindow_ = null;
-      for (i = 0; i < this.axes_.length; i++) {
-        if (this.axes_[i].valueWindow !== null) {
-          delete this.axes_[i].valueWindow;
-        }
-      }
-      this.drawGraph_();
-      if (this.getFunctionOption("zoomCallback")) {
-        this.getFunctionOption("zoomCallback").call(this,
-            minDate, maxDate, this.yAxisRanges());
-      }
-      return;
-    }
-
-    var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null;
-    if (dirtyX) {
-      oldWindow = this.xAxisRange();
-      newWindow = [minDate, maxDate];
-    }
-
-    if (dirtyY) {
-      oldValueRanges = this.yAxisRanges();
-      // TODO(danvk): this is pretty inefficient
-      var packed = this.gatherDatasets_(this.rolledSeries_, null);
-      var extremes = packed.extremes;
-
-      // this has the side-effect of modifying this.axes_.
-      // this doesn't make much sense in this context, but it's convenient (we
-      // need this.axes_[*].extremeValues) and not harmful since we'll be
-      // calling drawGraph_ shortly, which clobbers these values.
-      this.computeYAxisRanges_(extremes);
-
-      newValueRanges = [];
-      for (i = 0; i < this.axes_.length; i++) {
-        var axis = this.axes_[i];
-        newValueRanges.push((axis.valueRange !== null &&
-                             axis.valueRange !== undefined) ?
-                            axis.valueRange : axis.extremeRange);
-      }
-    }
-
-    var that = this;
-    this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges,
-        function() {
-          that.dateWindow_ = null;
-          for (var i = 0; i < that.axes_.length; i++) {
-            if (that.axes_[i].valueWindow !== null) {
-              delete that.axes_[i].valueWindow;
-            }
-          }
-          if (that.getFunctionOption("zoomCallback")) {
-            that.getFunctionOption("zoomCallback").call(that,
-                minDate, maxDate, that.yAxisRanges());
-          }
-        });
-  }
-};
-
-/**
- * Combined animation logic for all zoom functions.
- * either the x parameters or y parameters may be null.
- * @private
- */
-Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) {
-  var steps = this.getBooleanOption("animatedZooms") ?
-      Dygraph.ANIMATION_STEPS : 1;
-
-  var windows = [];
-  var valueRanges = [];
-  var step, frac;
-
-  if (oldXRange !== null && newXRange !== null) {
-    for (step = 1; step <= steps; step++) {
-      frac = Dygraph.zoomAnimationFunction(step, steps);
-      windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0],
-                         oldXRange[1]*(1-frac) + frac*newXRange[1]];
-    }
-  }
-
-  if (oldYRanges !== null && newYRanges !== null) {
-    for (step = 1; step <= steps; step++) {
-      frac = Dygraph.zoomAnimationFunction(step, steps);
-      var thisRange = [];
-      for (var j = 0; j < this.axes_.length; j++) {
-        thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0],
-                        oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]);
-      }
-      valueRanges[step-1] = thisRange;
-    }
-  }
-
-  var that = this;
-  Dygraph.repeatAndCleanup(function(step) {
-    if (valueRanges.length) {
-      for (var i = 0; i < that.axes_.length; i++) {
-        var w = valueRanges[step][i];
-        that.axes_[i].valueWindow = [w[0], w[1]];
-      }
-    }
-    if (windows.length) {
-      that.dateWindow_ = windows[step];
-    }
-    that.drawGraph_();
-  }, steps, Dygraph.ANIMATION_DURATION / steps, callback);
-};
-
-/**
- * Get the current graph's area object.
- *
- * Returns: {x, y, w, h}
- */
-Dygraph.prototype.getArea = function() {
-  return this.plotter_.area;
-};
-
-/**
- * Convert a mouse event to DOM coordinates relative to the graph origin.
- *
- * Returns a two-element array: [X, Y].
- */
-Dygraph.prototype.eventToDomCoords = function(event) {
-  if (event.offsetX && event.offsetY) {
-    return [ event.offsetX, event.offsetY ];
-  } else {
-    var eventElementPos = Dygraph.findPos(this.mouseEventElement_);
-    var canvasx = Dygraph.pageX(event) - eventElementPos.x;
-    var canvasy = Dygraph.pageY(event) - eventElementPos.y;
-    return [canvasx, canvasy];
-  }
-};
-
-/**
- * Given a canvas X coordinate, find the closest row.
- * @param {number} domX graph-relative DOM X coordinate
- * Returns {number} row number.
- * @private
- */
-Dygraph.prototype.findClosestRow = function(domX) {
-  var minDistX = Infinity;
-  var closestRow = -1;
-  var sets = this.layout_.points;
-  for (var i = 0; i < sets.length; i++) {
-    var points = sets[i];
-    var len = points.length;
-    for (var j = 0; j < len; j++) {
-      var point = points[j];
-      if (!Dygraph.isValidPoint(point, true)) continue;
-      var dist = Math.abs(point.canvasx - domX);
-      if (dist < minDistX) {
-        minDistX = dist;
-        closestRow = point.idx;
-      }
-    }
-  }
-
-  return closestRow;
-};
-
-/**
- * Given canvas X,Y coordinates, find the closest point.
- *
- * This finds the individual data point across all visible series
- * that's closest to the supplied DOM coordinates using the standard
- * Euclidean X,Y distance.
- *
- * @param {number} domX graph-relative DOM X coordinate
- * @param {number} domY graph-relative DOM Y coordinate
- * Returns: {row, seriesName, point}
- * @private
- */
-Dygraph.prototype.findClosestPoint = function(domX, domY) {
-  var minDist = Infinity;
-  var dist, dx, dy, point, closestPoint, closestSeries, closestRow;
-  for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) {
-    var points = this.layout_.points[setIdx];
-    for (var i = 0; i < points.length; ++i) {
-      point = points[i];
-      if (!Dygraph.isValidPoint(point)) continue;
-      dx = point.canvasx - domX;
-      dy = point.canvasy - domY;
-      dist = dx * dx + dy * dy;
-      if (dist < minDist) {
-        minDist = dist;
-        closestPoint = point;
-        closestSeries = setIdx;
-        closestRow = point.idx;
-      }
-    }
-  }
-  var name = this.layout_.setNames[closestSeries];
-  return {
-    row: closestRow,
-    seriesName: name,
-    point: closestPoint
-  };
-};
-
-/**
- * Given canvas X,Y coordinates, find the touched area in a stacked graph.
- *
- * This first finds the X data point closest to the supplied DOM X coordinate,
- * then finds the series which puts the Y coordinate on top of its filled area,
- * using linear interpolation between adjacent point pairs.
- *
- * @param {number} domX graph-relative DOM X coordinate
- * @param {number} domY graph-relative DOM Y coordinate
- * Returns: {row, seriesName, point}
- * @private
- */
-Dygraph.prototype.findStackedPoint = function(domX, domY) {
-  var row = this.findClosestRow(domX);
-  var closestPoint, closestSeries;
-  for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
-    var boundary = this.getLeftBoundary_(setIdx);
-    var rowIdx = row - boundary;
-    var points = this.layout_.points[setIdx];
-    if (rowIdx >= points.length) continue;
-    var p1 = points[rowIdx];
-    if (!Dygraph.isValidPoint(p1)) continue;
-    var py = p1.canvasy;
-    if (domX > p1.canvasx && rowIdx + 1 < points.length) {
-      // interpolate series Y value using next point
-      var p2 = points[rowIdx + 1];
-      if (Dygraph.isValidPoint(p2)) {
-        var dx = p2.canvasx - p1.canvasx;
-        if (dx > 0) {
-          var r = (domX - p1.canvasx) / dx;
-          py += r * (p2.canvasy - p1.canvasy);
-        }
-      }
-    } else if (domX < p1.canvasx && rowIdx > 0) {
-      // interpolate series Y value using previous point
-      var p0 = points[rowIdx - 1];
-      if (Dygraph.isValidPoint(p0)) {
-        var dx = p1.canvasx - p0.canvasx;
-        if (dx > 0) {
-          var r = (p1.canvasx - domX) / dx;
-          py += r * (p0.canvasy - p1.canvasy);
-        }
-      }
-    }
-    // Stop if the point (domX, py) is above this series' upper edge
-    if (setIdx === 0 || py < domY) {
-      closestPoint = p1;
-      closestSeries = setIdx;
-    }
-  }
-  var name = this.layout_.setNames[closestSeries];
-  return {
-    row: row,
-    seriesName: name,
-    point: closestPoint
-  };
-};
-
-/**
- * When the mouse moves in the canvas, display information about a nearby data
- * point and draw dots over those points in the data series. This function
- * takes care of cleanup of previously-drawn dots.
- * @param {Object} event The mousemove event from the browser.
- * @private
- */
-Dygraph.prototype.mouseMove_ = function(event) {
-  // This prevents JS errors when mousing over the canvas before data loads.
-  var points = this.layout_.points;
-  if (points === undefined || points === null) return;
-
-  var canvasCoords = this.eventToDomCoords(event);
-  var canvasx = canvasCoords[0];
-  var canvasy = canvasCoords[1];
-
-  var highlightSeriesOpts = this.getOption("highlightSeriesOpts");
-  var selectionChanged = false;
-  if (highlightSeriesOpts && !this.isSeriesLocked()) {
-    var closest;
-    if (this.getBooleanOption("stackedGraph")) {
-      closest = this.findStackedPoint(canvasx, canvasy);
-    } else {
-      closest = this.findClosestPoint(canvasx, canvasy);
-    }
-    selectionChanged = this.setSelection(closest.row, closest.seriesName);
-  } else {
-    var idx = this.findClosestRow(canvasx);
-    selectionChanged = this.setSelection(idx);
-  }
-
-  var callback = this.getFunctionOption("highlightCallback");
-  if (callback && selectionChanged) {
-    callback.call(this, event,
-        this.lastx_,
-        this.selPoints_,
-        this.lastRow_,
-        this.highlightSet_);
-  }
-};
-
-/**
- * Fetch left offset from the specified set index or if not passed, the
- * first defined boundaryIds record (see bug #236).
- * @private
- */
-Dygraph.prototype.getLeftBoundary_ = function(setIdx) {
-  if (this.boundaryIds_[setIdx]) {
-      return this.boundaryIds_[setIdx][0];
-  } else {
-    for (var i = 0; i < this.boundaryIds_.length; i++) {
-      if (this.boundaryIds_[i] !== undefined) {
-        return this.boundaryIds_[i][0];
-      }
-    }
-    return 0;
-  }
-};
-
-Dygraph.prototype.animateSelection_ = function(direction) {
-  var totalSteps = 10;
-  var millis = 30;
-  if (this.fadeLevel === undefined) this.fadeLevel = 0;
-  if (this.animateId === undefined) this.animateId = 0;
-  var start = this.fadeLevel;
-  var steps = direction < 0 ? start : totalSteps - start;
-  if (steps <= 0) {
-    if (this.fadeLevel) {
-      this.updateSelection_(1.0);
-    }
-    return;
-  }
-
-  var thisId = ++this.animateId;
-  var that = this;
-  var cleanupIfClearing = function() {
-    // if we haven't reached fadeLevel 0 in the max frame time,
-    // ensure that the clear happens and just go to 0
-    if (that.fadeLevel !== 0 && direction < 0) {
-      that.fadeLevel = 0;
-      that.clearSelection();
-    }
-  };
-  Dygraph.repeatAndCleanup(
-    function(n) {
-      // ignore simultaneous animations
-      if (that.animateId != thisId) return;
-
-      that.fadeLevel += direction;
-      if (that.fadeLevel === 0) {
-        that.clearSelection();
-      } else {
-        that.updateSelection_(that.fadeLevel / totalSteps);
-      }
-    },
-    steps, millis, cleanupIfClearing);
-};
-
-/**
- * Draw dots over the selectied points in the data series. This function
- * takes care of cleanup of previously-drawn dots.
- * @private
- */
-Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
-  /*var defaultPrevented = */
-  this.cascadeEvents_('select', {
-    selectedRow: this.lastRow_,
-    selectedX: this.lastx_,
-    selectedPoints: this.selPoints_
-  });
-  // TODO(danvk): use defaultPrevented here?
-
-  // Clear the previously drawn vertical, if there is one
-  var i;
-  var ctx = this.canvas_ctx_;
-  if (this.getOption('highlightSeriesOpts')) {
-    ctx.clearRect(0, 0, this.width_, this.height_);
-    var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
-    if (alpha) {
-      // Activating background fade includes an animation effect for a gradual
-      // fade. TODO(klausw): make this independently configurable if it causes
-      // issues? Use a shared preference to control animations?
-      var animateBackgroundFade = true;
-      if (animateBackgroundFade) {
-        if (opt_animFraction === undefined) {
-          // start a new animation
-          this.animateSelection_(1);
-          return;
-        }
-        alpha *= opt_animFraction;
-      }
-      ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
-      ctx.fillRect(0, 0, this.width_, this.height_);
-    }
-
-    // Redraw only the highlighted series in the interactive canvas (not the
-    // static plot canvas, which is where series are usually drawn).
-    this.plotter_._renderLineChart(this.highlightSet_, ctx);
-  } else if (this.previousVerticalX_ >= 0) {
-    // Determine the maximum highlight circle size.
-    var maxCircleSize = 0;
-    var labels = this.attr_('labels');
-    for (i = 1; i < labels.length; i++) {
-      var r = this.getNumericOption('highlightCircleSize', labels[i]);
-      if (r > maxCircleSize) maxCircleSize = r;
-    }
-    var px = this.previousVerticalX_;
-    ctx.clearRect(px - maxCircleSize - 1, 0,
-                  2 * maxCircleSize + 2, this.height_);
-  }
-
-  if (this.selPoints_.length > 0) {
-    // Draw colored circles over the center of each selected point
-    var canvasx = this.selPoints_[0].canvasx;
-    ctx.save();
-    for (i = 0; i < this.selPoints_.length; i++) {
-      var pt = this.selPoints_[i];
-      if (!Dygraph.isOK(pt.canvasy)) continue;
-
-      var circleSize = this.getNumericOption('highlightCircleSize', pt.name);
-      var callback = this.getFunctionOption("drawHighlightPointCallback", pt.name);
-      var color = this.plotter_.colors[pt.name];
-      if (!callback) {
-        callback = Dygraph.Circles.DEFAULT;
-      }
-      ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name);
-      ctx.strokeStyle = color;
-      ctx.fillStyle = color;
-      callback.call(this, this, pt.name, ctx, canvasx, pt.canvasy,
-          color, circleSize, pt.idx);
-    }
-    ctx.restore();
-
-    this.previousVerticalX_ = canvasx;
-  }
-};
-
-/**
- * Manually set the selected points and display information about them in the
- * legend. The selection can be cleared using clearSelection() and queried
- * using getSelection().
- * @param {number} row Row number that should be highlighted (i.e. appear with
- * hover dots on the chart).
- * @param {seriesName} optional series name to highlight that series with the
- * the highlightSeriesOpts setting.
- * @param { locked } optional If true, keep seriesName selected when mousing
- * over the graph, disabling closest-series highlighting. Call clearSelection()
- * to unlock it.
- */
-Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) {
-  // Extract the points we've selected
-  this.selPoints_ = [];
-
-  var changed = false;
-  if (row !== false && row >= 0) {
-    if (row != this.lastRow_) changed = true;
-    this.lastRow_ = row;
-    for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
-      var points = this.layout_.points[setIdx];
-      // Check if the point at the appropriate index is the point we're looking
-      // for.  If it is, just use it, otherwise search the array for a point
-      // in the proper place.
-      var setRow = row - this.getLeftBoundary_(setIdx);
-      if (setRow < points.length && points[setRow].idx == row) {
-        var point = points[setRow];
-        if (point.yval !== null) this.selPoints_.push(point);
-      } else {
-        for (var pointIdx = 0; pointIdx < points.length; ++pointIdx) {
-          var point = points[pointIdx];
-          if (point.idx == row) {
-            if (point.yval !== null) {
-              this.selPoints_.push(point);
-            }
-            break;
-          }
-        }
-      }
-    }
-  } else {
-    if (this.lastRow_ >= 0) changed = true;
-    this.lastRow_ = -1;
-  }
-
-  if (this.selPoints_.length) {
-    this.lastx_ = this.selPoints_[0].xval;
-  } else {
-    this.lastx_ = -1;
-  }
-
-  if (opt_seriesName !== undefined) {
-    if (this.highlightSet_ !== opt_seriesName) changed = true;
-    this.highlightSet_ = opt_seriesName;
-  }
-
-  if (opt_locked !== undefined) {
-    this.lockedSet_ = opt_locked;
-  }
-
-  if (changed) {
-    this.updateSelection_(undefined);
-  }
-  return changed;
-};
-
-/**
- * The mouse has left the canvas. Clear out whatever artifacts remain
- * @param {Object} event the mouseout event from the browser.
- * @private
- */
-Dygraph.prototype.mouseOut_ = function(event) {
-  if (this.getFunctionOption("unhighlightCallback")) {
-    this.getFunctionOption("unhighlightCallback").call(this, event);
-  }
-
-  if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_) {
-    this.clearSelection();
-  }
-};
-
-/**
- * Clears the current selection (i.e. points that were highlighted by moving
- * the mouse over the chart).
- */
-Dygraph.prototype.clearSelection = function() {
-  this.cascadeEvents_('deselect', {});
-
-  this.lockedSet_ = false;
-  // Get rid of the overlay data
-  if (this.fadeLevel) {
-    this.animateSelection_(-1);
-    return;
-  }
-  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
-  this.fadeLevel = 0;
-  this.selPoints_ = [];
-  this.lastx_ = -1;
-  this.lastRow_ = -1;
-  this.highlightSet_ = null;
-};
-
-/**
- * Returns the number of the currently selected row. To get data for this row,
- * you can use the getValue method.
- * @return {number} row number, or -1 if nothing is selected
- */
-Dygraph.prototype.getSelection = function() {
-  if (!this.selPoints_ || this.selPoints_.length < 1) {
-    return -1;
-  }
-
-  for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) {
-    var points = this.layout_.points[setIdx];
-    for (var row = 0; row < points.length; row++) {
-      if (points[row].x == this.selPoints_[0].x) {
-        return points[row].idx;
-      }
-    }
-  }
-  return -1;
-};
-
-/**
- * Returns the name of the currently-highlighted series.
- * Only available when the highlightSeriesOpts option is in use.
- */
-Dygraph.prototype.getHighlightSeries = function() {
-  return this.highlightSet_;
-};
-
-/**
- * Returns true if the currently-highlighted series was locked
- * via setSelection(..., seriesName, true).
- */
-Dygraph.prototype.isSeriesLocked = function() {
-  return this.lockedSet_;
-};
-
-/**
- * Fires when there's data available to be graphed.
- * @param {string} data Raw CSV data to be plotted
- * @private
- */
-Dygraph.prototype.loadedEvent_ = function(data) {
-  this.rawData_ = this.parseCSV_(data);
-  this.cascadeDataDidUpdateEvent_();
-  this.predraw_();
-};
-
-/**
- * Add ticks on the x-axis representing years, months, quarters, weeks, or days
- * @private
- */
-Dygraph.prototype.addXTicks_ = function() {
-  // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
-  var range;
-  if (this.dateWindow_) {
-    range = [this.dateWindow_[0], this.dateWindow_[1]];
-  } else {
-    range = this.xAxisExtremes();
-  }
-
-  var xAxisOptionsView = this.optionsViewForAxis_('x');
-  var xTicks = xAxisOptionsView('ticker')(
-      range[0],
-      range[1],
-      this.plotter_.area.w,  // TODO(danvk): should be area.width
-      xAxisOptionsView,
-      this);
-  // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
-  // console.log(msg);
-  this.layout_.setXTicks(xTicks);
-};
-
-/**
- * Returns the correct handler class for the currently set options.
- * @private
- */
-Dygraph.prototype.getHandlerClass_ = function() {
-  var handlerClass;
-  if (this.attr_('dataHandler')) {
-    handlerClass =  this.attr_('dataHandler');
-  } else if (this.fractions_) {
-    if (this.getBooleanOption('errorBars')) {
-      handlerClass = Dygraph.DataHandlers.FractionsBarsHandler;
-    } else {
-      handlerClass = Dygraph.DataHandlers.DefaultFractionHandler;
-    }
-  } else if (this.getBooleanOption('customBars')) {
-    handlerClass = Dygraph.DataHandlers.CustomBarsHandler;
-  } else if (this.getBooleanOption('errorBars')) {
-    handlerClass = Dygraph.DataHandlers.ErrorBarsHandler;
-  } else {
-    handlerClass = Dygraph.DataHandlers.DefaultHandler;
-  }
-  return handlerClass;
-};
-
-/**
- * @private
- * This function is called once when the chart's data is changed or the options
- * dictionary is updated. It is _not_ called when the user pans or zooms. The
- * idea is that values derived from the chart's data can be computed here,
- * rather than every time the chart is drawn. This includes things like the
- * number of axes, rolling averages, etc.
- */
-Dygraph.prototype.predraw_ = function() {
-  var start = new Date();
-
-  // Create the correct dataHandler
-  this.dataHandler_ = new (this.getHandlerClass_())();
-
-  this.layout_.computePlotArea();
-
-  // TODO(danvk): move more computations out of drawGraph_ and into here.
-  this.computeYAxes_();
-
-  if (!this.is_initial_draw_) {
-    this.canvas_ctx_.restore();
-    this.hidden_ctx_.restore();
-  }
-
-  this.canvas_ctx_.save();
-  this.hidden_ctx_.save();
-
-  // Create a new plotter.
-  this.plotter_ = new DygraphCanvasRenderer(this,
-                                            this.hidden_,
-                                            this.hidden_ctx_,
-                                            this.layout_);
-
-  // The roller sits in the bottom left corner of the chart. We don't know where
-  // this will be until the options are available, so it's positioned here.
-  this.createRollInterface_();
-
-  this.cascadeEvents_('predraw');
-
-  // Convert the raw data (a 2D array) into the internal format and compute
-  // rolling averages.
-  this.rolledSeries_ = [null];  // x-axis is the first series and it's special
-  for (var i = 1; i < this.numColumns(); i++) {
-    // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too.
-    var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_);
-    if (this.rollPeriod_ > 1) {
-      series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_);
-    }
-
-    this.rolledSeries_.push(series);
-  }
-
-  // If the data or options have changed, then we'd better redraw.
-  this.drawGraph_();
-
-  // This is used to determine whether to do various animations.
-  var end = new Date();
-  this.drawingTimeMs_ = (end - start);
-};
-
-/**
- * Point structure.
- *
- * xval_* and yval_* are the original unscaled data values,
- * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
- * yval_stacked is the cumulative Y value used for stacking graphs,
- * and bottom/top/minus/plus are used for error bar graphs.
- *
- * @typedef {{
- *     idx: number,
- *     name: string,
- *     x: ?number,
- *     xval: ?number,
- *     y_bottom: ?number,
- *     y: ?number,
- *     y_stacked: ?number,
- *     y_top: ?number,
- *     yval_minus: ?number,
- *     yval: ?number,
- *     yval_plus: ?number,
- *     yval_stacked
- * }}
- */
-Dygraph.PointType = undefined;
-
-/**
- * Calculates point stacking for stackedGraph=true.
- *
- * For stacking purposes, interpolate or extend neighboring data across
- * NaN values based on stackedGraphNaNFill settings. This is for display
- * only, the underlying data value as shown in the legend remains NaN.
- *
- * @param {Array.<Dygraph.PointType>} points Point array for a single series.
- *     Updates each Point's yval_stacked property.
- * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
- *     values for the series seen so far. Index is the row number. Updated
- *     based on the current series's values.
- * @param {Array.<number>} seriesExtremes Min and max values, updated
- *     to reflect the stacked values.
- * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
- *     'none'.
- * @private
- */
-Dygraph.stackPoints_ = function(
-    points, cumulativeYval, seriesExtremes, fillMethod) {
-  var lastXval = null;
-  var prevPoint = null;
-  var nextPoint = null;
-  var nextPointIdx = -1;
-
-  // Find the next stackable point starting from the given index.
-  var updateNextPoint = function(idx) {
-    // If we've previously found a non-NaN point and haven't gone past it yet,
-    // just use that.
-    if (nextPointIdx >= idx) return;
-
-    // We haven't found a non-NaN point yet or have moved past it,
-    // look towards the right to find a non-NaN point.
-    for (var j = idx; j < points.length; ++j) {
-      // Clear out a previously-found point (if any) since it's no longer
-      // valid, we shouldn't use it for interpolation anymore.
-      nextPoint = null;
-      if (!isNaN(points[j].yval) && points[j].yval !== null) {
-        nextPointIdx = j;
-        nextPoint = points[j];
-        break;
-      }
-    }
-  };
-
-  for (var i = 0; i < points.length; ++i) {
-    var point = points[i];
-    var xval = point.xval;
-    if (cumulativeYval[xval] === undefined) {
-      cumulativeYval[xval] = 0;
-    }
-
-    var actualYval = point.yval;
-    if (isNaN(actualYval) || actualYval === null) {
-      if(fillMethod == 'none') {
-        actualYval = 0;
-      } else {
-        // Interpolate/extend for stacking purposes if possible.
-        updateNextPoint(i);
-        if (prevPoint && nextPoint && fillMethod != 'none') {
-          // Use linear interpolation between prevPoint and nextPoint.
-          actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) *
-              ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval));
-        } else if (prevPoint && fillMethod == 'all') {
-          actualYval = prevPoint.yval;
-        } else if (nextPoint && fillMethod == 'all') {
-          actualYval = nextPoint.yval;
-        } else {
-          actualYval = 0;
-        }
-      }
-    } else {
-      prevPoint = point;
-    }
-
-    var stackedYval = cumulativeYval[xval];
-    if (lastXval != xval) {
-      // If an x-value is repeated, we ignore the duplicates.
-      stackedYval += actualYval;
-      cumulativeYval[xval] = stackedYval;
-    }
-    lastXval = xval;
-
-    point.yval_stacked = stackedYval;
-
-    if (stackedYval > seriesExtremes[1]) {
-      seriesExtremes[1] = stackedYval;
-    }
-    if (stackedYval < seriesExtremes[0]) {
-      seriesExtremes[0] = stackedYval;
-    }
-  }
-};
-
-
-/**
- * Loop over all fields and create datasets, calculating extreme y-values for
- * each series and extreme x-indices as we go.
- *
- * dateWindow is passed in as an explicit parameter so that we can compute
- * extreme values "speculatively", i.e. without actually setting state on the
- * dygraph.
- *
- * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
- *     rolledSeries[seriesIndex][row] = raw point, where
- *     seriesIndex is the column number starting with 1, and
- *     rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
- * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
- * @return {{
- *     points: Array.<Array.<Dygraph.PointType>>,
- *     seriesExtremes: Array.<Array.<number>>,
- *     boundaryIds: Array.<number>}}
- * @private
- */
-Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
-  var boundaryIds = [];
-  var points = [];
-  var cumulativeYval = [];  // For stacked series.
-  var extremes = {};  // series name -> [low, high]
-  var seriesIdx, sampleIdx;
-  var firstIdx, lastIdx;
-  var axisIdx;
-
-  // Loop over the fields (series).  Go from the last to the first,
-  // because if they're stacked that's how we accumulate the values.
-  var num_series = rolledSeries.length - 1;
-  var series;
-  for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) {
-    if (!this.visibility()[seriesIdx - 1]) continue;
-
-    // Prune down to the desired range, if necessary (for zooming)
-    // Because there can be lines going to points outside of the visible area,
-    // we actually prune to visible points, plus one on either side.
-    if (dateWindow) {
-      series = rolledSeries[seriesIdx];
-      var low = dateWindow[0];
-      var high = dateWindow[1];
-
-      // TODO(danvk): do binary search instead of linear search.
-      // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
-      firstIdx = null;
-      lastIdx = null;
-      for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) {
-        if (series[sampleIdx][0] >= low && firstIdx === null) {
-          firstIdx = sampleIdx;
-        }
-        if (series[sampleIdx][0] <= high) {
-          lastIdx = sampleIdx;
-        }
-      }
-
-      if (firstIdx === null) firstIdx = 0;
-      var correctedFirstIdx = firstIdx;
-      var isInvalidValue = true;
-      while (isInvalidValue && correctedFirstIdx > 0) {
-        correctedFirstIdx--;
-        // check if the y value is null.
-        isInvalidValue = series[correctedFirstIdx][1] === null;
-      }
-
-      if (lastIdx === null) lastIdx = series.length - 1;
-      var correctedLastIdx = lastIdx;
-      isInvalidValue = true;
-      while (isInvalidValue && correctedLastIdx < series.length - 1) {
-        correctedLastIdx++;
-        isInvalidValue = series[correctedLastIdx][1] === null;
-      }
-
-      if (correctedFirstIdx!==firstIdx) {
-        firstIdx = correctedFirstIdx;
-      }
-      if (correctedLastIdx !== lastIdx) {
-        lastIdx = correctedLastIdx;
-      }
-
-      boundaryIds[seriesIdx-1] = [firstIdx, lastIdx];
-
-      // .slice's end is exclusive, we want to include lastIdx.
-      series = series.slice(firstIdx, lastIdx + 1);
-    } else {
-      series = rolledSeries[seriesIdx];
-      boundaryIds[seriesIdx-1] = [0, series.length-1];
-    }
-
-    var seriesName = this.attr_("labels")[seriesIdx];
-    var seriesExtremes = this.dataHandler_.getExtremeYValues(series,
-        dateWindow, this.getBooleanOption("stepPlot",seriesName));
-
-    var seriesPoints = this.dataHandler_.seriesToPoints(series,
-        seriesName, boundaryIds[seriesIdx-1][0]);
-
-    if (this.getBooleanOption("stackedGraph")) {
-      axisIdx = this.attributes_.axisForSeries(seriesName);
-      if (cumulativeYval[axisIdx] === undefined) {
-        cumulativeYval[axisIdx] = [];
-      }
-      Dygraph.stackPoints_(seriesPoints, cumulativeYval[axisIdx], seriesExtremes,
-                           this.getBooleanOption("stackedGraphNaNFill"));
-    }
-
-    extremes[seriesName] = seriesExtremes;
-    points[seriesIdx] = seriesPoints;
-  }
-
-  return { points: points, extremes: extremes, boundaryIds: boundaryIds };
-};
-
-/**
- * Update the graph with new data. This method is called when the viewing area
- * has changed. If the underlying data or options have changed, predraw_ will
- * be called before drawGraph_ is called.
- *
- * @private
- */
-Dygraph.prototype.drawGraph_ = function() {
-  var start = new Date();
-
-  // This is used to set the second parameter to drawCallback, below.
-  var is_initial_draw = this.is_initial_draw_;
-  this.is_initial_draw_ = false;
-
-  this.layout_.removeAllDatasets();
-  this.setColors_();
-  this.attrs_.pointSize = 0.5 * this.getNumericOption('highlightCircleSize');
-
-  var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
-  var points = packed.points;
-  var extremes = packed.extremes;
-  this.boundaryIds_ = packed.boundaryIds;
-
-  this.setIndexByName_ = {};
-  var labels = this.attr_("labels");
-  if (labels.length > 0) {
-    this.setIndexByName_[labels[0]] = 0;
-  }
-  var dataIdx = 0;
-  for (var i = 1; i < points.length; i++) {
-    this.setIndexByName_[labels[i]] = i;
-    if (!this.visibility()[i - 1]) continue;
-    this.layout_.addDataset(labels[i], points[i]);
-    this.datasetIndex_[i] = dataIdx++;
-  }
-
-  this.computeYAxisRanges_(extremes);
-  this.layout_.setYAxes(this.axes_);
-
-  this.addXTicks_();
-
-  // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
-  var tmp_zoomed_x = this.zoomed_x_;
-  // Tell PlotKit to use this new data and render itself
-  this.zoomed_x_ = tmp_zoomed_x;
-  this.layout_.evaluate();
-  this.renderGraph_(is_initial_draw);
-
-  if (this.getStringOption("timingName")) {
-    var end = new Date();
-    console.log(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms");
-  }
-};
-
-/**
- * This does the work of drawing the chart. It assumes that the layout and axis
- * scales have already been set (e.g. by predraw_).
- *
- * @private
- */
-Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
-  this.cascadeEvents_('clearChart');
-  this.plotter_.clear();
-
-  if (this.getFunctionOption('underlayCallback')) {
-    // NOTE: we pass the dygraph object to this callback twice to avoid breaking
-    // users who expect a deprecated form of this callback.
-    this.getFunctionOption('underlayCallback').call(this,
-        this.hidden_ctx_, this.layout_.getPlotArea(), this, this);
-  }
-
-  var e = {
-    canvas: this.hidden_,
-    drawingContext: this.hidden_ctx_
-  };
-  this.cascadeEvents_('willDrawChart', e);
-  this.plotter_.render();
-  this.cascadeEvents_('didDrawChart', e);
-  this.lastRow_ = -1;  // because plugins/legend.js clears the legend
-
-  // TODO(danvk): is this a performance bottleneck when panning?
-  // The interaction canvas should already be empty in that situation.
-  this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_);
-
-  if (this.getFunctionOption("drawCallback") !== null) {
-    this.getFunctionOption("drawCallback")(this, is_initial_draw);
-  }
-  if (is_initial_draw) {
-    this.readyFired_ = true;
-    while (this.readyFns_.length > 0) {
-      var fn = this.readyFns_.pop();
-      fn(this);
-    }
-  }
-};
-
-/**
- * @private
- * Determine properties of the y-axes which are independent of the data
- * currently being displayed. This includes things like the number of axes and
- * the style of the axes. It does not include the range of each axis and its
- * tick marks.
- * This fills in this.axes_.
- * axes_ = [ { options } ]
- *   indices are into the axes_ array.
- */
-Dygraph.prototype.computeYAxes_ = function() {
-  // Preserve valueWindow settings if they exist, and if the user hasn't
-  // specified a new valueRange.
-  var valueWindows, axis, index, opts, v;
-  if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
-    valueWindows = [];
-    for (index = 0; index < this.axes_.length; index++) {
-      valueWindows.push(this.axes_[index].valueWindow);
-    }
-  }
-
-  // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
-  // data computation as well as options storage.
-  // Go through once and add all the axes.
-  this.axes_ = [];
-
-  for (axis = 0; axis < this.attributes_.numAxes(); axis++) {
-    // Add a new axis, making a copy of its per-axis options.
-    opts = { g : this };
-    Dygraph.update(opts, this.attributes_.axisOptions(axis));
-    this.axes_[axis] = opts;
-  }
-
-
-  // Copy global valueRange option over to the first axis.
-  // NOTE(konigsberg): Are these two statements necessary?
-  // I tried removing it. The automated tests pass, and manually
-  // messing with tests/zoom.html showed no trouble.
-  v = this.attr_('valueRange');
-  if (v) this.axes_[0].valueRange = v;
-
-  if (valueWindows !== undefined) {
-    // Restore valueWindow settings.
-
-    // When going from two axes back to one, we only restore
-    // one axis.
-    var idxCount = Math.min(valueWindows.length, this.axes_.length);
-
-    for (index = 0; index < idxCount; index++) {
-      this.axes_[index].valueWindow = valueWindows[index];
-    }
-  }
-
-  for (axis = 0; axis < this.axes_.length; axis++) {
-    if (axis === 0) {
-      opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
-      v = opts("valueRange");
-      if (v) this.axes_[axis].valueRange = v;
-    } else {  // To keep old behavior
-      var axes = this.user_attrs_.axes;
-      if (axes && axes.y2) {
-        v = axes.y2.valueRange;
-        if (v) this.axes_[axis].valueRange = v;
-      }
-    }
-  }
-};
-
-/**
- * Returns the number of y-axes on the chart.
- * @return {number} the number of axes.
- */
-Dygraph.prototype.numAxes = function() {
-  return this.attributes_.numAxes();
-};
-
-/**
- * @private
- * Returns axis properties for the given series.
- * @param {string} setName The name of the series for which to get axis
- * properties, e.g. 'Y1'.
- * @return {Object} The axis properties.
- */
-Dygraph.prototype.axisPropertiesForSeries = function(series) {
-  // TODO(danvk): handle errors.
-  return this.axes_[this.attributes_.axisForSeries(series)];
-};
-
-/**
- * @private
- * Determine the value range and tick marks for each axis.
- * @param {Object} extremes A mapping from seriesName -> [low, high]
- * This fills in the valueRange and ticks fields in each entry of this.axes_.
- */
-Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
-  var isNullUndefinedOrNaN = function(num) {
-    return isNaN(parseFloat(num));
-  };
-  var numAxes = this.attributes_.numAxes();
-  var ypadCompat, span, series, ypad;
-
-  var p_axis;
-
-  // Compute extreme values, a span and tick marks for each axis.
-  for (var i = 0; i < numAxes; i++) {
-    var axis = this.axes_[i];
-    var logscale = this.attributes_.getForAxis("logscale", i);
-    var includeZero = this.attributes_.getForAxis("includeZero", i);
-    var independentTicks = this.attributes_.getForAxis("independentTicks", i);
-    series = this.attributes_.seriesForAxis(i);
-
-    // Add some padding. This supports two Y padding operation modes:
-    //
-    // - backwards compatible (yRangePad not set):
-    //   10% padding for automatic Y ranges, but not for user-supplied
-    //   ranges, and move a close-to-zero edge to zero except if
-    //   avoidMinZero is set, since drawing at the edge results in
-    //   invisible lines. Unfortunately lines drawn at the edge of a
-    //   user-supplied range will still be invisible. If logscale is
-    //   set, add a variable amount of padding at the top but
-    //   none at the bottom.
-    //
-    // - new-style (yRangePad set by the user):
-    //   always add the specified Y padding.
-    //
-    ypadCompat = true;
-    ypad = 0.1; // add 10%
-    if (this.getNumericOption('yRangePad') !== null) {
-      ypadCompat = false;
-      // Convert pixel padding to ratio
-      ypad = this.getNumericOption('yRangePad') / this.plotter_.area.h;
-    }
-
-    if (series.length === 0) {
-      // If no series are defined or visible then use a reasonable default
-      axis.extremeRange = [0, 1];
-    } else {
-      // Calculate the extremes of extremes.
-      var minY = Infinity;  // extremes[series[0]][0];
-      var maxY = -Infinity;  // extremes[series[0]][1];
-      var extremeMinY, extremeMaxY;
-
-      for (var j = 0; j < series.length; j++) {
-        // this skips invisible series
-        if (!extremes.hasOwnProperty(series[j])) continue;
-
-        // Only use valid extremes to stop null data series' from corrupting the scale.
-        extremeMinY = extremes[series[j]][0];
-        if (extremeMinY !== null) {
-          minY = Math.min(extremeMinY, minY);
-        }
-        extremeMaxY = extremes[series[j]][1];
-        if (extremeMaxY !== null) {
-          maxY = Math.max(extremeMaxY, maxY);
-        }
-      }
-
-      // Include zero if requested by the user.
-      if (includeZero && !logscale) {
-        if (minY > 0) minY = 0;
-        if (maxY < 0) maxY = 0;
-      }
-
-      // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
-      if (minY == Infinity) minY = 0;
-      if (maxY == -Infinity) maxY = 1;
-
-      span = maxY - minY;
-      // special case: if we have no sense of scale, center on the sole value.
-      if (span === 0) {
-        if (maxY !== 0) {
-          span = Math.abs(maxY);
-        } else {
-          // ... and if the sole value is zero, use range 0-1.
-          maxY = 1;
-          span = 1;
-        }
-      }
-
-      var maxAxisY, minAxisY;
-      if (logscale) {
-        if (ypadCompat) {
-          maxAxisY = maxY + ypad * span;
-          minAxisY = minY;
-        } else {
-          var logpad = Math.exp(Math.log(span) * ypad);
-          maxAxisY = maxY * logpad;
-          minAxisY = minY / logpad;
-        }
-      } else {
-        maxAxisY = maxY + ypad * span;
-        minAxisY = minY - ypad * span;
-
-        // Backwards-compatible behavior: Move the span to start or end at zero if it's
-        // close to zero, but not if avoidMinZero is set.
-        if (ypadCompat && !this.getBooleanOption("avoidMinZero")) {
-          if (minAxisY < 0 && minY >= 0) minAxisY = 0;
-          if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
-        }
-      }
-      axis.extremeRange = [minAxisY, maxAxisY];
-    }
-    if (axis.valueWindow) {
-      // This is only set if the user has zoomed on the y-axis. It is never set
-      // by a user. It takes precedence over axis.valueRange because, if you set
-      // valueRange, you'd still expect to be able to pan.
-      axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
-    } else if (axis.valueRange) {
-      // This is a user-set value range for this axis.
-      var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
-      var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
-      if (!ypadCompat) {
-        if (axis.logscale) {
-          var logpad = Math.exp(Math.log(span) * ypad);
-          y0 *= logpad;
-          y1 /= logpad;
-        } else {
-          span = y1 - y0;
-          y0 -= span * ypad;
-          y1 += span * ypad;
-        }
-      }
-      axis.computedValueRange = [y0, y1];
-    } else {
-      axis.computedValueRange = axis.extremeRange;
-    }
-
-
-    if (independentTicks) {
-      axis.independentTicks = independentTicks;
-      var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
-      var ticker = opts('ticker');
-      axis.ticks = ticker(axis.computedValueRange[0],
-              axis.computedValueRange[1],
-              this.plotter_.area.h,
-              opts,
-              this);
-      // Define the first independent axis as primary axis.
-      if (!p_axis) p_axis = axis;
-    }
-  }
-  if (p_axis === undefined) {
-    throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
-  }
-  // Add ticks. By default, all axes inherit the tick positions of the
-  // primary axis. However, if an axis is specifically marked as having
-  // independent ticks, then that is permissible as well.
-  for (var i = 0; i < numAxes; i++) {
-    var axis = this.axes_[i];
-
-    if (!axis.independentTicks) {
-      var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
-      var ticker = opts('ticker');
-      var p_ticks = p_axis.ticks;
-      var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
-      var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
-      var tick_values = [];
-      for (var k = 0; k < p_ticks.length; k++) {
-        var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale;
-        var y_val = axis.computedValueRange[0] + y_frac * scale;
-        tick_values.push(y_val);
-      }
-
-      axis.ticks = ticker(axis.computedValueRange[0],
-                          axis.computedValueRange[1],
-                          this.plotter_.area.h,
-                          opts,
-                          this,
-                          tick_values);
-    }
-  }
-};
-
-/**
- * Detects the type of the str (date or numeric) and sets the various
- * formatting attributes in this.attrs_ based on this type.
- * @param {string} str An x value.
- * @private
- */
-Dygraph.prototype.detectTypeFromString_ = function(str) {
-  var isDate = false;
-  var dashPos = str.indexOf('-');  // could be 2006-01-01 _or_ 1.0e-2
-  if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) ||
-      str.indexOf('/') >= 0 ||
-      isNaN(parseFloat(str))) {
-    isDate = true;
-  } else if (str.length == 8 && str > '19700101' && str < '20371231') {
-    // TODO(danvk): remove support for this format.
-    isDate = true;
-  }
-
-  this.setXAxisOptions_(isDate);
-};
-
-Dygraph.prototype.setXAxisOptions_ = function(isDate) {
-  if (isDate) {
-    this.attrs_.xValueParser = Dygraph.dateParser;
-    this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter;
-    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter;
-  } else {
-    /** @private (shut up, jsdoc!) */
-    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
-    // TODO(danvk): use Dygraph.numberValueFormatter here?
-    /** @private (shut up, jsdoc!) */
-    this.attrs_.axes.x.valueFormatter = function(x) { return x; };
-    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
-    this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
-  }
-};
-
-/**
- * @private
- * Parses a string in a special csv format.  We expect a csv file where each
- * line is a date point, and the first field in each line is the date string.
- * We also expect that all remaining fields represent series.
- * if the errorBars attribute is set, then interpret the fields as:
- * date, series1, stddev1, series2, stddev2, ...
- * @param {[Object]} data See above.
- *
- * @return [Object] An array with one entry for each row. These entries
- * are an array of cells in that row. The first entry is the parsed x-value for
- * the row. The second, third, etc. are the y-values. These can take on one of
- * three forms, depending on the CSV and constructor parameters:
- * 1. numeric value
- * 2. [ value, stddev ]
- * 3. [ low value, center value, high value ]
- */
-Dygraph.prototype.parseCSV_ = function(data) {
-  var ret = [];
-  var line_delimiter = Dygraph.detectLineDelimiter(data);
-  var lines = data.split(line_delimiter || "\n");
-  var vals, j;
-
-  // Use the default delimiter or fall back to a tab if that makes sense.
-  var delim = this.getStringOption('delimiter');
-  if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
-    delim = '\t';
-  }
-
-  var start = 0;
-  if (!('labels' in this.user_attrs_)) {
-    // User hasn't explicitly set labels, so they're (presumably) in the CSV.
-    start = 1;
-    this.attrs_.labels = lines[0].split(delim);  // NOTE: _not_ user_attrs_.
-    this.attributes_.reparseSeries();
-  }
-  var line_no = 0;
-
-  var xParser;
-  var defaultParserSet = false;  // attempt to auto-detect x value type
-  var expectedCols = this.attr_("labels").length;
-  var outOfOrder = false;
-  for (var i = start; i < lines.length; i++) {
-    var line = lines[i];
-    line_no = i;
-    if (line.length === 0) continue;  // skip blank lines
-    if (line[0] == '#') continue;    // skip comment lines
-    var inFields = line.split(delim);
-    if (inFields.length < 2) continue;
-
-    var fields = [];
-    if (!defaultParserSet) {
-      this.detectTypeFromString_(inFields[0]);
-      xParser = this.getFunctionOption("xValueParser");
-      defaultParserSet = true;
-    }
-    fields[0] = xParser(inFields[0], this);
-
-    // If fractions are expected, parse the numbers as "A/B"
-    if (this.fractions_) {
-      for (j = 1; j < inFields.length; j++) {
-        // TODO(danvk): figure out an appropriate way to flag parse errors.
-        vals = inFields[j].split("/");
-        if (vals.length != 2) {
-          console.error('Expected fractional "num/den" values in CSV data ' +
-                        "but found a value '" + inFields[j] + "' on line " +
-                        (1 + i) + " ('" + line + "') which is not of this form.");
-          fields[j] = [0, 0];
-        } else {
-          fields[j] = [Dygraph.parseFloat_(vals[0], i, line),
-                       Dygraph.parseFloat_(vals[1], i, line)];
-        }
-      }
-    } else if (this.getBooleanOption("errorBars")) {
-      // If there are error bars, values are (value, stddev) pairs
-      if (inFields.length % 2 != 1) {
-        console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
-                      'but line ' + (1 + i) + ' has an odd number of values (' +
-                      (inFields.length - 1) + "): '" + line + "'");
-      }
-      for (j = 1; j < inFields.length; j += 2) {
-        fields[(j + 1) / 2] = [Dygraph.parseFloat_(inFields[j], i, line),
-                               Dygraph.parseFloat_(inFields[j + 1], i, line)];
-      }
-    } else if (this.getBooleanOption("customBars")) {
-      // Bars are a low;center;high tuple
-      for (j = 1; j < inFields.length; j++) {
-        var val = inFields[j];
-        if (/^ *$/.test(val)) {
-          fields[j] = [null, null, null];
-        } else {
-          vals = val.split(";");
-          if (vals.length == 3) {
-            fields[j] = [ Dygraph.parseFloat_(vals[0], i, line),
-                          Dygraph.parseFloat_(vals[1], i, line),
-                          Dygraph.parseFloat_(vals[2], i, line) ];
-          } else {
-            console.warn('When using customBars, values must be either blank ' +
-                         'or "low;center;high" tuples (got "' + val +
-                         '" on line ' + (1+i));
-          }
-        }
-      }
-    } else {
-      // Values are just numbers
-      for (j = 1; j < inFields.length; j++) {
-        fields[j] = Dygraph.parseFloat_(inFields[j], i, line);
-      }
-    }
-    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
-      outOfOrder = true;
-    }
-
-    if (fields.length != expectedCols) {
-      console.error("Number of columns in line " + i + " (" + fields.length +
-                    ") does not agree with number of labels (" + expectedCols +
-                    ") " + line);
-    }
-
-    // If the user specified the 'labels' option and none of the cells of the
-    // first row parsed correctly, then they probably double-specified the
-    // labels. We go with the values set in the option, discard this row and
-    // log a warning to the JS console.
-    if (i === 0 && this.attr_('labels')) {
-      var all_null = true;
-      for (j = 0; all_null && j < fields.length; j++) {
-        if (fields[j]) all_null = false;
-      }
-      if (all_null) {
-        console.warn("The dygraphs 'labels' option is set, but the first row " +
-                     "of CSV data ('" + line + "') appears to also contain " +
-                     "labels. Will drop the CSV labels and use the option " +
-                     "labels.");
-        continue;
-      }
-    }
-    ret.push(fields);
-  }
-
-  if (outOfOrder) {
-    console.warn("CSV is out of order; order it correctly to speed loading.");
-    ret.sort(function(a,b) { return a[0] - b[0]; });
-  }
-
-  return ret;
-};
-
-/**
- * The user has provided their data as a pre-packaged JS array. If the x values
- * are numeric, this is the same as dygraphs' internal format. If the x values
- * are dates, we need to convert them from Date objects to ms since epoch.
- * @param {!Array} data
- * @return {Object} data with numeric x values.
- * @private
- */
-Dygraph.prototype.parseArray_ = function(data) {
-  // Peek at the first x value to see if it's numeric.
-  if (data.length === 0) {
-    console.error("Can't plot empty data set");
-    return null;
-  }
-  if (data[0].length === 0) {
-    console.error("Data set cannot contain an empty row");
-    return null;
-  }
-
-  var i;
-  if (this.attr_("labels") === null) {
-    console.warn("Using default labels. Set labels explicitly via 'labels' " +
-                 "in the options parameter");
-    this.attrs_.labels = [ "X" ];
-    for (i = 1; i < data[0].length; i++) {
-      this.attrs_.labels.push("Y" + i); // Not user_attrs_.
-    }
-    this.attributes_.reparseSeries();
-  } else {
-    var num_labels = this.attr_("labels");
-    if (num_labels.length != data[0].length) {
-      console.error("Mismatch between number of labels (" + num_labels + ")" +
-                    " and number of columns in array (" + data[0].length + ")");
-      return null;
-    }
-  }
-
-  if (Dygraph.isDateLike(data[0][0])) {
-    // Some intelligent defaults for a date x-axis.
-    this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter;
-    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter;
-
-    // Assume they're all dates.
-    var parsedData = Dygraph.clone(data);
-    for (i = 0; i < data.length; i++) {
-      if (parsedData[i].length === 0) {
-        console.error("Row " + (1 + i) + " of data is empty");
-        return null;
-      }
-      if (parsedData[i][0] === null ||
-          typeof(parsedData[i][0].getTime) != 'function' ||
-          isNaN(parsedData[i][0].getTime())) {
-        console.error("x value in row " + (1 + i) + " is not a Date");
-        return null;
-      }
-      parsedData[i][0] = parsedData[i][0].getTime();
-    }
-    return parsedData;
-  } else {
-    // Some intelligent defaults for a numeric x-axis.
-    /** @private (shut up, jsdoc!) */
-    this.attrs_.axes.x.valueFormatter = function(x) { return x; };
-    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
-    return data;
-  }
-};
-
-/**
- * Parses a DataTable object from gviz.
- * The data is expected to have a first column that is either a date or a
- * number. All subsequent columns must be numbers. If there is a clear mismatch
- * between this.xValueParser_ and the type of the first column, it will be
- * fixed. Fills out rawData_.
- * @param {!google.visualization.DataTable} data See above.
- * @private
- */
-Dygraph.prototype.parseDataTable_ = function(data) {
-  var shortTextForAnnotationNum = function(num) {
-    // converts [0-9]+ [A-Z][a-z]*
-    // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
-    // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
-    var shortText = String.fromCharCode(65 /* A */ + num % 26);
-    num = Math.floor(num / 26);
-    while ( num > 0 ) {
-      shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
-      num = Math.floor((num - 1) / 26);
-    }
-    return shortText;
-  };
-
-  var cols = data.getNumberOfColumns();
-  var rows = data.getNumberOfRows();
-
-  var indepType = data.getColumnType(0);
-  if (indepType == 'date' || indepType == 'datetime') {
-    this.attrs_.xValueParser = Dygraph.dateParser;
-    this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter;
-    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter;
-  } else if (indepType == 'number') {
-    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
-    this.attrs_.axes.x.valueFormatter = function(x) { return x; };
-    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
-    this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
-  } else {
-    console.error("only 'date', 'datetime' and 'number' types are supported " +
-                  "for column 1 of DataTable input (Got '" + indepType + "')");
-    return null;
-  }
-
-  // Array of the column indices which contain data (and not annotations).
-  var colIdx = [];
-  var annotationCols = {};  // data index -> [annotation cols]
-  var hasAnnotations = false;
-  var i, j;
-  for (i = 1; i < cols; i++) {
-    var type = data.getColumnType(i);
-    if (type == 'number') {
-      colIdx.push(i);
-    } else if (type == 'string' && this.getBooleanOption('displayAnnotations')) {
-      // This is OK -- it's an annotation column.
-      var dataIdx = colIdx[colIdx.length - 1];
-      if (!annotationCols.hasOwnProperty(dataIdx)) {
-        annotationCols[dataIdx] = [i];
-      } else {
-        annotationCols[dataIdx].push(i);
-      }
-      hasAnnotations = true;
-    } else {
-      console.error("Only 'number' is supported as a dependent type with Gviz." +
-                    " 'string' is only supported if displayAnnotations is true");
-    }
-  }
-
-  // Read column labels
-  // TODO(danvk): add support back for errorBars
-  var labels = [data.getColumnLabel(0)];
-  for (i = 0; i < colIdx.length; i++) {
-    labels.push(data.getColumnLabel(colIdx[i]));
-    if (this.getBooleanOption("errorBars")) i += 1;
-  }
-  this.attrs_.labels = labels;
-  cols = labels.length;
-
-  var ret = [];
-  var outOfOrder = false;
-  var annotations = [];
-  for (i = 0; i < rows; i++) {
-    var row = [];
-    if (typeof(data.getValue(i, 0)) === 'undefined' ||
-        data.getValue(i, 0) === null) {
-      console.warn("Ignoring row " + i +
-                   " of DataTable because of undefined or null first column.");
-      continue;
-    }
-
-    if (indepType == 'date' || indepType == 'datetime') {
-      row.push(data.getValue(i, 0).getTime());
-    } else {
-      row.push(data.getValue(i, 0));
-    }
-    if (!this.getBooleanOption("errorBars")) {
-      for (j = 0; j < colIdx.length; j++) {
-        var col = colIdx[j];
-        row.push(data.getValue(i, col));
-        if (hasAnnotations &&
-            annotationCols.hasOwnProperty(col) &&
-            data.getValue(i, annotationCols[col][0]) !== null) {
-          var ann = {};
-          ann.series = data.getColumnLabel(col);
-          ann.xval = row[0];
-          ann.shortText = shortTextForAnnotationNum(annotations.length);
-          ann.text = '';
-          for (var k = 0; k < annotationCols[col].length; k++) {
-            if (k) ann.text += "\n";
-            ann.text += data.getValue(i, annotationCols[col][k]);
-          }
-          annotations.push(ann);
-        }
-      }
-
-      // Strip out infinities, which give dygraphs problems later on.
-      for (j = 0; j < row.length; j++) {
-        if (!isFinite(row[j])) row[j] = null;
-      }
-    } else {
-      for (j = 0; j < cols - 1; j++) {
-        row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
-      }
-    }
-    if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
-      outOfOrder = true;
-    }
-    ret.push(row);
-  }
-
-  if (outOfOrder) {
-    console.warn("DataTable is out of order; order it correctly to speed loading.");
-    ret.sort(function(a,b) { return a[0] - b[0]; });
-  }
-  this.rawData_ = ret;
-
-  if (annotations.length > 0) {
-    this.setAnnotations(annotations, true);
-  }
-  this.attributes_.reparseSeries();
-};
-
-/**
- * Signals to plugins that the chart data has updated.
- * This happens after the data has updated but before the chart has redrawn.
- */
-Dygraph.prototype.cascadeDataDidUpdateEvent_ = function() {
-  // TODO(danvk): there are some issues checking xAxisRange() and using
-  // toDomCoords from handlers of this event. The visible range should be set
-  // when the chart is drawn, not derived from the data.
-  this.cascadeEvents_('dataDidUpdate', {});
-};
-
-/**
- * Get the CSV data. If it's in a function, call that function. If it's in a
- * file, do an XMLHttpRequest to get it.
- * @private
- */
-Dygraph.prototype.start_ = function() {
-  var data = this.file_;
-
-  // Functions can return references of all other types.
-  if (typeof data == 'function') {
-    data = data();
-  }
-
-  if (Dygraph.isArrayLike(data)) {
-    this.rawData_ = this.parseArray_(data);
-    this.cascadeDataDidUpdateEvent_();
-    this.predraw_();
-  } else if (typeof data == 'object' &&
-             typeof data.getColumnRange == 'function') {
-    // must be a DataTable from gviz.
-    this.parseDataTable_(data);
-    this.cascadeDataDidUpdateEvent_();
-    this.predraw_();
-  } else if (typeof data == 'string') {
-    // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
-    var line_delimiter = Dygraph.detectLineDelimiter(data);
-    if (line_delimiter) {
-      this.loadedEvent_(data);
-    } else {
-      // REMOVE_FOR_IE
-      var req;
-      if (window.XMLHttpRequest) {
-        // Firefox, Opera, IE7, and other browsers will use the native object
-        req = new XMLHttpRequest();
-      } else {
-        // IE 5 and 6 will use the ActiveX control
-        req = new ActiveXObject("Microsoft.XMLHTTP");
-      }
-
-      var caller = this;
-      req.onreadystatechange = function () {
-        if (req.readyState == 4) {
-          if (req.status === 200 ||  // Normal http
-              req.status === 0) {    // Chrome w/ --allow-file-access-from-files
-            caller.loadedEvent_(req.responseText);
-          }
-        }
-      };
-
-      req.open("GET", data, true);
-      req.send(null);
-    }
-  } else {
-    console.error("Unknown data format: " + (typeof data));
-  }
-};
-
-/**
- * Changes various properties of the graph. These can include:
- * <ul>
- * <li>file: changes the source data for the graph</li>
- * <li>errorBars: changes whether the data contains stddev</li>
- * </ul>
- *
- * There's a huge variety of options that can be passed to this method. For a
- * full list, see http://dygraphs.com/options.html.
- *
- * @param {Object} input_attrs The new properties and values
- * @param {boolean} block_redraw Usually the chart is redrawn after every
- *     call to updateOptions(). If you know better, you can pass true to
- *     explicitly block the redraw. This can be useful for chaining
- *     updateOptions() calls, avoiding the occasional infinite loop and
- *     preventing redraws when it's not necessary (e.g. when updating a
- *     callback).
- */
-Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
-  if (typeof(block_redraw) == 'undefined') block_redraw = false;
-
-  // copyUserAttrs_ drops the "file" parameter as a convenience to us.
-  var file = input_attrs.file;
-  var attrs = Dygraph.copyUserAttrs_(input_attrs);
-
-  // TODO(danvk): this is a mess. Move these options into attr_.
-  if ('rollPeriod' in attrs) {
-    this.rollPeriod_ = attrs.rollPeriod;
-  }
-  if ('dateWindow' in attrs) {
-    this.dateWindow_ = attrs.dateWindow;
-    if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
-      this.zoomed_x_ = (attrs.dateWindow !== null);
-    }
-  }
-  if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
-    this.zoomed_y_ = (attrs.valueRange !== null);
-  }
-
-  // TODO(danvk): validate per-series options.
-  // Supported:
-  // strokeWidth
-  // pointSize
-  // drawPoints
-  // highlightCircleSize
-
-  // Check if this set options will require new points.
-  var requiresNewPoints = Dygraph.isPixelChangingOptionList(this.attr_("labels"), attrs);
-
-  Dygraph.updateDeep(this.user_attrs_, attrs);
-
-  this.attributes_.reparseSeries();
-
-  if (file) {
-    // This event indicates that the data is about to change, but hasn't yet.
-    // TODO(danvk): support cancelation of the update via this event.
-    this.cascadeEvents_('dataWillUpdate', {});
-
-    this.file_ = file;
-    if (!block_redraw) this.start_();
-  } else {
-    if (!block_redraw) {
-      if (requiresNewPoints) {
-        this.predraw_();
-      } else {
-        this.renderGraph_(false);
-      }
-    }
-  }
-};
-
-/**
- * Make a copy of input attributes, removing file as a convenience.
- */
-Dygraph.copyUserAttrs_ = function(attrs) {
-  var my_attrs = {};
-  for (var k in attrs) {
-    if (!attrs.hasOwnProperty(k)) continue;
-    if (k == 'file') continue;
-    if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
-  }
-  return my_attrs;
-};
-
-/**
- * Resizes the dygraph. If no parameters are specified, resizes to fill the
- * containing div (which has presumably changed size since the dygraph was
- * instantiated. If the width/height are specified, the div will be resized.
- *
- * This is far more efficient than destroying and re-instantiating a
- * Dygraph, since it doesn't have to reparse the underlying data.
- *
- * @param {number} width Width (in pixels)
- * @param {number} height Height (in pixels)
- */
-Dygraph.prototype.resize = function(width, height) {
-  if (this.resize_lock) {
-    return;
-  }
-  this.resize_lock = true;
-
-  if ((width === null) != (height === null)) {
-    console.warn("Dygraph.resize() should be called with zero parameters or " +
-                 "two non-NULL parameters. Pretending it was zero.");
-    width = height = null;
-  }
-
-  var old_width = this.width_;
-  var old_height = this.height_;
-
-  if (width) {
-    this.maindiv_.style.width = width + "px";
-    this.maindiv_.style.height = height + "px";
-    this.width_ = width;
-    this.height_ = height;
-  } else {
-    this.width_ = this.maindiv_.clientWidth;
-    this.height_ = this.maindiv_.clientHeight;
-  }
-
-  if (old_width != this.width_ || old_height != this.height_) {
-    // Resizing a canvas erases it, even when the size doesn't change, so
-    // any resize needs to be followed by a redraw.
-    this.resizeElements_();
-    this.predraw_();
-  }
-
-  this.resize_lock = false;
-};
-
-/**
- * Adjusts the number of points in the rolling average. Updates the graph to
- * reflect the new averaging period.
- * @param {number} length Number of points over which to average the data.
- */
-Dygraph.prototype.adjustRoll = function(length) {
-  this.rollPeriod_ = length;
-  this.predraw_();
-};
-
-/**
- * Returns a boolean array of visibility statuses.
- */
-Dygraph.prototype.visibility = function() {
-  // Do lazy-initialization, so that this happens after we know the number of
-  // data series.
-  if (!this.getOption("visibility")) {
-    this.attrs_.visibility = [];
-  }
-  // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
-  while (this.getOption("visibility").length < this.numColumns() - 1) {
-    this.attrs_.visibility.push(true);
-  }
-  return this.getOption("visibility");
-};
-
-/**
- * Changes the visibility of one or more series.
- *
- * @param {number|number[]} num the series index or an array of series indices
- * @param {boolean} value true or false, identifying the visibility.
- */
-Dygraph.prototype.setVisibility = function(num, value) {
-  var x = this.visibility();
-
-  if (num.constructor !== Array) num = [num];
-
-  for (var i = 0; i < num.length; i++) {
-    if (num[i] < 0 || num[i] >= x.length) {
-      console.warn("invalid series number in setVisibility: " + num[i]);
-    } else {
-      x[num[i]] = value;
-    }
-  }
-
-  this.predraw_();
-};
-
-/**
- * How large of an area will the dygraph render itself in?
- * This is used for testing.
- * @return A {width: w, height: h} object.
- * @private
- */
-Dygraph.prototype.size = function() {
-  return { width: this.width_, height: this.height_ };
-};
-
-/**
- * Update the list of annotations and redraw the chart.
- * See dygraphs.com/annotations.html for more info on how to use annotations.
- * @param ann {Array} An array of annotation objects.
- * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
- */
-Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
-  // Only add the annotation CSS rule once we know it will be used.
-  Dygraph.addAnnotationRule();
-  this.annotations_ = ann;
-  if (!this.layout_) {
-    console.warn("Tried to setAnnotations before dygraph was ready. " +
-                 "Try setting them in a ready() block. See " +
-                 "dygraphs.com/tests/annotation.html");
-    return;
-  }
-
-  this.layout_.setAnnotations(this.annotations_);
-  if (!suppressDraw) {
-    this.predraw_();
-  }
-};
-
-/**
- * Return the list of annotations.
- */
-Dygraph.prototype.annotations = function() {
-  return this.annotations_;
-};
-
-/**
- * Get the list of label names for this graph. The first column is the
- * x-axis, so the data series names start at index 1.
- *
- * Returns null when labels have not yet been defined.
- */
-Dygraph.prototype.getLabels = function() {
-  var labels = this.attr_("labels");
-  return labels ? labels.slice() : null;
-};
-
-/**
- * Get the index of a series (column) given its name. The first column is the
- * x-axis, so the data series start with index 1.
- */
-Dygraph.prototype.indexFromSetName = function(name) {
-  return this.setIndexByName_[name];
-};
-
-/**
- * Find the row number corresponding to the given x-value.
- * Returns null if there is no such x-value in the data.
- * If there are multiple rows with the same x-value, this will return the
- * first one.
- * @param {number} xVal The x-value to look for (e.g. millis since epoch).
- * @return {?number} The row number, which you can pass to getValue(), or null.
- */
-Dygraph.prototype.getRowForX = function(xVal) {
-  var low = 0,
-      high = this.numRows() - 1;
-
-  while (low <= high) {
-    var idx = (high + low) >> 1;
-    var x = this.getValue(idx, 0);
-    if (x < xVal) {
-      low = idx + 1;
-    } else if (x > xVal) {
-      high = idx - 1;
-    } else if (low != idx) {  // equal, but there may be an earlier match.
-      high = idx;
-    } else {
-      return idx;
-    }
-  }
-
-  return null;
-};
-
-/**
- * Trigger a callback when the dygraph has drawn itself and is ready to be
- * manipulated. This is primarily useful when dygraphs has to do an XHR for the
- * data (i.e. a URL is passed as the data source) and the chart is drawn
- * asynchronously. If the chart has already drawn, the callback will fire
- * immediately.
- *
- * This is a good place to call setAnnotation().
- *
- * @param {function(!Dygraph)} callback The callback to trigger when the chart
- *     is ready.
- */
-Dygraph.prototype.ready = function(callback) {
-  if (this.is_initial_draw_) {
-    this.readyFns_.push(callback);
-  } else {
-    callback.call(this, this);
-  }
-};
-
-/**
- * @private
- * Adds a default style for the annotation CSS classes to the document. This is
- * only executed when annotations are actually used. It is designed to only be
- * called once -- all calls after the first will return immediately.
- */
-Dygraph.addAnnotationRule = function() {
-  // TODO(danvk): move this function into plugins/annotations.js?
-  if (Dygraph.addedAnnotationCSS) return;
-
-  var rule = "border: 1px solid black; " +
-             "background-color: white; " +
-             "text-align: center;";
-
-  var styleSheetElement = document.createElement("style");
-  styleSheetElement.type = "text/css";
-  document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
-
-  // Find the first style sheet that we can access.
-  // We may not add a rule to a style sheet from another domain for security
-  // reasons. This sometimes comes up when using gviz, since the Google gviz JS
-  // adds its own style sheets from google.com.
-  for (var i = 0; i < document.styleSheets.length; i++) {
-    if (document.styleSheets[i].disabled) continue;
-    var mysheet = document.styleSheets[i];
-    try {
-      if (mysheet.insertRule) {  // Firefox
-        var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
-        mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
-      } else if (mysheet.addRule) {  // IE
-        mysheet.addRule(".dygraphDefaultAnnotation", rule);
-      }
-      Dygraph.addedAnnotationCSS = true;
-      return;
-    } catch(err) {
-      // Was likely a security exception.
-    }
-  }
-
-  console.warn("Unable to add default annotation CSS rule; display may be off.");
-};
-
-return Dygraph;
-
-})();
diff --git a/extras/hairlines.js b/extras/hairlines.js
deleted file mode 100644 (file)
index 904e432..0000000
+++ /dev/null
@@ -1,459 +0,0 @@
-/**
- * @license
- * Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- *
- * Note: This plugin requires jQuery and jQuery UI Draggable.
- *
- * See high-level documentation at
- * https://docs.google.com/document/d/1OHNE8BNNmMtFlRQ969DACIYIJ9VVJ7w3dSPRJDEeIew/edit#
- */
-
-/*global Dygraph:false */
-
-Dygraph.Plugins.Hairlines = (function() {
-
-"use strict";
-
-/**
- * @typedef {
- *   xval:  number,      // x-value (i.e. millis or a raw number)
- *   interpolated: bool,  // alternative is to snap to closest
- *   lineDiv: !Element    // vertical hairline div
- *   infoDiv: !Element    // div containing info about the nearest points
- *   selected: boolean    // whether this hairline is selected
- * } Hairline
- */
-
-// We have to wait a few ms after clicks to give the user a chance to
-// double-click to unzoom. This sets that delay period.
-var CLICK_DELAY_MS = 300;
-
-var hairlines = function(opt_options) {
-  /* @type {!Array.<!Hairline>} */
-  this.hairlines_ = [];
-
-  // Used to detect resizes (which require the divs to be repositioned).
-  this.lastWidth_ = -1;
-  this.lastHeight = -1;
-  this.dygraph_ = null;
-
-  this.addTimer_ = null;
-  opt_options = opt_options || {};
-
-  this.divFiller_ = opt_options['divFiller'] || null;
-};
-
-hairlines.prototype.toString = function() {
-  return "Hairlines Plugin";
-};
-
-hairlines.prototype.activate = function(g) {
-  this.dygraph_ = g;
-  this.hairlines_ = [];
-
-  return {
-    didDrawChart: this.didDrawChart,
-    click: this.click,
-    dblclick: this.dblclick,
-    dataDidUpdate: this.dataDidUpdate
-  };
-};
-
-hairlines.prototype.detachLabels = function() {
-  for (var i = 0; i < this.hairlines_.length; i++) {
-    var h = this.hairlines_[i];
-    $(h.lineDiv).remove();
-    $(h.infoDiv).remove();
-    this.hairlines_[i] = null;
-  }
-  this.hairlines_ = [];
-};
-
-hairlines.prototype.hairlineWasDragged = function(h, event, ui) {
-  var area = this.dygraph_.getArea();
-  var oldXVal = h.xval;
-  h.xval = this.dygraph_.toDataXCoord(ui.position.left);
-  this.moveHairlineToTop(h);
-  this.updateHairlineDivPositions();
-  this.updateHairlineInfo();
-  this.updateHairlineStyles();
-  $(this).triggerHandler('hairlineMoved', {
-    oldXVal: oldXVal,
-    newXVal: h.xval
-  });
-  $(this).triggerHandler('hairlinesChanged', {});
-};
-
-// This creates the hairline object and returns it.
-// It does not position it and does not attach it to the chart.
-hairlines.prototype.createHairline = function(props) {
-  var h;
-  var self = this;
-
-  var $lineContainerDiv = $('<div/>').css({
-      'width': '6px',
-      'margin-left': '-3px',
-      'position': 'absolute',
-      'z-index': '10'
-    })
-    .addClass('dygraph-hairline');
-
-  var $lineDiv = $('<div/>').css({
-    'width': '1px',
-    'position': 'relative',
-    'left': '3px',
-    'background': 'black',
-    'height': '100%'
-  });
-  $lineDiv.appendTo($lineContainerDiv);
-
-  var $infoDiv = $('#hairline-template').clone().removeAttr('id').css({
-      'position': 'absolute'
-    })
-    .show();
-
-  // Surely there's a more jQuery-ish way to do this!
-  $([$infoDiv.get(0), $lineContainerDiv.get(0)])
-    .draggable({
-      'axis': 'x',
-      'drag': function(event, ui) {
-        self.hairlineWasDragged(h, event, ui);
-      }
-      // TODO(danvk): set cursor here
-    });
-
-  h = $.extend({
-    interpolated: true,
-    selected: false,
-    lineDiv: $lineContainerDiv.get(0),
-    infoDiv: $infoDiv.get(0)
-  }, props);
-
-  var that = this;
-  $infoDiv.on('click', '.hairline-kill-button', function(e) {
-    that.removeHairline(h);
-    $(that).triggerHandler('hairlineDeleted', {
-      xval: h.xval
-    });
-    $(that).triggerHandler('hairlinesChanged', {});
-    e.stopPropagation();  // don't want .click() to trigger, below.
-  }).on('click', function() {
-    that.moveHairlineToTop(h);
-  });
-
-  return h;
-};
-
-// Moves a hairline's divs to the top of the z-ordering.
-hairlines.prototype.moveHairlineToTop = function(h) {
-  var div = this.dygraph_.graphDiv;
-  $(h.infoDiv).appendTo(div);
-  $(h.lineDiv).appendTo(div);
-
-  var idx = this.hairlines_.indexOf(h);
-  this.hairlines_.splice(idx, 1);
-  this.hairlines_.push(h);
-};
-
-// Positions existing hairline divs.
-hairlines.prototype.updateHairlineDivPositions = function() {
-  var g = this.dygraph_;
-  var layout = this.dygraph_.getArea();
-  var chartLeft = layout.x, chartRight = layout.x + layout.w;
-  var div = this.dygraph_.graphDiv;
-  var pos = Dygraph.findPos(div);
-  var box = [layout.x + pos.x, layout.y + pos.y];
-  box.push(box[0] + layout.w);
-  box.push(box[1] + layout.h);
-
-  $.each(this.hairlines_, function(idx, h) {
-    var left = g.toDomXCoord(h.xval);
-    h.domX = left;  // See comments in this.dataDidUpdate
-    $(h.lineDiv).css({
-      'left': left + 'px',
-      'top': layout.y + 'px',
-      'height': layout.h + 'px'
-    });  // .draggable("option", "containment", box);
-    $(h.infoDiv).css({
-      'left': left + 'px',
-      'top': layout.y + 'px',
-    }).draggable("option", "containment", box);
-
-    var visible = (left >= chartLeft && left <= chartRight);
-    $([h.infoDiv, h.lineDiv]).toggle(visible);
-  });
-};
-
-// Sets styles on the hairline (i.e. "selected")
-hairlines.prototype.updateHairlineStyles = function() {
-  $.each(this.hairlines_, function(idx, h) {
-    $([h.infoDiv, h.lineDiv]).toggleClass('selected', h.selected);
-  });
-};
-
-// Find prevRow and nextRow such that
-// g.getValue(prevRow, 0) <= xval
-// g.getValue(nextRow, 0) >= xval
-// g.getValue({prev,next}Row, col) != null, NaN or undefined
-// and there's no other row such that:
-//   g.getValue(prevRow, 0) < g.getValue(row, 0) < g.getValue(nextRow, 0)
-//   g.getValue(row, col) != null, NaN or undefined.
-// Returns [prevRow, nextRow]. Either can be null (but not both).
-hairlines.findPrevNextRows = function(g, xval, col) {
-  var prevRow = null, nextRow = null;
-  var numRows = g.numRows();
-  for (var row = 0; row < numRows; row++) {
-    var yval = g.getValue(row, col);
-    if (yval === null || yval === undefined || isNaN(yval)) continue;
-
-    var rowXval = g.getValue(row, 0);
-    if (rowXval <= xval) prevRow = row;
-
-    if (rowXval >= xval) {
-      nextRow = row;
-      break;
-    }
-  }
-
-  return [prevRow, nextRow];
-};
-
-// Fills out the info div based on current coordinates.
-hairlines.prototype.updateHairlineInfo = function() {
-  var mode = 'closest';
-
-  var g = this.dygraph_;
-  var xRange = g.xAxisRange();
-  var that = this;
-  $.each(this.hairlines_, function(idx, h) {
-    // To use generateLegendHTML, we synthesize an array of selected points.
-    var selPoints = [];
-    var labels = g.getLabels();
-    var row, prevRow, nextRow;
-
-    if (!h.interpolated) {
-      // "closest point" mode.
-      // TODO(danvk): make findClosestRow method public
-      row = g.findClosestRow(g.toDomXCoord(h.xval));
-      for (var i = 1; i < g.numColumns(); i++) {
-        selPoints.push({
-          canvasx: 1,  // TODO(danvk): real coordinate
-          canvasy: 1,  // TODO(danvk): real coordinate
-          xval: h.xval,
-          yval: g.getValue(row, i),
-          name: labels[i]
-        });
-      }
-    } else {
-      // "interpolated" mode.
-      for (var i = 1; i < g.numColumns(); i++) {
-        var prevNextRow = hairlines.findPrevNextRows(g, h.xval, i);
-        prevRow = prevNextRow[0], nextRow = prevNextRow[1];
-
-        // For x-values outside the domain, interpolate "between" the extreme
-        // point and itself.
-        if (prevRow === null) prevRow = nextRow;
-        if (nextRow === null) nextRow = prevRow;
-
-        // linear interpolation
-        var prevX = g.getValue(prevRow, 0),
-            nextX = g.getValue(nextRow, 0),
-            prevY = g.getValue(prevRow, i),
-            nextY = g.getValue(nextRow, i),
-            frac = prevRow == nextRow ? 0 : (h.xval - prevX) / (nextX - prevX),
-            yval = frac * nextY + (1 - frac) * prevY;
-
-        selPoints.push({
-          canvasx: 1,  // TODO(danvk): real coordinate
-          canvasy: 1,  // TODO(danvk): real coordinate
-          xval: h.xval,
-          yval: yval,
-          prevRow: prevRow,
-          nextRow: nextRow,
-          name: labels[i]
-        });
-      }
-    }
-
-    if (that.divFiller_) {
-      that.divFiller_(h.infoDiv, {
-        closestRow: row,
-        points: selPoints,
-        hairline: that.createPublicHairline_(h),
-        dygraph: g
-      });
-    } else {
-      var html = Dygraph.Plugins.Legend.generateLegendHTML(g, h.xval, selPoints, 10);
-      $('.hairline-legend', h.infoDiv).html(html);
-    }
-  });
-};
-
-// After a resize, the hairline divs can get dettached from the chart.
-// This reattaches them.
-hairlines.prototype.attachHairlinesToChart_ = function() {
-  var div = this.dygraph_.graphDiv;
-  $.each(this.hairlines_, function(idx, h) {
-    $([h.lineDiv, h.infoDiv]).appendTo(div);
-  });
-};
-
-// Deletes a hairline and removes it from the chart.
-hairlines.prototype.removeHairline = function(h) {
-  var idx = this.hairlines_.indexOf(h);
-  if (idx >= 0) {
-    this.hairlines_.splice(idx, 1);
-    $([h.lineDiv, h.infoDiv]).remove();
-  } else {
-    Dygraph.warn('Tried to remove non-existent hairline.');
-  }
-};
-
-hairlines.prototype.didDrawChart = function(e) {
-  var g = e.dygraph;
-
-  // Early out in the (common) case of zero hairlines.
-  if (this.hairlines_.length === 0) return;
-
-  this.updateHairlineDivPositions();
-  this.attachHairlinesToChart_();
-  this.updateHairlineInfo();
-  this.updateHairlineStyles();
-};
-
-hairlines.prototype.dataDidUpdate = function(e) {
-  // When the data in the chart updates, the hairlines should stay in the same
-  // position on the screen. didDrawChart stores a domX parameter for each
-  // hairline. We use that to reposition them on data updates.
-  var g = this.dygraph_;
-  $.each(this.hairlines_, function(idx, h) {
-    if (h.hasOwnProperty('domX')) {
-      h.xval = g.toDataXCoord(h.domX);
-    }
-  });
-};
-
-hairlines.prototype.click = function(e) {
-  if (this.addTimer_) {
-    // Another click is in progress; ignore this one.
-    return;
-  }
-
-  var area = e.dygraph.getArea();
-  var xval = this.dygraph_.toDataXCoord(e.canvasx);
-
-  var that = this;
-  this.addTimer_ = setTimeout(function() {
-    that.addTimer_ = null;
-    that.hairlines_.push(that.createHairline({xval: xval}));
-
-    that.updateHairlineDivPositions();
-    that.updateHairlineInfo();
-    that.updateHairlineStyles();
-    that.attachHairlinesToChart_();
-
-    $(that).triggerHandler('hairlineCreated', {
-      xval: xval
-    });
-    $(that).triggerHandler('hairlinesChanged', {});
-  }, CLICK_DELAY_MS);
-};
-
-hairlines.prototype.dblclick = function(e) {
-  if (this.addTimer_) {
-    clearTimeout(this.addTimer_);
-    this.addTimer_ = null;
-  }
-};
-
-hairlines.prototype.destroy = function() {
-  this.detachLabels();
-};
-
-
-// Public API
-
-/**
- * This is a restricted view of this.hairlines_ which doesn't expose
- * implementation details like the handle divs.
- *
- * @typedef {
- *   xval:  number,       // x-value (i.e. millis or a raw number)
- *   interpolated: bool,  // alternative is to snap to closest
- *   selected: bool       // whether the hairline is selected.
- * } PublicHairline
- */
-
-/**
- * @param {!Hairline} h Internal hairline.
- * @return {!PublicHairline} Restricted public view of the hairline.
- */
-hairlines.prototype.createPublicHairline_ = function(h) {
-  return {
-    xval: h.xval,
-    interpolated: h.interpolated,
-    selected: h.selected
-  };
-};
-
-/**
- * @return {!Array.<!PublicHairline>} The current set of hairlines, ordered
- *     from back to front.
- */
-hairlines.prototype.get = function() {
-  var result = [];
-  for (var i = 0; i < this.hairlines_.length; i++) {
-    var h = this.hairlines_[i];
-    result.push(this.createPublicHairline_(h));
-  }
-  return result;
-};
-
-/**
- * Calling this will result in a hairlinesChanged event being triggered, no
- * matter whether it consists of additions, deletions, moves or no changes at
- * all.
- *
- * @param {!Array.<!PublicHairline>} hairlines The new set of hairlines,
- *     ordered from back to front.
- */
-hairlines.prototype.set = function(hairlines) {
-  // Re-use divs from the old hairlines array so far as we can.
-  // They're already correctly z-ordered.
-  var anyCreated = false;
-  for (var i = 0; i < hairlines.length; i++) {
-    var h = hairlines[i];
-
-    if (this.hairlines_.length > i) {
-      this.hairlines_[i].xval = h.xval;
-      this.hairlines_[i].interpolated = h.interpolated;
-      this.hairlines_[i].selected = h.selected;
-    } else {
-      this.hairlines_.push(this.createHairline({
-        xval: h.xval,
-        interpolated: h.interpolated,
-        selected: h.selected
-      }));
-      anyCreated = true;
-    }
-  }
-
-  // If there are any remaining hairlines, destroy them.
-  while (hairlines.length < this.hairlines_.length) {
-    this.removeHairline(this.hairlines_[hairlines.length]);
-  }
-
-  this.updateHairlineDivPositions();
-  this.updateHairlineInfo();
-  this.updateHairlineStyles();
-  if (anyCreated) {
-    this.attachHairlinesToChart_();
-  }
-
-  $(this).triggerHandler('hairlinesChanged', {});
-};
-
-return hairlines;
-
-})();
diff --git a/extras/shapes.js b/extras/shapes.js
deleted file mode 100644 (file)
index a453177..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * @license
- * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/**
- * @fileoverview
- * Including this file will add several additional shapes to Dygraph.Circles
- * which can be passed to drawPointCallback.
- * See tests/custom-circles.html for usage.
- */
-
-(function() {
-
-/**
- * @param {!CanvasRenderingContext2D} ctx the canvas context
- * @param {number} sides the number of sides in the shape.
- * @param {number} radius the radius of the image.
- * @param {number} cx center x coordate
- * @param {number} cy center y coordinate
- * @param {number=} rotationRadians the shift of the initial angle, in radians.
- * @param {number=} delta the angle shift for each line. If missing, creates a
- *     regular polygon.
- */
-var regularShape = function(
-    ctx, sides, radius, cx, cy, rotationRadians, delta) {
-  rotationRadians = rotationRadians || 0;
-  delta = delta || Math.PI * 2 / sides;
-
-  ctx.beginPath();
-  var initialAngle = rotationRadians;
-  var angle = initialAngle;
-
-  var computeCoordinates = function() {
-    var x = cx + (Math.sin(angle) * radius);
-    var y = cy + (-Math.cos(angle) * radius);
-    return [x, y];
-  };
-
-  var initialCoordinates = computeCoordinates();
-  var x = initialCoordinates[0];
-  var y = initialCoordinates[1];
-  ctx.moveTo(x, y);
-
-  for (var idx = 0; idx < sides; idx++) {
-    angle = (idx == sides - 1) ? initialAngle : (angle + delta);
-    var coords = computeCoordinates();
-    ctx.lineTo(coords[0], coords[1]);
-  }
-  ctx.fill();
-  ctx.stroke();
-};
-
-/**
- * TODO(danvk): be more specific on the return type.
- * @param {number} sides
- * @param {number=} rotationRadians
- * @param {number=} delta
- * @return {Function}
- * @private
- */
-var shapeFunction = function(sides, rotationRadians, delta) {
-  return function(g, name, ctx, cx, cy, color, radius) {
-    ctx.strokeStyle = color;
-    ctx.fillStyle = "white";
-    regularShape(ctx, sides, radius, cx, cy, rotationRadians, delta);
-  };
-};
-
-Dygraph.update(Dygraph.Circles, {
-  TRIANGLE : shapeFunction(3),
-  SQUARE : shapeFunction(4, Math.PI / 4),
-  DIAMOND : shapeFunction(4),
-  PENTAGON : shapeFunction(5),
-  HEXAGON : shapeFunction(6),
-  CIRCLE : function(g, name, ctx, cx, cy, color, radius) {
-    ctx.beginPath();
-    ctx.strokeStyle = color;
-    ctx.fillStyle = "white";
-    ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
-    ctx.fill();
-    ctx.stroke();
-  },
-  STAR : shapeFunction(5, 0, 4 * Math.PI / 5),
-  PLUS : function(g, name, ctx, cx, cy, color, radius) {
-    ctx.strokeStyle = color;
-
-    ctx.beginPath();
-    ctx.moveTo(cx + radius, cy);
-    ctx.lineTo(cx - radius, cy);
-    ctx.closePath();
-    ctx.stroke();
-
-    ctx.beginPath();
-    ctx.moveTo(cx, cy + radius);
-    ctx.lineTo(cx, cy - radius);
-    ctx.closePath();
-    ctx.stroke();
-  },
-  EX : function(g, name, ctx, cx, cy, color, radius) {
-    ctx.strokeStyle = color;
-
-    ctx.beginPath();
-    ctx.moveTo(cx + radius, cy + radius);
-    ctx.lineTo(cx - radius, cy - radius);
-    ctx.closePath();
-    ctx.stroke();
-
-    ctx.beginPath();
-    ctx.moveTo(cx + radius, cy - radius);
-    ctx.lineTo(cx - radius, cy + radius);
-    ctx.closePath();
-    ctx.stroke();
-  }
-});
-
-})();
diff --git a/extras/smooth-plotter.js b/extras/smooth-plotter.js
deleted file mode 100644 (file)
index 119bf3f..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-var smoothPlotter = (function() {
-"use strict";
-
-/**
- * Given three sequential points, p0, p1 and p2, find the left and right
- * control points for p1.
- *
- * The three points are expected to have x and y properties.
- *
- * The alpha parameter controls the amount of smoothing.
- * If Î±=0, then both control points will be the same as p1 (i.e. no smoothing).
- *
- * Returns [l1x, l1y, r1x, r1y]
- *
- * It's guaranteed that the line from (l1x, l1y)-(r1x, r1y) passes through p1.
- * Unless allowFalseExtrema is set, then it's also guaranteed that:
- *   l1y âˆˆ [p0.y, p1.y]
- *   r1y âˆˆ [p1.y, p2.y]
- *
- * The basic algorithm is:
- * 1. Put the control points l1 and r1 Î± of the way down (p0, p1) and (p1, p2).
- * 2. Shift l1 and r2 so that the line l1–r1 passes through p1
- * 3. Adjust to prevent false extrema while keeping p1 on the l1–r1 line.
- *
- * This is loosely based on the HighCharts algorithm.
- */
-function getControlPoints(p0, p1, p2, opt_alpha, opt_allowFalseExtrema) {
-  var alpha = (opt_alpha !== undefined) ? opt_alpha : 1/3;  // 0=no smoothing, 1=crazy smoothing
-  var allowFalseExtrema = opt_allowFalseExtrema || false;
-
-  if (!p2) {
-    return [p1.x, p1.y, null, null];
-  }
-
-  // Step 1: Position the control points along each line segment.
-  var l1x = (1 - alpha) * p1.x + alpha * p0.x,
-      l1y = (1 - alpha) * p1.y + alpha * p0.y,
-      r1x = (1 - alpha) * p1.x + alpha * p2.x,
-      r1y = (1 - alpha) * p1.y + alpha * p2.y;
-
-  // Step 2: shift the points up so that p1 is on the l1–r1 line.
-  if (l1x != r1x) {
-    // This can be derived w/ some basic algebra.
-    var deltaY = p1.y - r1y - (p1.x - r1x) * (l1y - r1y) / (l1x - r1x);
-    l1y += deltaY;
-    r1y += deltaY;
-  }
-
-  // Step 3: correct to avoid false extrema.
-  if (!allowFalseExtrema) {
-    if (l1y > p0.y && l1y > p1.y) {
-      l1y = Math.max(p0.y, p1.y);
-      r1y = 2 * p1.y - l1y;
-    } else if (l1y < p0.y && l1y < p1.y) {
-      l1y = Math.min(p0.y, p1.y);
-      r1y = 2 * p1.y - l1y;
-    }
-
-    if (r1y > p1.y && r1y > p2.y) {
-      r1y = Math.max(p1.y, p2.y);
-      l1y = 2 * p1.y - r1y;
-    } else if (r1y < p1.y && r1y < p2.y) {
-      r1y = Math.min(p1.y, p2.y);
-      l1y = 2 * p1.y - r1y;
-    }
-  }
-
-  return [l1x, l1y, r1x, r1y];
-}
-
-
-// A plotter which uses splines to create a smooth curve.
-// See tests/plotters.html for a demo.
-// Can be controlled via smoothPlotter.smoothing
-function smoothPlotter(e) {
-  var ctx = e.drawingContext,
-      points = e.points;
-
-  ctx.beginPath();
-  ctx.moveTo(points[0].canvasx, points[0].canvasy);
-
-  // right control point for previous point
-  var lastRightX = points[0].canvasx, lastRightY = points[0].canvasy;
-  var isOK = Dygraph.isOK;  // i.e. is none of (null, undefined, NaN)
-
-  for (var i = 1; i < points.length; i++) {
-    var p0 = points[i - 1],
-        p1 = points[i],
-        p2 = points[i + 1];
-    p0 = p0 && isOK(p0.canvasy) ? p0 : null;
-    p1 = p1 && isOK(p1.canvasy) ? p1 : null;
-    p2 = p2 && isOK(p2.canvasy) ? p2 : null;
-    if (p0 && p1) {
-      var controls = getControlPoints({x: p0.canvasx, y: p0.canvasy},
-                                      {x: p1.canvasx, y: p1.canvasy},
-                                      p2 && {x: p2.canvasx, y: p2.canvasy},
-                                      smoothPlotter.smoothing);
-      // Uncomment to show the control points:
-      // ctx.lineTo(lastRightX, lastRightY);
-      // ctx.lineTo(controls[0], controls[1]);
-      // ctx.lineTo(p1.canvasx, p1.canvasy);
-      lastRightX = (lastRightX !== null) ? lastRightX : p0.canvasx;
-      lastRightY = (lastRightY !== null) ? lastRightY : p0.canvasy;
-      ctx.bezierCurveTo(lastRightX, lastRightY,
-                        controls[0], controls[1],
-                        p1.canvasx, p1.canvasy);
-      lastRightX = controls[2];
-      lastRightY = controls[3];
-    } else if (p1) {
-      // We're starting again after a missing point.
-      ctx.moveTo(p1.canvasx, p1.canvasy);
-      lastRightX = p1.canvasx;
-      lastRightY = p1.canvasy;
-    } else {
-      lastRightX = lastRightY = null;
-    }
-  }
-
-  ctx.stroke();
-}
-smoothPlotter.smoothing = 1/3;
-smoothPlotter._getControlPoints = getControlPoints;  // for testing
-
-return smoothPlotter;
-
-})();
diff --git a/extras/super-annotations.js b/extras/super-annotations.js
deleted file mode 100644 (file)
index b8b10c6..0000000
+++ /dev/null
@@ -1,474 +0,0 @@
-/**
- * @license
- * Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- *
- * Note: This plugin requires jQuery and jQuery UI Draggable.
- *
- * See high-level documentation at
- * https://docs.google.com/document/d/1OHNE8BNNmMtFlRQ969DACIYIJ9VVJ7w3dSPRJDEeIew/edit#
- */
-
-/*global Dygraph:false */
-
-Dygraph.Plugins.SuperAnnotations = (function() {
-
-"use strict";
-
-/**
- * These are just the basic requirements -- annotations can have whatever other
- * properties the code that displays them wants them to have.
- *
- * @typedef {
- *   xval:  number,      // x-value (i.e. millis or a raw number)
- *   series: string,     // series name
- *   yFrac: ?number,     // y-positioning. Default is a few px above the point.
- *   lineDiv: !Element   // vertical div connecting point to info div.
- *   infoDiv: !Element   // div containing info about the annotation.
- * } Annotation
- */
-
-var annotations = function(opt_options) {
-  /* @type {!Array.<!Annotation>} */
-  this.annotations_ = [];
-  // Used to detect resizes (which require the divs to be repositioned).
-  this.lastWidth_ = -1;
-  this.lastHeight = -1;
-  this.dygraph_ = null;
-
-  opt_options = opt_options || {};
-  this.defaultAnnotationProperties_ = $.extend({
-    'text': 'Description'
-  }, opt_options['defaultAnnotationProperties']);
-};
-
-annotations.prototype.toString = function() {
-  return "SuperAnnotations Plugin";
-};
-
-annotations.prototype.activate = function(g) {
-  this.dygraph_ = g;
-  this.annotations_ = [];
-
-  return {
-    didDrawChart: this.didDrawChart,
-    pointClick: this.pointClick  // TODO(danvk): implement in dygraphs
-  };
-};
-
-annotations.prototype.detachLabels = function() {
-  for (var i = 0; i < this.annotations_.length; i++) {
-    var a = this.annotations_[i];
-    $(a.lineDiv).remove();
-    $(a.infoDiv).remove();
-    this.annotations_[i] = null;
-  }
-  this.annotations_ = [];
-};
-
-annotations.prototype.annotationWasDragged = function(a, event, ui) {
-  var g = this.dygraph_;
-  var area = g.getArea();
-  var oldYFrac = a.yFrac;
-
-  var infoDiv = a.infoDiv;
-  var newYFrac = ((infoDiv.offsetTop + infoDiv.offsetHeight) - area.y) / area.h;
-  if (newYFrac == oldYFrac) return;
-
-  a.yFrac = newYFrac;
-
-  this.moveAnnotationToTop(a);
-  this.updateAnnotationDivPositions();
-  this.updateAnnotationInfo();
-  $(this).triggerHandler('annotationMoved', {
-    annotation: a,
-    oldYFrac: oldYFrac,
-    newYFrac: a.yFrac
-  });
-  $(this).triggerHandler('annotationsChanged', {});
-};
-
-annotations.prototype.makeAnnotationEditable = function(a) {
-  if (a.editable == true) return;
-  this.moveAnnotationToTop(a);
-
-  // Note: we have to fill out the HTML ourselves because
-  // updateAnnotationInfo() won't touch editable annotations.
-  a.editable = true;
-  var editableTemplateDiv = $('#annotation-editable-template').get(0);
-  a.infoDiv.innerHTML = this.getTemplateHTML(editableTemplateDiv, a);
-  $(a.infoDiv).toggleClass('editable', !!a.editable);
-  $(this).triggerHandler('beganEditAnnotation', a);
-};
-
-// This creates the hairline object and returns it.
-// It does not position it and does not attach it to the chart.
-annotations.prototype.createAnnotation = function(a) {
-  var self = this;
-
-  var color = this.getColorForSeries_(a.series);
-
-  var $lineDiv = $('<div/>').css({
-    'width': '1px',
-    'left': '3px',
-    'background': 'black',
-    'height': '100%',
-    'position': 'absolute',
-    // TODO(danvk): use border-color here for consistency?
-    'background-color': color,
-    'z-index': 10
-  }).addClass('dygraph-annotation-line');
-
-  var $infoDiv = $('#annotation-template').clone().removeAttr('id').css({
-      'position': 'absolute',
-      'border-color': color,
-      'z-index': 10
-    })
-    .show();
-
-  $.extend(a, {
-    lineDiv: $lineDiv.get(0),
-    infoDiv: $infoDiv.get(0)
-  });
-
-  var that = this;
-
-  $infoDiv.draggable({
-    'start': function(event, ui) {
-      $(this).css({'bottom': ''});
-      a.isDragging = true;
-    },
-    'drag': function(event, ui) {
-      self.annotationWasDragged(a, event, ui);
-    },
-    'stop': function(event, ui) {
-      $(this).css({'top': ''});
-      a.isDragging = false;
-      self.updateAnnotationDivPositions();
-    },
-    'axis': 'y',
-    'containment': 'parent'
-  });
-
-  // TODO(danvk): use 'on' instead of delegate/dblclick
-  $infoDiv.on('click', '.annotation-kill-button', function() {
-    that.removeAnnotation(a);
-    $(that).triggerHandler('annotationDeleted', a);
-    $(that).triggerHandler('annotationsChanged', {});
-  });
-
-  $infoDiv.on('dblclick', function() {
-    that.makeAnnotationEditable(a);
-  });
-  $infoDiv.on('click', '.annotation-update', function() {
-    self.extractUpdatedProperties_($infoDiv.get(0), a);
-    a.editable = false;
-    self.updateAnnotationInfo();
-    $(that).triggerHandler('annotationEdited', a);
-    $(that).triggerHandler('annotationsChanged', {});
-  });
-  $infoDiv.on('click', '.annotation-cancel', function() {
-    a.editable = false;
-    self.updateAnnotationInfo();
-    $(that).triggerHandler('cancelEditAnnotation', a);
-  });
-
-  return a;
-};
-
-// Find the index of a point in a series.
-// Returns a 2-element array, [row, col], which can be used with
-// dygraph.getValue() to get the value at this point.
-// Returns null if there's no match.
-annotations.prototype.findPointIndex_ = function(series, xval) {
-  var col = this.dygraph_.getLabels().indexOf(series);
-  if (col == -1) return null;
-
-  var lowIdx = 0, highIdx = this.dygraph_.numRows() - 1;
-  while (lowIdx <= highIdx) {
-    var idx = Math.floor((lowIdx + highIdx) / 2);
-    var xAtIdx = this.dygraph_.getValue(idx, 0);
-    if (xAtIdx == xval) {
-      return [idx, col];
-    } else if (xAtIdx < xval) {
-      lowIdx = idx + 1;
-    } else {
-      highIdx = idx - 1;
-    }
-  }
-  return null;
-};
-
-annotations.prototype.getColorForSeries_ = function(series) {
-  var colors = this.dygraph_.getColors();
-  var col = this.dygraph_.getLabels().indexOf(series);
-  if (col == -1) return null;
-
-  return colors[(col - 1) % colors.length];
-};
-
-// Moves a hairline's divs to the top of the z-ordering.
-annotations.prototype.moveAnnotationToTop = function(a) {
-  var div = this.dygraph_.graphDiv;
-  $(a.infoDiv).appendTo(div);
-  $(a.lineDiv).appendTo(div);
-
-  var idx = this.annotations_.indexOf(a);
-  this.annotations_.splice(idx, 1);
-  this.annotations_.push(a);
-};
-
-// Positions existing hairline divs.
-annotations.prototype.updateAnnotationDivPositions = function() {
-  var layout = this.dygraph_.getArea();
-  var chartLeft = layout.x, chartRight = layout.x + layout.w;
-  var chartTop = layout.y, chartBottom = layout.y + layout.h;
-  var div = this.dygraph_.graphDiv;
-  var pos = Dygraph.findPos(div);
-  var box = [layout.x + pos.x, layout.y + pos.y];
-  box.push(box[0] + layout.w);
-  box.push(box[1] + layout.h);
-
-  var g = this.dygraph_;
-
-  var that = this;
-  $.each(this.annotations_, function(idx, a) {
-    var row_col = that.findPointIndex_(a.series, a.xval);
-    if (row_col == null) {
-      $([a.lineDiv, a.infoDiv]).hide();
-      return;
-    } else {
-      // TODO(danvk): only do this if they're invisible?
-      $([a.lineDiv, a.infoDiv]).show();
-    }
-    var xy = g.toDomCoords(a.xval, g.getValue(row_col[0], row_col[1]));
-    var x = xy[0], pointY = xy[1];
-
-    var lineHeight = 6;  // TODO(danvk): option?
-
-    var y = pointY;
-    if (a.yFrac !== undefined) {
-      y = layout.y + layout.h * a.yFrac;
-    } else {
-      y -= lineHeight;
-    }
-
-    var lineHeight = y < pointY ? (pointY - y) : (y - pointY - a.infoDiv.offsetHeight);
-    $(a.lineDiv).css({
-      'left': x + 'px',
-      'top': Math.min(y, pointY) + 'px',
-      'height': lineHeight + 'px'
-    });
-    $(a.infoDiv).css({
-      'left': x + 'px',
-    });
-    if (!a.isDragging) {
-      // jQuery UI draggable likes to set 'top', whereas superannotations sets
-      // 'bottom'. Setting both will make the annotation grow and contract as
-      // the user drags it, which looks bad.
-      $(a.infoDiv).css({
-        'bottom': (div.offsetHeight - y) + 'px'
-      })  //.draggable("option", "containment", box);
-
-      var visible = (x >= chartLeft && x <= chartRight) &&
-                    (pointY >= chartTop && pointY <= chartBottom);
-      $([a.infoDiv, a.lineDiv]).toggle(visible);
-    }
-  });
-};
-
-// Fills out the info div based on current coordinates.
-annotations.prototype.updateAnnotationInfo = function() {
-  var g = this.dygraph_;
-
-  var that = this;
-  var templateDiv = $('#annotation-template').get(0);
-  $.each(this.annotations_, function(idx, a) {
-    // We should never update an editable div -- doing so may kill unsaved
-    // edits to an annotation.
-    $(a.infoDiv).toggleClass('editable', !!a.editable);
-    if (a.editable) return;
-    a.infoDiv.innerHTML = that.getTemplateHTML(templateDiv, a);
-  });
-};
-
-/**
- * @param {!Annotation} a Internal annotation
- * @return {!PublicAnnotation} a view of the annotation for the public API.
- */
-annotations.prototype.createPublicAnnotation_ = function(a, opt_props) {
-  var displayAnnotation = $.extend({}, a, opt_props);
-  delete displayAnnotation['infoDiv'];
-  delete displayAnnotation['lineDiv'];
-  delete displayAnnotation['isDragging'];
-  delete displayAnnotation['editable'];
-  return displayAnnotation;
-};
-
-// Fill out a div using the values in the annotation object.
-// The div's html is expected to have text of the form "{{key}}"
-annotations.prototype.getTemplateHTML = function(div, a) {
-  var g = this.dygraph_;
-  var row_col = this.findPointIndex_(a.series, a.xval);
-  if (row_col == null) return;  // perhaps it's no longer a real point?
-  var row = row_col[0];
-  var col = row_col[1];
-
-  var yOptView = g.optionsViewForAxis_('y1');  // TODO: support secondary, too
-  var xvf = g.getOptionForAxis('valueFormatter', 'x');
-
-  var x = xvf.call(g, a.xval);
-  var y = g.getOption('valueFormatter', a.series).call(
-      g, g.getValue(row, col), yOptView);
-
-  var displayAnnotation = this.createPublicAnnotation_(a, {x:x, y:y});
-  var html = div.innerHTML;
-  for (var k in displayAnnotation) {
-    var v = displayAnnotation[k];
-    if (typeof(v) == 'object') continue;  // e.g. infoDiv or lineDiv
-    html = html.replace(new RegExp('\{\{' + k + '\}\}', 'g'), v);
-  }
-  return html;
-};
-
-// Update the annotation object by looking for elements with a 'dg-ann-field'
-// attribute. For example, <input type='text' dg-ann-field='text' /> will have
-// its value placed in the 'text' attribute of the annotation.
-annotations.prototype.extractUpdatedProperties_ = function(div, a) {
-  $(div).find('[dg-ann-field]').each(function(idx, el) {
-    var k = $(el).attr('dg-ann-field');
-    var v = $(el).val();
-    a[k] = v;
-  });
-};
-
-// After a resize, the hairline divs can get dettached from the chart.
-// This reattaches them.
-annotations.prototype.attachAnnotationsToChart_ = function() {
-  var div = this.dygraph_.graphDiv;
-  $.each(this.annotations_, function(idx, a) {
-    // Re-attaching an editable div to the DOM can clear its focus.
-    // This makes typing really difficult!
-    if (a.editable) return;
-
-    $([a.lineDiv, a.infoDiv]).appendTo(div);
-  });
-};
-
-// Deletes a hairline and removes it from the chart.
-annotations.prototype.removeAnnotation = function(a) {
-  var idx = this.annotations_.indexOf(a);
-  if (idx >= 0) {
-    this.annotations_.splice(idx, 1);
-    $([a.lineDiv, a.infoDiv]).remove();
-  } else {
-    Dygraph.warn('Tried to remove non-existent annotation.');
-  }
-};
-
-annotations.prototype.didDrawChart = function(e) {
-  var g = e.dygraph;
-
-  // Early out in the (common) case of zero annotations.
-  if (this.annotations_.length === 0) return;
-
-  this.updateAnnotationDivPositions();
-  this.attachAnnotationsToChart_();
-  this.updateAnnotationInfo();
-};
-
-annotations.prototype.pointClick = function(e) {
-  // Prevent any other behavior based on this click, e.g. creation of a hairline.
-  e.preventDefault();
-
-  var a = $.extend({}, this.defaultAnnotationProperties_, {
-    series: e.point.name,
-    xval: e.point.xval
-  });
-  this.annotations_.push(this.createAnnotation(a));
-
-  this.updateAnnotationDivPositions();
-  this.updateAnnotationInfo();
-  this.attachAnnotationsToChart_();
-
-  $(this).triggerHandler('annotationCreated', a);
-  $(this).triggerHandler('annotationsChanged', {});
-
-  // Annotations should begin life editable.
-  this.makeAnnotationEditable(a);
-};
-
-annotations.prototype.destroy = function() {
-  this.detachLabels();
-};
-
-
-// Public API
-
-/**
- * This is a restricted view of this.annotations_ which doesn't expose
- * implementation details like the line / info divs.
- *
- * @typedef {
- *   xval:  number,      // x-value (i.e. millis or a raw number)
- *   series: string,     // series name
- * } PublicAnnotation
- */
-
-/**
- * @return {!Array.<!PublicAnnotation>} The current set of annotations, ordered
- *     from back to front.
- */
-annotations.prototype.get = function() {
-  var result = [];
-  for (var i = 0; i < this.annotations_.length; i++) {
-    result.push(this.createPublicAnnotation_(this.annotations_[i]));
-  }
-  return result;
-};
-
-/**
- * Calling this will result in an annotationsChanged event being triggered, no
- * matter whether it consists of additions, deletions, moves or no changes at
- * all.
- *
- * @param {!Array.<!PublicAnnotation>} annotations The new set of annotations,
- *     ordered from back to front.
- */
-annotations.prototype.set = function(annotations) {
-  // Re-use divs from the old annotations array so far as we can.
-  // They're already correctly z-ordered.
-  var anyCreated = false;
-  for (var i = 0; i < annotations.length; i++) {
-    var a = annotations[i];
-
-    if (this.annotations_.length > i) {
-      // Only the divs need to be preserved.
-      var oldA = this.annotations_[i];
-      this.annotations_[i] = $.extend({
-        infoDiv: oldA.infoDiv,
-        lineDiv: oldA.lineDiv
-      }, a);
-    } else {
-      this.annotations_.push(this.createAnnotation(a));
-      anyCreated = true;
-    }
-  }
-
-  // If there are any remaining annotations, destroy them.
-  while (annotations.length < this.annotations_.length) {
-    this.removeAnnotation(this.annotations_[annotations.length]);
-  }
-
-  this.updateAnnotationDivPositions();
-  this.updateAnnotationInfo();
-  if (anyCreated) {
-    this.attachAnnotationsToChart_();
-  }
-
-  $(this).triggerHandler('annotationsChanged', {});
-};
-
-return annotations;
-
-})();
diff --git a/extras/synchronizer.js b/extras/synchronizer.js
deleted file mode 100644 (file)
index 16e4d96..0000000
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * Synchronize zooming and/or selections between a set of dygraphs.
- *
- * Usage:
- *
- *   var g1 = new Dygraph(...),
- *       g2 = new Dygraph(...),
- *       ...;
- *   var sync = Dygraph.synchronize(g1, g2, ...);
- *   // charts are now synchronized
- *   sync.detach();
- *   // charts are no longer synchronized
- *
- * You can set options using the last parameter, for example:
- *
- *   var sync = Dygraph.synchronize(g1, g2, g3, {
- *      selection: true,
- *      zoom: true
- *   });
- *
- * The default is to synchronize both of these.
- *
- * Instead of passing one Dygraph object as each parameter, you may also pass an
- * array of dygraphs:
- *
- *   var sync = Dygraph.synchronize([g1, g2, g3], {
- *      selection: false,
- *      zoom: true
- *   });
- *
- * You may also set `range: false` if you wish to only sync the x-axis.
- * The `range` option has no effect unless `zoom` is true (the default).
- */
-(function() {
-/* global Dygraph:false */
-'use strict';
-
-Dygraph.synchronize = function(/* dygraphs..., opts */) {
-  if (arguments.length === 0) {
-    throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
-  }
-
-  var OPTIONS = ['selection', 'zoom', 'range'];
-  var opts = {
-    selection: true,
-    zoom: true,
-    range: true
-  };
-  var dygraphs = [];
-
-  var prevCallbacks = {
-    draw: null,
-    highlight: null,
-    unhighlight: null
-  };
-
-  var parseOpts = function(obj) {
-    if (!(obj instanceof Object)) {
-      throw 'Last argument must be either Dygraph or Object.';
-    } else {
-      for (var i = 0; i < OPTIONS.length; i++) {
-        var optName = OPTIONS[i];
-        if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
-      }
-    }
-  };
-
-  if (arguments[0] instanceof Dygraph) {
-    // Arguments are Dygraph objects.
-    for (var i = 0; i < arguments.length; i++) {
-      if (arguments[i] instanceof Dygraph) {
-        dygraphs.push(arguments[i]);
-      } else {
-        break;
-      }
-    }
-    if (i < arguments.length - 1) {
-      throw 'Invalid invocation of Dygraph.synchronize(). ' +
-            'All but the last argument must be Dygraph objects.';
-    } else if (i == arguments.length - 1) {
-      parseOpts(arguments[arguments.length - 1]);
-    }
-  } else if (arguments[0].length) {
-    // Invoked w/ list of dygraphs, options
-    for (var i = 0; i < arguments[0].length; i++) {
-      dygraphs.push(arguments[0][i]);
-    }
-    if (arguments.length == 2) {
-      parseOpts(arguments[1]);
-    } else if (arguments.length > 2) {
-      throw 'Invalid invocation of Dygraph.synchronize(). ' +
-            'Expected two arguments: array and optional options argument.';
-    }  // otherwise arguments.length == 1, which is fine.
-  } else {
-    throw 'Invalid invocation of Dygraph.synchronize(). ' +
-          'First parameter must be either Dygraph or list of Dygraphs.';
-  }
-
-  if (dygraphs.length < 2) {
-    throw 'Invalid invocation of Dygraph.synchronize(). ' +
-          'Need two or more dygraphs to synchronize.';
-  }
-  
-  var readycount = dygraphs.length;
-  for (var i = 0; i < dygraphs.length; i++) {
-    var g = dygraphs[i];
-    g.ready( function() {
-      if (--readycount == 0) {
-        // Listen for draw, highlight, unhighlight callbacks.
-        if (opts.zoom) {
-          attachZoomHandlers(dygraphs, opts, prevCallbacks);
-        }
-
-        if (opts.selection) {
-          attachSelectionHandlers(dygraphs, prevCallbacks);
-        }
-      }
-    });
-  }
-  return {
-    detach: function() {
-      for (var i = 0; i < dygraphs.length; i++) {
-        var g = dygraphs[i];
-        if (opts.zoom) {
-          g.updateOptions({drawCallback: prevCallbacks.draw});
-        }
-        if (opts.selection) {
-          g.updateOptions({
-            highlightCallback: prevCallbacks.highlight,
-            unhighlightCallback: prevCallbacks.unhighlight
-          });
-        }
-      }
-      // release references & make subsequent calls throw.
-      dygraphs = null;
-      opts = null;
-      prevCallbacks = null;
-    }
-  };
-};
-
-function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
-  var block = false;
-  for (var i = 0; i < gs.length; i++) {
-    var g = gs[i];
-    prevCallbacks.draw = g.getFunctionOption('drawCallback');
-    g.updateOptions({
-      drawCallback: function(me, initial) {
-        if (prevCallbacks.draw) prevCallbacks.draw(me, initial);
-        if (block || initial) return;
-        block = true;
-        var opts = {
-          dateWindow: me.xAxisRange()
-        };
-        if (syncOpts.range) opts.valueRange = me.yAxisRange();
-
-        for (var j = 0; j < gs.length; j++) {
-          if (gs[j] == me) continue;
-          gs[j].updateOptions(opts);
-        }
-        block = false;
-      }
-    }, false /* no need to redraw */);
-  }
-}
-
-function attachSelectionHandlers(gs, prevCallbacks) {
-  var block = false;
-  for (var i = 0; i < gs.length; i++) {
-    var g = gs[i];
-    prevCallbacks.highlight = g.getFunctionOption('highlightCallback');
-    prevCallbacks.unhighlight = g.getFunctionOption('unhighlightCallback');
-    g.updateOptions({
-      highlightCallback: function(event, x, points, row, seriesName) {
-        if (prevCallbacks.highlight) {
-            prevCallbacks.highlight(event, x, points, row, seriesName);
-        }
-        if (block) return;
-        block = true;
-        var me = this;
-        for (var i = 0; i < gs.length; i++) {
-          if (me == gs[i]) continue;
-          var idx = gs[i].getRowForX(x);
-          if (idx !== null) {
-            gs[i].setSelection(idx, seriesName);
-          }
-        }
-        block = false;
-      },
-      unhighlightCallback: function(event) {
-        if (prevCallbacks.unhighlight) prevCallbacks.unhighlight(event);
-        if (block) return;
-        block = true;
-        var me = this;
-        for (var i = 0; i < gs.length; i++) {
-          if (me == gs[i]) continue;
-          gs[i].clearSelection();
-        }
-        block = false;
-      }
-    });
-  }
-}
-
-})();
diff --git a/extras/unzoom.js b/extras/unzoom.js
deleted file mode 100644 (file)
index fac1bbc..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) 2013 Google, Inc.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-// THE SOFTWARE.
-
-/*global Dygraph:false */
-
-/**
- * @fileoverview Plug-in for providing unzoom-on-hover.
- *
- * @author konigsberg@google.com (Robert Konigsberg)
- */
-Dygraph.Plugins.Unzoom = (function() {
-
-  "use strict";
-
-  /**
-   * Create a new instance.
-   *
-   * @constructor
-   */
-  var unzoom = function() {
-    this.button_ = null;
-
-    // True when the mouse is over the canvas. Must be tracked
-    // because the unzoom button state can change even when the
-    // mouse-over state hasn't.
-    this.over_ = false;
-  };
-
-  unzoom.prototype.toString = function() {
-    return 'Unzoom Plugin';
-  };
-
-  unzoom.prototype.activate = function(g) {
-    return {
-      willDrawChart: this.willDrawChart
-    };
-  };
-
-  unzoom.prototype.willDrawChart = function(e) {
-    var g = e.dygraph;
-
-    if (this.button_ !== null) {
-      // short-circuit: show the button only when we're moused over, and zoomed in.
-      var showButton = g.isZoomed() && this.over_;
-      this.show(showButton);
-      return;
-    }
-
-    this.button_ = document.createElement('button');
-    this.button_.innerHTML = 'Reset Zoom';
-    this.button_.style.display = 'none';
-    this.button_.style.position = 'absolute';
-    var area = g.plotter_.area;
-    this.button_.style.top = (area.y + 4) + 'px';
-    this.button_.style.left = (area.x + 4) + 'px';
-    this.button_.style.zIndex = 11;
-    var parent = g.graphDiv;
-    parent.insertBefore(this.button_, parent.firstChild);
-
-    var self = this;
-    this.button_.onclick = function() {
-      g.resetZoom();
-    };
-
-    g.addAndTrackEvent(parent, 'mouseover', function() {
-      if (g.isZoomed()) {
-        self.show(true);
-      }
-      self.over_ = true;
-    });
-
-    g.addAndTrackEvent(parent, 'mouseout', function() {
-      self.show(false);
-      self.over_ = false;
-    });
-  };
-
-  unzoom.prototype.show = function(enabled) {
-    this.button_.style.display = enabled ? '' : 'none';
-  };
-
-  unzoom.prototype.destroy = function() {
-    this.button_.parentElement.removeChild(this.button_);
-  };
-
-  return unzoom;
-
-})();
diff --git a/gadget.xml b/gadget.xml
deleted file mode 100644 (file)
index 07a1aba..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Module>
-
-  <ModulePrefs
-      title="dygraphs Gadget"
-      description="Interactive, zoomable chart"
-      author="Dan Vanderkam"
-      author_email="dan@dygraphs.com"
-      thumbnail="http://dygraphs.com/thumbnail.png"
-      screenshot="http://dygraphs.com/screenshot.png"
-      author_location="US"
-      >
-    <Require feature="idi" />
-    <Require feature="locked-domain" />
-  </ModulePrefs>
-
-  <UserPref name="_table_query_url" display_name="Data source url" required="true"/>
-  <UserPref name="_table_query_refresh_interval" display_name="Data refresh interval (minutes)" default_value="300" datatype="enum" required="false">
-    <EnumValue value="0" display_value="Do not refresh"/>
-    <EnumValue value="60" display_value="1"/>
-    <EnumValue value="300" display_value="5"/>
-    <EnumValue value="1800" display_value="30"/>
-  </UserPref>
-  <UserPref name="_dg_rollPeriod" display_name="Roll Period" required="false" default_value="1" />
-  <UserPref name="_dg_showRoller" display_name="Show Roller" required="false" default_value="false" datatype="bool" />
-  <UserPref name="_dg_minY" display_name="Min Y Value" required="false" default_value="" />
-  <UserPref name="_dg_maxY" display_name="Max Y Value" required="false" default_value="" />
-  <UserPref name="_dg_kmb" display_name="KMB labels" required="false" default_value="false" datatype="bool" />
-  <UserPref name="_dg_errorbars" display_name="Error Bars" required="false" default_value="false" datatype="bool" />
-  <UserPref name="_dg_fillGraph" display_name="Fill Chart" required="false" default_value="false" datatype="bool" />
-
-  <Content type="html"><![CDATA[
-
-  <!-- Load the Google common loader, that is later used to load the Visualization API. -->
-  <script src="http://www.google.com/jsapi" type="text/javascript"></script>
-  <script src="http://dygraphs.com/dygraph-combined.js" type="text/javascript"></script>
-
-  <div id="chartdiv" style="overflow: auto;"><img src="http://www.google.com/ig/images/spinner.gif" /></div>
-
-  <script>
-    var gadgetHelper = null;
-    var table = null;
-    var kPadding = 5;  // pixels of padding on any side of the chart.
-
-    _IG_RegisterOnloadHandler(loadVisualizationAPI);
-
-    /**
-     * Load the Google Visualization API
-     */
-    function loadVisualizationAPI() {
-      google.load("visualization", "1");
-      google.setOnLoadCallback(sendQuery);
-    }
-
-    /**
-     * Create a query from the user prefs, and then send it to the data source.
-     * This method is called once the visualization API is fully loaded.
-     * Note that in the last line, a callback function is specified to be
-     * called once the response is received from the data source.
-     */
-    function sendQuery() {
-      var prefs = new _IG_Prefs(); // User preferences
-      var chartDiv = _gel('chartdiv');
-      chartDiv.style.width = (document.body.clientWidth - 2 * kPadding) + 'px';
-      chartDiv.style.height = (document.body.clientHeight - 2 * kPadding) + 'px';
-      chartDiv.style.left = kPadding + 'px';
-      chartDiv.style.top = kPadding + 'px';
-      chartDiv.style.position = 'absolute';
-      chart = new DateGraph.GVizChart(chartDiv);
-
-      gadgetHelper = new google.visualization.GadgetHelper();
-      var query = gadgetHelper.createQueryFromPrefs(prefs);
-      query.send(handleQueryResponse);
-    }
-
-    /**
-     * Query response handler function.
-     * Called by the Google Visualization API once the response is received.
-     * Takes the query response and formats it as a table.
-     */
-    function handleQueryResponse(response) {
-      // Use the visualization GadgetHelper class to validate the data, and
-      // for error handling.
-      if (!gadgetHelper.validateResponse(response)) {
-        // Default error handling was done, just leave.
-        return;
-      };
-      var data = response.getDataTable();
-
-      // Take the data table from the response, and format it.
-      // var options = {showRowNumber: true};
-      var prefs = new _IG_Prefs(); // User preferences
-      var showRoller = prefs.getBool("_dg_showRoller");
-      var rollPeriod = prefs.getInt("_dg_rollPeriod");
-      var labelsKMB = prefs.getBool("_dg_kmb");
-      var errorBars = prefs.getBool("_dg_errorbars");
-      var fillGraph = prefs.getBool("_dg_fillGraph");
-      var opts = {
-        showRoller: showRoller,
-        rollPeriod: rollPeriod,
-        labelsKMB: labelsKMB,
-        errorBars: errorBars,
-        fillGraph: fillGraph
-      };
-
-      var minY = prefs.getString("_dg_minY");
-      var maxY = prefs.getString("_dg_maxY");
-      if (minY && maxY) {
-        opts.valueRange = [parseInt(minY), parseInt(maxY)];
-      }
-
-      chart.draw(data, opts);
-    };
-
-  </script>
-
-  ]]>
-  </Content>
-</Module>
diff --git a/generate-combined.sh b/generate-combined.sh
deleted file mode 100755 (executable)
index 26d4a42..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/bin/bash
-# Generates a single JS file that's easier to include.
-
-GetSources () {
-  # Include dyraph-options-reference only if DEBUG environment variable is set.
-  if [ ! -z "$DEBUG" ]; then
-    maybe_options_reference=dygraph-options-reference.js
-  else
-    maybe_options_reference=''
-  fi
-
-  # This list needs to be kept in sync w/ the one in dygraph-dev.js
-  # and the one in jsTestDriver.conf. Order matters, except for the plugins.
-  for F in \
-    polyfills/console.js \
-    dashed-canvas.js \
-    dygraph-options.js \
-    dygraph-layout.js \
-    dygraph-canvas.js \
-    dygraph.js \
-    dygraph-utils.js \
-    dygraph-gviz.js \
-    dygraph-interaction-model.js \
-    dygraph-tickers.js \
-    dygraph-plugin-base.js \
-    plugins/*.js \
-    dygraph-plugin-install.js \
-    $maybe_options_reference \
-    datahandler/datahandler.js \
-    datahandler/default.js \
-    datahandler/default-fractions.js \
-    datahandler/bars.js \
-    datahandler/bars-custom.js \
-    datahandler/bars-error.js \
-    datahandler/bars-fractions.js 
-  do
-      echo "$F"
-  done
-}
-
-# Pack all the JS together.
-CatSources () {
-  GetSources \
-  | xargs cat 
-}
-
-Copyright () {
-  echo '/*! @license Copyright 2014 Dan Vanderkam (danvdk@gmail.com) MIT-licensed (http://opensource.org/licenses/MIT) */'
-}
-
-CatCompressed () {
-  node_modules/uglify-js/bin/uglifyjs \
-    $(GetSources | xargs) \
-    --compress warnings=false \
-    --mangle \
-    --define DEBUG=false \
-    --preamble "$(Copyright)" \
-    $*
-}
-
-ACTION="${1:-update}"
-case "$ACTION" in
-ls)
-  GetSources
-  ;;
-cat)
-  Copyright
-  CatSources
-  ;;
-cat-dev)
-  DEBUG=true
-  Copyright
-  CatSources
-  ;;
-compress*|cat_compress*)
-  CatCompressed
-  ;;
-update)
-  CatCompressed --source-map dygraph-combined.js.map \
-    > dygraph-combined.js
-  chmod a+r dygraph-combined.js dygraph-combined.js.map
-  ;;
-*)
-  echo >&2 "Unknown action '$ACTION'"
-  exit 1
-  ;;
-esac
diff --git a/generate-documentation.py b/generate-documentation.py
deleted file mode 100755 (executable)
index 31781b9..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-#!/usr/bin/env python
-
-# Generate docs/options.html
-
-import glob
-import json
-import os
-import re
-import sys
-
-# Set this to the path to a test file to get debug output for just that test
-# file. Can be helpful to figure out why a test is not being shown for a
-# particular option.
-debug_tests = []  # [ 'tests/zoom.html' ]
-
-# Pull options reference JSON out of dygraph.js
-js = ''
-in_json = False
-for line in file('dygraph-options-reference.js'):
-  if '<JSON>' in line:
-    in_json = True
-  elif '</JSON>' in line:
-    in_json = False
-  elif in_json:
-    js += line
-
-# TODO(danvk): better errors here.
-assert js
-docs = json.loads(js)
-
-# Go through the tests and find uses of each option.
-for opt in docs:
-  docs[opt]['tests'] = []
-  docs[opt]['gallery'] = []
-
-# This is helpful for differentiating uses of options like 'width' and 'height'
-# from appearances of identically-named options in CSS.
-def find_braces(txt):
-  """Really primitive method to find text inside of {..} braces.
-  Doesn't work if there's an unmatched brace in a string, e.g. '{'. """
-  out = ''
-  level = 0
-  for char in txt:
-    if char == '{':
-      level += 1
-    if level >= 1:
-      out += char
-    if char == '}':
-      level -= 1
-  return out
-
-def search_files(type, files):
-  # Find text followed by a colon. These won't all be options, but those that
-  # have the same name as a Dygraph option probably will be.
-  prop_re = re.compile(r'\b([a-zA-Z0-9]+) *:')
-  for test_file in files:
-    if os.path.isfile(test_file): # Basically skips directories
-      text = file(test_file).read()
-
-      # Hack for slipping past gallery demos that have title in their attributes
-      # so they don't appear as reasons for the demo to have 'title' options.
-      if type == "gallery":
-        idx = text.find("function(")
-        if idx >= 0:
-          text = text[idx:]
-      braced_html = find_braces(text)
-      if debug_tests:
-        print braced_html
-
-      ms = re.findall(prop_re, braced_html)
-      for opt in ms:
-        if debug_tests: print '\n'.join(ms)
-        if opt in docs and test_file not in docs[opt][type]:
-          docs[opt][type].append(test_file)
-
-search_files("tests", glob.glob("tests/*.html"))
-search_files("gallery", glob.glob("gallery/*.js")) #TODO add grep "Gallery.register\("
-
-if debug_tests: sys.exit(0)
-
-# Extract a labels list.
-labels = []
-for _, opt in docs.iteritems():
-  for label in opt['labels']:
-    if label not in labels:
-      labels.append(label)
-
-print """
-<!--#include virtual="header.html" -->
-
-<!--
-  DO NOT EDIT THIS FILE!
-
-  This file is generated by generate-documentation.py.
--->
-
-<link rel=stylesheet href="options.css" />
-
-"""
-
-print """
-<div class="col-lg-3">
-<div class="dygraphs-side-nav affix-top" data-spy="affix" data-offset-top="0">
-<ul class='nav'>
-  <li><a href="#usage">Usage</a>
-"""
-for label in sorted(labels):
-  print '  <li><a href="#%s">%s</a>\n' % (label, label)
-print '</ul></div></div>\n\n'
-
-print """
-<div id='content' class='col-lg-9'>
-<h2>Options Reference</h2>
-<p>Dygraphs tries to do a good job of displaying your data without any further configuration. But inevitably, you're going to want to tinker. Dygraphs provides a rich set of options for configuring its display and behavior.</p>
-
-<a name="usage"></a><h3>Usage</h3>
-<p>You specify options in the third parameter to the dygraphs constructor:</p>
-<pre>g = new Dygraph(div,
-                data,
-                {
-                  option1: value1,
-                  option2: value2,
-                  ...
-                });
-</pre>
-
-<p>After you've created a Dygraph, you can change an option by calling the <code>updateOptions</code> method:</p>
-<pre>g.updateOptions({
-                  new_option1: value1,
-                  new_option2: value2
-                });
-</pre>
-
-<p>Some options can be set on a per-axis and per-series basis. See the docs on <a href="per-axis.html">per-axis and per-series options</a> to learn how to do this. The options which may be set in this way are marked as such on this page.</p>
-
-<p>For options which are functions (e.g. callbacks and formatters), the value of <code>this</code> is set to the Dygraph object.</p>
-
-<p>And, without further ado, here's the complete list of options:</p>
-"""
-
-def test_name(f):
-  """Takes 'tests/demo.html' -> 'demo'"""
-  return f.replace('tests/', '').replace('.html', '')
-
-def gallery_name(f):
-  """Takes 'gallery/demo.js' -> 'demo'"""
-  return f.replace('gallery/', '').replace('.js', '')
-
-def urlify_gallery(f):
-  """Takes 'gallery/demo.js' -> 'demo'"""
-  return f.replace('gallery/', 'gallery/#g/').replace('.js', '')
-
-
-for label in sorted(labels):
-  print '<a name="%s"></a><h3>%s</h3>\n' % (label, label)
-
-  for opt_name in sorted(docs.keys()):
-    opt = docs[opt_name]
-    if label not in opt['labels']: continue
-    tests = opt['tests']
-    if not tests:
-      examples_html = '<font color=red>NONE</font>'
-    else:
-      examples_html = ' '.join(
-        '<a href="%s">%s</a>' % (f, test_name(f)) for f in tests)
-
-    gallery = opt['gallery']
-    if not gallery:
-      gallery_html = '<font color=red>NONE</font>'
-    else:
-      gallery_html = ' '.join(
-        '<a href="%s">%s</a>' % (urlify_gallery(f), gallery_name(f)) for f in gallery)
-
-    if 'parameters' in opt:
-      parameters = opt['parameters']
-      parameters_html = '\n'.join("<i>%s</i>: %s<br/>" % (p[0], p[1]) for p in parameters)
-      parameters_html = "\n  <div class='parameters'>\n%s</div>" % (parameters_html);
-    else:
-      parameters_html = ''
-
-    if not opt['type']: opt['type'] = '(missing)'
-    if not opt['default']: opt['default'] = '(missing)'
-    if not opt['description']: opt['description'] = '(missing)'
-
-    print """
-  <div class='option'><a name="%(name)s"></a><b>%(name)s</b>
-  <a class="link" href="#%(name)s">#</a>
-  <br/>
-  <p>%(desc)s</p>
-  <i>Type: %(type)s</i><br/>%(parameters)s
-  <i>Default: %(default)s</i></p>
-  Gallery Samples: %(gallery_html)s<br/>
-  Other Examples: %(examples_html)s<br/>
-  <br/></div>
-  """ % { 'name': opt_name,
-          'type': opt['type'],
-          'parameters': parameters_html,
-          'default': opt['default'],
-          'desc': opt['description'],
-          'examples_html': examples_html,
-          'gallery_html': gallery_html}
-
-
-print """
-<a name="point_properties"></a><h3>Point Properties</h3>
-Some callbacks take a point argument. Its properties are:<br/>
-<ul>
-<li>xval/yval: The data coordinates of the point (with dates/times as millis since epoch)</li>
-<li>canvasx/canvasy: The canvas coordinates at which the point is drawn.</li>
-<li>name: The name of the data series to which the point belongs</li>
-<li>idx: The row number of the point in the data set</li>
-</ul>
-</div> <!-- #content -->
-
-<!--#include virtual="footer.html" -->
-"""
-
-# This page was super-helpful:
-# http://jsbeautifier.org/
diff --git a/generate-download.py b/generate-download.py
deleted file mode 100755 (executable)
index 2d2dd48..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-
-# Generates docs/download.html
-# Run:
-# ./generate-download.py > docs/download.html
-
-import json
-
-releases = json.load(file('releases.json'))
-
-def file_links(release):
-  v = release['version']
-  return ['<a href="%(v)s/%(f)s">%(f)s</a>' % {
-    'f': f, 'v': v} for f in release['files']]
-
-
-# Validation of releases.json
-for idx, release in enumerate(releases):
-  if idx == 0: continue
-  assert 'version' in release, 'Release missing version: %s' % release
-  assert 'files' in release, 'Release missing files: %s' % release
-  assert release['version'] < releases[idx - 1]['version'], (
-      'Releases should be in reverse chronological order in releases.json')
-
-current_html = '<p>' + ('</p><p>'.join(file_links(releases[0]))) + '</p>'
-
-
-previous_lis = []
-for release in releases[1:]:
-  previous_lis.append('<li>%(v)s: %(files)s (<a href="%(v)s/">%(v)s docs</a>)' % {
-      'v': release['version'],
-      'files': ', '.join(file_links(release))
-    })
-
-
-print '''
-<!--#include virtual="header.html" -->
-
-<!--
-  DO NOT EDIT THIS FILE!
-
-  This file is generated by generate-download.py.
--->
-
-<script src="modernizr.custom.18445.js"></script>
-<p>The current version of dygraphs is <b>%(version)s</b>. Most users will want to download minified files for this version:</p>
-
-<div id="current-release" class="panel">
-%(current_html)s
-</div>
-
-<p>There's a hosted version of dygraphs on <a href="https://cdnjs.com/libraries/dygraph">cdnjs.com</a>:</p>
-
-<pre>&lt;script src="//cdnjs.cloudflare.com/ajax/libs/dygraph/%(version)s/dygraph-combined.js"&gt;&lt;/script&gt;</pre>
-
-<p>You can install dygraphs using <a href="https://www.npmjs.org/package/dygraphs">NPM</a> or <a href="http://bower.io/search/?q=dygraphs">Bower</a>.</p>
-
-<p>To install using NPM:</p>
-<pre>$ npm install dygraphs
-# dygraphs is now in node_modules/dygraphs/dygraph-combined.js</pre>
-
-<p>To install using bower:</p>
-<pre>$ bower install dygraphs
-# dygraphs is now in bower_components/dygraphs/dygraph-combined.js</pre>
-
-<p>Most distributions include a source map. For non-concatenated JS, see <a href="https://github.com/danvk/dygraphs/blob/master/dygraph-dev.js">dygraph-dev.js</a> on <a href="https://github.com/danvk/dygraphs/">github</a>.</a>
-
-<p>To generate your own minified JS, run:</p>
-
-<pre>git clone https://github.com/danvk/dygraphs.git
-./generate-combined.sh
-</pre>
-
-<p>This will create a dygraph.min.js file in the dygraphs directory.</p>
-
-<p>You may also download files for previously-released versions:</p>
-
-<ul>
-%(previous_lis)s
-</ul>
-
-<p>See <a href="/versions.html">Version History</a> for more information on each release.</p>
-
-
-<!--#include virtual="footer.html" -->
-''' % {
-    'version': releases[0]['version'],
-    'current_html': current_html,
-    'previous_lis': '\n'.join(previous_lis)
-    }
diff --git a/generate-jar.sh b/generate-jar.sh
deleted file mode 100755 (executable)
index 2c1d12d..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-# Generates a JAR file that can be used from GWT to load Dygraphs
-
-jar -cf dygraph-gwt.jar -C gwt org
diff --git a/generate-jsdoc.sh b/generate-jsdoc.sh
deleted file mode 100755 (executable)
index 6512fb0..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# Generates JSDoc in the /jsdoc dir. Clears any existing jsdoc there.
-
-rm -rf jsdoc
-echo Generating JSDoc...
-java -jar jsdoc-toolkit/jsrun.jar \
-  jsdoc-toolkit/app/run.js \
-  -d=jsdoc -t=jsdoc-toolkit/templates/jsdoc \
-  dygraph.js \
-| tee /tmp/dygraphs-jsdocerrors.txt
-
-if [ -s /tmp/dygraphs-jsdocerrors.txt ]; then
-  echo Please fix any jsdoc errors/warnings before sending patches.
-fi
-
-chmod -R a+rX jsdoc
-
-echo Done
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644 (file)
index 0000000..b1592bd
--- /dev/null
@@ -0,0 +1,168 @@
+var gulp = require('gulp');
+var plugins = require('gulp-load-plugins')();
+var karma = require('karma').server;
+var path = require('path');
+
+var dev = false;
+var src = {
+  base: "src",
+  polyfills: {
+    base: "src/polyfills",
+    files: [
+      "console.js",
+      "dashed-canvas.js"
+    ]
+  },
+  main: {
+    base: "src",
+    files: [
+      "dygraph-options.js",
+      "dygraph-layout.js",
+      "dygraph-canvas.js",
+      "dygraph.js",
+      "dygraph-utils.js",
+      "dygraph-gviz.js",
+      "dygraph-interaction-model.js",
+      "dygraph-tickers.js",
+      "dygraph-plugin-base.js"
+    ]
+  } ,
+  plugins: {
+    base: "src/plugins",
+    files: [
+      "annotations.js",
+      "axes.js",
+      "chart-labels.js",
+      "grid.js",
+      "legend.js",
+      "range-selector.js",
+      "../dygraph-plugin-install.js"
+    ]
+  },
+  // Only used by dynamic loader
+  devOptions: {
+    base: "src",
+    files: ["dygraph-options-reference.js"]
+  },
+  datahandlers: {
+    base: "src/datahandler",
+    files: [
+      "datahandler.js",
+      "default.js",
+      "default-fractions.js",
+      "bars.js",
+      "bars-error.js",
+      "bars-custom.js",
+      "bars-fractions.js"
+    ]
+  }
+};
+
+// Convenience function to merge multiple arrays into one
+var mergePaths = function() {
+  var paths = [];
+  var pathobj = null;
+  if (arguments.length > 0) {
+    for (var i = 0; i < arguments.length; i++) {
+      pathObj = arguments[i];
+      pathObj.files.map(function(filename) {
+        paths.push(path.join(pathObj.base, filename));
+      });
+    }
+
+    return paths;
+  } else {
+    return [];
+  }
+};
+
+var copyright = '/*! @license Copyright 2015 Dan Vanderkam (danvdk@gmail.com) MIT-licensed (http://opensource.org/licenses/MIT) */';
+
+/*
+// Creates the dygraph-autoloader
+gulp.task('create-loader', function() {
+  // Create string ready for javascript array
+  var files = mergePaths(src.lib, src.main, src.plugins, src.devOptions, src.datahandlers)
+    .map(function(filename) {
+      // Make the path relative to dist file and add quotes
+      return "'" + filename.replace(src.base, '../../' + src.base) + "'";
+    })
+    .join(",");
+
+  return gulp.src(src.base + '../dygraph/dygraph-autoloader.js')
+    .pipe(plugins.replace(/\/\* REPLACEME \*\//, files))
+    .pipe(gulp.dest('dist/scratch'));
+});
+*/
+
+gulp.task('create-dev', function() {
+  var dest = 'dist';
+  return gulp.src(mergePaths(src.polyfills, src.main, src.plugins, src.devOptions, src.datahandlers), {base: '.'})
+    .pipe(plugins.sourcemaps.init({debug:true}))
+    .pipe(plugins.concat('dygraph-combined-dev.js'))
+    .pipe(plugins.header(copyright))
+    .pipe(plugins.sourcemaps.write('.'))  // '.' = external sourcemap
+    .pipe(gulp.dest(dest));
+});
+
+gulp.task('concat', function() {
+  var dest = 'dist';
+  return gulp.src(mergePaths(src.polyfills, src.main, src.plugins, src.datahandlers), {base: '.'})
+     .pipe(plugins.sourcemaps.init())
+     .pipe(plugins.concat('scratch'))
+     .pipe(plugins.header(copyright))
+     .pipe(gulp.dest(dest))
+     .pipe(plugins.uglify({
+      compress: {
+        global_defs: { DEBUG: false }
+      },
+      warnings: false,
+      preserveComments: "none"
+     }))
+     .pipe(plugins.header(copyright))
+     .pipe(plugins.rename('dygraph-combined.js'))
+     .pipe(plugins.sourcemaps.write('.'))
+     .pipe(gulp.dest(dest));
+
+});
+
+gulp.task('gwt-dist', ['concat'], function() {
+  // Copy package structure to dist folder
+  gulp.src('gwt/**', {'base': '.'})
+    .pipe(gulp.dest('dist'));
+
+  gulp.src('dygraph-combined.js')
+    .pipe(gulp.dest('dist/gwt/org/danvk'));
+
+  // Generate jar
+  gulp.src('')
+    .pipe(plugins.shell([
+      'bash -c "jar -cf dist/dygraph-gwt.jar -C dist/gwt org"'
+    ]))
+});
+
+gulp.task('test', ['concat', 'create-dev'], function(done) {
+  karma.start({
+    configFile: process.cwd() + '/auto_tests/karma.conf.js',
+    singleRun: true
+  }, done);
+});
+
+gulp.task('coveralls', ['test'], plugins.shell.task([
+  './scripts/transform-coverage.js ' +
+      'dist/dygraph-combined-dev.js.map ' +
+      'dist/coverage/report-lcov/lcov.info ' +
+  '| ./node_modules/.bin/coveralls'
+]));
+
+gulp.task('watch', function() {
+  gulp.watch('src/**', ['concat']);
+});
+
+gulp.task('watch-test', function() {
+  gulp.watch(['src/**', 'auto_tests/tests/**'], ['test']);
+});
+
+gulp.task('dist', ['gwt-dist', 'concat', 'create-dev']);
+gulp.task('travis', ['test', 'coveralls']);
+gulp.task('default', ['test', 'dist']);
diff --git a/gviz-api.js b/gviz-api.js
deleted file mode 100644 (file)
index b29fb26..0000000
+++ /dev/null
@@ -1,461 +0,0 @@
-// Copyright 2009 Google Inc.
-// All Rights Reserved.
-
-/**
- * This file exposes the external Google Visualization API.
- *
- * The file can be used to enable auto complete of objects and methods provided by the
- * Google Visualization API, and for easier exploration of the API.
- *
- * To enable auto complete in a development environment - copy the file into the project
- * you are working on where the development tool you are using can index the file.
- *
- * Disclaimer: there may be missing classes and methods and the file may
- * be updated and/or changed. For the most up to date API reference please visit:
- * {@link http://code.google.com/intl/iw/apis/visualization/documentation/reference.html}
- */
-
-var google = {};
-google.visualization = {};
-
-/** @constructor */
-google.visualization.DataTable = function(opt_data, opt_version) {};
-google.visualization.DataTable.prototype.getNumberOfRows = function() {};
-google.visualization.DataTable.prototype.getNumberOfColumns = function() {};
-google.visualization.DataTable.prototype.clone = function() {};
-google.visualization.DataTable.prototype.getColumnId = function(columnIndex) {};
-google.visualization.DataTable.prototype.getColumnIndex = function(columnId) {};
-google.visualization.DataTable.prototype.getColumnLabel = function(columnIndex) {};
-google.visualization.DataTable.prototype.getColumnPattern = function(columnIndex) {};
-google.visualization.DataTable.prototype.getColumnRole = function(columnIndex) {};
-google.visualization.DataTable.prototype.getColumnType = function(columnIndex) {};
-google.visualization.DataTable.prototype.getValue = function(rowIndex, columnIndex) {};
-google.visualization.DataTable.prototype.getFormattedValue = function(rowIndex, columnIndex) {};
-google.visualization.DataTable.prototype.getProperty = function(rowIndex, columnIndex, property) {};
-google.visualization.DataTable.prototype.getProperties = function(rowIndex, columnIndex) {};
-google.visualization.DataTable.prototype.getTableProperties = function() {};
-google.visualization.DataTable.prototype.getTableProperty = function(property) {};
-google.visualization.DataTable.prototype.setTableProperties = function(properties) {};
-google.visualization.DataTable.prototype.setTableProperty = function(property, value) {};
-google.visualization.DataTable.prototype.setValue = function(rowIndex, columnIndex, value) {};
-google.visualization.DataTable.prototype.setFormattedValue = function(rowIndex, columnIndex, formattedValue) {};
-google.visualization.DataTable.prototype.setProperties = function(rowIndex, columnIndex, properties) {};
-google.visualization.DataTable.prototype.setProperty = function(rowIndex, columnIndex, property, value) {};
-google.visualization.DataTable.prototype.setCell = function(rowIndex, columnIndex, opt_value, opt_formattedValue, opt_properties) {};
-google.visualization.DataTable.prototype.setRowProperties = function(rowIndex, properties) {};
-google.visualization.DataTable.prototype.setRowProperty = function(rowIndex, property, value) {};
-google.visualization.DataTable.prototype.getRowProperty = function(rowIndex, property) {};
-google.visualization.DataTable.prototype.getRowProperties = function(rowIndex) {};
-google.visualization.DataTable.prototype.setColumnLabel = function(columnIndex, newLabel) {};
-google.visualization.DataTable.prototype.setColumnProperties = function(columnIndex, properties) {};
-google.visualization.DataTable.prototype.setColumnProperty = function(columnIndex, property, value) {};
-google.visualization.DataTable.prototype.getColumnProperty = function(columnIndex, property) {};
-google.visualization.DataTable.prototype.getColumnProperties = function(columnIndex) {};
-google.visualization.DataTable.prototype.insertColumn = function(atColIndex, type, opt_label, opt_id) {};
-google.visualization.DataTable.prototype.addColumn = function(type, opt_label, opt_id) {};
-google.visualization.DataTable.prototype.insertRows = function(atRowIndex, numOrArray) {};
-google.visualization.DataTable.prototype.addRows = function(numOrArray) {};
-google.visualization.DataTable.prototype.addRow = function(opt_cellArray) {};
-google.visualization.DataTable.prototype.getColumnRange = function(columnIndex) {};
-google.visualization.DataTable.prototype.getSortedRows = function(sortColumns) {};
-google.visualization.DataTable.prototype.sort = function(sortColumns) {};
-google.visualization.DataTable.prototype.getDistinctValues = function(column) {};
-google.visualization.DataTable.prototype.getFilteredRows = function(columnFilters) {};
-google.visualization.DataTable.prototype.removeRows = function(fromRowIndex, numRows) {};
-google.visualization.DataTable.prototype.removeRow = function(rowIndex) {};
-google.visualization.DataTable.prototype.removeColumns = function(fromColIndex, numCols) {};
-google.visualization.DataTable.prototype.removeColumn = function(colIndex) {};
-
-/** @return {string} JSON representation. */
-google.visualization.DataTable.prototype.toJSON = function() {};
-
-/** @constructor */
-google.visualization.QueryResponse = function(responseObj) {};
-google.visualization.QueryResponse.getVersionFromResponse = function(responseObj) {};
-google.visualization.QueryResponse.prototype.getVersion = function() {};
-google.visualization.QueryResponse.prototype.getExecutionStatus = function() {};
-google.visualization.QueryResponse.prototype.isError = function() {};
-google.visualization.QueryResponse.prototype.hasWarning = function() {};
-google.visualization.QueryResponse.prototype.containsReason = function(reason) {};
-google.visualization.QueryResponse.prototype.getDataSignature = function() {};
-google.visualization.QueryResponse.prototype.getDataTable = function() {};
-google.visualization.QueryResponse.prototype.getReasons = function() {};
-google.visualization.QueryResponse.prototype.getMessage = function() {};
-google.visualization.QueryResponse.prototype.getDetailedMessage = function() {};
-
-/** @constructor */
-google.visualization.Query = function(dataSourceUrl, opt_options) {};
-google.visualization.Query.refreshAllQueries = function() {};
-google.visualization.Query.setResponse = function(response) {};
-google.visualization.Query.prototype.setRefreshInterval = function(intervalSeconds) {};
-google.visualization.Query.prototype.send = function(responseHandler) {};
-google.visualization.Query.prototype.makeRequest = function(responseHandler, opt_params) {};
-google.visualization.Query.prototype.abort = function() {};
-google.visualization.Query.prototype.setTimeout = function(timeoutSeconds) {};
-google.visualization.Query.prototype.setRefreshable = function(refreshable) {};
-google.visualization.Query.prototype.setQuery = function(queryString) {};
-
-google.visualization.errors = {};
-google.visualization.errors.addError = function(container, message, opt_detailedMessage, opt_options) {};
-google.visualization.errors.removeAll = function(container) {};
-google.visualization.errors.addErrorFromQueryResponse = function(container, response) {};
-google.visualization.errors.removeError = function(id) {};
-google.visualization.errors.getContainer = function(errorId) {};
-
-google.visualization.events = {};
-google.visualization.events.addListener = function(eventSource, eventName, eventHandler) {};
-google.visualization.events.trigger = function(eventSource, eventName, eventDetails) {};
-google.visualization.events.removeListener = function(listener) {};
-google.visualization.events.removeAllListeners = function(eventSource) {};
-
-/** @constructor */
-google.visualization.DataView = function(dataTable) {};
-google.visualization.DataView.fromJSON = function(dataTable, view) {};
-google.visualization.DataView.prototype.setColumns = function(colIndices) {};
-google.visualization.DataView.prototype.setRows = function(arg0, opt_arg1) {};
-google.visualization.DataView.prototype.getViewColumns = function() {};
-google.visualization.DataView.prototype.getViewRows = function() {};
-google.visualization.DataView.prototype.hideColumns = function(colIndices) {};
-google.visualization.DataView.prototype.hideRows = function(arg0, opt_arg1) {};
-google.visualization.DataView.prototype.getViewColumnIndex = function(tableColumnIndex) {};
-google.visualization.DataView.prototype.getViewRowIndex = function(tableRowIndex) {};
-google.visualization.DataView.prototype.getTableColumnIndex = function(viewColumnIndex) {};
-google.visualization.DataView.prototype.getUnderlyingTableColumnIndex = function(viewColumnIndex) {};
-google.visualization.DataView.prototype.getTableRowIndex = function(viewRowIndex) {};
-google.visualization.DataView.prototype.getUnderlyingTableRowIndex = function(viewRowIndex) {};
-google.visualization.DataView.prototype.getNumberOfRows = function() {};
-google.visualization.DataView.prototype.getNumberOfColumns = function() {};
-google.visualization.DataView.prototype.getColumnId = function(columnIndex) {};
-google.visualization.DataView.prototype.getColumnIndex = function(columnId) {};
-google.visualization.DataView.prototype.getColumnLabel = function(columnIndex) {};
-google.visualization.DataView.prototype.getColumnPattern = function(columnIndex) {};
-google.visualization.DataView.prototype.getColumnRole = function(columnIndex) {};
-google.visualization.DataView.prototype.getColumnType = function(columnIndex) {};
-google.visualization.DataView.prototype.getValue = function(rowIndex, columnIndex) {};
-google.visualization.DataView.prototype.getFormattedValue = function(rowIndex, columnIndex) {};
-google.visualization.DataView.prototype.getProperty = function(rowIndex, columnIndex, property) {};
-google.visualization.DataView.prototype.getColumnProperty = function(columnIndex, property) {};
-google.visualization.DataView.prototype.getColumnProperties = function(columnIndex) {};
-google.visualization.DataView.prototype.getTableProperty = function(property) {};
-google.visualization.DataView.prototype.getTableProperties = function() {};
-google.visualization.DataView.prototype.getRowProperty = function(rowIndex, property) {};
-google.visualization.DataView.prototype.getRowProperties = function(rowIndex) {};
-google.visualization.DataView.prototype.getColumnRange = function(columnIndex) {};
-google.visualization.DataView.prototype.getDistinctValues = function(columnIndex) {};
-google.visualization.DataView.prototype.getSortedRows = function(sortColumns) {};
-google.visualization.DataView.prototype.getFilteredRows = function(columnFilters) {};
-google.visualization.DataView.prototype.toDataTable = function() {};
-
-/** @return {string} JSON representation. */
-google.visualization.DataView.prototype.toJSON = function() {};
-
-/** @constructor */
-google.visualization.ArrowFormat = function(opt_options) {};
-google.visualization.ArrowFormat.prototype.format = function(dataTable, columnIndex) {};
-
-/** @constructor */
-google.visualization.BarFormat = function(opt_options) {};
-google.visualization.BarFormat.prototype.format = function(dataTable, columnIndex) {};
-
-/** @constructor */
-google.visualization.ColorFormat = function() {};
-google.visualization.ColorFormat.prototype.addRange = function(from, to, color, bgcolor) {};
-google.visualization.ColorFormat.prototype.addGradientRange = function(from, to, color, fromBgColor, toBgColor) {};
-google.visualization.ColorFormat.prototype.format = function(dataTable, columnIndex) {};
-
-/** @constructor */
-google.visualization.DateFormat = function(opt_options) {};
-google.visualization.DateFormat.prototype.format = function(dataTable, columnIndex) {};
-google.visualization.DateFormat.prototype.formatValue = function(value) {};
-
-/** @constructor */
-google.visualization.NumberFormat = function(opt_options) {};
-google.visualization.NumberFormat.prototype.format = function(dataTable, columnIndex) {};
-google.visualization.NumberFormat.prototype.formatValue = function(value) {};
-google.visualization.NumberFormat.DECIMAL_SEP;
-google.visualization.NumberFormat.GROUP_SEP;
-google.visualization.NumberFormat.DECIMAL_PATTERN;
-
-/** @constructor */
-google.visualization.PatternFormat = function(pattern) {};
-google.visualization.PatternFormat.prototype.format = function(dataTable, srcColumnIndices, opt_dstColumnIndex) {};
-
-/** @constructor */
-google.visualization.GadgetHelper = function() {};
-google.visualization.GadgetHelper.prototype.createQueryFromPrefs = function(prefs) {};
-google.visualization.GadgetHelper.prototype.validateResponse = function(response) {};
-
-/** @constructor */
-google.visualization.AnnotatedTimeLine = function(container) {};
-google.visualization.AnnotatedTimeLine.prototype.draw = function(data, opt_options) {};
-google.visualization.AnnotatedTimeLine.prototype.getSelection = function() {};
-google.visualization.AnnotatedTimeLine.prototype.getVisibleChartRange = function() {};
-google.visualization.AnnotatedTimeLine.prototype.setVisibleChartRange = function(firstDate, lastDate, opt_animate) {};
-google.visualization.AnnotatedTimeLine.prototype.showDataColumns = function(columnIndexes) {};
-google.visualization.AnnotatedTimeLine.prototype.hideDataColumns = function(columnIndexes) {};
-
-/** @constructor */
-google.visualization.AreaChart = function(container) {};
-google.visualization.AreaChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.AreaChart.prototype.clearChart = function() {};
-google.visualization.AreaChart.prototype.getSelection = function() {};
-google.visualization.AreaChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.BarChart = function(container) {};
-google.visualization.BarChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.BarChart.prototype.clearChart = function() {};
-google.visualization.BarChart.prototype.getSelection = function() {};
-google.visualization.BarChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.BubbleChart = function(container) {};
-google.visualization.BubbleChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.BubbleChart.prototype.clearChart = function() {};
-google.visualization.BubbleChart.prototype.getSelection = function() {};
-google.visualization.BubbleChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.CandlestickChart = function(container) {};
-google.visualization.CandlestickChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.CandlestickChart.prototype.clearChart = function() {};
-google.visualization.CandlestickChart.prototype.getSelection = function() {};
-google.visualization.CandlestickChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.ColumnChart = function(container) {};
-google.visualization.ColumnChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.ColumnChart.prototype.clearChart = function() {};
-google.visualization.ColumnChart.prototype.getSelection = function() {};
-google.visualization.ColumnChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.ComboChart = function(container) {};
-google.visualization.ComboChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.ComboChart.prototype.clearChart = function() {};
-google.visualization.ComboChart.prototype.getSelection = function() {};
-google.visualization.ComboChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.Gauge = function(container) {};
-google.visualization.Gauge.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.Gauge.prototype.clearChart = function() {};
-
-/** @constructor */
-google.visualization.GeoChart = function(container) {};
-google.visualization.GeoChart.mapExists = function(userOptions) {};
-google.visualization.GeoChart.prototype.clearChart = function() {};
-google.visualization.GeoChart.prototype.draw = function(dataTable, userOptions, opt_state) {};
-google.visualization.GeoChart.prototype.getSelection = function() {};
-google.visualization.GeoChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.GeoMap = function(container) {};
-google.visualization.GeoMap.clickOnRegion = function(id, zoomLevel, segmentBy, instanceIndex) {};
-google.visualization.GeoMap.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.GeoMap.prototype.getSelection = function() {};
-google.visualization.GeoMap.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.Map = function(container) {};
-google.visualization.Map.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.Map.prototype.getSelection = function() {};
-google.visualization.Map.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.ImageAreaChart = function(container) {};
-google.visualization.ImageAreaChart.prototype.draw = function(data, opt_options) {};
-
-/** @constructor */
-google.visualization.ImageBarChart = function(container) {};
-google.visualization.ImageBarChart.prototype.draw = function(data, opt_options) {};
-
-/** @constructor */
-google.visualization.ImageCandlestickChart = function(container) {};
-google.visualization.ImageCandlestickChart.prototype.draw = function(data, opt_options) {};
-
-/** @constructor */
-google.visualization.ImageChart = function(container) {};
-google.visualization.ImageChart.prototype.draw = function(data, opt_options) {};
-
-/** @constructor */
-google.visualization.ImageLineChart = function(container) {};
-google.visualization.ImageLineChart.prototype.draw = function(data, opt_options) {};
-
-/** @constructor */
-google.visualization.ImagePieChart = function(container) {};
-google.visualization.ImagePieChart.prototype.draw = function(data, opt_options) {};
-
-/** @constructor */
-google.visualization.ImageSparkLine = function(container, opt_domHelper) {};
-google.visualization.ImageSparkLine.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.ImageSparkLine.prototype.getSelection = function() {};
-google.visualization.ImageSparkLine.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.IntensityMap = function(container) {};
-google.visualization.IntensityMap.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.IntensityMap.prototype.getSelection = function() {};
-google.visualization.IntensityMap.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.LineChart = function(container) {};
-google.visualization.LineChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.LineChart.prototype.clearChart = function() {};
-google.visualization.LineChart.prototype.getSelection = function() {};
-google.visualization.LineChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.MotionChart = function(container) {};
-google.visualization.MotionChart.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.MotionChart.prototype.getState = function() {};
-
-/** @constructor */
-google.visualization.OrgChart = function(container) {};
-google.visualization.OrgChart.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.OrgChart.prototype.getSelection = function() {};
-google.visualization.OrgChart.prototype.setSelection = function(selection) {};
-google.visualization.OrgChart.prototype.getCollapsedNodes = function() {};
-google.visualization.OrgChart.prototype.getChildrenIndexes = function(rowInd) {};
-google.visualization.OrgChart.prototype.collapse = function(rowInd, collapse) {};
-
-/** @constructor */
-google.visualization.PieChart = function(container) {};
-google.visualization.PieChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.PieChart.prototype.clearChart = function() {};
-google.visualization.PieChart.prototype.getSelection = function() {};
-google.visualization.PieChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.ScatterChart = function(container) {};
-google.visualization.ScatterChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.ScatterChart.prototype.clearChart = function() {};
-google.visualization.ScatterChart.prototype.getSelection = function() {};
-google.visualization.ScatterChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.SparklineChart = function(container) {};
-google.visualization.SparklineChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.SparklineChart.prototype.clearChart = function() {};
-google.visualization.SparklineChart.prototype.getSelection = function() {};
-google.visualization.SparklineChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.SteppedAreaChart = function(container) {};
-google.visualization.SteppedAreaChart.prototype.draw = function(data, opt_options, opt_state) {};
-google.visualization.SteppedAreaChart.prototype.clearChart = function() {};
-google.visualization.SteppedAreaChart.prototype.getSelection = function() {};
-google.visualization.SteppedAreaChart.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.Table = function(container) {};
-google.visualization.Table.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.Table.prototype.clearChart = function() {};
-google.visualization.Table.prototype.getSortInfo = function() {};
-google.visualization.Table.prototype.getSelection = function() {};
-google.visualization.Table.prototype.setSelection = function(selection) {};
-
-/** @constructor */
-google.visualization.TreeMap = function(container) {};
-google.visualization.TreeMap.prototype.draw = function(dataTable, opt_options) {};
-google.visualization.TreeMap.prototype.clearChart = function() {};
-google.visualization.TreeMap.prototype.getSelection = function() {};
-google.visualization.TreeMap.prototype.setSelection = function(selection) {};
-
-google.visualization.drawToolbar = function(container, components) {};
-
-/** @constructor */
-google.visualization.ChartWrapper = function(opt_specification) {};
-google.visualization.ChartWrapper.prototype.draw = function(opt_container) {};
-google.visualization.ChartWrapper.prototype.getDataSourceUrl = function() {};
-google.visualization.ChartWrapper.prototype.getDataTable = function() {};
-google.visualization.ChartWrapper.prototype.getChartName = function() {};
-google.visualization.ChartWrapper.prototype.getChartType = function() {};
-google.visualization.ChartWrapper.prototype.getContainerId = function() {};
-google.visualization.ChartWrapper.prototype.getQuery = function() {};
-google.visualization.ChartWrapper.prototype.getRefreshInterval = function() {};
-google.visualization.ChartWrapper.prototype.getView = function() {};
-google.visualization.ChartWrapper.prototype.getOption = function(key, opt_default) {};
-google.visualization.ChartWrapper.prototype.getOptions = function() {};
-google.visualization.ChartWrapper.prototype.setDataSourceUrl = function(dataSourceUrl) {};
-google.visualization.ChartWrapper.prototype.setDataTable = function(dataTable) {};
-google.visualization.ChartWrapper.prototype.setChartName = function(chartName) {};
-google.visualization.ChartWrapper.prototype.setChartType = function(chartType) {};
-google.visualization.ChartWrapper.prototype.setContainerId = function(containerId) {};
-google.visualization.ChartWrapper.prototype.setQuery = function(query) {};
-google.visualization.ChartWrapper.prototype.setRefreshInterval = function(refreshInterval) {};
-google.visualization.ChartWrapper.prototype.setView = function(view) {};
-google.visualization.ChartWrapper.prototype.setOption = function(key, value) {};
-google.visualization.ChartWrapper.prototype.setOptions = function(options) {};
-
-/** @return {string} JSON representation. */
-google.visualization.ChartWrapper.prototype.toJSON = function() {};
-
-/** @constructor */
-google.visualization.ControlWrapper = function(opt_specification) {};
-google.visualization.ControlWrapper.prototype.draw = function(opt_container) {};
-google.visualization.ControlWrapper.prototype.toJSON = function() {};
-google.visualization.ControlWrapper.prototype.getDataSourceUrl = function() {};
-google.visualization.ControlWrapper.prototype.getDataTable = function() {};
-google.visualization.ControlWrapper.prototype.getControlName = function() {};
-google.visualization.ControlWrapper.prototype.getControlType = function() {};
-google.visualization.ControlWrapper.prototype.getContainerId = function() {};
-google.visualization.ControlWrapper.prototype.getQuery = function() {};
-google.visualization.ControlWrapper.prototype.getRefreshInterval = function() {};
-google.visualization.ControlWrapper.prototype.getView = function() {};
-google.visualization.ControlWrapper.prototype.getOption = function(key, opt_default) {};
-google.visualization.ControlWrapper.prototype.getOptions = function() {};
-google.visualization.ControlWrapper.prototype.setDataSourceUrl = function(dataSourceUrl) {};
-google.visualization.ControlWrapper.prototype.setDataTable = function(dataTable) {};
-google.visualization.ControlWrapper.prototype.setControlName = function(controlName) {};
-google.visualization.ControlWrapper.prototype.setControlType = function(controlType) {};
-google.visualization.ControlWrapper.prototype.setContainerId = function(containerId) {};
-google.visualization.ControlWrapper.prototype.setQuery = function(query) {};
-google.visualization.ControlWrapper.prototype.setRefreshInterval = function(refreshInterval) {};
-google.visualization.ControlWrapper.prototype.setView = function(view) {};
-google.visualization.ControlWrapper.prototype.setOption = function(key, value) {};
-google.visualization.ControlWrapper.prototype.setOptions = function(options) {};
-
-// NOTE: I (danvk): commented this out because of compiler warnings.
-/** @return {string} JSON representation. */
-// google.visualization.ChartWrapper.prototype.toJSON = function() {};
-
-/** @constructor */
-google.visualization.ChartEditor = function(opt_config) {};
-google.visualization.ChartEditor.prototype.openDialog = function(specification, opt_options) {};
-google.visualization.ChartEditor.prototype.getChartWrapper = function() {};
-google.visualization.ChartEditor.prototype.setChartWrapper = function(chartWrapper) {};
-google.visualization.ChartEditor.prototype.closeDialog = function() {};
-
-/** @constructor */
-google.visualization.Dashboard = function(container) {};
-google.visualization.Dashboard.prototype.bind = function(controls, participants) {};
-google.visualization.Dashboard.prototype.draw = function(dataTable) {};
-
-/** @constructor */
-google.visualization.StringFilter = function(container) {};
-google.visualization.StringFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
-google.visualization.StringFilter.prototype.applyFilter = function() {};
-google.visualization.StringFilter.prototype.getState = function() {};
-google.visualization.StringFilter.prototype.resetControl = function() {};
-
-/** @constructor */
-google.visualization.NumberRangeFilter = function(container) {};
-google.visualization.NumberRangeFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
-google.visualization.NumberRangeFilter.prototype.applyFilter = function() {};
-google.visualization.NumberRangeFilter.prototype.getState = function() {};
-google.visualization.NumberRangeFilter.prototype.resetControl = function() {};
-
-/** @constructor */
-google.visualization.CategoryFilter = function(container) {};
-google.visualization.CategoryFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
-google.visualization.CategoryFilter.prototype.applyFilter = function() {};
-google.visualization.CategoryFilter.prototype.getState = function() {};
-google.visualization.CategoryFilter.prototype.resetControl = function() {};
-
-/** @constructor */
-google.visualization.ChartRangeFilter = function(container) {};
-google.visualization.ChartRangeFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
-google.visualization.ChartRangeFilter.prototype.applyFilter = function() {};
-google.visualization.ChartRangeFilter.prototype.getState = function() {};
-google.visualization.ChartRangeFilter.prototype.resetControl = function() {};
diff --git a/gwt/org/danvk/dygraph-combined.js b/gwt/org/danvk/dygraph-combined.js
deleted file mode 120000 (symlink)
index d64425f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../../dygraph-combined.js
\ No newline at end of file
diff --git a/jsTestDriver.conf b/jsTestDriver.conf
deleted file mode 100644 (file)
index ec581b3..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-server: http://localhost:9876
-
-# This list needs to be kept in sync w/ the one in dygraph-dev.js
-# and the one in generate-combined.sh.
-load:
-  - dashed-canvas.js
-  - dygraph-layout.js
-  - dygraph-canvas.js
-  - dygraph-options.js
-  - dygraph.js
-  - dygraph-utils.js
-  - dygraph-gviz.js
-  - dygraph-interaction-model.js
-  - dygraph-options-reference.js
-  - dygraph-tickers.js
-  - dygraph-dev.js
-  - dygraph-plugin-base.js
-  - plugins/*.js
-  - dygraph-plugin-install.js
-  - datahandler/datahandler.js
-  - datahandler/default.js
-  - datahandler/default-fractions.js
-  - datahandler/bars.js
-  - datahandler/bars-error.js
-  - datahandler/bars-custom.js
-  - datahandler/bars-fractions.js
-  - extras/smooth-plotter.js
-  - auto_tests/tests/*.js
-  - auto_tests/lib/jquery-1.4.2.js
-
-plugin:
- - name: "coverage"
-   jar: "auto_tests/lib/coverage-1.3.5.jar"
-   module: "com.google.jstestdriver.coverage.CoverageModule"
index 25aff77..de85acc 100644 (file)
   "devDependencies": {
     "closure-compiler": "^0.2.6",
     "coveralls": "^2.11.2",
+    "gulp": "^3.8.10",
+    "gulp-concat": "^2.4.3",
+    "gulp-eslint": "^0.2.0",
+    "gulp-header": "^1.2.2",
+    "gulp-load-plugins": "^0.8.0",
+    "gulp-rename": "^1.2.0",
+    "gulp-replace": "^0.5.0",
+    "gulp-shell": "^0.4.0",
+    "gulp-sourcemaps": "^1.3.0",
+    "gulp-uglify": "^1.0.2",
     "jshint": "^2.5.10",
+    "karma": "^0.12.31",
+    "karma-chai-plugins": "^0.2.4",
+    "karma-chrome-launcher": "^0.1.7",
+    "karma-coverage": "^0.2.7",
+    "karma-mocha": "^0.1.10",
+    "karma-mocha-reporter": "^1.0.2",
+    "karma-phantomjs-launcher": "^0.1.4",
+    "karma-spec-reporter": "0.0.16",
+    "lcov-parse": "0.0.9",
+    "mocha": "^2.1.0",
     "obvious-closure-library": "^20140401.0.2",
+    "parse-data-uri": "^0.2.0",
     "phantomjs": "^1.9.7-8",
+    "source-map": "^0.4.2",
     "uglify-js": "^2"
   },
   "scripts": {
diff --git a/phantom-driver.js b/phantom-driver.js
deleted file mode 100644 (file)
index c534964..0000000
+++ /dev/null
@@ -1,203 +0,0 @@
-// Invoke via: ./test.sh
-//
-// or phantomjs phantom-driver.js [testCase.test]
-//
-// For more on phantomjs, visit www.phantomjs.org.
-
-var RunAllAutoTests = function(done_callback) {
-
-var page = require('webpage').create();
-
-// NOTE: Cannot include '#' or '?' in this URL.
-var url = 'auto_tests/misc/local.html';
-
-// NOTE: changing the line below to this:
-// page.open(url, function(status)) {
-// makes phantomjs hang.
-page.open(url, function(status) {
-  if (status !== 'success') {
-    console.warn('Page status: ' + status);
-    console.log(page);
-    phantom.exit();
-  }
-
-  var testCase, test;
-  var verbose = false;
-  var optIdx = 0;
-  if (phantom.args.length > 0 && phantom.args[0] === "--verbose") {
-    verbose = true;
-    optIdx = 1;
-  }
-  if (phantom.args.length == optIdx + 1) {
-    var parts = phantom.args[optIdx].split('.');
-    if (2 != parts.length) {
-      console.warn('Usage: phantomjs phantom-driver.js [--verbose] [testCase.test]');
-      phantom.exit();
-    }
-    testCase = parts[0];
-    test = parts[1];
-  }
-
-  var loggingOn = false;
-
-  page.onConsoleMessage = function (msg) {
-    if (msg == 'Running ' + test) {
-      loggingOn = true;
-    } else if (msg.substr(0, 'Running'.length) == 'Running') {
-      loggingOn = false;
-    }
-    if (verbose || loggingOn) console.log(msg);
-  };
-
-  page.onError = function (msg, trace) {
-    console.log(msg);
-    trace.forEach(function(item) {
-        console.log('  ', item.file, ':', item.line);
-    })
-  };
-
-  var results;
-  
-  // Run all tests.
-  var start = new Date().getTime();
-  results = page.evaluate(function() {
-    var num_passing = 0, num_failing = 0;
-    var failures = [];
-
-    jstestdriver.attachListener({
-      finish : function(tc, name, result, e) {
-        console.log("Result:", tc.name, name, result, e);
-        if (result) {
-          // console.log(testCase + '.' + test + ' passed');
-          num_passing++;
-        } else {
-          num_failing++;
-          failures.push(tc.name + '.' + name);
-        }
-      }
-    });
-    var testCases = getAllTestCases();
-    for (var idx in testCases) {
-      var entry = testCases[idx];
-
-      var prototype = entry.testCase;
-      var tc = new entry.testCase();
-      var result = tc.runAllTests();
-    }
-    return {
-      num_passing : num_passing,
-      num_failing : num_failing,
-      failures : failures
-    };
-  });
-  var end = new Date().getTime();
-  var elapsed = (end - start) / 1000;
-
-  console.log('Ran ' + (results.num_passing + results.num_failing) + ' tests in ' + elapsed + 's.');
-  console.log(results.num_passing + ' test(s) passed');
-  console.log(results.num_failing + ' test(s) failed:');
-  for (var i = 0; i < results.failures.length; i++) {
-    // TODO(danvk): print an auto_test/misc/local URL that runs this test.
-    console.log('  ' + results.failures[i] + ' failed.');
-  }
-
-  done_callback(results.num_failing, results.num_passing);
-});
-
-};
-
-// Load all "tests/" pages.
-var LoadAllManualTests = function(totally_done_callback) {
-
-var fs = require('fs');
-var tests = fs.list('tests');
-var pages = [];
-
-function make_barrier_closure(n, fn) {
-  var calls = 0;
-  return function() {
-    calls++;
-    if (calls == n) {
-      fn();
-    } else {
-      // console.log('' + calls + ' done, ' + (n - calls) + ' remain');
-    }
-  };
-}
-
-var tasks = [];
-for (var i = 0; i < tests.length; i++) {
-  if (tests[i].substr(-5) != '.html') continue;
-  tasks.push(tests[i]);
-}
-tasks = [ 'independent-series.html' ];
-
-var loaded_page = make_barrier_closure(tasks.length, function() {
-  // Wait 2 secs to allow JS errors to happen after page load.
-  setTimeout(function() {
-    var success = 0, failures = 0;
-    for (var i = 0; i < pages.length; i++) {
-      if (pages[i].success && !pages[i].hasErrors) {
-        success++;
-      } else {
-        failures++;
-      }
-    }
-    console.log('Successfully loaded ' + success + ' / ' +
-                (success + failures) + ' test pages.');
-    totally_done_callback(failures, success);
-  }, 2000);
-});
-
-
-for (var i = 0; i < tasks.length; i++) {
-  var url = 'file://' + fs.absolute('tests/' + tasks[i]);
-  pages.push(function(path, url) {
-    var page = require('webpage').create();
-    page.success = false;
-    page.hasErrors = false;
-    page.onError = function (msg, trace) {
-      console.log(path + ': ' + msg);
-      page.hasErrors = true;
-      trace.forEach(function(item) {
-        console.log('  ', item.file, ':', item.line);
-      });
-    };
-    page.onLoadFinished = function(status) {
-      if (status == 'success') {
-        page.success = true;
-      }
-      if (!page.done) loaded_page();
-      page.done = true;
-    };
-    page.open(url);
-    return page;
-  }(tasks[i], url));
-}
-
-};
-
-
-// First run all auto_tests.
-// If they all pass, load the manual tests.
-RunAllAutoTests(function(num_failing, num_passing) {
-  if (num_failing !== 0) {
-    console.log('FAIL');
-    phantom.exit();
-  } else {
-    console.log('PASS');
-  }
-  phantom.exit();
-
-  // This is not yet reliable enough to be useful:
-  /*
-  LoadAllManualTests(function(failing, passing) {
-    if (failing !== 0) {
-      console.log('FAIL');
-    } else {
-      console.log('PASS');
-    }
-    phantom.exit();
-  });
-  */
-});
diff --git a/phantom-perf.js b/phantom-perf.js
deleted file mode 100644 (file)
index b98c3f0..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-// dygraphs command-line performance benchmark.
-//
-// Invoke as:
-//
-//   phantomjs phantom-perf.js 1000 100 5
-//
-// If the test succeeds, it will print out something like:
-//
-//   1000/100/5: 1309.4+/-43.4 ms (1284, 1245, 1336, 1346, 1336)
-//
-// This is mean +/- standard deviation, followed by a list of the individual
-// times for each repetition, in milliseconds.
-
-var system = require('system'),
-    fs = require('fs');
-
-var tmpfile = "tmp-dygraph-test-params.js";
-var url = 'tests/dygraph-many-points-benchmark.html';
-
-function fail(msg){
-  console.warn(msg);
-  phantom.exit(1);
-}
-
-function assert(condition, msg){
-  if (condition) return;
-  fail(msg);
-}
-
-var config_file_template =
-    "document.getElementById('points').value = (points);\n" +
-    "document.getElementById('series').value = (series);\n" +
-    "document.getElementById('repetitions').value = (repetitions);\n";
-
-var RunBenchmark = function(points, series, repetitions, done_callback) {
-  var page = require('webpage').create();
-
-  // page.evalute() is seriously locked down.
-  // This was the only way I could find to pass the parameters to the page.
-  fs.write(tmpfile,
-      config_file_template
-        .replace("(points)", points)
-        .replace("(series)", series)
-        .replace("(repetitions)", repetitions),
-      "w");
-
-  page.open(url, function(status) {
-    if (status !== 'success') {
-      console.warn('Page status: ' + status);
-      console.log(page);
-      phantom.exit();
-    }
-
-    assert(page.injectJs(tmpfile), "Unable to inject JS.");
-    fs.remove(tmpfile);
-
-    var start = new Date().getTime();
-    var rep_times = page.evaluate(function() {
-      var rep_times = updatePlot();
-      return rep_times;
-    });
-    var end = new Date().getTime();
-    var elapsed = (end - start) / 1000;
-    done_callback(rep_times);
-  });
-};
-
-
-var points, series, repetitions;
-if (4 != system.args.length) {
-  console.warn('Usage: phantomjs phantom-driver.js (points) (series) (repititions)');
-  phantom.exit();
-}
-
-points = parseInt(system.args[1]);
-series = parseInt(system.args[2]);
-repetitions = parseInt(system.args[3]);
-assert(points != null, "Couldn't parse " + system.args[1]);
-assert(series != null, "Couldn't parse " + system.args[2]);
-assert(repetitions != null, "Couldn't parse " + system.args[3]);
-
-
-RunBenchmark(points, series, repetitions, function(rep_times) {
-  var mean = 0.0, std = 0.0;
-  rep_times.forEach(function(x) { mean += x; } );
-  mean /= rep_times.length;
-  rep_times.forEach(function(x) { std += Math.pow(x - mean, 2); });
-  std = Math.sqrt(std / (rep_times.length - 1));
-
-  console.log(points + '/' + series + '/' + repetitions + ': ' +
-      mean.toFixed(1) + '+/-' + std.toFixed(1) + ' ms (' +
-      rep_times.join(', ') + ')');
-  phantom.exit();
-});
diff --git a/plugins/README b/plugins/README
deleted file mode 100644 (file)
index f216545..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-dygraphs plugins
-----------------
-
-A single dygraph is actually a collection of dygraphs plugins, each responsible
-for some portion of the chart: the plot area, the axes, the legend, the labels,
-etc.
-
-This forces the dygraphs code to be more modular and encourages better APIs.
-
-The "legend" plugin (plugins/legend.js) can be used as a template for new
-plugins.
-
-Here is a simplified version of it, with comments to explain the plugin
-lifecycle and APIs:
-
-----------------
-
-// (standard JavaScript library wrapper; prevents polluting global namespace)
-Dygraph.Plugins.Legend = (function() {
-
-// Plugin constructor. This is invoked once for every chart which uses the
-// plugin. You can't actually access the Dygraph object at this point, so the
-// initialization you do here shouldn't be chart-specific. (For that, use
-// the activate() method).
-var legend = function() {
-  this.div_ = null;
-};
-
-// Plugins are expected to implement this method for debugging purposes.
-legend.toString = function() {
-  return "Legend";
-};
-
-// This is called once the dygraph is ready. The chart data may not be
-// available yet, but the options specified in the constructor are.
-// 
-// Proper tasks to do here include:
-// - Reading your own options
-// - DOM manipulation
-// - Registering event listeners
-//
-// "dygraph" is the Dygraph object for which this instance is being activated.
-// "registerer" allows you to register event listeners.
-legend.prototype.activate = function(dygraph, registerer) {
-  // Create the legend div and attach it to the chart DOM.
-  this.div_ = document.createElement("div");
-  dygraph.graphDiv.appendChild(this.div_);
-
-  // Add event listeners. These will be called whenever points are selected
-  // (i.e. you hover over them) or deselected (i.e. you mouse away from the
-  // chart). This is your only chance to register event listeners! Once this
-  // method returns, the gig is up.
-  registerer.addEventListener('select', legend.select);
-  registerer.addEventListener('deselect', legend.deselect);
-};
-
-// The functions called by event listeners all take a single parameter, an
-// event object. This contains properties relevant to the particular event, but
-// you can always assume that it has:
-//
-// 1. A "dygraph" parameter which is a reference to the chart on which the
-//    event took place.
-// 2. A "stopPropagation" method, which you can call to prevent the event from
-//    being seen by any other plugins after you. This effectively cancels the
-//    event.
-// 3. A "preventDefault" method, which prevents dygraphs from performing the
-//    default action associated with this event.
-//
-legend.select = function(e) {
-  // These are two of the properties specific to the "select" event object:
-  var xValue = e.selectedX;
-  var points = e.selectedPoints;
-
-  var html = xValue + ':';
-  for (var i = 0; i < points.length; i++) {
-    var point = points[i];
-    html += ' ' + point.name + ':' + point.yval;
-  }
-
-  // In an event listener, "this" refers to your plugin object.
-  this.div_.innerHTML = html;
-};
-
-// This clears out the legend when the user mouses away from the chart.
-legend.deselect = function(e) {
-  this.div_.innerHTML = '';
-};
-
-return legend;
-})();
-
-----------------
-
-Plugin Events Reference:
-
-- predraw
-- clearChart
-- drawChart
-- select
-- deselect
-
-TODO(danvk): document all event properties for each event.
-
-
-Special methods:
-- (constructor)
-- activate
-- destroy
-
-
-----------------
-
-Notes on plugin registration and event cascade ordering/behavior.
diff --git a/plugins/annotations.js b/plugins/annotations.js
deleted file mode 100644 (file)
index 8576104..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-/**
- * @license
- * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/*global Dygraph:false */
-
-Dygraph.Plugins.Annotations = (function() {
-
-"use strict";
-
-/**
-Current bits of jankiness:
-- Uses dygraph.layout_ to get the parsed annotations.
-- Uses dygraph.plotter_.area
-
-It would be nice if the plugin didn't require so much special support inside
-the core dygraphs classes, but annotations involve quite a bit of parsing and
-layout.
-
-TODO(danvk): cache DOM elements.
-
-*/
-
-var annotations = function() {
-  this.annotations_ = [];
-};
-
-annotations.prototype.toString = function() {
-  return "Annotations Plugin";
-};
-
-annotations.prototype.activate = function(g) {
-  return {
-    clearChart: this.clearChart,
-    didDrawChart: this.didDrawChart
-  };
-};
-
-annotations.prototype.detachLabels = function() {
-  for (var i = 0; i < this.annotations_.length; i++) {
-    var a = this.annotations_[i];
-    if (a.parentNode) a.parentNode.removeChild(a);
-    this.annotations_[i] = null;
-  }
-  this.annotations_ = [];
-};
-
-annotations.prototype.clearChart = function(e) {
-  this.detachLabels();
-};
-
-annotations.prototype.didDrawChart = function(e) {
-  var g = e.dygraph;
-
-  // Early out in the (common) case of zero annotations.
-  var points = g.layout_.annotated_points;
-  if (!points || points.length === 0) return;
-
-  var containerDiv = e.canvas.parentNode;
-  var annotationStyle = {
-    "position": "absolute",
-    "fontSize": g.getOption('axisLabelFontSize') + "px",
-    "zIndex": 10,
-    "overflow": "hidden"
-  };
-
-  var bindEvt = function(eventName, classEventName, pt) {
-    return function(annotation_event) {
-      var a = pt.annotation;
-      if (a.hasOwnProperty(eventName)) {
-        a[eventName](a, pt, g, annotation_event);
-      } else if (g.getOption(classEventName)) {
-        g.getOption(classEventName)(a, pt, g, annotation_event );
-      }
-    };
-  };
-
-  // Add the annotations one-by-one.
-  var area = e.dygraph.plotter_.area;
-
-  // x-coord to sum of previous annotation's heights (used for stacking).
-  var xToUsedHeight = {};
-
-  for (var i = 0; i < points.length; i++) {
-    var p = points[i];
-    if (p.canvasx < area.x || p.canvasx > area.x + area.w ||
-        p.canvasy < area.y || p.canvasy > area.y + area.h) {
-      continue;
-    }
-
-    var a = p.annotation;
-    var tick_height = 6;
-    if (a.hasOwnProperty("tickHeight")) {
-      tick_height = a.tickHeight;
-    }
-
-    var div = document.createElement("div");
-    for (var name in annotationStyle) {
-      if (annotationStyle.hasOwnProperty(name)) {
-        div.style[name] = annotationStyle[name];
-      }
-    }
-    if (!a.hasOwnProperty('icon')) {
-      div.className = "dygraphDefaultAnnotation";
-    }
-    if (a.hasOwnProperty('cssClass')) {
-      div.className += " " + a.cssClass;
-    }
-
-    var width = a.hasOwnProperty('width') ? a.width : 16;
-    var height = a.hasOwnProperty('height') ? a.height : 16;
-    if (a.hasOwnProperty('icon')) {
-      var img = document.createElement("img");
-      img.src = a.icon;
-      img.width = width;
-      img.height = height;
-      div.appendChild(img);
-    } else if (p.annotation.hasOwnProperty('shortText')) {
-      div.appendChild(document.createTextNode(p.annotation.shortText));
-    }
-    var left = p.canvasx - width / 2;
-    div.style.left = left + "px";
-    var divTop = 0;
-    if (a.attachAtBottom) {
-      var y = (area.y + area.h - height - tick_height);
-      if (xToUsedHeight[left]) {
-        y -= xToUsedHeight[left];
-      } else {
-        xToUsedHeight[left] = 0;
-      }
-      xToUsedHeight[left] += (tick_height + height);
-      divTop = y;
-    } else {
-      divTop = p.canvasy - height - tick_height;
-    }
-    div.style.top = divTop + "px";
-    div.style.width = width + "px";
-    div.style.height = height + "px";
-    div.title = p.annotation.text;
-    div.style.color = g.colorsMap_[p.name];
-    div.style.borderColor = g.colorsMap_[p.name];
-    a.div = div;
-
-    g.addAndTrackEvent(div, 'click',
-        bindEvt('clickHandler', 'annotationClickHandler', p, this));
-    g.addAndTrackEvent(div, 'mouseover',
-        bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
-    g.addAndTrackEvent(div, 'mouseout',
-        bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
-    g.addAndTrackEvent(div, 'dblclick',
-        bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
-
-    containerDiv.appendChild(div);
-    this.annotations_.push(div);
-
-    var ctx = e.drawingContext;
-    ctx.save();
-    ctx.strokeStyle = g.colorsMap_[p.name];
-    ctx.beginPath();
-    if (!a.attachAtBottom) {
-      ctx.moveTo(p.canvasx, p.canvasy);
-      ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height);
-    } else {
-      var y = divTop + height;
-      ctx.moveTo(p.canvasx, y);
-      ctx.lineTo(p.canvasx, y + tick_height);
-    }
-    ctx.closePath();
-    ctx.stroke();
-    ctx.restore();
-  }
-};
-
-annotations.prototype.destroy = function() {
-  this.detachLabels();
-};
-
-return annotations;
-
-})();
diff --git a/plugins/axes.js b/plugins/axes.js
deleted file mode 100644 (file)
index aa142ce..0000000
+++ /dev/null
@@ -1,323 +0,0 @@
-/**
- * @license
- * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-
-/*global Dygraph:false */
-
-Dygraph.Plugins.Axes = (function() {
-
-'use strict';
-
-/*
-Bits of jankiness:
-- Direct layout access
-- Direct area access
-- Should include calculation of ticks, not just the drawing.
-
-Options left to make axis-friendly.
-  ('drawAxesAtZero')
-  ('xAxisHeight')
-*/
-
-/**
- * Draws the axes. This includes the labels on the x- and y-axes, as well
- * as the tick marks on the axes.
- * It does _not_ draw the grid lines which span the entire chart.
- */
-var axes = function() {
-  this.xlabels_ = [];
-  this.ylabels_ = [];
-};
-
-axes.prototype.toString = function() {
-  return 'Axes Plugin';
-};
-
-axes.prototype.activate = function(g) {
-  return {
-    layout: this.layout,
-    clearChart: this.clearChart,
-    willDrawChart: this.willDrawChart
-  };
-};
-
-axes.prototype.layout = function(e) {
-  var g = e.dygraph;
-
-  if (g.getOptionForAxis('drawAxis', 'y')) {
-    var w = g.getOptionForAxis('axisLabelWidth', 'y') + 2 * g.getOptionForAxis('axisTickSize', 'y');
-    e.reserveSpaceLeft(w);
-  }
-
-  if (g.getOptionForAxis('drawAxis', 'x')) {
-    var h;
-    // NOTE: I think this is probably broken now, since g.getOption() now
-    // hits the dictionary. (That is, g.getOption('xAxisHeight') now always
-    // has a value.)
-    if (g.getOption('xAxisHeight')) {
-      h = g.getOption('xAxisHeight');
-    } else {
-      h = g.getOptionForAxis('axisLabelFontSize', 'x') + 2 * g.getOptionForAxis('axisTickSize', 'x');
-    }
-    e.reserveSpaceBottom(h);
-  }
-
-  if (g.numAxes() == 2) {
-    if (g.getOptionForAxis('drawAxis', 'y2')) {
-      var w = g.getOptionForAxis('axisLabelWidth', 'y2') + 2 * g.getOptionForAxis('axisTickSize', 'y2');
-      e.reserveSpaceRight(w);
-    }
-  } else if (g.numAxes() > 2) {
-    g.error('Only two y-axes are supported at this time. (Trying ' +
-            'to use ' + g.numAxes() + ')');
-  }
-};
-
-axes.prototype.detachLabels = function() {
-  function removeArray(ary) {
-    for (var i = 0; i < ary.length; i++) {
-      var el = ary[i];
-      if (el.parentNode) el.parentNode.removeChild(el);
-    }
-  }
-
-  removeArray(this.xlabels_);
-  removeArray(this.ylabels_);
-  this.xlabels_ = [];
-  this.ylabels_ = [];
-};
-
-axes.prototype.clearChart = function(e) {
-  this.detachLabels();
-};
-
-axes.prototype.willDrawChart = function(e) {
-  var g = e.dygraph;
-
-  if (!g.getOptionForAxis('drawAxis', 'x') &&
-      !g.getOptionForAxis('drawAxis', 'y') &&
-      !g.getOptionForAxis('drawAxis', 'y2')) {
-    return;
-  }
-  
-  // Round pixels to half-integer boundaries for crisper drawing.
-  function halfUp(x)  { return Math.round(x) + 0.5; }
-  function halfDown(y){ return Math.round(y) - 0.5; }
-
-  var context = e.drawingContext;
-  var containerDiv = e.canvas.parentNode;
-  var canvasWidth = g.width_;  // e.canvas.width is affected by pixel ratio.
-  var canvasHeight = g.height_;
-
-  var label, x, y, tick, i;
-
-  var makeLabelStyle = function(axis) {
-    return {
-      position: 'absolute',
-      fontSize: g.getOptionForAxis('axisLabelFontSize', axis) + 'px',
-      zIndex: 10,
-      color: g.getOptionForAxis('axisLabelColor', axis),
-      width: g.getOptionForAxis('axisLabelWidth', axis) + 'px',
-      // height: g.getOptionForAxis('axisLabelFontSize', 'x') + 2 + "px",
-      lineHeight: 'normal',  // Something other than "normal" line-height screws up label positioning.
-      overflow: 'hidden'
-    };
-  };
-
-  var labelStyles = {
-    x : makeLabelStyle('x'),
-    y : makeLabelStyle('y'),
-    y2 : makeLabelStyle('y2')
-  };
-
-  var makeDiv = function(txt, axis, prec_axis) {
-    /*
-     * This seems to be called with the following three sets of axis/prec_axis:
-     * x: undefined
-     * y: y1
-     * y: y2
-     */
-    var div = document.createElement('div');
-    var labelStyle = labelStyles[prec_axis == 'y2' ? 'y2' : axis];
-    for (var name in labelStyle) {
-      if (labelStyle.hasOwnProperty(name)) {
-        div.style[name] = labelStyle[name];
-      }
-    }
-    var inner_div = document.createElement('div');
-    inner_div.className = 'dygraph-axis-label' +
-                          ' dygraph-axis-label-' + axis +
-                          (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
-    inner_div.innerHTML = txt;
-    div.appendChild(inner_div);
-    return div;
-  };
-
-  // axis lines
-  context.save();
-
-  var layout = g.layout_;
-  var area = e.dygraph.plotter_.area;
-
-  // Helper for repeated axis-option accesses.
-  var makeOptionGetter = function(axis) {
-    return function(option) {
-      return g.getOptionForAxis(option, axis);
-    };
-  };
-
-  if (g.getOptionForAxis('drawAxis', 'y')) {
-    if (layout.yticks && layout.yticks.length > 0) {
-      var num_axes = g.numAxes();
-      var getOptions = [makeOptionGetter('y'), makeOptionGetter('y2')];
-      for (i = 0; i < layout.yticks.length; i++) {
-        tick = layout.yticks[i];
-        if (typeof(tick) == 'function') return;  // <-- when would this happen?
-        x = area.x;
-        var sgn = 1;
-        var prec_axis = 'y1';
-        var getAxisOption = getOptions[0];
-        if (tick[0] == 1) {  // right-side y-axis
-          x = area.x + area.w;
-          sgn = -1;
-          prec_axis = 'y2';
-          getAxisOption = getOptions[1];
-        }
-        var fontSize = getAxisOption('axisLabelFontSize');
-        y = area.y + tick[1] * area.h;
-
-        /* Tick marks are currently clipped, so don't bother drawing them.
-        context.beginPath();
-        context.moveTo(halfUp(x), halfDown(y));
-        context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
-        context.closePath();
-        context.stroke();
-        */
-
-        label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
-        var top = (y - fontSize / 2);
-        if (top < 0) top = 0;
-
-        if (top + fontSize + 3 > canvasHeight) {
-          label.style.bottom = '0';
-        } else {
-          label.style.top = top + 'px';
-        }
-        if (tick[0] === 0) {
-          label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
-          label.style.textAlign = 'right';
-        } else if (tick[0] == 1) {
-          label.style.left = (area.x + area.w +
-                              getAxisOption('axisTickSize')) + 'px';
-          label.style.textAlign = 'left';
-        }
-        label.style.width = getAxisOption('axisLabelWidth') + 'px';
-        containerDiv.appendChild(label);
-        this.ylabels_.push(label);
-      }
-
-      // The lowest tick on the y-axis often overlaps with the leftmost
-      // tick on the x-axis. Shift the bottom tick up a little bit to
-      // compensate if necessary.
-      var bottomTick = this.ylabels_[0];
-      // Interested in the y2 axis also?
-      var fontSize = g.getOptionForAxis('axisLabelFontSize', 'y');
-      var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
-      if (bottom > canvasHeight - fontSize) {
-        bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
-            fontSize / 2) + 'px';
-      }
-    }
-
-    // draw a vertical line on the left to separate the chart from the labels.
-    var axisX;
-    if (g.getOption('drawAxesAtZero')) {
-      var r = g.toPercentXCoord(0);
-      if (r > 1 || r < 0 || isNaN(r)) r = 0;
-      axisX = halfUp(area.x + r * area.w);
-    } else {
-      axisX = halfUp(area.x);
-    }
-
-    context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
-    context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
-
-    context.beginPath();
-    context.moveTo(axisX, halfDown(area.y));
-    context.lineTo(axisX, halfDown(area.y + area.h));
-    context.closePath();
-    context.stroke();
-
-    // if there's a secondary y-axis, draw a vertical line for that, too.
-    if (g.numAxes() == 2) {
-      context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
-      context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
-      context.beginPath();
-      context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
-      context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
-      context.closePath();
-      context.stroke();
-    }
-  }
-
-  if (g.getOptionForAxis('drawAxis', 'x')) {
-    if (layout.xticks) {
-      var getAxisOption = makeOptionGetter('x');
-      for (i = 0; i < layout.xticks.length; i++) {
-        tick = layout.xticks[i];
-        x = area.x + tick[0] * area.w;
-        y = area.y + area.h;
-
-        /* Tick marks are currently clipped, so don't bother drawing them.
-        context.beginPath();
-        context.moveTo(halfUp(x), halfDown(y));
-        context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
-        context.closePath();
-        context.stroke();
-        */
-
-        label = makeDiv(tick[1], 'x');
-        label.style.textAlign = 'center';
-        label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
-
-        var left = (x - getAxisOption('axisLabelWidth')/2);
-        if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
-          left = canvasWidth - getAxisOption('axisLabelWidth');
-          label.style.textAlign = 'right';
-        }
-        if (left < 0) {
-          left = 0;
-          label.style.textAlign = 'left';
-        }
-
-        label.style.left = left + 'px';
-        label.style.width = getAxisOption('axisLabelWidth') + 'px';
-        containerDiv.appendChild(label);
-        this.xlabels_.push(label);
-      }
-    }
-
-    context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
-    context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
-    context.beginPath();
-    var axisY;
-    if (g.getOption('drawAxesAtZero')) {
-      var r = g.toPercentYCoord(0, 0);
-      if (r > 1 || r < 0) r = 1;
-      axisY = halfDown(area.y + r * area.h);
-    } else {
-      axisY = halfDown(area.y + area.h);
-    }
-    context.moveTo(halfUp(area.x), axisY);
-    context.lineTo(halfUp(area.x + area.w), axisY);
-    context.closePath();
-    context.stroke();
-  }
-
-  context.restore();
-};
-
-return axes;
-})();
diff --git a/plugins/chart-labels.js b/plugins/chart-labels.js
deleted file mode 100644 (file)
index 504ed3a..0000000
+++ /dev/null
@@ -1,190 +0,0 @@
-/**
- * @license
- * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-/*global Dygraph:false */
-
-Dygraph.Plugins.ChartLabels = (function() {
-
-"use strict";
-
-// TODO(danvk): move chart label options out of dygraphs and into the plugin.
-// TODO(danvk): only tear down & rebuild the DIVs when it's necessary.
-
-var chart_labels = function() {
-  this.title_div_ = null;
-  this.xlabel_div_ = null;
-  this.ylabel_div_ = null;
-  this.y2label_div_ = null;
-};
-
-chart_labels.prototype.toString = function() {
-  return "ChartLabels Plugin";
-};
-
-chart_labels.prototype.activate = function(g) {
-  return {
-    layout: this.layout,
-    // clearChart: this.clearChart,
-    didDrawChart: this.didDrawChart
-  };
-};
-
-// QUESTION: should there be a plugin-utils.js?
-var createDivInRect = function(r) {
-  var div = document.createElement('div');
-  div.style.position = 'absolute';
-  div.style.left = r.x + 'px';
-  div.style.top = r.y + 'px';
-  div.style.width = r.w + 'px';
-  div.style.height = r.h + 'px';
-  return div;
-};
-
-// Detach and null out any existing nodes.
-chart_labels.prototype.detachLabels_ = function() {
-  var els = [ this.title_div_,
-              this.xlabel_div_,
-              this.ylabel_div_,
-              this.y2label_div_ ];
-  for (var i = 0; i < els.length; i++) {
-    var el = els[i];
-    if (!el) continue;
-    if (el.parentNode) el.parentNode.removeChild(el);
-  }
-
-  this.title_div_ = null;
-  this.xlabel_div_ = null;
-  this.ylabel_div_ = null;
-  this.y2label_div_ = null;
-};
-
-var createRotatedDiv = function(g, box, axis, classes, html) {
-  // TODO(danvk): is this outer div actually necessary?
-  var div = document.createElement("div");
-  div.style.position = 'absolute';
-  if (axis == 1) {
-    // NOTE: this is cheating. Should be positioned relative to the box.
-    div.style.left = '0px';
-  } else {
-    div.style.left = box.x + 'px';
-  }
-  div.style.top = box.y + 'px';
-  div.style.width = box.w + 'px';
-  div.style.height = box.h + 'px';
-  div.style.fontSize = (g.getOption('yLabelWidth') - 2) + 'px';
-
-  var inner_div = document.createElement("div");
-  inner_div.style.position = 'absolute';
-  inner_div.style.width = box.h + 'px';
-  inner_div.style.height = box.w + 'px';
-  inner_div.style.top = (box.h / 2 - box.w / 2) + 'px';
-  inner_div.style.left = (box.w / 2 - box.h / 2) + 'px';
-  inner_div.style.textAlign = 'center';
-
-  // CSS rotation is an HTML5 feature which is not standardized. Hence every
-  // browser has its own name for the CSS style.
-  var val = 'rotate(' + (axis == 1 ? '-' : '') + '90deg)';
-  inner_div.style.transform = val;        // HTML5
-  inner_div.style.WebkitTransform = val;  // Safari/Chrome
-  inner_div.style.MozTransform = val;     // Firefox
-  inner_div.style.OTransform = val;       // Opera
-  inner_div.style.msTransform = val;      // IE9
-
-  var class_div = document.createElement("div");
-  class_div.className = classes;
-  class_div.innerHTML = html;
-
-  inner_div.appendChild(class_div);
-  div.appendChild(inner_div);
-  return div;
-};
-
-chart_labels.prototype.layout = function(e) {
-  this.detachLabels_();
-
-  var g = e.dygraph;
-  var div = e.chart_div;
-  if (g.getOption('title')) {
-    // QUESTION: should this return an absolutely-positioned div instead?
-    var title_rect = e.reserveSpaceTop(g.getOption('titleHeight'));
-    this.title_div_ = createDivInRect(title_rect);
-    this.title_div_.style.textAlign = 'center';
-    this.title_div_.style.fontSize = (g.getOption('titleHeight') - 8) + 'px';
-    this.title_div_.style.fontWeight = 'bold';
-    this.title_div_.style.zIndex = 10;
-
-    var class_div = document.createElement("div");
-    class_div.className = 'dygraph-label dygraph-title';
-    class_div.innerHTML = g.getOption('title');
-    this.title_div_.appendChild(class_div);
-    div.appendChild(this.title_div_);
-  }
-
-  if (g.getOption('xlabel')) {
-    var x_rect = e.reserveSpaceBottom(g.getOption('xLabelHeight'));
-    this.xlabel_div_ = createDivInRect(x_rect);
-    this.xlabel_div_.style.textAlign = 'center';
-    this.xlabel_div_.style.fontSize = (g.getOption('xLabelHeight') - 2) + 'px';
-
-    var class_div = document.createElement("div");
-    class_div.className = 'dygraph-label dygraph-xlabel';
-    class_div.innerHTML = g.getOption('xlabel');
-    this.xlabel_div_.appendChild(class_div);
-    div.appendChild(this.xlabel_div_);
-  }
-
-  if (g.getOption('ylabel')) {
-    // It would make sense to shift the chart here to make room for the y-axis
-    // label, but the default yAxisLabelWidth is large enough that this results
-    // in overly-padded charts. The y-axis label should fit fine. If it
-    // doesn't, the yAxisLabelWidth option can be increased.
-    var y_rect = e.reserveSpaceLeft(0);
-
-    this.ylabel_div_ = createRotatedDiv(
-        g, y_rect,
-        1,  // primary (left) y-axis
-        'dygraph-label dygraph-ylabel',
-        g.getOption('ylabel'));
-    div.appendChild(this.ylabel_div_);
-  }
-
-  if (g.getOption('y2label') && g.numAxes() == 2) {
-    // same logic applies here as for ylabel.
-    var y2_rect = e.reserveSpaceRight(0);
-    this.y2label_div_ = createRotatedDiv(
-        g, y2_rect,
-        2,  // secondary (right) y-axis
-        'dygraph-label dygraph-y2label',
-        g.getOption('y2label'));
-    div.appendChild(this.y2label_div_);
-  }
-};
-
-chart_labels.prototype.didDrawChart = function(e) {
-  var g = e.dygraph;
-  if (this.title_div_) {
-    this.title_div_.children[0].innerHTML = g.getOption('title');
-  }
-  if (this.xlabel_div_) {
-    this.xlabel_div_.children[0].innerHTML = g.getOption('xlabel');
-  }
-  if (this.ylabel_div_) {
-    this.ylabel_div_.children[0].children[0].innerHTML = g.getOption('ylabel');
-  }
-  if (this.y2label_div_) {
-    this.y2label_div_.children[0].children[0].innerHTML = g.getOption('y2label');
-  }
-};
-
-chart_labels.prototype.clearChart = function() {
-};
-
-chart_labels.prototype.destroy = function() {
-  this.detachLabels_();
-};
-
-
-return chart_labels;
-})();
diff --git a/plugins/grid.js b/plugins/grid.js
deleted file mode 100644 (file)
index db1b42d..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * @license
- * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-/*global Dygraph:false */
-
-Dygraph.Plugins.Grid = (function() {
-
-/*
-
-Current bits of jankiness:
-- Direct layout access
-- Direct area access
-
-*/
-
-"use strict";
-
-
-/**
- * Draws the gridlines, i.e. the gray horizontal & vertical lines running the
- * length of the chart.
- *
- * @constructor
- */
-var grid = function() {
-};
-
-grid.prototype.toString = function() {
-  return "Gridline Plugin";
-};
-
-grid.prototype.activate = function(g) {
-  return {
-    willDrawChart: this.willDrawChart
-  };
-};
-
-grid.prototype.willDrawChart = function(e) {
-  // Draw the new X/Y grid. Lines appear crisper when pixels are rounded to
-  // half-integers. This prevents them from drawing in two rows/cols.
-  var g = e.dygraph;
-  var ctx = e.drawingContext;
-  var layout = g.layout_;
-  var area = e.dygraph.plotter_.area;
-
-  function halfUp(x)  { return Math.round(x) + 0.5; }
-  function halfDown(y){ return Math.round(y) - 0.5; }
-
-  var x, y, i, ticks;
-  if (g.getOptionForAxis('drawGrid', 'y')) {
-    var axes = ["y", "y2"];
-    var strokeStyles = [], lineWidths = [], drawGrid = [], stroking = [], strokePattern = [];
-    for (var i = 0; i < axes.length; i++) {
-      drawGrid[i] = g.getOptionForAxis('drawGrid', axes[i]);
-      if (drawGrid[i]) {
-        strokeStyles[i] = g.getOptionForAxis('gridLineColor', axes[i]);
-        lineWidths[i] = g.getOptionForAxis('gridLineWidth', axes[i]);
-        strokePattern[i] = g.getOptionForAxis('gridLinePattern', axes[i]);
-        stroking[i] = strokePattern[i] && (strokePattern[i].length >= 2);
-      }
-    }
-    ticks = layout.yticks;
-    ctx.save();
-    // draw grids for the different y axes
-    for (i = 0; i < ticks.length; i++) {
-      var axis = ticks[i][0];
-      if(drawGrid[axis]) {
-        if (stroking[axis]) {
-          ctx.installPattern(strokePattern[axis]);
-        }
-        ctx.strokeStyle = strokeStyles[axis];
-        ctx.lineWidth = lineWidths[axis];
-
-        x = halfUp(area.x);
-        y = halfDown(area.y + ticks[i][1] * area.h);
-        ctx.beginPath();
-        ctx.moveTo(x, y);
-        ctx.lineTo(x + area.w, y);
-        ctx.closePath();
-        ctx.stroke();
-
-        if (stroking[axis]) {
-          ctx.uninstallPattern();
-        }
-      }
-    }
-    ctx.restore();
-  }
-
-  // draw grid for x axis
-  if (g.getOptionForAxis('drawGrid', 'x')) {
-    ticks = layout.xticks;
-    ctx.save();
-    var strokePattern = g.getOptionForAxis('gridLinePattern', 'x');
-    var stroking = strokePattern && (strokePattern.length >= 2);
-    if (stroking) {
-      ctx.installPattern(strokePattern);
-    }
-    ctx.strokeStyle = g.getOptionForAxis('gridLineColor', 'x');
-    ctx.lineWidth = g.getOptionForAxis('gridLineWidth', 'x');
-    for (i = 0; i < ticks.length; i++) {
-      x = halfUp(area.x + ticks[i][0] * area.w);
-      y = halfDown(area.y + area.h);
-      ctx.beginPath();
-      ctx.moveTo(x, y);
-      ctx.lineTo(x, area.y);
-      ctx.closePath();
-      ctx.stroke();
-    }
-    if (stroking) {
-      ctx.uninstallPattern();
-    }
-    ctx.restore();
-  }
-};
-
-grid.prototype.destroy = function() {
-};
-
-return grid;
-
-})();
diff --git a/plugins/legend.js b/plugins/legend.js
deleted file mode 100644 (file)
index 3db4d07..0000000
+++ /dev/null
@@ -1,366 +0,0 @@
-/**
- * @license
- * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-/*global Dygraph:false */
-
-Dygraph.Plugins.Legend = (function() {
-/*
-Current bits of jankiness:
-- Uses two private APIs:
-    1. Dygraph.optionsViewForAxis_
-    2. dygraph.plotter_.area
-- Registers for a "predraw" event, which should be renamed.
-- I call calculateEmWidthInDiv more often than needed.
-*/
-
-/*global Dygraph:false */
-"use strict";
-
-
-/**
- * Creates the legend, which appears when the user hovers over the chart.
- * The legend can be either a user-specified or generated div.
- *
- * @constructor
- */
-var legend = function() {
-  this.legend_div_ = null;
-  this.is_generated_div_ = false;  // do we own this div, or was it user-specified?
-};
-
-legend.prototype.toString = function() {
-  return "Legend Plugin";
-};
-
-// (defined below)
-var generateLegendDashHTML;
-
-/**
- * This is called during the dygraph constructor, after options have been set
- * but before the data is available.
- *
- * Proper tasks to do here include:
- * - Reading your own options
- * - DOM manipulation
- * - Registering event listeners
- *
- * @param {Dygraph} g Graph instance.
- * @return {object.<string, function(ev)>} Mapping of event names to callbacks.
- */
-legend.prototype.activate = function(g) {
-  var div;
-  var divWidth = g.getOption('labelsDivWidth');
-
-  var userLabelsDiv = g.getOption('labelsDiv');
-  if (userLabelsDiv && null !== userLabelsDiv) {
-    if (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String) {
-      div = document.getElementById(userLabelsDiv);
-    } else {
-      div = userLabelsDiv;
-    }
-  } else {
-    // Default legend styles. These can be overridden in CSS by adding
-    // "!important" after your rule, e.g. "left: 30px !important;"
-    var messagestyle = {
-      "position": "absolute",
-      "fontSize": "14px",
-      "zIndex": 10,
-      "width": divWidth + "px",
-      "top": "0px",
-      "left": (g.size().width - divWidth - 2) + "px",
-      "background": "white",
-      "lineHeight": "normal",
-      "textAlign": "left",
-      "overflow": "hidden"};
-
-    // TODO(danvk): get rid of labelsDivStyles? CSS is better.
-    Dygraph.update(messagestyle, g.getOption('labelsDivStyles'));
-    div = document.createElement("div");
-    div.className = "dygraph-legend";
-    for (var name in messagestyle) {
-      if (!messagestyle.hasOwnProperty(name)) continue;
-
-      try {
-        div.style[name] = messagestyle[name];
-      } catch (e) {
-        console.warn("You are using unsupported css properties for your " +
-            "browser in labelsDivStyles");
-      }
-    }
-
-    // TODO(danvk): come up with a cleaner way to expose this.
-    g.graphDiv.appendChild(div);
-    this.is_generated_div_ = true;
-  }
-
-  this.legend_div_ = div;
-  this.one_em_width_ = 10;  // just a guess, will be updated.
-
-  return {
-    select: this.select,
-    deselect: this.deselect,
-    // TODO(danvk): rethink the name "predraw" before we commit to it in any API.
-    predraw: this.predraw,
-    didDrawChart: this.didDrawChart
-  };
-};
-
-// Needed for dashed lines.
-var calculateEmWidthInDiv = function(div) {
-  var sizeSpan = document.createElement('span');
-  sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
-  div.appendChild(sizeSpan);
-  var oneEmWidth=sizeSpan.offsetWidth;
-  div.removeChild(sizeSpan);
-  return oneEmWidth;
-};
-
-var escapeHTML = function(str) {
-  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
-};
-
-legend.prototype.select = function(e) {
-  var xValue = e.selectedX;
-  var points = e.selectedPoints;
-  var row = e.selectedRow;
-
-  var legendMode = e.dygraph.getOption('legend');
-  if (legendMode === 'never') {
-    this.legend_div_.style.display = 'none';
-    return;
-  }
-
-  if (legendMode === 'follow') {
-    // create floating legend div
-    var area = e.dygraph.plotter_.area;
-    var labelsDivWidth = e.dygraph.getOption('labelsDivWidth');
-    var yAxisLabelWidth = e.dygraph.getOptionForAxis('axisLabelWidth', 'y');
-    // determine floating [left, top] coordinates of the legend div
-    // within the plotter_ area
-    // offset 50 px to the right and down from the first selection point
-    // 50 px is guess based on mouse cursor size
-    var leftLegend = points[0].x * area.w + 50;
-    var topLegend  = points[0].y * area.h - 50;
-
-    // if legend floats to end of the chart area, it flips to the other
-    // side of the selection point
-    if ((leftLegend + labelsDivWidth + 1) > area.w) {
-      leftLegend = leftLegend - 2 * 50 - labelsDivWidth - (yAxisLabelWidth - area.x);
-    }
-
-    e.dygraph.graphDiv.appendChild(this.legend_div_);
-    this.legend_div_.style.left = yAxisLabelWidth + leftLegend + "px";
-    this.legend_div_.style.top = topLegend + "px";
-  }
-
-  var html = legend.generateLegendHTML(e.dygraph, xValue, points, this.one_em_width_, row);
-  this.legend_div_.innerHTML = html;
-  this.legend_div_.style.display = '';
-};
-
-legend.prototype.deselect = function(e) {
-  var legendMode = e.dygraph.getOption('legend');
-  if (legendMode !== 'always') {
-    this.legend_div_.style.display = "none";
-  }
-
-  // Have to do this every time, since styles might have changed.
-  var oneEmWidth = calculateEmWidthInDiv(this.legend_div_);
-  this.one_em_width_ = oneEmWidth;
-
-  var html = legend.generateLegendHTML(e.dygraph, undefined, undefined, oneEmWidth, null);
-  this.legend_div_.innerHTML = html;
-};
-
-legend.prototype.didDrawChart = function(e) {
-  this.deselect(e);
-};
-
-// Right edge should be flush with the right edge of the charting area (which
-// may not be the same as the right edge of the div, if we have two y-axes.
-// TODO(danvk): is any of this really necessary? Could just set "right" in "activate".
-/**
- * Position the labels div so that:
- * - its right edge is flush with the right edge of the charting area
- * - its top edge is flush with the top edge of the charting area
- * @private
- */
-legend.prototype.predraw = function(e) {
-  // Don't touch a user-specified labelsDiv.
-  if (!this.is_generated_div_) return;
-
-  // TODO(danvk): only use real APIs for this.
-  e.dygraph.graphDiv.appendChild(this.legend_div_);
-  var area = e.dygraph.plotter_.area;
-  var labelsDivWidth = e.dygraph.getOption("labelsDivWidth");
-  this.legend_div_.style.left = area.x + area.w - labelsDivWidth - 1 + "px";
-  this.legend_div_.style.top = area.y + "px";
-  this.legend_div_.style.width = labelsDivWidth + "px";
-};
-
-/**
- * Called when dygraph.destroy() is called.
- * You should null out any references and detach any DOM elements.
- */
-legend.prototype.destroy = function() {
-  this.legend_div_ = null;
-};
-
-/**
- * @private
- * Generates HTML for the legend which is displayed when hovering over the
- * chart. If no selected points are specified, a default legend is returned
- * (this may just be the empty string).
- * @param {number} x The x-value of the selected points.
- * @param {Object} sel_points List of selected points for the given
- *   x-value. Should have properties like 'name', 'yval' and 'canvasy'.
- * @param {number} oneEmWidth The pixel width for 1em in the legend. Only
- *   relevant when displaying a legend with no selection (i.e. {legend:
- *   'always'}) and with dashed lines.
- * @param {number} row The selected row index.
- */
-legend.generateLegendHTML = function(g, x, sel_points, oneEmWidth, row) {
-  // TODO(danvk): deprecate this option in place of {legend: 'never'}
-  if (g.getOption('showLabelsOnHighlight') !== true) return '';
-
-  // If no points are selected, we display a default legend. Traditionally,
-  // this has been blank. But a better default would be a conventional legend,
-  // which provides essential information for a non-interactive chart.
-  var html, sepLines, i, dash, strokePattern;
-  var labels = g.getLabels();
-
-  if (typeof(x) === 'undefined') {
-    if (g.getOption('legend') != 'always') {
-      return '';
-    }
-
-    sepLines = g.getOption('labelsSeparateLines');
-    html = '';
-    for (i = 1; i < labels.length; i++) {
-      var series = g.getPropertiesForSeries(labels[i]);
-      if (!series.visible) continue;
-
-      if (html !== '') html += (sepLines ? '<br/>' : ' ');
-      strokePattern = g.getOption("strokePattern", labels[i]);
-      dash = generateLegendDashHTML(strokePattern, series.color, oneEmWidth);
-      html += "<span style='font-weight: bold; color: " + series.color + ";'>" +
-          dash + " " + escapeHTML(labels[i]) + "</span>";
-    }
-    return html;
-  }
-
-  // TODO(danvk): remove this use of a private API
-  var xOptView = g.optionsViewForAxis_('x');
-  var xvf = xOptView('valueFormatter');
-  html = xvf.call(g, x, xOptView, labels[0], g, row, 0);
-  if (html !== '') {
-    html += ':';
-  }
-
-  var yOptViews = [];
-  var num_axes = g.numAxes();
-  for (i = 0; i < num_axes; i++) {
-    // TODO(danvk): remove this use of a private API
-    yOptViews[i] = g.optionsViewForAxis_('y' + (i ? 1 + i : ''));
-  }
-  var showZeros = g.getOption("labelsShowZeroValues");
-  sepLines = g.getOption("labelsSeparateLines");
-  var highlightSeries = g.getHighlightSeries();
-  for (i = 0; i < sel_points.length; i++) {
-    var pt = sel_points[i];
-    if (pt.yval === 0 && !showZeros) continue;
-    if (!Dygraph.isOK(pt.canvasy)) continue;
-    if (sepLines) html += "<br/>";
-
-    var series = g.getPropertiesForSeries(pt.name);
-    var yOptView = yOptViews[series.axis - 1];
-    var fmtFunc = yOptView('valueFormatter');
-    var yval = fmtFunc.call(g, pt.yval, yOptView, pt.name, g, row, labels.indexOf(pt.name));
-
-    var cls = (pt.name == highlightSeries) ? " class='highlight'" : "";
-
-    // TODO(danvk): use a template string here and make it an attribute.
-    html += "<span" + cls + ">" + " <b><span style='color: " + series.color + ";'>" +
-        escapeHTML(pt.name) + "</span></b>:&#160;" + yval + "</span>";
-  }
-  return html;
-};
-
-
-/**
- * Generates html for the "dash" displayed on the legend when using "legend: always".
- * In particular, this works for dashed lines with any stroke pattern. It will
- * try to scale the pattern to fit in 1em width. Or if small enough repeat the
- * pattern for 1em width.
- *
- * @param strokePattern The pattern
- * @param color The color of the series.
- * @param oneEmWidth The width in pixels of 1em in the legend.
- * @private
- */
-generateLegendDashHTML = function(strokePattern, color, oneEmWidth) {
-  // Easy, common case: a solid line
-  if (!strokePattern || strokePattern.length <= 1) {
-    return "<div style=\"display: inline-block; position: relative; " +
-    "bottom: .5ex; padding-left: 1em; height: 1px; " +
-    "border-bottom: 2px solid " + color + ";\"></div>";
-  }
-
-  var i, j, paddingLeft, marginRight;
-  var strokePixelLength = 0, segmentLoop = 0;
-  var normalizedPattern = [];
-  var loop;
-
-  // Compute the length of the pixels including the first segment twice, 
-  // since we repeat it.
-  for (i = 0; i <= strokePattern.length; i++) {
-    strokePixelLength += strokePattern[i%strokePattern.length];
-  }
-
-  // See if we can loop the pattern by itself at least twice.
-  loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0]));
-  if (loop > 1) {
-    // This pattern fits at least two times, no scaling just convert to em;
-    for (i = 0; i < strokePattern.length; i++) {
-      normalizedPattern[i] = strokePattern[i]/oneEmWidth;
-    }
-    // Since we are repeating the pattern, we don't worry about repeating the
-    // first segment in one draw.
-    segmentLoop = normalizedPattern.length;
-  } else {
-    // If the pattern doesn't fit in the legend we scale it to fit.
-    loop = 1;
-    for (i = 0; i < strokePattern.length; i++) {
-      normalizedPattern[i] = strokePattern[i]/strokePixelLength;
-    }
-    // For the scaled patterns we do redraw the first segment.
-    segmentLoop = normalizedPattern.length+1;
-  }
-
-  // Now make the pattern.
-  var dash = "";
-  for (j = 0; j < loop; j++) {
-    for (i = 0; i < segmentLoop; i+=2) {
-      // The padding is the drawn segment.
-      paddingLeft = normalizedPattern[i%normalizedPattern.length];
-      if (i < strokePattern.length) {
-        // The margin is the space segment.
-        marginRight = normalizedPattern[(i+1)%normalizedPattern.length];
-      } else {
-        // The repeated first segment has no right margin.
-        marginRight = 0;
-      }
-      dash += "<div style=\"display: inline-block; position: relative; " +
-        "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
-        paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
-        ";\"></div>";
-    }
-  }
-  return dash;
-};
-
-
-return legend;
-})();
diff --git a/plugins/range-selector.js b/plugins/range-selector.js
deleted file mode 100644 (file)
index 4c1e938..0000000
+++ /dev/null
@@ -1,798 +0,0 @@
-/**
- * @license
- * Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
- * MIT-licensed (http://opensource.org/licenses/MIT)
- */
-/*global Dygraph:false,TouchEvent:false */
-
-/**
- * @fileoverview This file contains the RangeSelector plugin used to provide
- * a timeline range selector widget for dygraphs.
- */
-
-Dygraph.Plugins.RangeSelector = (function() {
-
-/*global Dygraph:false */
-"use strict";
-
-var rangeSelector = function() {
-  this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
-  this.hasTouchInterface_ = typeof(TouchEvent) != 'undefined';
-  this.isMobileDevice_ = /mobile|android/gi.test(navigator.appVersion);
-  this.interfaceCreated_ = false;
-};
-
-rangeSelector.prototype.toString = function() {
-  return "RangeSelector Plugin";
-};
-
-rangeSelector.prototype.activate = function(dygraph) {
-  this.dygraph_ = dygraph;
-  if (this.getOption_('showRangeSelector')) {
-    this.createInterface_();
-  }
-  return {
-    layout: this.reserveSpace_,
-    predraw: this.renderStaticLayer_,
-    didDrawChart: this.renderInteractiveLayer_
-  };
-};
-
-rangeSelector.prototype.destroy = function() {
-  this.bgcanvas_ = null;
-  this.fgcanvas_ = null;
-  this.leftZoomHandle_ = null;
-  this.rightZoomHandle_ = null;
-};
-
-//------------------------------------------------------------------
-// Private methods
-//------------------------------------------------------------------
-
-rangeSelector.prototype.getOption_ = function(name, opt_series) {
-  return this.dygraph_.getOption(name, opt_series);
-};
-
-rangeSelector.prototype.setDefaultOption_ = function(name, value) {
-  this.dygraph_.attrs_[name] = value;
-};
-
-/**
- * @private
- * Creates the range selector elements and adds them to the graph.
- */
-rangeSelector.prototype.createInterface_ = function() {
-  this.createCanvases_();
-  this.createZoomHandles_();
-  this.initInteraction_();
-
-  // Range selector and animatedZooms have a bad interaction. See issue 359.
-  if (this.getOption_('animatedZooms')) {
-    console.warn('Animated zooms and range selector are not compatible; disabling animatedZooms.');
-    this.dygraph_.updateOptions({animatedZooms: false}, true);
-  }
-
-  this.interfaceCreated_ = true;
-  this.addToGraph_();
-};
-
-/**
- * @private
- * Adds the range selector to the graph.
- */
-rangeSelector.prototype.addToGraph_ = function() {
-  var graphDiv = this.graphDiv_ = this.dygraph_.graphDiv;
-  graphDiv.appendChild(this.bgcanvas_);
-  graphDiv.appendChild(this.fgcanvas_);
-  graphDiv.appendChild(this.leftZoomHandle_);
-  graphDiv.appendChild(this.rightZoomHandle_);
-};
-
-/**
- * @private
- * Removes the range selector from the graph.
- */
-rangeSelector.prototype.removeFromGraph_ = function() {
-  var graphDiv = this.graphDiv_;
-  graphDiv.removeChild(this.bgcanvas_);
-  graphDiv.removeChild(this.fgcanvas_);
-  graphDiv.removeChild(this.leftZoomHandle_);
-  graphDiv.removeChild(this.rightZoomHandle_);
-  this.graphDiv_ = null;
-};
-
-/**
- * @private
- * Called by Layout to allow range selector to reserve its space.
- */
-rangeSelector.prototype.reserveSpace_ = function(e) {
-  if (this.getOption_('showRangeSelector')) {
-    e.reserveSpaceBottom(this.getOption_('rangeSelectorHeight') + 4);
-  }
-};
-
-/**
- * @private
- * Renders the static portion of the range selector at the predraw stage.
- */
-rangeSelector.prototype.renderStaticLayer_ = function() {
-  if (!this.updateVisibility_()) {
-    return;
-  }
-  this.resize_();
-  this.drawStaticLayer_();
-};
-
-/**
- * @private
- * Renders the interactive portion of the range selector after the chart has been drawn.
- */
-rangeSelector.prototype.renderInteractiveLayer_ = function() {
-  if (!this.updateVisibility_() || this.isChangingRange_) {
-    return;
-  }
-  this.placeZoomHandles_();
-  this.drawInteractiveLayer_();
-};
-
-/**
- * @private
- * Check to see if the range selector is enabled/disabled and update visibility accordingly.
- */
-rangeSelector.prototype.updateVisibility_ = function() {
-  var enabled = this.getOption_('showRangeSelector');
-  if (enabled) {
-    if (!this.interfaceCreated_) {
-      this.createInterface_();
-    } else if (!this.graphDiv_ || !this.graphDiv_.parentNode) {
-      this.addToGraph_();
-    }
-  } else if (this.graphDiv_) {
-    this.removeFromGraph_();
-    var dygraph = this.dygraph_;
-    setTimeout(function() { dygraph.width_ = 0; dygraph.resize(); }, 1);
-  }
-  return enabled;
-};
-
-/**
- * @private
- * Resizes the range selector.
- */
-rangeSelector.prototype.resize_ = function() {
-  function setElementRect(canvas, context, rect) {
-    var canvasScale = Dygraph.getContextPixelRatio(context);
-
-    canvas.style.top = rect.y + 'px';
-    canvas.style.left = rect.x + 'px';
-    canvas.width = rect.w * canvasScale;
-    canvas.height = rect.h * canvasScale;
-    canvas.style.width = rect.w + 'px';
-    canvas.style.height = rect.h + 'px';
-
-    if(canvasScale != 1) {
-      context.scale(canvasScale, canvasScale);
-    }
-  }
-
-  var plotArea = this.dygraph_.layout_.getPlotArea();
-
-  var xAxisLabelHeight = 0;
-  if (this.dygraph_.getOptionForAxis('drawAxis', 'x')) {
-    xAxisLabelHeight = this.getOption_('xAxisHeight') || (this.getOption_('axisLabelFontSize') + 2 * this.getOption_('axisTickSize'));
-  }
-  this.canvasRect_ = {
-    x: plotArea.x,
-    y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
-    w: plotArea.w,
-    h: this.getOption_('rangeSelectorHeight')
-  };
-
-  setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_);
-  setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_);
-};
-
-/**
- * @private
- * Creates the background and foreground canvases.
- */
-rangeSelector.prototype.createCanvases_ = function() {
-  this.bgcanvas_ = Dygraph.createCanvas();
-  this.bgcanvas_.className = 'dygraph-rangesel-bgcanvas';
-  this.bgcanvas_.style.position = 'absolute';
-  this.bgcanvas_.style.zIndex = 9;
-  this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_);
-
-  this.fgcanvas_ = Dygraph.createCanvas();
-  this.fgcanvas_.className = 'dygraph-rangesel-fgcanvas';
-  this.fgcanvas_.style.position = 'absolute';
-  this.fgcanvas_.style.zIndex = 9;
-  this.fgcanvas_.style.cursor = 'default';
-  this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_);
-};
-
-/**
- * @private
- * Creates the zoom handle elements.
- */
-rangeSelector.prototype.createZoomHandles_ = function() {
-  var img = new Image();
-  img.className = 'dygraph-rangesel-zoomhandle';
-  img.style.position = 'absolute';
-  img.style.zIndex = 10;
-  img.style.visibility = 'hidden'; // Initially hidden so they don't show up in the wrong place.
-  img.style.cursor = 'col-resize';
-//TODO: change image to more options
-  if (/MSIE 7/.test(navigator.userAgent)) { // IE7 doesn't support embedded src data.
-    img.width = 7;
-    img.height = 14;
-    img.style.backgroundColor = 'white';
-    img.style.border = '1px solid #333333'; // Just show box in IE7.
-  } else {
-    img.width = 9;
-    img.height = 16;
-    img.src = 'data:image/png;base64,' +
-'iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA' +
-'zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv' +
-'bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl' +
-'6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s' +
-'qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
-  }
-
-  if (this.isMobileDevice_) {
-    img.width *= 2;
-    img.height *= 2;
-  }
-
-  this.leftZoomHandle_ = img;
-  this.rightZoomHandle_ = img.cloneNode(false);
-};
-
-/**
- * @private
- * Sets up the interaction for the range selector.
- */
-rangeSelector.prototype.initInteraction_ = function() {
-  var self = this;
-  var topElem = document;
-  var clientXLast = 0;
-  var handle = null;
-  var isZooming = false;
-  var isPanning = false;
-  var dynamic = !this.isMobileDevice_;
-
-  // We cover iframes during mouse interactions. See comments in
-  // dygraph-utils.js for more info on why this is a good idea.
-  var tarp = new Dygraph.IFrameTarp();
-
-  // functions, defined below.  Defining them this way (rather than with
-  // "function foo() {...}" makes JSHint happy.
-  var toXDataWindow, onZoomStart, onZoom, onZoomEnd, doZoom, isMouseInPanZone,
-      onPanStart, onPan, onPanEnd, doPan, onCanvasHover;
-
-  // Touch event functions
-  var onZoomHandleTouchEvent, onCanvasTouchEvent, addTouchEvents;
-
-  toXDataWindow = function(zoomHandleStatus) {
-    var xDataLimits = self.dygraph_.xAxisExtremes();
-    var fact = (xDataLimits[1] - xDataLimits[0])/self.canvasRect_.w;
-    var xDataMin = xDataLimits[0] + (zoomHandleStatus.leftHandlePos - self.canvasRect_.x)*fact;
-    var xDataMax = xDataLimits[0] + (zoomHandleStatus.rightHandlePos - self.canvasRect_.x)*fact;
-    return [xDataMin, xDataMax];
-  };
-
-  onZoomStart = function(e) {
-    Dygraph.cancelEvent(e);
-    isZooming = true;
-    clientXLast = e.clientX;
-    handle = e.target ? e.target : e.srcElement;
-    if (e.type === 'mousedown' || e.type === 'dragstart') {
-      // These events are removed manually.
-      Dygraph.addEvent(topElem, 'mousemove', onZoom);
-      Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
-    }
-    self.fgcanvas_.style.cursor = 'col-resize';
-    tarp.cover();
-    return true;
-  };
-
-  onZoom = function(e) {
-    if (!isZooming) {
-      return false;
-    }
-    Dygraph.cancelEvent(e);
-
-    var delX = e.clientX - clientXLast;
-    if (Math.abs(delX) < 4) {
-      return true;
-    }
-    clientXLast = e.clientX;
-
-    // Move handle.
-    var zoomHandleStatus = self.getZoomHandleStatus_();
-    var newPos;
-    if (handle == self.leftZoomHandle_) {
-      newPos = zoomHandleStatus.leftHandlePos + delX;
-      newPos = Math.min(newPos, zoomHandleStatus.rightHandlePos - handle.width - 3);
-      newPos = Math.max(newPos, self.canvasRect_.x);
-    } else {
-      newPos = zoomHandleStatus.rightHandlePos + delX;
-      newPos = Math.min(newPos, self.canvasRect_.x + self.canvasRect_.w);
-      newPos = Math.max(newPos, zoomHandleStatus.leftHandlePos + handle.width + 3);
-    }
-    var halfHandleWidth = handle.width/2;
-    handle.style.left = (newPos - halfHandleWidth) + 'px';
-    self.drawInteractiveLayer_();
-
-    // Zoom on the fly.
-    if (dynamic) {
-      doZoom();
-    }
-    return true;
-  };
-
-  onZoomEnd = function(e) {
-    if (!isZooming) {
-      return false;
-    }
-    isZooming = false;
-    tarp.uncover();
-    Dygraph.removeEvent(topElem, 'mousemove', onZoom);
-    Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
-    self.fgcanvas_.style.cursor = 'default';
-
-    // If on a slower device, zoom now.
-    if (!dynamic) {
-      doZoom();
-    }
-    return true;
-  };
-
-  doZoom = function() {
-    try {
-      var zoomHandleStatus = self.getZoomHandleStatus_();
-      self.isChangingRange_ = true;
-      if (!zoomHandleStatus.isZoomed) {
-        self.dygraph_.resetZoom();
-      } else {
-        var xDataWindow = toXDataWindow(zoomHandleStatus);
-        self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
-      }
-    } finally {
-      self.isChangingRange_ = false;
-    }
-  };
-
-  isMouseInPanZone = function(e) {
-    var rect = self.leftZoomHandle_.getBoundingClientRect();
-    var leftHandleClientX = rect.left + rect.width/2;
-    rect = self.rightZoomHandle_.getBoundingClientRect();
-    var rightHandleClientX = rect.left + rect.width/2;
-    return (e.clientX > leftHandleClientX && e.clientX < rightHandleClientX);
-  };
-
-  onPanStart = function(e) {
-    if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
-      Dygraph.cancelEvent(e);
-      isPanning = true;
-      clientXLast = e.clientX;
-      if (e.type === 'mousedown') {
-        // These events are removed manually.
-        Dygraph.addEvent(topElem, 'mousemove', onPan);
-        Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
-      }
-      return true;
-    }
-    return false;
-  };
-
-  onPan = function(e) {
-    if (!isPanning) {
-      return false;
-    }
-    Dygraph.cancelEvent(e);
-
-    var delX = e.clientX - clientXLast;
-    if (Math.abs(delX) < 4) {
-      return true;
-    }
-    clientXLast = e.clientX;
-
-    // Move range view
-    var zoomHandleStatus = self.getZoomHandleStatus_();
-    var leftHandlePos = zoomHandleStatus.leftHandlePos;
-    var rightHandlePos = zoomHandleStatus.rightHandlePos;
-    var rangeSize = rightHandlePos - leftHandlePos;
-    if (leftHandlePos + delX <= self.canvasRect_.x) {
-      leftHandlePos = self.canvasRect_.x;
-      rightHandlePos = leftHandlePos + rangeSize;
-    } else if (rightHandlePos + delX >= self.canvasRect_.x + self.canvasRect_.w) {
-      rightHandlePos = self.canvasRect_.x + self.canvasRect_.w;
-      leftHandlePos = rightHandlePos - rangeSize;
-    } else {
-      leftHandlePos += delX;
-      rightHandlePos += delX;
-    }
-    var halfHandleWidth = self.leftZoomHandle_.width/2;
-    self.leftZoomHandle_.style.left = (leftHandlePos - halfHandleWidth) + 'px';
-    self.rightZoomHandle_.style.left = (rightHandlePos - halfHandleWidth) + 'px';
-    self.drawInteractiveLayer_();
-
-    // Do pan on the fly.
-    if (dynamic) {
-      doPan();
-    }
-    return true;
-  };
-
-  onPanEnd = function(e) {
-    if (!isPanning) {
-      return false;
-    }
-    isPanning = false;
-    Dygraph.removeEvent(topElem, 'mousemove', onPan);
-    Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
-    // If on a slower device, do pan now.
-    if (!dynamic) {
-      doPan();
-    }
-    return true;
-  };
-
-  doPan = function() {
-    try {
-      self.isChangingRange_ = true;
-      self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
-      self.dygraph_.drawGraph_(false);
-    } finally {
-      self.isChangingRange_ = false;
-    }
-  };
-
-  onCanvasHover = function(e) {
-    if (isZooming || isPanning) {
-      return;
-    }
-    var cursor = isMouseInPanZone(e) ? 'move' : 'default';
-    if (cursor != self.fgcanvas_.style.cursor) {
-      self.fgcanvas_.style.cursor = cursor;
-    }
-  };
-
-  onZoomHandleTouchEvent = function(e) {
-    if (e.type == 'touchstart' && e.targetTouches.length == 1) {
-      if (onZoomStart(e.targetTouches[0])) {
-        Dygraph.cancelEvent(e);
-      }
-    } else if (e.type == 'touchmove' && e.targetTouches.length == 1) {
-      if (onZoom(e.targetTouches[0])) {
-        Dygraph.cancelEvent(e);
-      }
-    } else {
-      onZoomEnd(e);
-    }
-  };
-
-  onCanvasTouchEvent = function(e) {
-    if (e.type == 'touchstart' && e.targetTouches.length == 1) {
-      if (onPanStart(e.targetTouches[0])) {
-        Dygraph.cancelEvent(e);
-      }
-    } else if (e.type == 'touchmove' && e.targetTouches.length == 1) {
-      if (onPan(e.targetTouches[0])) {
-        Dygraph.cancelEvent(e);
-      }
-    } else {
-      onPanEnd(e);
-    }
-  };
-
-  addTouchEvents = function(elem, fn) {
-    var types = ['touchstart', 'touchend', 'touchmove', 'touchcancel'];
-    for (var i = 0; i < types.length; i++) {
-      self.dygraph_.addAndTrackEvent(elem, types[i], fn);
-    }
-  };
-
-  this.setDefaultOption_('interactionModel', Dygraph.Interaction.dragIsPanInteractionModel);
-  this.setDefaultOption_('panEdgeFraction', 0.0001);
-
-  var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
-  this.dygraph_.addAndTrackEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
-  this.dygraph_.addAndTrackEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
-
-  this.dygraph_.addAndTrackEvent(this.fgcanvas_, 'mousedown', onPanStart);
-  this.dygraph_.addAndTrackEvent(this.fgcanvas_, 'mousemove', onCanvasHover);
-
-  // Touch events
-  if (this.hasTouchInterface_) {
-    addTouchEvents(this.leftZoomHandle_, onZoomHandleTouchEvent);
-    addTouchEvents(this.rightZoomHandle_, onZoomHandleTouchEvent);
-    addTouchEvents(this.fgcanvas_, onCanvasTouchEvent);
-  }
-};
-
-/**
- * @private
- * Draws the static layer in the background canvas.
- */
-rangeSelector.prototype.drawStaticLayer_ = function() {
-  var ctx = this.bgcanvas_ctx_;
-  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
-  try {
-    this.drawMiniPlot_();
-  } catch(ex) {
-    console.warn(ex);
-  }
-
-  var margin = 0.5;
-  this.bgcanvas_ctx_.lineWidth = this.getOption_('rangeSelectorBackgroundLineWidth');
-  ctx.strokeStyle = this.getOption_('rangeSelectorBackgroundStrokeColor');
-  ctx.beginPath();
-  ctx.moveTo(margin, margin);
-  ctx.lineTo(margin, this.canvasRect_.h-margin);
-  ctx.lineTo(this.canvasRect_.w-margin, this.canvasRect_.h-margin);
-  ctx.lineTo(this.canvasRect_.w-margin, margin);
-  ctx.stroke();
-};
-
-
-/**
- * @private
- * Draws the mini plot in the background canvas.
- */
-rangeSelector.prototype.drawMiniPlot_ = function() {
-  var fillStyle = this.getOption_('rangeSelectorPlotFillColor');
-  var fillGradientStyle = this.getOption_('rangeSelectorPlotFillGradientColor');
-  var strokeStyle = this.getOption_('rangeSelectorPlotStrokeColor');
-  if (!fillStyle && !strokeStyle) {
-    return;
-  }
-
-  var stepPlot = this.getOption_('stepPlot');
-
-  var combinedSeriesData = this.computeCombinedSeriesAndLimits_();
-  var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin;
-
-  // Draw the mini plot.
-  var ctx = this.bgcanvas_ctx_;
-  var margin = 0.5;
-
-  var xExtremes = this.dygraph_.xAxisExtremes();
-  var xRange = Math.max(xExtremes[1] - xExtremes[0], 1.e-30);
-  var xFact = (this.canvasRect_.w - margin)/xRange;
-  var yFact = (this.canvasRect_.h - margin)/yRange;
-  var canvasWidth = this.canvasRect_.w - margin;
-  var canvasHeight = this.canvasRect_.h - margin;
-
-  var prevX = null, prevY = null;
-
-  ctx.beginPath();
-  ctx.moveTo(margin, canvasHeight);
-  for (var i = 0; i < combinedSeriesData.data.length; i++) {
-    var dataPoint = combinedSeriesData.data[i];
-    var x = ((dataPoint[0] !== null) ? ((dataPoint[0] - xExtremes[0])*xFact) : NaN);
-    var y = ((dataPoint[1] !== null) ? (canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact) : NaN);
-
-    // Skip points that don't change the x-value. Overly fine-grained points
-    // can cause major slowdowns with the ctx.fill() call below.
-    if (!stepPlot && prevX !== null && Math.round(x) == Math.round(prevX)) {
-      continue;
-    }
-
-    if (isFinite(x) && isFinite(y)) {
-      if(prevX === null) {
-        ctx.lineTo(x, canvasHeight);
-      }
-      else if (stepPlot) {
-        ctx.lineTo(x, prevY);
-      }
-      ctx.lineTo(x, y);
-      prevX = x;
-      prevY = y;
-    }
-    else {
-      if(prevX !== null) {
-        if (stepPlot) {
-          ctx.lineTo(x, prevY);
-          ctx.lineTo(x, canvasHeight);
-        }
-        else {
-          ctx.lineTo(prevX, canvasHeight);
-        }
-      }
-      prevX = prevY = null;
-    }
-  }
-  ctx.lineTo(canvasWidth, canvasHeight);
-  ctx.closePath();
-
-  if (fillStyle) {
-    var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
-    if (fillGradientStyle) {
-      lingrad.addColorStop(0, fillGradientStyle);
-    }
-    lingrad.addColorStop(1, fillStyle);
-    this.bgcanvas_ctx_.fillStyle = lingrad;
-    ctx.fill();
-  }
-
-  if (strokeStyle) {
-    this.bgcanvas_ctx_.strokeStyle = strokeStyle;
-    this.bgcanvas_ctx_.lineWidth = this.getOption_('rangeSelectorPlotLineWidth');
-    ctx.stroke();
-  }
-};
-
-/**
- * @private
- * Computes and returns the combined series data along with min/max for the mini plot.
- * The combined series consists of averaged values for all series.
- * When series have error bars, the error bars are ignored.
- * @return {Object} An object containing combined series array, ymin, ymax.
- */
-rangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
-  var g = this.dygraph_;
-  var logscale = this.getOption_('logscale');
-  var i;
-
-  // Select series to combine. By default, all series are combined.
-  var numColumns = g.numColumns();
-  var labels = g.getLabels();
-  var includeSeries = new Array(numColumns);
-  var anySet = false;
-  for (i = 1; i < numColumns; i++) {
-    var include = this.getOption_('showInRangeSelector', labels[i]);
-    includeSeries[i] = include;
-    if (include !== null) anySet = true;  // it's set explicitly for this series
-  }
-  if (!anySet) {
-    for (i = 0; i < includeSeries.length; i++) includeSeries[i] = true;
-  }
-
-  // Create a combined series (average of selected series values).
-  // TODO(danvk): short-circuit if there's only one series.
-  var rolledSeries = [];
-  var dataHandler = g.dataHandler_;
-  var options = g.attributes_;
-  for (i = 1; i < g.numColumns(); i++) {
-    if (!includeSeries[i]) continue;
-    var series = dataHandler.extractSeries(g.rawData_, i, options);
-    if (g.rollPeriod() > 1) {
-      series = dataHandler.rollingAverage(series, g.rollPeriod(), options);
-    }
-
-    rolledSeries.push(series);
-  }
-
-  var combinedSeries = [];
-  for (i = 0; i < rolledSeries[0].length; i++) {
-    var sum = 0;
-    var count = 0;
-    for (var j = 0; j < rolledSeries.length; j++) {
-      var y = rolledSeries[j][i][1];
-      if (y === null || isNaN(y)) continue;
-      count++;
-      sum += y;
-    }
-    combinedSeries.push([rolledSeries[0][i][0], sum / count]);
-  }
-
-  // Compute the y range.
-  var yMin = Number.MAX_VALUE;
-  var yMax = -Number.MAX_VALUE;
-  for (i = 0; i < combinedSeries.length; i++) {
-    var yVal = combinedSeries[i][1];
-    if (yVal !== null && isFinite(yVal) && (!logscale || yVal > 0)) {
-      yMin = Math.min(yMin, yVal);
-      yMax = Math.max(yMax, yVal);
-    }
-  }
-
-  // Convert Y data to log scale if needed.
-  // Also, expand the Y range to compress the mini plot a little.
-  var extraPercent = 0.25;
-  if (logscale) {
-    yMax = Dygraph.log10(yMax);
-    yMax += yMax*extraPercent;
-    yMin = Dygraph.log10(yMin);
-    for (i = 0; i < combinedSeries.length; i++) {
-      combinedSeries[i][1] = Dygraph.log10(combinedSeries[i][1]);
-    }
-  } else {
-    var yExtra;
-    var yRange = yMax - yMin;
-    if (yRange <= Number.MIN_VALUE) {
-      yExtra = yMax*extraPercent;
-    } else {
-      yExtra = yRange*extraPercent;
-    }
-    yMax += yExtra;
-    yMin -= yExtra;
-  }
-
-  return {data: combinedSeries, yMin: yMin, yMax: yMax};
-};
-
-/**
- * @private
- * Places the zoom handles in the proper position based on the current X data window.
- */
-rangeSelector.prototype.placeZoomHandles_ = function() {
-  var xExtremes = this.dygraph_.xAxisExtremes();
-  var xWindowLimits = this.dygraph_.xAxisRange();
-  var xRange = xExtremes[1] - xExtremes[0];
-  var leftPercent = Math.max(0, (xWindowLimits[0] - xExtremes[0])/xRange);
-  var rightPercent = Math.max(0, (xExtremes[1] - xWindowLimits[1])/xRange);
-  var leftCoord = this.canvasRect_.x + this.canvasRect_.w*leftPercent;
-  var rightCoord = this.canvasRect_.x + this.canvasRect_.w*(1 - rightPercent);
-  var handleTop = Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height)/2);
-  var halfHandleWidth = this.leftZoomHandle_.width/2;
-  this.leftZoomHandle_.style.left = (leftCoord - halfHandleWidth) + 'px';
-  this.leftZoomHandle_.style.top = handleTop + 'px';
-  this.rightZoomHandle_.style.left = (rightCoord - halfHandleWidth) + 'px';
-  this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top;
-
-  this.leftZoomHandle_.style.visibility = 'visible';
-  this.rightZoomHandle_.style.visibility = 'visible';
-};
-
-/**
- * @private
- * Draws the interactive layer in the foreground canvas.
- */
-rangeSelector.prototype.drawInteractiveLayer_ = function() {
-  var ctx = this.fgcanvas_ctx_;
-  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
-  var margin = 1;
-  var width = this.canvasRect_.w - margin;
-  var height = this.canvasRect_.h - margin;
-  var zoomHandleStatus = this.getZoomHandleStatus_();
-
-  ctx.strokeStyle = this.getOption_('rangeSelectorForegroundStrokeColor');
-  ctx.lineWidth = this.getOption_('rangeSelectorForegroundLineWidth');
-  if (!zoomHandleStatus.isZoomed) {
-    ctx.beginPath();
-    ctx.moveTo(margin, margin);
-    ctx.lineTo(margin, height);
-    ctx.lineTo(width, height);
-    ctx.lineTo(width, margin);
-    ctx.stroke();
-  } else {
-    var leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x);
-    var rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x);
-
-    ctx.fillStyle = 'rgba(240, 240, 240, ' + this.getOption_('rangeSelectorAlpha').toString() + ')';
-    ctx.fillRect(0, 0, leftHandleCanvasPos, this.canvasRect_.h);
-    ctx.fillRect(rightHandleCanvasPos, 0, this.canvasRect_.w - rightHandleCanvasPos, this.canvasRect_.h);
-
-    ctx.beginPath();
-    ctx.moveTo(margin, margin);
-    ctx.lineTo(leftHandleCanvasPos, margin);
-    ctx.lineTo(leftHandleCanvasPos, height);
-    ctx.lineTo(rightHandleCanvasPos, height);
-    ctx.lineTo(rightHandleCanvasPos, margin);
-    ctx.lineTo(width, margin);
-    ctx.stroke();
-  }
-};
-
-/**
- * @private
- * Returns the current zoom handle position information.
- * @return {Object} The zoom handle status.
- */
-rangeSelector.prototype.getZoomHandleStatus_ = function() {
-  var halfHandleWidth = this.leftZoomHandle_.width/2;
-  var leftHandlePos = parseFloat(this.leftZoomHandle_.style.left) + halfHandleWidth;
-  var rightHandlePos = parseFloat(this.rightZoomHandle_.style.left) + halfHandleWidth;
-  return {
-      leftHandlePos: leftHandlePos,
-      rightHandlePos: rightHandlePos,
-      isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
-  };
-};
-
-return rangeSelector;
-
-})();
diff --git a/polyfills/console.js b/polyfills/console.js
deleted file mode 100644 (file)
index 4a6f542..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-// Console-polyfill. MIT license.
-// https://github.com/paulmillr/console-polyfill
-// Make it safe to do console.log() always.
-(function(con) {
-  'use strict';
-  var prop, method;
-  var empty = {};
-  var dummy = function() {};
-  var properties = 'memory'.split(',');
-  var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
-     'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
-     'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(',');
-  while (prop = properties.pop()) con[prop] = con[prop] || empty;
-  while (method = methods.pop()) con[method] = con[method] || dummy;
-})(this.console = this.console || {}); // Using `this` for web workers.
diff --git a/push-to-web.sh b/push-to-web.sh
deleted file mode 100755 (executable)
index 0ea59ac..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/bash
-# This script generates the combined JS file, pushes all content to a web site
-# and then reverts the combined file.
-
-if [ "$1" == "" ] ; then
-  echo "usage: $0 destination"
-  exit 1
-fi
-
-set -x
-site=$1
-
-# Produce dygraph-combined.js and dygraph-combined-dev.js
-./generate-combined.sh
-./generate-combined.sh cat-dev > dygraph-combined-dev.js
-
-# Generate documentation.
-./generate-documentation.py > docs/options.html
-chmod a+r docs/options.html
-if [ -s docs/options.html ] ; then
-  ./generate-jsdoc.sh
-  ./generate-download.py > docs/download.html
-
-  temp_dir=$(mktemp -d /tmp/dygraphs-docs.XXXX)
-  cd docs
-  ./ssi_expander.py $temp_dir
-  cd ..
-
-  # Make sure everything will be readable on the web.
-  # This is like "chmod -R a+rX", but excludes the .git directory.
-  find . -path ./.git -prune -o -print | xargs chmod a+rX
-
-  # Copy everything to the site.
-  rsync -avzr gallery common tests jsdoc experimental plugins datahandler polyfills extras $site \
-  && \
-  rsync -avzr --copy-links dashed-canvas.js dygraph*.js gadget.xml thumbnail.png screenshot.png $temp_dir/* $site/
-else
-  echo "generate-documentation.py failed"
-fi
-
-# Revert changes to dygraph-combined.js and docs.
-make clean-combined-test
-rm dygraph-combined-dev.js
-git checkout docs/download.html
-rm docs/options.html
-rm -rf $temp_dir
diff --git a/release.sh b/release.sh
deleted file mode 100755 (executable)
index 21c0c34..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/bin/bash
-# This script "releases" a version of dygraphs.
-
-if [ $# -ne 1 ]; then
-  echo "Usage: $0 X.Y.Z" >&2
-  exit 1
-fi
-
-VERSION=$1
-echo $VERSION | egrep '\d+\.\d+\.\d+' > /dev/null
-if [ $? -ne 0 ]; then
-  echo "Version must be of the form 1.2.3 (got '$VERSION')" >&2
-  exit 1
-fi
-
-# Make sure this is being run from a release branch with the correct name.
-branch=$(git rev-parse --abbrev-ref HEAD)
-if [ $branch != "release-$VERSION" ]; then
-  echo "Must be on a branch named 'release-$VERSION' (found '$branch')" >&2
-  exit 1
-fi
-
-git status | grep 'working directory clean' > /dev/null
-if [ $? -ne 0 ]; then
-  echo "Must release with a clean working directory. Commit your changes." >&2
-  exit 1
-fi
-
-grep "$VERSION" package.json
-if [ $? -ne 0 ]; then
-  echo "Version in package.json doesn't match command line argument." >&2
-  exit 1
-fi
-
-grep "v$VERSION" bower.json
-if [ $? -ne 0 ]; then
-  echo "Version in bower.json doesn't match command line argument." >&2
-  exit 1
-fi
-
-grep "$VERSION" releases.json
-if [ $? -ne 0 ]; then
-  echo "Version $VERSION does not appear in releases.json." >&2
-  exit 1
-fi
-
-rm dygraph-combined.js  # changes to this will make the tests fail.
-make lint test test-combined
-if [ $? -ne 0 ]; then
-  echo "Tests failed. Won't release!" >&2
-  exit 1
-fi
-git reset --hard  # make test-combined deletes the source map
-
-# Push a permanent copy of documentation & generated files to a versioned copy
-# of the site. This is where the downloadable files are generated.
-# TODO(danvk): make sure this actually generates the downloadable files!
-echo "Pushing docs and generated files to dygraphs.com/$VERSION"
-./push-to-web.sh dygraphs.com:dygraphs.com/$VERSION
-if [ $? -ne 0 ]; then
-  echo "Push to web failed" >&2
-  exit 1
-fi
-
-# Everything is good.
-# Switch to the "releases" branch, merge this change and tag it.
-echo "Switching branches to do the release."
-git checkout releases
-git merge --no-ff $branch
-
-COMMIT=$(git rev-parse HEAD)
-echo "Tagging commit $COMMIT as version $VERSION"
-git tag -a "v$VERSION" -m "Release of version $VERSION"
-git push --tags
-
-echo "Release was successful!"
-echo "Pushing the new version to dygraphs.com..."
-./push-to-web.sh dygraphs.com:dygraphs.com
-
-echo "Success!\n"
-echo "Don't forget to merge changes on this branch back into master:"
-echo "git merge --no-ff $branch"
-
-# Discourage users from working on the "releases" branch.
-git checkout master
diff --git a/scripts/generate-documentation.py b/scripts/generate-documentation.py
new file mode 100755 (executable)
index 0000000..31781b9
--- /dev/null
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+
+# Generate docs/options.html
+
+import glob
+import json
+import os
+import re
+import sys
+
+# Set this to the path to a test file to get debug output for just that test
+# file. Can be helpful to figure out why a test is not being shown for a
+# particular option.
+debug_tests = []  # [ 'tests/zoom.html' ]
+
+# Pull options reference JSON out of dygraph.js
+js = ''
+in_json = False
+for line in file('dygraph-options-reference.js'):
+  if '<JSON>' in line:
+    in_json = True
+  elif '</JSON>' in line:
+    in_json = False
+  elif in_json:
+    js += line
+
+# TODO(danvk): better errors here.
+assert js
+docs = json.loads(js)
+
+# Go through the tests and find uses of each option.
+for opt in docs:
+  docs[opt]['tests'] = []
+  docs[opt]['gallery'] = []
+
+# This is helpful for differentiating uses of options like 'width' and 'height'
+# from appearances of identically-named options in CSS.
+def find_braces(txt):
+  """Really primitive method to find text inside of {..} braces.
+  Doesn't work if there's an unmatched brace in a string, e.g. '{'. """
+  out = ''
+  level = 0
+  for char in txt:
+    if char == '{':
+      level += 1
+    if level >= 1:
+      out += char
+    if char == '}':
+      level -= 1
+  return out
+
+def search_files(type, files):
+  # Find text followed by a colon. These won't all be options, but those that
+  # have the same name as a Dygraph option probably will be.
+  prop_re = re.compile(r'\b([a-zA-Z0-9]+) *:')
+  for test_file in files:
+    if os.path.isfile(test_file): # Basically skips directories
+      text = file(test_file).read()
+
+      # Hack for slipping past gallery demos that have title in their attributes
+      # so they don't appear as reasons for the demo to have 'title' options.
+      if type == "gallery":
+        idx = text.find("function(")
+        if idx >= 0:
+          text = text[idx:]
+      braced_html = find_braces(text)
+      if debug_tests:
+        print braced_html
+
+      ms = re.findall(prop_re, braced_html)
+      for opt in ms:
+        if debug_tests: print '\n'.join(ms)
+        if opt in docs and test_file not in docs[opt][type]:
+          docs[opt][type].append(test_file)
+
+search_files("tests", glob.glob("tests/*.html"))
+search_files("gallery", glob.glob("gallery/*.js")) #TODO add grep "Gallery.register\("
+
+if debug_tests: sys.exit(0)
+
+# Extract a labels list.
+labels = []
+for _, opt in docs.iteritems():
+  for label in opt['labels']:
+    if label not in labels:
+      labels.append(label)
+
+print """
+<!--#include virtual="header.html" -->
+
+<!--
+  DO NOT EDIT THIS FILE!
+
+  This file is generated by generate-documentation.py.
+-->
+
+<link rel=stylesheet href="options.css" />
+
+"""
+
+print """
+<div class="col-lg-3">
+<div class="dygraphs-side-nav affix-top" data-spy="affix" data-offset-top="0">
+<ul class='nav'>
+  <li><a href="#usage">Usage</a>
+"""
+for label in sorted(labels):
+  print '  <li><a href="#%s">%s</a>\n' % (label, label)
+print '</ul></div></div>\n\n'
+
+print """
+<div id='content' class='col-lg-9'>
+<h2>Options Reference</h2>
+<p>Dygraphs tries to do a good job of displaying your data without any further configuration. But inevitably, you're going to want to tinker. Dygraphs provides a rich set of options for configuring its display and behavior.</p>
+
+<a name="usage"></a><h3>Usage</h3>
+<p>You specify options in the third parameter to the dygraphs constructor:</p>
+<pre>g = new Dygraph(div,
+                data,
+                {
+                  option1: value1,
+                  option2: value2,
+                  ...
+                });
+</pre>
+
+<p>After you've created a Dygraph, you can change an option by calling the <code>updateOptions</code> method:</p>
+<pre>g.updateOptions({
+                  new_option1: value1,
+                  new_option2: value2
+                });
+</pre>
+
+<p>Some options can be set on a per-axis and per-series basis. See the docs on <a href="per-axis.html">per-axis and per-series options</a> to learn how to do this. The options which may be set in this way are marked as such on this page.</p>
+
+<p>For options which are functions (e.g. callbacks and formatters), the value of <code>this</code> is set to the Dygraph object.</p>
+
+<p>And, without further ado, here's the complete list of options:</p>
+"""
+
+def test_name(f):
+  """Takes 'tests/demo.html' -> 'demo'"""
+  return f.replace('tests/', '').replace('.html', '')
+
+def gallery_name(f):
+  """Takes 'gallery/demo.js' -> 'demo'"""
+  return f.replace('gallery/', '').replace('.js', '')
+
+def urlify_gallery(f):
+  """Takes 'gallery/demo.js' -> 'demo'"""
+  return f.replace('gallery/', 'gallery/#g/').replace('.js', '')
+
+
+for label in sorted(labels):
+  print '<a name="%s"></a><h3>%s</h3>\n' % (label, label)
+
+  for opt_name in sorted(docs.keys()):
+    opt = docs[opt_name]
+    if label not in opt['labels']: continue
+    tests = opt['tests']
+    if not tests:
+      examples_html = '<font color=red>NONE</font>'
+    else:
+      examples_html = ' '.join(
+        '<a href="%s">%s</a>' % (f, test_name(f)) for f in tests)
+
+    gallery = opt['gallery']
+    if not gallery:
+      gallery_html = '<font color=red>NONE</font>'
+    else:
+      gallery_html = ' '.join(
+        '<a href="%s">%s</a>' % (urlify_gallery(f), gallery_name(f)) for f in gallery)
+
+    if 'parameters' in opt:
+      parameters = opt['parameters']
+      parameters_html = '\n'.join("<i>%s</i>: %s<br/>" % (p[0], p[1]) for p in parameters)
+      parameters_html = "\n  <div class='parameters'>\n%s</div>" % (parameters_html);
+    else:
+      parameters_html = ''
+
+    if not opt['type']: opt['type'] = '(missing)'
+    if not opt['default']: opt['default'] = '(missing)'
+    if not opt['description']: opt['description'] = '(missing)'
+
+    print """
+  <div class='option'><a name="%(name)s"></a><b>%(name)s</b>
+  <a class="link" href="#%(name)s">#</a>
+  <br/>
+  <p>%(desc)s</p>
+  <i>Type: %(type)s</i><br/>%(parameters)s
+  <i>Default: %(default)s</i></p>
+  Gallery Samples: %(gallery_html)s<br/>
+  Other Examples: %(examples_html)s<br/>
+  <br/></div>
+  """ % { 'name': opt_name,
+          'type': opt['type'],
+          'parameters': parameters_html,
+          'default': opt['default'],
+          'desc': opt['description'],
+          'examples_html': examples_html,
+          'gallery_html': gallery_html}
+
+
+print """
+<a name="point_properties"></a><h3>Point Properties</h3>
+Some callbacks take a point argument. Its properties are:<br/>
+<ul>
+<li>xval/yval: The data coordinates of the point (with dates/times as millis since epoch)</li>
+<li>canvasx/canvasy: The canvas coordinates at which the point is drawn.</li>
+<li>name: The name of the data series to which the point belongs</li>
+<li>idx: The row number of the point in the data set</li>
+</ul>
+</div> <!-- #content -->
+
+<!--#include virtual="footer.html" -->
+"""
+
+# This page was super-helpful:
+# http://jsbeautifier.org/
diff --git a/scripts/generate-download.py b/scripts/generate-download.py
new file mode 100755 (executable)
index 0000000..2d2dd48
--- /dev/null
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+
+# Generates docs/download.html
+# Run:
+# ./generate-download.py > docs/download.html
+
+import json
+
+releases = json.load(file('releases.json'))
+
+def file_links(release):
+  v = release['version']
+  return ['<a href="%(v)s/%(f)s">%(f)s</a>' % {
+    'f': f, 'v': v} for f in release['files']]
+
+
+# Validation of releases.json
+for idx, release in enumerate(releases):
+  if idx == 0: continue
+  assert 'version' in release, 'Release missing version: %s' % release
+  assert 'files' in release, 'Release missing files: %s' % release
+  assert release['version'] < releases[idx - 1]['version'], (
+      'Releases should be in reverse chronological order in releases.json')
+
+current_html = '<p>' + ('</p><p>'.join(file_links(releases[0]))) + '</p>'
+
+
+previous_lis = []
+for release in releases[1:]:
+  previous_lis.append('<li>%(v)s: %(files)s (<a href="%(v)s/">%(v)s docs</a>)' % {
+      'v': release['version'],
+      'files': ', '.join(file_links(release))
+    })
+
+
+print '''
+<!--#include virtual="header.html" -->
+
+<!--
+  DO NOT EDIT THIS FILE!
+
+  This file is generated by generate-download.py.
+-->
+
+<script src="modernizr.custom.18445.js"></script>
+<p>The current version of dygraphs is <b>%(version)s</b>. Most users will want to download minified files for this version:</p>
+
+<div id="current-release" class="panel">
+%(current_html)s
+</div>
+
+<p>There's a hosted version of dygraphs on <a href="https://cdnjs.com/libraries/dygraph">cdnjs.com</a>:</p>
+
+<pre>&lt;script src="//cdnjs.cloudflare.com/ajax/libs/dygraph/%(version)s/dygraph-combined.js"&gt;&lt;/script&gt;</pre>
+
+<p>You can install dygraphs using <a href="https://www.npmjs.org/package/dygraphs">NPM</a> or <a href="http://bower.io/search/?q=dygraphs">Bower</a>.</p>
+
+<p>To install using NPM:</p>
+<pre>$ npm install dygraphs
+# dygraphs is now in node_modules/dygraphs/dygraph-combined.js</pre>
+
+<p>To install using bower:</p>
+<pre>$ bower install dygraphs
+# dygraphs is now in bower_components/dygraphs/dygraph-combined.js</pre>
+
+<p>Most distributions include a source map. For non-concatenated JS, see <a href="https://github.com/danvk/dygraphs/blob/master/dygraph-dev.js">dygraph-dev.js</a> on <a href="https://github.com/danvk/dygraphs/">github</a>.</a>
+
+<p>To generate your own minified JS, run:</p>
+
+<pre>git clone https://github.com/danvk/dygraphs.git
+./generate-combined.sh
+</pre>
+
+<p>This will create a dygraph.min.js file in the dygraphs directory.</p>
+
+<p>You may also download files for previously-released versions:</p>
+
+<ul>
+%(previous_lis)s
+</ul>
+
+<p>See <a href="/versions.html">Version History</a> for more information on each release.</p>
+
+
+<!--#include virtual="footer.html" -->
+''' % {
+    'version': releases[0]['version'],
+    'current_html': current_html,
+    'previous_lis': '\n'.join(previous_lis)
+    }
diff --git a/scripts/generate-jsdoc.sh b/scripts/generate-jsdoc.sh
new file mode 100755 (executable)
index 0000000..6512fb0
--- /dev/null
@@ -0,0 +1,19 @@
+#!/bin/bash
+#
+# Generates JSDoc in the /jsdoc dir. Clears any existing jsdoc there.
+
+rm -rf jsdoc
+echo Generating JSDoc...
+java -jar jsdoc-toolkit/jsrun.jar \
+  jsdoc-toolkit/app/run.js \
+  -d=jsdoc -t=jsdoc-toolkit/templates/jsdoc \
+  dygraph.js \
+| tee /tmp/dygraphs-jsdocerrors.txt
+
+if [ -s /tmp/dygraphs-jsdocerrors.txt ]; then
+  echo Please fix any jsdoc errors/warnings before sending patches.
+fi
+
+chmod -R a+rX jsdoc
+
+echo Done
diff --git a/scripts/push-to-web.sh b/scripts/push-to-web.sh
new file mode 100755 (executable)
index 0000000..0ea59ac
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/bash
+# This script generates the combined JS file, pushes all content to a web site
+# and then reverts the combined file.
+
+if [ "$1" == "" ] ; then
+  echo "usage: $0 destination"
+  exit 1
+fi
+
+set -x
+site=$1
+
+# Produce dygraph-combined.js and dygraph-combined-dev.js
+./generate-combined.sh
+./generate-combined.sh cat-dev > dygraph-combined-dev.js
+
+# Generate documentation.
+./generate-documentation.py > docs/options.html
+chmod a+r docs/options.html
+if [ -s docs/options.html ] ; then
+  ./generate-jsdoc.sh
+  ./generate-download.py > docs/download.html
+
+  temp_dir=$(mktemp -d /tmp/dygraphs-docs.XXXX)
+  cd docs
+  ./ssi_expander.py $temp_dir
+  cd ..
+
+  # Make sure everything will be readable on the web.
+  # This is like "chmod -R a+rX", but excludes the .git directory.
+  find . -path ./.git -prune -o -print | xargs chmod a+rX
+
+  # Copy everything to the site.
+  rsync -avzr gallery common tests jsdoc experimental plugins datahandler polyfills extras $site \
+  && \
+  rsync -avzr --copy-links dashed-canvas.js dygraph*.js gadget.xml thumbnail.png screenshot.png $temp_dir/* $site/
+else
+  echo "generate-documentation.py failed"
+fi
+
+# Revert changes to dygraph-combined.js and docs.
+make clean-combined-test
+rm dygraph-combined-dev.js
+git checkout docs/download.html
+rm docs/options.html
+rm -rf $temp_dir
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100755 (executable)
index 0000000..21c0c34
--- /dev/null
@@ -0,0 +1,85 @@
+#!/bin/bash
+# This script "releases" a version of dygraphs.
+
+if [ $# -ne 1 ]; then
+  echo "Usage: $0 X.Y.Z" >&2
+  exit 1
+fi
+
+VERSION=$1
+echo $VERSION | egrep '\d+\.\d+\.\d+' > /dev/null
+if [ $? -ne 0 ]; then
+  echo "Version must be of the form 1.2.3 (got '$VERSION')" >&2
+  exit 1
+fi
+
+# Make sure this is being run from a release branch with the correct name.
+branch=$(git rev-parse --abbrev-ref HEAD)
+if [ $branch != "release-$VERSION" ]; then
+  echo "Must be on a branch named 'release-$VERSION' (found '$branch')" >&2
+  exit 1
+fi
+
+git status | grep 'working directory clean' > /dev/null
+if [ $? -ne 0 ]; then
+  echo "Must release with a clean working directory. Commit your changes." >&2
+  exit 1
+fi
+
+grep "$VERSION" package.json
+if [ $? -ne 0 ]; then
+  echo "Version in package.json doesn't match command line argument." >&2
+  exit 1
+fi
+
+grep "v$VERSION" bower.json
+if [ $? -ne 0 ]; then
+  echo "Version in bower.json doesn't match command line argument." >&2
+  exit 1
+fi
+
+grep "$VERSION" releases.json
+if [ $? -ne 0 ]; then
+  echo "Version $VERSION does not appear in releases.json." >&2
+  exit 1
+fi
+
+rm dygraph-combined.js  # changes to this will make the tests fail.
+make lint test test-combined
+if [ $? -ne 0 ]; then
+  echo "Tests failed. Won't release!" >&2
+  exit 1
+fi
+git reset --hard  # make test-combined deletes the source map
+
+# Push a permanent copy of documentation & generated files to a versioned copy
+# of the site. This is where the downloadable files are generated.
+# TODO(danvk): make sure this actually generates the downloadable files!
+echo "Pushing docs and generated files to dygraphs.com/$VERSION"
+./push-to-web.sh dygraphs.com:dygraphs.com/$VERSION
+if [ $? -ne 0 ]; then
+  echo "Push to web failed" >&2
+  exit 1
+fi
+
+# Everything is good.
+# Switch to the "releases" branch, merge this change and tag it.
+echo "Switching branches to do the release."
+git checkout releases
+git merge --no-ff $branch
+
+COMMIT=$(git rev-parse HEAD)
+echo "Tagging commit $COMMIT as version $VERSION"
+git tag -a "v$VERSION" -m "Release of version $VERSION"
+git push --tags
+
+echo "Release was successful!"
+echo "Pushing the new version to dygraphs.com..."
+./push-to-web.sh dygraphs.com:dygraphs.com
+
+echo "Success!\n"
+echo "Don't forget to merge changes on this branch back into master:"
+echo "git merge --no-ff $branch"
+
+# Discourage users from working on the "releases" branch.
+git checkout master
diff --git a/scripts/transform-coverage.js b/scripts/transform-coverage.js
new file mode 100755 (executable)
index 0000000..a492f26
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+/**
+ * This script applies a source map to LCOV data. If you have coverage data for
+ * a concatenated file, plus a source map, this will output LCOV data for your
+ * original source files.
+ *
+ * Usage:
+ *
+ *   transform-coverage.js path/to/soure.map path/to/coverage.lcov > out.lcov
+ */
+
+// TODO: make this a command line argument
+var SOURCE = 'src/';  // only report files under this directory
+
+var assert = require('assert'),
+    fs = require('fs'),
+    lcovParse = require('lcov-parse'),
+    parseDataUri = require('parse-data-uri'),
+    sourcemap = require('source-map');
+
+var sourcemapFile = process.argv[2];
+var lcovFile = process.argv[3];
+
+var sourcemapData = fs.readFileSync(sourcemapFile).toString();
+var sourcemap = new sourcemap.SourceMapConsumer(sourcemapData);
+
+lcovParse(lcovFile, function(err, data) {
+  assert(!err);
+  // TODO: 0 --> the correct file
+  var lines = data[0].lines.details;
+
+  var fileToCov = {};  // filename -> { line num -> hits }
+
+  lines.forEach(function(line) {
+    var pos = sourcemap.originalPositionFor({line: line.line, column: 0});
+    if (pos == null) {
+      return;
+    }
+
+    var filename = pos.source;
+
+    // Test coverage of node_modules is never interesting.
+    if (!filename || filename.indexOf('node_modules') >= 0) {
+      return;
+    }
+
+    // Strip paths down to the source root.
+    var base = filename.indexOf(SOURCE);
+    if (base == -1) return;
+    filename = filename.slice(base);
+
+    if (!fileToCov[filename]) fileToCov[filename] = [];
+    fileToCov[filename][pos.line] = line.hit;
+  });
+
+  // Other LCOV fields to translate:
+  // FN:2454
+  // FNF:465
+  // FNH:410
+  // FNDA:1,(anonymous_1)
+  // LF:4570
+  // LH:4002
+  // BRDA:13,1,0,1
+  // BRF:2213
+  // BRH:1684
+
+  // Convert to LCOV format
+  for (var filename in fileToCov) {
+    var cov = fileToCov[filename]
+    console.log('SF:' + filename);
+    for (var i = 0; i < cov.length; i++) {
+      if (cov[i] != null) {
+        console.log('DA:' + i + ',' + cov[i]);
+      }
+    }
+    console.log('end_of_record');
+  }
+});
diff --git a/src/datahandler/bars-custom.js b/src/datahandler/bars-custom.js
new file mode 100644 (file)
index 0000000..7313ca2
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DataHandler implementation for the custom bars option.
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ */
+
+(function() {
+
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * @constructor
+ * @extends Dygraph.DataHandlers.BarsHandler
+ */
+Dygraph.DataHandlers.CustomBarsHandler = function() {
+};
+
+var CustomBarsHandler = Dygraph.DataHandlers.CustomBarsHandler;
+CustomBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler();
+
+/** @inheritDoc */
+CustomBarsHandler.prototype.extractSeries = function(rawData, i, options) {
+  // TODO(danvk): pre-allocate series here.
+  var series = [];
+  var x, y, point;
+  var logScale = options.get('logscale');
+  for ( var j = 0; j < rawData.length; j++) {
+    x = rawData[j][0];
+    point = rawData[j][i];
+    if (logScale && point !== null) {
+      // On the log scale, points less than zero do not exist.
+      // This will create a gap in the chart.
+      if (point[0] <= 0 || point[1] <= 0 || point[2] <= 0) {
+        point = null;
+      }
+    }
+    // Extract to the unified data format.
+    if (point !== null) {
+      y = point[1];
+      if (y !== null && !isNaN(y)) {
+        series.push([ x, y, [ point[0], point[2] ] ]);
+      } else {
+        series.push([ x, y, [ y, y ] ]);
+      }
+    } else {
+      series.push([ x, null, [ null, null ] ]);
+    }
+  }
+  return series;
+};
+
+/** @inheritDoc */
+CustomBarsHandler.prototype.rollingAverage =
+    function(originalData, rollPeriod, options) {
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+  var y, low, high, mid,count, i, extremes;
+
+  low = 0;
+  mid = 0;
+  high = 0;
+  count = 0;
+  for (i = 0; i < originalData.length; i++) {
+    y = originalData[i][1];
+    extremes = originalData[i][2];
+    rollingData[i] = originalData[i];
+
+    if (y !== null && !isNaN(y)) {
+      low += extremes[0];
+      mid += y;
+      high += extremes[1];
+      count += 1;
+    }
+    if (i - rollPeriod >= 0) {
+      var prev = originalData[i - rollPeriod];
+      if (prev[1] !== null && !isNaN(prev[1])) {
+        low -= prev[2][0];
+        mid -= prev[1];
+        high -= prev[2][1];
+        count -= 1;
+      }
+    }
+    if (count) {
+      rollingData[i] = [
+          originalData[i][0],
+          1.0 * mid / count, 
+          [ 1.0 * low / count,
+            1.0 * high / count ] ];
+    } else {
+      rollingData[i] = [ originalData[i][0], null, [ null, null ] ];
+    }
+  }
+
+  return rollingData;
+};
+
+})();
diff --git a/src/datahandler/bars-error.js b/src/datahandler/bars-error.js
new file mode 100644 (file)
index 0000000..71dbe34
--- /dev/null
@@ -0,0 +1,102 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DataHandler implementation for the error bars option.
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ */
+
+(function() {
+
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * @constructor
+ * @extends Dygraph.DataHandlers.BarsHandler
+ */
+Dygraph.DataHandlers.ErrorBarsHandler = function() {
+};
+
+var ErrorBarsHandler = Dygraph.DataHandlers.ErrorBarsHandler;
+ErrorBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler();
+
+/** @inheritDoc */
+ErrorBarsHandler.prototype.extractSeries = function(rawData, i, options) {
+  // TODO(danvk): pre-allocate series here.
+  var series = [];
+  var x, y, variance, point;
+  var sigma = options.get("sigma");
+  var logScale = options.get('logscale');
+  for ( var j = 0; j < rawData.length; j++) {
+    x = rawData[j][0];
+    point = rawData[j][i];
+    if (logScale && point !== null) {
+      // On the log scale, points less than zero do not exist.
+      // This will create a gap in the chart.
+      if (point[0] <= 0 || point[0] - sigma * point[1] <= 0) {
+        point = null;
+      }
+    }
+    // Extract to the unified data format.
+    if (point !== null) {
+      y = point[0];
+      if (y !== null && !isNaN(y)) {
+        variance = sigma * point[1];
+        // preserve original error value in extras for further
+        // filtering
+        series.push([ x, y, [ y - variance, y + variance, point[1] ] ]);
+      } else {
+        series.push([ x, y, [ y, y, y ] ]);
+      }
+    } else {
+      series.push([ x, null, [ null, null, null ] ]);
+    }
+  }
+  return series;
+};
+
+/** @inheritDoc */
+ErrorBarsHandler.prototype.rollingAverage =
+    function(originalData, rollPeriod, options) {
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+  var sigma = options.get("sigma");
+
+  var i, j, y, v, sum, num_ok, stddev, variance, value;
+
+  // Calculate the rolling average for the first rollPeriod - 1 points
+  // where there is not enough data to roll over the full number of points
+  for (i = 0; i < originalData.length; i++) {
+    sum = 0;
+    variance = 0;
+    num_ok = 0;
+    for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+      y = originalData[j][1];
+      if (y === null || isNaN(y))
+        continue;
+      num_ok++;
+      sum += y;
+      variance += Math.pow(originalData[j][2][2], 2);
+    }
+    if (num_ok) {
+      stddev = Math.sqrt(variance) / num_ok;
+      value = sum / num_ok;
+      rollingData[i] = [ originalData[i][0], value,
+          [value - sigma * stddev, value + sigma * stddev] ];
+    } else {
+      // This explicitly preserves NaNs to aid with "independent
+      // series".
+      // See testRollingAveragePreservesNaNs.
+      v = (rollPeriod == 1) ? originalData[i][1] : null;
+      rollingData[i] = [ originalData[i][0], v, [ v, v ] ];
+    }
+  }
+
+  return rollingData;
+};
+
+})();
diff --git a/src/datahandler/bars-fractions.js b/src/datahandler/bars-fractions.js
new file mode 100644 (file)
index 0000000..8594c07
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DataHandler implementation for the combination 
+ * of error bars and fractions options.
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ */
+
+(function() {
+
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * @constructor
+ * @extends Dygraph.DataHandlers.BarsHandler
+ */
+Dygraph.DataHandlers.FractionsBarsHandler = function() {
+};
+
+var FractionsBarsHandler = Dygraph.DataHandlers.FractionsBarsHandler;
+FractionsBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler();
+
+/** @inheritDoc */
+FractionsBarsHandler.prototype.extractSeries = function(rawData, i, options) {
+  // TODO(danvk): pre-allocate series here.
+  var series = [];
+  var x, y, point, num, den, value, stddev, variance;
+  var mult = 100.0;
+  var sigma = options.get("sigma");
+  var logScale = options.get('logscale');
+  for ( var j = 0; j < rawData.length; j++) {
+    x = rawData[j][0];
+    point = rawData[j][i];
+    if (logScale && point !== null) {
+      // On the log scale, points less than zero do not exist.
+      // This will create a gap in the chart.
+      if (point[0] <= 0 || point[1] <= 0) {
+        point = null;
+      }
+    }
+    // Extract to the unified data format.
+    if (point !== null) {
+      num = point[0];
+      den = point[1];
+      if (num !== null && !isNaN(num)) {
+        value = den ? num / den : 0.0;
+        stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
+        variance = mult * stddev;
+        y = mult * value;
+        // preserve original values in extras for further filtering
+        series.push([ x, y, [ y - variance, y + variance, num, den ] ]);
+      } else {
+        series.push([ x, num, [ num, num, num, den ] ]);
+      }
+    } else {
+      series.push([ x, null, [ null, null, null, null ] ]);
+    }
+  }
+  return series;
+};
+
+/** @inheritDoc */
+FractionsBarsHandler.prototype.rollingAverage =
+    function(originalData, rollPeriod, options) {
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+  var sigma = options.get("sigma");
+  var wilsonInterval = options.get("wilsonInterval");
+
+  var low, high, i, stddev;
+  var num = 0;
+  var den = 0; // numerator/denominator
+  var mult = 100.0;
+  for (i = 0; i < originalData.length; i++) {
+    num += originalData[i][2][2];
+    den += originalData[i][2][3];
+    if (i - rollPeriod >= 0) {
+      num -= originalData[i - rollPeriod][2][2];
+      den -= originalData[i - rollPeriod][2][3];
+    }
+
+    var date = originalData[i][0];
+    var value = den ? num / den : 0.0;
+    if (wilsonInterval) {
+      // For more details on this confidence interval, see:
+      // http://en.wikipedia.org/wiki/Binomial_confidence_interval
+      if (den) {
+        var p = value < 0 ? 0 : value, n = den;
+        var pm = sigma * Math.sqrt(p * (1 - p) / n + sigma * sigma / (4 * n * n));
+        var denom = 1 + sigma * sigma / den;
+        low = (p + sigma * sigma / (2 * den) - pm) / denom;
+        high = (p + sigma * sigma / (2 * den) + pm) / denom;
+        rollingData[i] = [ date, p * mult,
+            [ low * mult, high * mult ] ];
+      } else {
+        rollingData[i] = [ date, 0, [ 0, 0 ] ];
+      }
+    } else {
+      stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
+      rollingData[i] = [ date, mult * value, 
+                         [ mult * (value - stddev), mult * (value + stddev) ] ];
+    }
+  }
+
+  return rollingData;
+};
+
+})();
diff --git a/src/datahandler/bars.js b/src/datahandler/bars.js
new file mode 100644 (file)
index 0000000..7100148
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DataHandler base implementation for the "bar" 
+ * data formats. This implementation must be extended and the
+ * extractSeries and rollingAverage must be implemented.
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ */
+
+(function() {
+
+/*global Dygraph:false */
+/*global DygraphLayout:false */
+"use strict";
+
+/**
+ * @constructor
+ * @extends {Dygraph.DataHandler}
+ */
+Dygraph.DataHandlers.BarsHandler = function() {
+  Dygraph.DataHandler.call(this);
+};
+Dygraph.DataHandlers.BarsHandler.prototype = new Dygraph.DataHandler();
+
+// alias for the rest of the implementation
+var BarsHandler = Dygraph.DataHandlers.BarsHandler;
+
+// TODO(danvk): figure out why the jsdoc has to be copy/pasted from superclass.
+//   (I get closure compiler errors if this isn't here.)
+/**
+ * @override
+ * @param {!Array.<Array>} rawData The raw data passed into dygraphs where 
+ *     rawData[i] = [x,ySeries1,...,ySeriesN].
+ * @param {!number} seriesIndex Index of the series to extract. All other
+ *     series should be ignored.
+ * @param {!DygraphOptions} options Dygraph options.
+ * @return {Array.<[!number,?number,?]>} The series in the unified data format
+ *     where series[i] = [x,y,{extras}]. 
+ */
+BarsHandler.prototype.extractSeries = function(rawData, seriesIndex, options) {
+  // Not implemented here must be extended
+};
+
+/**
+ * @override
+ * @param {!Array.<[!number,?number,?]>} series The series in the unified 
+ *          data format where series[i] = [x,y,{extras}].
+ * @param {!number} rollPeriod The number of points over which to average the data
+ * @param {!DygraphOptions} options The dygraph options.
+ * TODO(danvk): be more specific than "Array" here.
+ * @return {!Array.<[!number,?number,?]>} the rolled series.
+ */
+BarsHandler.prototype.rollingAverage =
+    function(series, rollPeriod, options) {
+  // Not implemented here, must be extended.
+};
+
+/** @inheritDoc */
+BarsHandler.prototype.onPointsCreated_ = function(series, points) {
+  for (var i = 0; i < series.length; ++i) {
+    var item = series[i];
+    var point = points[i];
+    point.y_top = NaN;
+    point.y_bottom = NaN;
+    point.yval_minus = Dygraph.DataHandler.parseFloat(item[2][0]);
+    point.yval_plus = Dygraph.DataHandler.parseFloat(item[2][1]);
+  }
+};
+
+/** @inheritDoc */
+BarsHandler.prototype.getExtremeYValues = function(series, dateWindow, options) {
+  var minY = null, maxY = null, y;
+
+  var firstIdx = 0;
+  var lastIdx = series.length - 1;
+
+  for ( var j = firstIdx; j <= lastIdx; j++) {
+    y = series[j][1];
+    if (y === null || isNaN(y)) continue;
+
+    var low = series[j][2][0];
+    var high = series[j][2][1];
+
+    if (low > y) low = y; // this can happen with custom bars,
+    if (high < y) high = y; // e.g. in tests/custom-bars.html
+
+    if (maxY === null || high > maxY) maxY = high;
+    if (minY === null || low < minY) minY = low;
+  }
+
+  return [ minY, maxY ];
+};
+
+/** @inheritDoc */
+BarsHandler.prototype.onLineEvaluated = function(points, axis, logscale) {
+  var point;
+  for (var j = 0; j < points.length; j++) {
+    // Copy over the error terms
+    point = points[j];
+    point.y_top = DygraphLayout.calcYNormal_(axis, point.yval_minus, logscale);
+    point.y_bottom = DygraphLayout.calcYNormal_(axis, point.yval_plus, logscale);
+  }
+};
+
+})();
diff --git a/src/datahandler/datahandler.js b/src/datahandler/datahandler.js
new file mode 100644 (file)
index 0000000..b3eae91
--- /dev/null
@@ -0,0 +1,270 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview This file contains the managment of data handlers
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * 
+ * The idea is to define a common, generic data format that works for all data
+ * structures supported by dygraphs. To make this possible, the DataHandler
+ * interface is introduced. This makes it possible, that dygraph itself can work
+ * with the same logic for every data type independent of the actual format and
+ * the DataHandler takes care of the data format specific jobs. 
+ * DataHandlers are implemented for all data types supported by Dygraphs and
+ * return Dygraphs compliant formats.
+ * By default the correct DataHandler is chosen based on the options set.
+ * Optionally the user may use his own DataHandler (similar to the plugin
+ * system).
+ * 
+ * 
+ * The unified data format returend by each handler is defined as so: 
+ * series[n][point] = [x,y,(extras)] 
+ * 
+ * This format contains the common basis that is needed to draw a simple line
+ * series extended by optional extras for more complex graphing types. It
+ * contains a primitive x value as first array entry, a primitive y value as
+ * second array entry and an optional extras object for additional data needed.
+ * 
+ * x must always be a number.
+ * y must always be a number, NaN of type number or null.
+ * extras is optional and must be interpreted by the DataHandler. It may be of
+ * any type. 
+ * 
+ * In practice this might look something like this:
+ * default: [x, yVal]
+ * errorBar / customBar: [x, yVal, [yTopVariance, yBottomVariance] ]
+ * 
+ */
+/*global Dygraph:false */
+/*global DygraphLayout:false */
+
+/**
+ * 
+ * The data handler is responsible for all data specific operations. All of the
+ * series data it receives and returns is always in the unified data format.
+ * Initially the unified data is created by the extractSeries method
+ * @constructor
+ */
+Dygraph.DataHandler = function () {
+};
+
+/**
+ * A collection of functions to create and retrieve data handlers.
+ * @type {Object.<!Dygraph.DataHandler>}
+ */
+Dygraph.DataHandlers = {};
+
+(function() {
+
+"use strict";
+
+var handler = Dygraph.DataHandler;
+
+/**
+ * X-value array index constant for unified data samples.
+ * @const
+ * @type {number}
+ */
+handler.X = 0;
+
+/**
+ * Y-value array index constant for unified data samples.
+ * @const
+ * @type {number}
+ */
+handler.Y = 1;
+
+/**
+ * Extras-value array index constant for unified data samples.
+ * @const
+ * @type {number}
+ */
+handler.EXTRAS = 2;
+
+/**
+ * Extracts one series from the raw data (a 2D array) into an array of the
+ * unified data format.
+ * This is where undesirable points (i.e. negative values on log scales and
+ * missing values through which we wish to connect lines) are dropped.
+ * TODO(danvk): the "missing values" bit above doesn't seem right.
+ * 
+ * @param {!Array.<Array>} rawData The raw data passed into dygraphs where 
+ *     rawData[i] = [x,ySeries1,...,ySeriesN].
+ * @param {!number} seriesIndex Index of the series to extract. All other
+ *     series should be ignored.
+ * @param {!DygraphOptions} options Dygraph options.
+ * @return {Array.<[!number,?number,?]>} The series in the unified data format
+ *     where series[i] = [x,y,{extras}]. 
+ */
+handler.prototype.extractSeries = function(rawData, seriesIndex, options) {
+};
+
+/**
+ * Converts a series to a Point array.  The resulting point array must be
+ * returned in increasing order of idx property.
+ * 
+ * @param {!Array.<[!number,?number,?]>} series The series in the unified 
+ *          data format where series[i] = [x,y,{extras}].
+ * @param {!string} setName Name of the series.
+ * @param {!number} boundaryIdStart Index offset of the first point, equal to the
+ *          number of skipped points left of the date window minimum (if any).
+ * @return {!Array.<Dygraph.PointType>} List of points for this series.
+ */
+handler.prototype.seriesToPoints = function(series, setName, boundaryIdStart) {
+  // TODO(bhs): these loops are a hot-spot for high-point-count charts. In
+  // fact,
+  // on chrome+linux, they are 6 times more expensive than iterating through
+  // the
+  // points and drawing the lines. The brunt of the cost comes from allocating
+  // the |point| structures.
+  var points = [];
+  for ( var i = 0; i < series.length; ++i) {
+    var item = series[i];
+    var yraw = item[1];
+    var yval = yraw === null ? null : handler.parseFloat(yraw);
+    var point = {
+      x : NaN,
+      y : NaN,
+      xval : handler.parseFloat(item[0]),
+      yval : yval,
+      name : setName, // TODO(danvk): is this really necessary?
+      idx : i + boundaryIdStart
+    };
+    points.push(point);
+  }
+  this.onPointsCreated_(series, points);
+  return points;
+};
+
+/**
+ * Callback called for each series after the series points have been generated
+ * which will later be used by the plotters to draw the graph.
+ * Here data may be added to the seriesPoints which is needed by the plotters.
+ * The indexes of series and points are in sync meaning the original data
+ * sample for series[i] is points[i].
+ * 
+ * @param {!Array.<[!number,?number,?]>} series The series in the unified 
+ *     data format where series[i] = [x,y,{extras}].
+ * @param {!Array.<Dygraph.PointType>} points The corresponding points passed 
+ *     to the plotter.
+ * @protected
+ */
+handler.prototype.onPointsCreated_ = function(series, points) {
+};
+
+/**
+ * Calculates the rolling average of a data set.
+ * 
+ * @param {!Array.<[!number,?number,?]>} series The series in the unified 
+ *          data format where series[i] = [x,y,{extras}].
+ * @param {!number} rollPeriod The number of points over which to average the data
+ * @param {!DygraphOptions} options The dygraph options.
+ * @return {!Array.<[!number,?number,?]>} the rolled series.
+ */
+handler.prototype.rollingAverage = function(series, rollPeriod, options) {
+};
+
+/**
+ * Computes the range of the data series (including confidence intervals).
+ * 
+ * @param {!Array.<[!number,?number,?]>} series The series in the unified 
+ *     data format where series[i] = [x, y, {extras}].
+ * @param {!Array.<number>} dateWindow The x-value range to display with 
+ *     the format: [min, max].
+ * @param {!DygraphOptions} options The dygraph options.
+ * @return {Array.<number>} The low and high extremes of the series in the
+ *     given window with the format: [low, high].
+ */
+handler.prototype.getExtremeYValues = function(series, dateWindow, options) {
+};
+
+/**
+ * Callback called for each series after the layouting data has been
+ * calculated before the series is drawn. Here normalized positioning data
+ * should be calculated for the extras of each point.
+ * 
+ * @param {!Array.<Dygraph.PointType>} points The points passed to 
+ *          the plotter.
+ * @param {!Object} axis The axis on which the series will be plotted.
+ * @param {!boolean} logscale Weather or not to use a logscale.
+ */
+handler.prototype.onLineEvaluated = function(points, axis, logscale) {
+};
+
+/**
+ * Helper method that computes the y value of a line defined by the points p1
+ * and p2 and a given x value.
+ * 
+ * @param {!Array.<number>} p1 left point ([x,y]).
+ * @param {!Array.<number>} p2 right point ([x,y]).
+ * @param {!number} xValue The x value to compute the y-intersection for.
+ * @return {number} corresponding y value to x on the line defined by p1 and p2.
+ * @private
+ */
+handler.prototype.computeYInterpolation_ = function(p1, p2, xValue) {
+  var deltaY = p2[1] - p1[1];
+  var deltaX = p2[0] - p1[0];
+  var gradient = deltaY / deltaX;
+  var growth = (xValue - p1[0]) * gradient;
+  return p1[1] + growth;
+};
+
+/**
+ * Helper method that returns the first and the last index of the given series
+ * that lie inside the given dateWindow.
+ * 
+ * @param {!Array.<[!number,?number,?]>} series The series in the unified 
+ *     data format where series[i] = [x,y,{extras}].
+ * @param {!Array.<number>} dateWindow The x-value range to display with 
+ *     the format: [min,max].
+ * @return {!Array.<[!number,?number,?]>} The samples of the series that 
+ *     are in the given date window.
+ * @private
+ */
+handler.prototype.getIndexesInWindow_ = function(series, dateWindow) {
+  var firstIdx = 0, lastIdx = series.length - 1;
+  if (dateWindow) {
+    var idx = 0;
+    var low = dateWindow[0];
+    var high = dateWindow[1];
+
+    // Start from each side of the array to minimize the performance
+    // needed.
+    while (idx < series.length - 1 && series[idx][0] < low) {
+      firstIdx++;
+      idx++;
+    }
+    idx = series.length - 1;
+    while (idx > 0 && series[idx][0] > high) {
+      lastIdx--;
+      idx--;
+    }
+  }
+  if (firstIdx <= lastIdx) {
+    return [ firstIdx, lastIdx ];
+  } else {
+    return [ 0, series.length - 1 ];
+  }
+};
+
+/**
+ * Optimized replacement for parseFloat, which was way too slow when almost
+ * all values were type number, with few edge cases, none of which were strings.
+ * @param {?number} val
+ * @return {number}
+ * @protected
+ */
+handler.parseFloat = function(val) {
+  // parseFloat(null) is NaN
+  if (val === null) {
+    return NaN;
+  }
+
+  // Assume it's a number or NaN. If it's something else, I'll be shocked.
+  return val;
+};
+
+})();
diff --git a/src/datahandler/default-fractions.js b/src/datahandler/default-fractions.js
new file mode 100644 (file)
index 0000000..35d36eb
--- /dev/null
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DataHandler implementation for the fractions option.
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ */
+
+(function() {
+
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * @extends Dygraph.DataHandlers.DefaultHandler
+ * @constructor
+ */
+Dygraph.DataHandlers.DefaultFractionHandler = function() {
+};
+  
+var DefaultFractionHandler = Dygraph.DataHandlers.DefaultFractionHandler;
+DefaultFractionHandler.prototype = new Dygraph.DataHandlers.DefaultHandler();
+
+DefaultFractionHandler.prototype.extractSeries = function(rawData, i, options) {
+  // TODO(danvk): pre-allocate series here.
+  var series = [];
+  var x, y, point, num, den, value;
+  var mult = 100.0;
+  var logScale = options.get('logscale');
+  for ( var j = 0; j < rawData.length; j++) {
+    x = rawData[j][0];
+    point = rawData[j][i];
+    if (logScale && point !== null) {
+      // On the log scale, points less than zero do not exist.
+      // This will create a gap in the chart.
+      if (point[0] <= 0 || point[1] <= 0) {
+        point = null;
+      }
+    }
+    // Extract to the unified data format.
+    if (point !== null) {
+      num = point[0];
+      den = point[1];
+      if (num !== null && !isNaN(num)) {
+        value = den ? num / den : 0.0;
+        y = mult * value;
+        // preserve original values in extras for further filtering
+        series.push([ x, y, [ num, den ] ]);
+      } else {
+        series.push([ x, num, [ num, den ] ]);
+      }
+    } else {
+      series.push([ x, null, [ null, null ] ]);
+    }
+  }
+  return series;
+};
+
+DefaultFractionHandler.prototype.rollingAverage = function(originalData, rollPeriod,
+    options) {
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+
+  var i;
+  var num = 0;
+  var den = 0; // numerator/denominator
+  var mult = 100.0;
+  for (i = 0; i < originalData.length; i++) {
+    num += originalData[i][2][0];
+    den += originalData[i][2][1];
+    if (i - rollPeriod >= 0) {
+      num -= originalData[i - rollPeriod][2][0];
+      den -= originalData[i - rollPeriod][2][1];
+    }
+
+    var date = originalData[i][0];
+    var value = den ? num / den : 0.0;
+    rollingData[i] = [ date, mult * value ];
+  }
+
+  return rollingData;
+};
+
+})();
diff --git a/src/datahandler/default.js b/src/datahandler/default.js
new file mode 100644 (file)
index 0000000..e42b92b
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * @license
+ * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DataHandler default implementation used for simple line charts.
+ * @author David Eberlein (david.eberlein@ch.sauter-bc.com)
+ */
+
+(function() {
+
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * @constructor
+ * @extends Dygraph.DataHandler
+ */
+Dygraph.DataHandlers.DefaultHandler = function() {
+};
+
+var DefaultHandler = Dygraph.DataHandlers.DefaultHandler;
+DefaultHandler.prototype = new Dygraph.DataHandler();
+
+/** @inheritDoc */
+DefaultHandler.prototype.extractSeries = function(rawData, i, options) {
+  // TODO(danvk): pre-allocate series here.
+  var series = [];
+  var logScale = options.get('logscale');
+  for ( var j = 0; j < rawData.length; j++) {
+    var x = rawData[j][0];
+    var point = rawData[j][i];
+    if (logScale) {
+      // On the log scale, points less than zero do not exist.
+      // This will create a gap in the chart.
+      if (point <= 0) {
+        point = null;
+      }
+    }
+    series.push([ x, point ]);
+  }
+  return series;
+};
+
+/** @inheritDoc */
+DefaultHandler.prototype.rollingAverage = function(originalData, rollPeriod,
+    options) {
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+
+  var i, j, y, sum, num_ok;
+  // Calculate the rolling average for the first rollPeriod - 1 points
+  // where
+  // there is not enough data to roll over the full number of points
+  if (rollPeriod == 1) {
+    return originalData;
+  }
+  for (i = 0; i < originalData.length; i++) {
+    sum = 0;
+    num_ok = 0;
+    for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+      y = originalData[j][1];
+      if (y === null || isNaN(y))
+        continue;
+      num_ok++;
+      sum += originalData[j][1];
+    }
+    if (num_ok) {
+      rollingData[i] = [ originalData[i][0], sum / num_ok ];
+    } else {
+      rollingData[i] = [ originalData[i][0], null ];
+    }
+  }
+
+  return rollingData;
+};
+
+/** @inheritDoc */
+DefaultHandler.prototype.getExtremeYValues = function(series, dateWindow,
+    options) {
+  var minY = null, maxY = null, y;
+  var firstIdx = 0, lastIdx = series.length - 1;
+
+  for ( var j = firstIdx; j <= lastIdx; j++) {
+    y = series[j][1];
+    if (y === null || isNaN(y))
+      continue;
+    if (maxY === null || y > maxY) {
+      maxY = y;
+    }
+    if (minY === null || y < minY) {
+      minY = y;
+    }
+  }
+  return [ minY, maxY ];
+};
+
+})();
diff --git a/src/dygraph-canvas.js b/src/dygraph-canvas.js
new file mode 100644 (file)
index 0000000..a688c43
--- /dev/null
@@ -0,0 +1,862 @@
+/**
+ * @license
+ * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
+ * needs of dygraphs.
+ *
+ * In particular, support for:
+ * - grid overlays
+ * - error bars
+ * - dygraphs attribute system
+ */
+
+/**
+ * The DygraphCanvasRenderer class does the actual rendering of the chart onto
+ * a canvas. It's based on PlotKit.CanvasRenderer.
+ * @param {Object} element The canvas to attach to
+ * @param {Object} elementContext The 2d context of the canvas (injected so it
+ * can be mocked for testing.)
+ * @param {Layout} layout The DygraphLayout object for this graph.
+ * @constructor
+ */
+
+var DygraphCanvasRenderer = (function() {
+/*global Dygraph:false */
+"use strict";
+
+
+/**
+ * @constructor
+ *
+ * This gets called when there are "new points" to chart. This is generally the
+ * case when the underlying data being charted has changed. It is _not_ called
+ * in the common case that the user has zoomed or is panning the view.
+ *
+ * The chart canvas has already been created by the Dygraph object. The
+ * renderer simply gets a drawing context.
+ *
+ * @param {Dygraph} dygraph The chart to which this renderer belongs.
+ * @param {HTMLCanvasElement} element The &lt;canvas&gt; DOM element on which to draw.
+ * @param {CanvasRenderingContext2D} elementContext The drawing context.
+ * @param {DygraphLayout} layout The chart's DygraphLayout object.
+ *
+ * TODO(danvk): remove the elementContext property.
+ */
+var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
+  this.dygraph_ = dygraph;
+
+  this.layout = layout;
+  this.element = element;
+  this.elementContext = elementContext;
+
+  this.height = dygraph.height_;
+  this.width = dygraph.width_;
+
+  // --- check whether everything is ok before we return
+  if (!Dygraph.isCanvasSupported(this.element)) {
+    throw "Canvas is not supported.";
+  }
+
+  // internal state
+  this.area = layout.getPlotArea();
+
+  // Set up a clipping area for the canvas (and the interaction canvas).
+  // This ensures that we don't overdraw.
+  // on Android 3 and 4, setting a clipping area on a canvas prevents it from
+  // displaying anything.
+  if (!Dygraph.isAndroid()) {
+    var ctx = this.dygraph_.canvas_ctx_;
+    ctx.beginPath();
+    ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
+    ctx.clip();
+
+    ctx = this.dygraph_.hidden_ctx_;
+    ctx.beginPath();
+    ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
+    ctx.clip();
+  }
+};
+
+/**
+ * Clears out all chart content and DOM elements.
+ * This is called immediately before render() on every frame, including
+ * during zooms and pans.
+ * @private
+ */
+DygraphCanvasRenderer.prototype.clear = function() {
+  this.elementContext.clearRect(0, 0, this.width, this.height);
+};
+
+/**
+ * This method is responsible for drawing everything on the chart, including
+ * lines, error bars, fills and axes.
+ * It is called immediately after clear() on every frame, including during pans
+ * and zooms.
+ * @private
+ */
+DygraphCanvasRenderer.prototype.render = function() {
+  // attaches point.canvas{x,y}
+  this._updatePoints();
+
+  // actually draws the chart.
+  this._renderLineChart();
+};
+
+/**
+ * Returns a predicate to be used with an iterator, which will
+ * iterate over points appropriately, depending on whether
+ * connectSeparatedPoints is true. When it's false, the predicate will
+ * skip over points with missing yVals.
+ */
+DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
+  return connectSeparatedPoints ?
+      DygraphCanvasRenderer._predicateThatSkipsEmptyPoints :
+      null;
+};
+
+DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
+    function(array, idx) {
+  return array[idx].yval !== null;
+};
+
+/**
+ * Draws a line with the styles passed in and calls all the drawPointCallbacks.
+ * @param {Object} e The dictionary passed to the plotter function.
+ * @private
+ */
+DygraphCanvasRenderer._drawStyledLine = function(e,
+    color, strokeWidth, strokePattern, drawPoints,
+    drawPointCallback, pointSize) {
+  var g = e.dygraph;
+  // TODO(konigsberg): Compute attributes outside this method call.
+  var stepPlot = g.getBooleanOption("stepPlot", e.setName);
+
+  if (!Dygraph.isArrayLike(strokePattern)) {
+    strokePattern = null;
+  }
+
+  var drawGapPoints = g.getBooleanOption('drawGapEdgePoints', e.setName);
+
+  var points = e.points;
+  var setName = e.setName;
+  var iter = Dygraph.createIterator(points, 0, points.length,
+      DygraphCanvasRenderer._getIteratorPredicate(
+          g.getBooleanOption("connectSeparatedPoints", setName)));
+
+  var stroking = strokePattern && (strokePattern.length >= 2);
+
+  var ctx = e.drawingContext;
+  ctx.save();
+  if (stroking) {
+    ctx.installPattern(strokePattern);
+  }
+
+  var pointsOnLine = DygraphCanvasRenderer._drawSeries(
+      e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color);
+  DygraphCanvasRenderer._drawPointsOnLine(
+      e, pointsOnLine, drawPointCallback, color, pointSize);
+
+  if (stroking) {
+    ctx.uninstallPattern();
+  }
+
+  ctx.restore();
+};
+
+/**
+ * This does the actual drawing of lines on the canvas, for just one series.
+ * Returns a list of [canvasx, canvasy] pairs for points for which a
+ * drawPointCallback should be fired.  These include isolated points, or all
+ * points if drawPoints=true.
+ * @param {Object} e The dictionary passed to the plotter function.
+ * @private
+ */
+DygraphCanvasRenderer._drawSeries = function(e,
+    iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color) {
+
+  var prevCanvasX = null;
+  var prevCanvasY = null;
+  var nextCanvasY = null;
+  var isIsolated; // true if this point is isolated (no line segments)
+  var point; // the point being processed in the while loop
+  var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
+  var first = true; // the first cycle through the while loop
+
+  var ctx = e.drawingContext;
+  ctx.beginPath();
+  ctx.strokeStyle = color;
+  ctx.lineWidth = strokeWidth;
+
+  // NOTE: we break the iterator's encapsulation here for about a 25% speedup.
+  var arr = iter.array_;
+  var limit = iter.end_;
+  var predicate = iter.predicate_;
+
+  for (var i = iter.start_; i < limit; i++) {
+    point = arr[i];
+    if (predicate) {
+      while (i < limit && !predicate(arr, i)) {
+        i++;
+      }
+      if (i == limit) break;
+      point = arr[i];
+    }
+
+    // FIXME: The 'canvasy != canvasy' test here catches NaN values but the test
+    // doesn't catch Infinity values. Could change this to
+    // !isFinite(point.canvasy), but I assume it avoids isNaN for performance?
+    if (point.canvasy === null || point.canvasy != point.canvasy) {
+      if (stepPlot && prevCanvasX !== null) {
+        // Draw a horizontal line to the start of the missing data
+        ctx.moveTo(prevCanvasX, prevCanvasY);
+        ctx.lineTo(point.canvasx, prevCanvasY);
+      }
+      prevCanvasX = prevCanvasY = null;
+    } else {
+      isIsolated = false;
+      if (drawGapPoints || !prevCanvasX) {
+        iter.nextIdx_ = i;
+        iter.next();
+        nextCanvasY = iter.hasNext ? iter.peek.canvasy : null;
+
+        var isNextCanvasYNullOrNaN = nextCanvasY === null ||
+            nextCanvasY != nextCanvasY;
+        isIsolated = (!prevCanvasX && isNextCanvasYNullOrNaN);
+        if (drawGapPoints) {
+          // Also consider a point to be "isolated" if it's adjacent to a
+          // null point, excluding the graph edges.
+          if ((!first && !prevCanvasX) ||
+              (iter.hasNext && isNextCanvasYNullOrNaN)) {
+            isIsolated = true;
+          }
+        }
+      }
+
+      if (prevCanvasX !== null) {
+        if (strokeWidth) {
+          if (stepPlot) {
+            ctx.moveTo(prevCanvasX, prevCanvasY);
+            ctx.lineTo(point.canvasx, prevCanvasY);
+          }
+
+          ctx.lineTo(point.canvasx, point.canvasy);
+        }
+      } else {
+        ctx.moveTo(point.canvasx, point.canvasy);
+      }
+      if (drawPoints || isIsolated) {
+        pointsOnLine.push([point.canvasx, point.canvasy, point.idx]);
+      }
+      prevCanvasX = point.canvasx;
+      prevCanvasY = point.canvasy;
+    }
+    first = false;
+  }
+  ctx.stroke();
+  return pointsOnLine;
+};
+
+/**
+ * This fires the drawPointCallback functions, which draw dots on the points by
+ * default. This gets used when the "drawPoints" option is set, or when there
+ * are isolated points.
+ * @param {Object} e The dictionary passed to the plotter function.
+ * @private
+ */
+DygraphCanvasRenderer._drawPointsOnLine = function(
+    e, pointsOnLine, drawPointCallback, color, pointSize) {
+  var ctx = e.drawingContext;
+  for (var idx = 0; idx < pointsOnLine.length; idx++) {
+    var cb = pointsOnLine[idx];
+    ctx.save();
+    drawPointCallback.call(e.dygraph,
+        e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize, cb[2]);
+    ctx.restore();
+  }
+};
+
+/**
+ * Attaches canvas coordinates to the points array.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._updatePoints = function() {
+  // Update Points
+  // TODO(danvk): here
+  //
+  // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
+  // transformations can be pushed into the canvas via linear transformation
+  // matrices.
+  // NOTE(danvk): this is trickier than it sounds at first. The transformation
+  // needs to be done before the .moveTo() and .lineTo() calls, but must be
+  // undone before the .stroke() call to ensure that the stroke width is
+  // unaffected.  An alternative is to reduce the stroke width in the
+  // transformed coordinate space, but you can't specify different values for
+  // each dimension (as you can with .scale()). The speedup here is ~12%.
+  var sets = this.layout.points;
+  for (var i = sets.length; i--;) {
+    var points = sets[i];
+    for (var j = points.length; j--;) {
+      var point = points[j];
+      point.canvasx = this.area.w * point.x + this.area.x;
+      point.canvasy = this.area.h * point.y + this.area.y;
+    }
+  }
+};
+
+/**
+ * Add canvas Actually draw the lines chart, including error bars.
+ *
+ * This function can only be called if DygraphLayout's points array has been
+ * updated with canvas{x,y} attributes, i.e. by
+ * DygraphCanvasRenderer._updatePoints.
+ *
+ * @param {string=} opt_seriesName when specified, only that series will
+ *     be drawn. (This is used for expedited redrawing with highlightSeriesOpts)
+ * @param {CanvasRenderingContext2D} opt_ctx when specified, the drawing
+ *     context.  However, lines are typically drawn on the object's
+ *     elementContext.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) {
+  var ctx = opt_ctx || this.elementContext;
+  var i;
+
+  var sets = this.layout.points;
+  var setNames = this.layout.setNames;
+  var setName;
+
+  this.colors = this.dygraph_.colorsMap_;
+
+  // Determine which series have specialized plotters.
+  var plotter_attr = this.dygraph_.getOption("plotter");
+  var plotters = plotter_attr;
+  if (!Dygraph.isArrayLike(plotters)) {
+    plotters = [plotters];
+  }
+
+  var setPlotters = {};  // series name -> plotter fn.
+  for (i = 0; i < setNames.length; i++) {
+    setName = setNames[i];
+    var setPlotter = this.dygraph_.getOption("plotter", setName);
+    if (setPlotter == plotter_attr) continue;  // not specialized.
+
+    setPlotters[setName] = setPlotter;
+  }
+
+  for (i = 0; i < plotters.length; i++) {
+    var plotter = plotters[i];
+    var is_last = (i == plotters.length - 1);
+
+    for (var j = 0; j < sets.length; j++) {
+      setName = setNames[j];
+      if (opt_seriesName && setName != opt_seriesName) continue;
+
+      var points = sets[j];
+
+      // Only throw in the specialized plotters on the last iteration.
+      var p = plotter;
+      if (setName in setPlotters) {
+        if (is_last) {
+          p = setPlotters[setName];
+        } else {
+          // Don't use the standard plotters in this case.
+          continue;
+        }
+      }
+
+      var color = this.colors[setName];
+      var strokeWidth = this.dygraph_.getOption("strokeWidth", setName);
+
+      ctx.save();
+      ctx.strokeStyle = color;
+      ctx.lineWidth = strokeWidth;
+      p({
+        points: points,
+        setName: setName,
+        drawingContext: ctx,
+        color: color,
+        strokeWidth: strokeWidth,
+        dygraph: this.dygraph_,
+        axis: this.dygraph_.axisPropertiesForSeries(setName),
+        plotArea: this.area,
+        seriesIndex: j,
+        seriesCount: sets.length,
+        singleSeriesName: opt_seriesName,
+        allSeriesPoints: sets
+      });
+      ctx.restore();
+    }
+  }
+};
+
+/**
+ * Standard plotters. These may be used by clients via Dygraph.Plotters.
+ * See comments there for more details.
+ */
+DygraphCanvasRenderer._Plotters = {
+  linePlotter: function(e) {
+    DygraphCanvasRenderer._linePlotter(e);
+  },
+
+  fillPlotter: function(e) {
+    DygraphCanvasRenderer._fillPlotter(e);
+  },
+
+  errorPlotter: function(e) {
+    DygraphCanvasRenderer._errorPlotter(e);
+  }
+};
+
+/**
+ * Plotter which draws the central lines for a series.
+ * @private
+ */
+DygraphCanvasRenderer._linePlotter = function(e) {
+  var g = e.dygraph;
+  var setName = e.setName;
+  var strokeWidth = e.strokeWidth;
+
+  // TODO(danvk): Check if there's any performance impact of just calling
+  // getOption() inside of _drawStyledLine. Passing in so many parameters makes
+  // this code a bit nasty.
+  var borderWidth = g.getNumericOption("strokeBorderWidth", setName);
+  var drawPointCallback = g.getOption("drawPointCallback", setName) ||
+      Dygraph.Circles.DEFAULT;
+  var strokePattern = g.getOption("strokePattern", setName);
+  var drawPoints = g.getBooleanOption("drawPoints", setName);
+  var pointSize = g.getNumericOption("pointSize", setName);
+
+  if (borderWidth && strokeWidth) {
+    DygraphCanvasRenderer._drawStyledLine(e,
+        g.getOption("strokeBorderColor", setName),
+        strokeWidth + 2 * borderWidth,
+        strokePattern,
+        drawPoints,
+        drawPointCallback,
+        pointSize
+        );
+  }
+
+  DygraphCanvasRenderer._drawStyledLine(e,
+      e.color,
+      strokeWidth,
+      strokePattern,
+      drawPoints,
+      drawPointCallback,
+      pointSize
+  );
+};
+
+/**
+ * Draws the shaded error bars/confidence intervals for each series.
+ * This happens before the center lines are drawn, since the center lines
+ * need to be drawn on top of the error bars for all series.
+ * @private
+ */
+DygraphCanvasRenderer._errorPlotter = function(e) {
+  var g = e.dygraph;
+  var setName = e.setName;
+  var errorBars = g.getBooleanOption("errorBars") ||
+      g.getBooleanOption("customBars");
+  if (!errorBars) return;
+
+  var fillGraph = g.getBooleanOption("fillGraph", setName);
+  if (fillGraph) {
+    console.warn("Can't use fillGraph option with error bars");
+  }
+
+  var ctx = e.drawingContext;
+  var color = e.color;
+  var fillAlpha = g.getNumericOption('fillAlpha', setName);
+  var stepPlot = g.getBooleanOption("stepPlot", setName);
+  var points = e.points;
+
+  var iter = Dygraph.createIterator(points, 0, points.length,
+      DygraphCanvasRenderer._getIteratorPredicate(
+          g.getBooleanOption("connectSeparatedPoints", setName)));
+
+  var newYs;
+
+  // setup graphics context
+  var prevX = NaN;
+  var prevY = NaN;
+  var prevYs = [-1, -1];
+  // should be same color as the lines but only 15% opaque.
+  var rgb = Dygraph.toRGB_(color);
+  var err_color =
+      'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
+  ctx.fillStyle = err_color;
+  ctx.beginPath();
+
+  var isNullUndefinedOrNaN = function(x) {
+    return (x === null ||
+            x === undefined ||
+            isNaN(x));
+  };
+
+  while (iter.hasNext) {
+    var point = iter.next();
+    if ((!stepPlot && isNullUndefinedOrNaN(point.y)) ||
+        (stepPlot && !isNaN(prevY) && isNullUndefinedOrNaN(prevY))) {
+      prevX = NaN;
+      continue;
+    }
+
+    newYs = [ point.y_bottom, point.y_top ];
+    if (stepPlot) {
+      prevY = point.y;
+    }
+
+    // The documentation specifically disallows nulls inside the point arrays,
+    // but in case it happens we should do something sensible.
+    if (isNaN(newYs[0])) newYs[0] = point.y;
+    if (isNaN(newYs[1])) newYs[1] = point.y;
+
+    newYs[0] = e.plotArea.h * newYs[0] + e.plotArea.y;
+    newYs[1] = e.plotArea.h * newYs[1] + e.plotArea.y;
+    if (!isNaN(prevX)) {
+      if (stepPlot) {
+        ctx.moveTo(prevX, prevYs[0]);
+        ctx.lineTo(point.canvasx, prevYs[0]);
+        ctx.lineTo(point.canvasx, prevYs[1]);
+      } else {
+        ctx.moveTo(prevX, prevYs[0]);
+        ctx.lineTo(point.canvasx, newYs[0]);
+        ctx.lineTo(point.canvasx, newYs[1]);
+      }
+      ctx.lineTo(prevX, prevYs[1]);
+      ctx.closePath();
+    }
+    prevYs = newYs;
+    prevX = point.canvasx;
+  }
+  ctx.fill();
+};
+
+
+/**
+ * Proxy for CanvasRenderingContext2D which drops moveTo/lineTo calls which are
+ * superfluous. It accumulates all movements which haven't changed the x-value
+ * and only applies the two with the most extreme y-values.
+ * 
+ * Calls to lineTo/moveTo must have non-decreasing x-values.
+ */
+DygraphCanvasRenderer._fastCanvasProxy = function(context) {
+  var pendingActions = [];  // array of [type, x, y] tuples
+  var lastRoundedX = null;
+
+  var LINE_TO = 1,
+      MOVE_TO = 2;
+
+  var actionCount = 0;  // number of moveTos and lineTos passed to context.
+
+  // Drop superfluous motions
+  // Assumes all pendingActions have the same (rounded) x-value.
+  var compressActions = function(opt_losslessOnly) {
+    if (pendingActions.length <= 1) return;
+
+    // Lossless compression: drop inconsequential moveTos.
+    for (var i = pendingActions.length - 1; i > 0; i--) {
+      var action = pendingActions[i];
+      if (action[0] == MOVE_TO) {
+        var prevAction = pendingActions[i - 1];
+        if (prevAction[1] == action[1] && prevAction[2] == action[2]) {
+          pendingActions.splice(i, 1);
+        }
+      }
+    }
+
+    // Lossless compression: ... drop consecutive moveTos ...
+    for (var i = 0; i < pendingActions.length - 1; /* incremented internally */) {
+      var action = pendingActions[i];
+      if (action[0] == MOVE_TO && pendingActions[i + 1][0] == MOVE_TO) {
+        pendingActions.splice(i, 1);
+      } else {
+        i++;
+      }
+    }
+
+    // Lossy compression: ... drop all but the extreme y-values ...
+    if (pendingActions.length > 2 && !opt_losslessOnly) {
+      // keep an initial moveTo, but drop all others.
+      var startIdx = 0;
+      if (pendingActions[0][0] == MOVE_TO) startIdx++;
+      var minIdx = null, maxIdx = null;
+      for (var i = startIdx; i < pendingActions.length; i++) {
+        var action = pendingActions[i];
+        if (action[0] != LINE_TO) continue;
+        if (minIdx === null && maxIdx === null) {
+          minIdx = i;
+          maxIdx = i;
+        } else {
+          var y = action[2];
+          if (y < pendingActions[minIdx][2]) {
+            minIdx = i;
+          } else if (y > pendingActions[maxIdx][2]) {
+            maxIdx = i;
+          }
+        }
+      }
+      var minAction = pendingActions[minIdx],
+          maxAction = pendingActions[maxIdx];
+      pendingActions.splice(startIdx, pendingActions.length - startIdx);
+      if (minIdx < maxIdx) {
+        pendingActions.push(minAction);
+        pendingActions.push(maxAction);
+      } else if (minIdx > maxIdx) {
+        pendingActions.push(maxAction);
+        pendingActions.push(minAction);
+      } else {
+        pendingActions.push(minAction);
+      }
+    }
+  };
+
+  var flushActions = function(opt_noLossyCompression) {
+    compressActions(opt_noLossyCompression);
+    for (var i = 0, len = pendingActions.length; i < len; i++) {
+      var action = pendingActions[i];
+      if (action[0] == LINE_TO) {
+        context.lineTo(action[1], action[2]);
+      } else if (action[0] == MOVE_TO) {
+        context.moveTo(action[1], action[2]);
+      }
+    }
+    actionCount += pendingActions.length;
+    pendingActions = [];
+  };
+
+  var addAction = function(action, x, y) {
+    var rx = Math.round(x);
+    if (lastRoundedX === null || rx != lastRoundedX) {
+      flushActions();
+      lastRoundedX = rx;
+    }
+    pendingActions.push([action, x, y]);
+  };
+
+  return {
+    moveTo: function(x, y) {
+      addAction(MOVE_TO, x, y);
+    },
+    lineTo: function(x, y) {
+      addAction(LINE_TO, x, y);
+    },
+
+    // for major operations like stroke/fill, we skip compression to ensure
+    // that there are no artifacts at the right edge.
+    stroke:    function() { flushActions(true); context.stroke(); },
+    fill:      function() { flushActions(true); context.fill(); },
+    beginPath: function() { flushActions(true); context.beginPath(); },
+    closePath: function() { flushActions(true); context.closePath(); },
+
+    _count: function() { return actionCount; }
+  };
+};
+
+/**
+ * Draws the shaded regions when "fillGraph" is set. Not to be confused with
+ * error bars.
+ *
+ * For stacked charts, it's more convenient to handle all the series
+ * simultaneously. So this plotter plots all the points on the first series
+ * it's asked to draw, then ignores all the other series.
+ *
+ * @private
+ */
+DygraphCanvasRenderer._fillPlotter = function(e) {
+  // Skip if we're drawing a single series for interactive highlight overlay.
+  if (e.singleSeriesName) return;
+
+  // We'll handle all the series at once, not one-by-one.
+  if (e.seriesIndex !== 0) return;
+
+  var g = e.dygraph;
+  var setNames = g.getLabels().slice(1);  // remove x-axis
+
+  // getLabels() includes names for invisible series, which are not included in
+  // allSeriesPoints. We remove those to make the two match.
+  // TODO(danvk): provide a simpler way to get this information.
+  for (var i = setNames.length; i >= 0; i--) {
+    if (!g.visibility()[i]) setNames.splice(i, 1);
+  }
+
+  var anySeriesFilled = (function() {
+    for (var i = 0; i < setNames.length; i++) {
+      if (g.getBooleanOption("fillGraph", setNames[i])) return true;
+    }
+    return false;
+  })();
+
+  if (!anySeriesFilled) return;
+
+  var area = e.plotArea;
+  var sets = e.allSeriesPoints;
+  var setCount = sets.length;
+
+  var fillAlpha = g.getNumericOption('fillAlpha');
+  var stackedGraph = g.getBooleanOption("stackedGraph");
+  var colors = g.getColors();
+
+  // For stacked graphs, track the baseline for filling.
+  //
+  // The filled areas below graph lines are trapezoids with two
+  // vertical edges. The top edge is the line segment being drawn, and
+  // the baseline is the bottom edge. Each baseline corresponds to the
+  // top line segment from the previous stacked line. In the case of
+  // step plots, the trapezoids are rectangles.
+  var baseline = {};
+  var currBaseline;
+  var prevStepPlot;  // for different line drawing modes (line/step) per series
+
+  // Helper function to trace a line back along the baseline.
+  var traceBackPath = function(ctx, baselineX, baselineY, pathBack) {
+    ctx.lineTo(baselineX, baselineY);
+    if (stackedGraph) {
+      for (var i = pathBack.length - 1; i >= 0; i--) {
+        var pt = pathBack[i];
+        ctx.lineTo(pt[0], pt[1]);
+      }
+    }
+  };
+
+  // process sets in reverse order (needed for stacked graphs)
+  for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) {
+    var ctx = e.drawingContext;
+    var setName = setNames[setIdx];
+    if (!g.getBooleanOption('fillGraph', setName)) continue;
+
+    var stepPlot = g.getBooleanOption('stepPlot', setName);
+    var color = colors[setIdx];
+    var axis = g.axisPropertiesForSeries(setName);
+    var axisY = 1.0 + axis.minyval * axis.yscale;
+    if (axisY < 0.0) axisY = 0.0;
+    else if (axisY > 1.0) axisY = 1.0;
+    axisY = area.h * axisY + area.y;
+
+    var points = sets[setIdx];
+    var iter = Dygraph.createIterator(points, 0, points.length,
+        DygraphCanvasRenderer._getIteratorPredicate(
+            g.getBooleanOption("connectSeparatedPoints", setName)));
+
+    // setup graphics context
+    var prevX = NaN;
+    var prevYs = [-1, -1];
+    var newYs;
+    // should be same color as the lines but only 15% opaque.
+    var rgb = Dygraph.toRGB_(color);
+    var err_color =
+        'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
+    ctx.fillStyle = err_color;
+    ctx.beginPath();
+    var last_x, is_first = true;
+
+    // If the point density is high enough, dropping segments on their way to
+    // the canvas justifies the overhead of doing so.
+    if (points.length > 2 * g.width_) {
+      ctx = DygraphCanvasRenderer._fastCanvasProxy(ctx);
+    }
+
+    // For filled charts, we draw points from left to right, then back along
+    // the x-axis to complete a shape for filling.
+    // For stacked plots, this "back path" is a more complex shape. This array
+    // stores the [x, y] values needed to trace that shape.
+    var pathBack = [];
+
+    // TODO(danvk): there are a lot of options at play in this loop.
+    //     The logic would be much clearer if some (e.g. stackGraph and
+    //     stepPlot) were split off into separate sub-plotters.
+    var point;
+    while (iter.hasNext) {
+      point = iter.next();
+      if (!Dygraph.isOK(point.y) && !stepPlot) {
+        traceBackPath(ctx, prevX, prevYs[1], pathBack);
+        pathBack = [];
+        prevX = NaN;
+        if (point.y_stacked !== null && !isNaN(point.y_stacked)) {
+          baseline[point.canvasx] = area.h * point.y_stacked + area.y;
+        }
+        continue;
+      }
+      if (stackedGraph) {
+        if (!is_first && last_x == point.xval) {
+          continue;
+        } else {
+          is_first = false;
+          last_x = point.xval;
+        }
+
+        currBaseline = baseline[point.canvasx];
+        var lastY;
+        if (currBaseline === undefined) {
+          lastY = axisY;
+        } else {
+          if(prevStepPlot) {
+            lastY = currBaseline[0];
+          } else {
+            lastY = currBaseline;
+          }
+        }
+        newYs = [ point.canvasy, lastY ];
+
+        if (stepPlot) {
+          // Step plots must keep track of the top and bottom of
+          // the baseline at each point.
+          if (prevYs[0] === -1) {
+            baseline[point.canvasx] = [ point.canvasy, axisY ];
+          } else {
+            baseline[point.canvasx] = [ point.canvasy, prevYs[0] ];
+          }
+        } else {
+          baseline[point.canvasx] = point.canvasy;
+        }
+
+      } else {
+        if (isNaN(point.canvasy) && stepPlot) {
+          newYs = [ area.y + area.h, axisY ];
+        } else {
+          newYs = [ point.canvasy, axisY ];
+        }
+      }
+      if (!isNaN(prevX)) {
+        // Move to top fill point
+        if (stepPlot) {
+          ctx.lineTo(point.canvasx, prevYs[0]);
+          ctx.lineTo(point.canvasx, newYs[0]);
+        } else {
+          ctx.lineTo(point.canvasx, newYs[0]);
+        }
+
+        // Record the baseline for the reverse path.
+        if (stackedGraph) {
+          pathBack.push([prevX, prevYs[1]]);
+          if (prevStepPlot && currBaseline) {
+            // Draw to the bottom of the baseline
+            pathBack.push([point.canvasx, currBaseline[1]]);
+          } else {
+            pathBack.push([point.canvasx, newYs[1]]);
+          }
+        }
+      } else {
+        ctx.moveTo(point.canvasx, newYs[1]);
+        ctx.lineTo(point.canvasx, newYs[0]);
+      }
+      prevYs = newYs;
+      prevX = point.canvasx;
+    }
+    prevStepPlot = stepPlot;
+    if (newYs && point) {
+      traceBackPath(ctx, point.canvasx, newYs[1], pathBack);
+      pathBack = [];
+    }
+    ctx.fill();
+  }
+};
+
+return DygraphCanvasRenderer;
+
+})();
diff --git a/src/dygraph-combined.js b/src/dygraph-combined.js
new file mode 100644 (file)
index 0000000..e6380de
--- /dev/null
@@ -0,0 +1,7 @@
+This is not the file you are looking for.
+A reasonably up-to-date version can be found at http://dygraphs.com/dygraph-combined.js
+
+dygraph-combined.js is a "packed" version of the larger dygraphs JS files. It is
+smaller and loads more quickly, but is harder to debug.
+
+To generate this file, run "make" or generate-combined.sh.
diff --git a/src/dygraph-gviz.js b/src/dygraph-gviz.js
new file mode 100644 (file)
index 0000000..d2d7a0d
--- /dev/null
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview A wrapper around the Dygraph class which implements the
+ * interface for a GViz (aka Google Visualization API) visualization.
+ * It is designed to be a drop-in replacement for Google's AnnotatedTimeline,
+ * so the documentation at
+ * http://code.google.com/apis/chart/interactive/docs/gallery/annotatedtimeline.html
+ * translates over directly.
+ *
+ * For a full demo, see:
+ * - http://dygraphs.com/tests/gviz.html
+ * - http://dygraphs.com/tests/annotation-gviz.html
+ */
+
+(function() {
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * A wrapper around Dygraph that implements the gviz API.
+ * @param {!HTMLDivElement} container The DOM object the visualization should
+ *     live in.
+ * @constructor
+ */
+Dygraph.GVizChart = function(container) {
+  this.container = container;
+};
+
+/**
+ * @param {GVizDataTable} data
+ * @param {Object.<*>} options
+ */
+Dygraph.GVizChart.prototype.draw = function(data, options) {
+  // Clear out any existing dygraph.
+  // TODO(danvk): would it make more sense to simply redraw using the current
+  // date_graph object?
+  this.container.innerHTML = '';
+  if (typeof(this.date_graph) != 'undefined') {
+    this.date_graph.destroy();
+  }
+
+  this.date_graph = new Dygraph(this.container, data, options);
+};
+
+/**
+ * Google charts compatible setSelection
+ * Only row selection is supported, all points in the row will be highlighted
+ * @param {Array.<{row:number}>} selection_array array of the selected cells
+ * @public
+ */
+Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
+  var row = false;
+  if (selection_array.length) {
+    row = selection_array[0].row;
+  }
+  this.date_graph.setSelection(row);
+};
+
+/**
+ * Google charts compatible getSelection implementation
+ * @return {Array.<{row:number,column:number}>} array of the selected cells
+ * @public
+ */
+Dygraph.GVizChart.prototype.getSelection = function() {
+  var selection = [];
+
+  var row = this.date_graph.getSelection();
+
+  if (row < 0) return selection;
+
+  var points = this.date_graph.layout_.points;
+  for (var setIdx = 0; setIdx < points.length; ++setIdx) {
+    selection.push({row: row, column: setIdx + 1});
+  }
+
+  return selection;
+};
+
+})();
diff --git a/src/dygraph-interaction-model.js b/src/dygraph-interaction-model.js
new file mode 100644 (file)
index 0000000..53e2f44
--- /dev/null
@@ -0,0 +1,757 @@
+/**
+ * @license
+ * Copyright 2011 Robert Konigsberg (konigsberg@google.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview The default interaction model for Dygraphs. This is kept out
+ * of dygraph.js for better navigability.
+ * @author Robert Konigsberg (konigsberg@google.com)
+ */
+
+(function() {
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * You can drag this many pixels past the edge of the chart and still have it
+ * be considered a zoom. This makes it easier to zoom to the exact edge of the
+ * chart, a fairly common operation.
+ */
+var DRAG_EDGE_MARGIN = 100;
+
+/**
+ * A collection of functions to facilitate build custom interaction models.
+ * @class
+ */
+Dygraph.Interaction = {};
+
+/**
+ * Checks whether the beginning & ending of an event were close enough that it
+ * should be considered a click. If it should, dispatch appropriate events.
+ * Returns true if the event was treated as a click.
+ *
+ * @param {Event} event
+ * @param {Dygraph} g
+ * @param {Object} context
+ */
+Dygraph.Interaction.maybeTreatMouseOpAsClick = function(event, g, context) {
+  context.dragEndX = Dygraph.dragGetX_(event, context);
+  context.dragEndY = Dygraph.dragGetY_(event, context);
+  var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
+  var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
+
+  if (regionWidth < 2 && regionHeight < 2 &&
+      g.lastx_ !== undefined && g.lastx_ != -1) {
+    Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
+  }
+
+  context.regionWidth = regionWidth;
+  context.regionHeight = regionHeight;
+};
+
+/**
+ * Called in response to an interaction model operation that
+ * should start the default panning behavior.
+ *
+ * It's used in the default callback for "mousedown" operations.
+ * Custom interaction model builders can use it to provide the default
+ * panning behavior.
+ *
+ * @param {Event} event the event object which led to the startPan call.
+ * @param {Dygraph} g The dygraph on which to act.
+ * @param {Object} context The dragging context object (with
+ *     dragStartX/dragStartY/etc. properties). This function modifies the
+ *     context.
+ */
+Dygraph.Interaction.startPan = function(event, g, context) {
+  var i, axis;
+  context.isPanning = true;
+  var xRange = g.xAxisRange();
+
+  if (g.getOptionForAxis("logscale", "x")) {
+    context.initialLeftmostDate = Dygraph.log10(xRange[0]);
+    context.dateRange = Dygraph.log10(xRange[1]) - Dygraph.log10(xRange[0]);
+  } else {
+    context.initialLeftmostDate = xRange[0];    
+    context.dateRange = xRange[1] - xRange[0];
+  }
+  context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
+
+  if (g.getNumericOption("panEdgeFraction")) {
+    var maxXPixelsToDraw = g.width_ * g.getNumericOption("panEdgeFraction");
+    var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
+
+    var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
+    var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
+
+    var boundedLeftDate = g.toDataXCoord(boundedLeftX);
+    var boundedRightDate = g.toDataXCoord(boundedRightX);
+    context.boundedDates = [boundedLeftDate, boundedRightDate];
+
+    var boundedValues = [];
+    var maxYPixelsToDraw = g.height_ * g.getNumericOption("panEdgeFraction");
+
+    for (i = 0; i < g.axes_.length; i++) {
+      axis = g.axes_[i];
+      var yExtremes = axis.extremeRange;
+
+      var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
+      var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
+
+      var boundedTopValue = g.toDataYCoord(boundedTopY, i);
+      var boundedBottomValue = g.toDataYCoord(boundedBottomY, i);
+
+      boundedValues[i] = [boundedTopValue, boundedBottomValue];
+    }
+    context.boundedValues = boundedValues;
+  }
+
+  // Record the range of each y-axis at the start of the drag.
+  // If any axis has a valueRange or valueWindow, then we want a 2D pan.
+  // We can't store data directly in g.axes_, because it does not belong to us
+  // and could change out from under us during a pan (say if there's a data
+  // update).
+  context.is2DPan = false;
+  context.axes = [];
+  for (i = 0; i < g.axes_.length; i++) {
+    axis = g.axes_[i];
+    var axis_data = {};
+    var yRange = g.yAxisRange(i);
+    // TODO(konigsberg): These values should be in |context|.
+    // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
+    var logscale = g.attributes_.getForAxis("logscale", i);
+    if (logscale) {
+      axis_data.initialTopValue = Dygraph.log10(yRange[1]);
+      axis_data.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
+    } else {
+      axis_data.initialTopValue = yRange[1];
+      axis_data.dragValueRange = yRange[1] - yRange[0];
+    }
+    axis_data.unitsPerPixel = axis_data.dragValueRange / (g.plotter_.area.h - 1);
+    context.axes.push(axis_data);
+
+    // While calculating axes, set 2dpan.
+    if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
+  }
+};
+
+/**
+ * Called in response to an interaction model operation that
+ * responds to an event that pans the view.
+ *
+ * It's used in the default callback for "mousemove" operations.
+ * Custom interaction model builders can use it to provide the default
+ * panning behavior.
+ *
+ * @param {Event} event the event object which led to the movePan call.
+ * @param {Dygraph} g The dygraph on which to act.
+ * @param {Object} context The dragging context object (with
+ *     dragStartX/dragStartY/etc. properties). This function modifies the
+ *     context.
+ */
+Dygraph.Interaction.movePan = function(event, g, context) {
+  context.dragEndX = Dygraph.dragGetX_(event, context);
+  context.dragEndY = Dygraph.dragGetY_(event, context);
+
+  var minDate = context.initialLeftmostDate -
+    (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
+  if (context.boundedDates) {
+    minDate = Math.max(minDate, context.boundedDates[0]);
+  }
+  var maxDate = minDate + context.dateRange;
+  if (context.boundedDates) {
+    if (maxDate > context.boundedDates[1]) {
+      // Adjust minDate, and recompute maxDate.
+      minDate = minDate - (maxDate - context.boundedDates[1]);
+      maxDate = minDate + context.dateRange;
+    }
+  }
+
+  if (g.getOptionForAxis("logscale", "x")) {
+    g.dateWindow_ = [ Math.pow(Dygraph.LOG_SCALE, minDate),
+                      Math.pow(Dygraph.LOG_SCALE, maxDate) ];
+  } else {
+    g.dateWindow_ = [minDate, maxDate];    
+  }
+
+  // y-axis scaling is automatic unless this is a full 2D pan.
+  if (context.is2DPan) {
+
+    var pixelsDragged = context.dragEndY - context.dragStartY;
+
+    // Adjust each axis appropriately.
+    for (var i = 0; i < g.axes_.length; i++) {
+      var axis = g.axes_[i];
+      var axis_data = context.axes[i];
+      var unitsDragged = pixelsDragged * axis_data.unitsPerPixel;
+
+      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
+
+      // In log scale, maxValue and minValue are the logs of those values.
+      var maxValue = axis_data.initialTopValue + unitsDragged;
+      if (boundedValue) {
+        maxValue = Math.min(maxValue, boundedValue[1]);
+      }
+      var minValue = maxValue - axis_data.dragValueRange;
+      if (boundedValue) {
+        if (minValue < boundedValue[0]) {
+          // Adjust maxValue, and recompute minValue.
+          maxValue = maxValue - (minValue - boundedValue[0]);
+          minValue = maxValue - axis_data.dragValueRange;
+        }
+      }
+      if (g.attributes_.getForAxis("logscale", i)) {
+        axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
+                             Math.pow(Dygraph.LOG_SCALE, maxValue) ];
+      } else {
+        axis.valueWindow = [ minValue, maxValue ];
+      }
+    }
+  }
+
+  g.drawGraph_(false);
+};
+
+/**
+ * Called in response to an interaction model operation that
+ * responds to an event that ends panning.
+ *
+ * It's used in the default callback for "mouseup" operations.
+ * Custom interaction model builders can use it to provide the default
+ * panning behavior.
+ *
+ * @param {Event} event the event object which led to the endPan call.
+ * @param {Dygraph} g The dygraph on which to act.
+ * @param {Object} context The dragging context object (with
+ *     dragStartX/dragStartY/etc. properties). This function modifies the
+ *     context.
+ */
+Dygraph.Interaction.endPan = Dygraph.Interaction.maybeTreatMouseOpAsClick;
+
+/**
+ * Called in response to an interaction model operation that
+ * responds to an event that starts zooming.
+ *
+ * It's used in the default callback for "mousedown" operations.
+ * Custom interaction model builders can use it to provide the default
+ * zooming behavior.
+ *
+ * @param {Event} event the event object which led to the startZoom call.
+ * @param {Dygraph} g The dygraph on which to act.
+ * @param {Object} context The dragging context object (with
+ *     dragStartX/dragStartY/etc. properties). This function modifies the
+ *     context.
+ */
+Dygraph.Interaction.startZoom = function(event, g, context) {
+  context.isZooming = true;
+  context.zoomMoved = false;
+};
+
+/**
+ * Called in response to an interaction model operation that
+ * responds to an event that defines zoom boundaries.
+ *
+ * It's used in the default callback for "mousemove" operations.
+ * Custom interaction model builders can use it to provide the default
+ * zooming behavior.
+ *
+ * @param {Event} event the event object which led to the moveZoom call.
+ * @param {Dygraph} g The dygraph on which to act.
+ * @param {Object} context The dragging context object (with
+ *     dragStartX/dragStartY/etc. properties). This function modifies the
+ *     context.
+ */
+Dygraph.Interaction.moveZoom = function(event, g, context) {
+  context.zoomMoved = true;
+  context.dragEndX = Dygraph.dragGetX_(event, context);
+  context.dragEndY = Dygraph.dragGetY_(event, context);
+
+  var xDelta = Math.abs(context.dragStartX - context.dragEndX);
+  var yDelta = Math.abs(context.dragStartY - context.dragEndY);
+
+  // drag direction threshold for y axis is twice as large as x axis
+  context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
+
+  g.drawZoomRect_(
+      context.dragDirection,
+      context.dragStartX,
+      context.dragEndX,
+      context.dragStartY,
+      context.dragEndY,
+      context.prevDragDirection,
+      context.prevEndX,
+      context.prevEndY);
+
+  context.prevEndX = context.dragEndX;
+  context.prevEndY = context.dragEndY;
+  context.prevDragDirection = context.dragDirection;
+};
+
+/**
+ * TODO(danvk): move this logic into dygraph.js
+ * @param {Dygraph} g
+ * @param {Event} event
+ * @param {Object} context
+ */
+Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
+  var clickCallback = g.getFunctionOption('clickCallback');
+  var pointClickCallback = g.getFunctionOption('pointClickCallback');
+
+  var selectedPoint = null;
+
+  // Find out if the click occurs on a point.
+  var closestIdx = -1;
+  var closestDistance = Number.MAX_VALUE;
+
+  // check if the click was on a particular point.
+  for (var i = 0; i < g.selPoints_.length; i++) {
+    var p = g.selPoints_[i];
+    var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
+                   Math.pow(p.canvasy - context.dragEndY, 2);
+    if (!isNaN(distance) &&
+        (closestIdx == -1 || distance < closestDistance)) {
+      closestDistance = distance;
+      closestIdx = i;
+    }
+  }
+
+  // Allow any click within two pixels of the dot.
+  var radius = g.getNumericOption('highlightCircleSize') + 2;
+  if (closestDistance <= radius * radius) {
+    selectedPoint = g.selPoints_[closestIdx];
+  }
+
+  if (selectedPoint) {
+    var e = {
+      cancelable: true,
+      point: selectedPoint,
+      canvasx: context.dragEndX,
+      canvasy: context.dragEndY
+    };
+    var defaultPrevented = g.cascadeEvents_('pointClick', e);
+    if (defaultPrevented) {
+      // Note: this also prevents click / clickCallback from firing.
+      return;
+    }
+    if (pointClickCallback) {
+      pointClickCallback.call(g, event, selectedPoint);
+    }
+  }
+
+  var e = {
+    cancelable: true,
+    xval: g.lastx_,  // closest point by x value
+    pts: g.selPoints_,
+    canvasx: context.dragEndX,
+    canvasy: context.dragEndY
+  };
+  if (!g.cascadeEvents_('click', e)) {
+    if (clickCallback) {
+      // TODO(danvk): pass along more info about the points, e.g. 'x'
+      clickCallback.call(g, event, g.lastx_, g.selPoints_);
+    }
+  }
+};
+
+/**
+ * Called in response to an interaction model operation that
+ * responds to an event that performs a zoom based on previously defined
+ * bounds..
+ *
+ * It's used in the default callback for "mouseup" operations.
+ * Custom interaction model builders can use it to provide the default
+ * zooming behavior.
+ *
+ * @param {Event} event the event object which led to the endZoom call.
+ * @param {Dygraph} g The dygraph on which to end the zoom.
+ * @param {Object} context The dragging context object (with
+ *     dragStartX/dragStartY/etc. properties). This function modifies the
+ *     context.
+ */
+Dygraph.Interaction.endZoom = function(event, g, context) {
+  g.clearZoomRect_();
+  context.isZooming = false;
+  Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
+
+  // The zoom rectangle is visibly clipped to the plot area, so its behavior
+  // should be as well.
+  // See http://code.google.com/p/dygraphs/issues/detail?id=280
+  var plotArea = g.getArea();
+  if (context.regionWidth >= 10 &&
+      context.dragDirection == Dygraph.HORIZONTAL) {
+    var left = Math.min(context.dragStartX, context.dragEndX),
+        right = Math.max(context.dragStartX, context.dragEndX);
+    left = Math.max(left, plotArea.x);
+    right = Math.min(right, plotArea.x + plotArea.w);
+    if (left < right) {
+      g.doZoomX_(left, right);
+    }
+    context.cancelNextDblclick = true;
+  } else if (context.regionHeight >= 10 &&
+             context.dragDirection == Dygraph.VERTICAL) {
+    var top = Math.min(context.dragStartY, context.dragEndY),
+        bottom = Math.max(context.dragStartY, context.dragEndY);
+    top = Math.max(top, plotArea.y);
+    bottom = Math.min(bottom, plotArea.y + plotArea.h);
+    if (top < bottom) {
+      g.doZoomY_(top, bottom);
+    }
+    context.cancelNextDblclick = true;
+  }
+  context.dragStartX = null;
+  context.dragStartY = null;
+};
+
+/**
+ * @private
+ */
+Dygraph.Interaction.startTouch = function(event, g, context) {
+  event.preventDefault();  // touch browsers are all nice.
+  if (event.touches.length > 1) {
+    // If the user ever puts two fingers down, it's not a double tap.
+    context.startTimeForDoubleTapMs = null;
+  }
+
+  var touches = [];
+  for (var i = 0; i < event.touches.length; i++) {
+    var t = event.touches[i];
+    // we dispense with 'dragGetX_' because all touchBrowsers support pageX
+    touches.push({
+      pageX: t.pageX,
+      pageY: t.pageY,
+      dataX: g.toDataXCoord(t.pageX),
+      dataY: g.toDataYCoord(t.pageY)
+      // identifier: t.identifier
+    });
+  }
+  context.initialTouches = touches;
+
+  if (touches.length == 1) {
+    // This is just a swipe.
+    context.initialPinchCenter = touches[0];
+    context.touchDirections = { x: true, y: true };
+  } else if (touches.length >= 2) {
+    // It's become a pinch!
+    // In case there are 3+ touches, we ignore all but the "first" two.
+
+    // only screen coordinates can be averaged (data coords could be log scale).
+    context.initialPinchCenter = {
+      pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
+      pageY: 0.5 * (touches[0].pageY + touches[1].pageY),
+
+      // TODO(danvk): remove
+      dataX: 0.5 * (touches[0].dataX + touches[1].dataX),
+      dataY: 0.5 * (touches[0].dataY + touches[1].dataY)
+    };
+
+    // Make pinches in a 45-degree swath around either axis 1-dimensional zooms.
+    var initialAngle = 180 / Math.PI * Math.atan2(
+        context.initialPinchCenter.pageY - touches[0].pageY,
+        touches[0].pageX - context.initialPinchCenter.pageX);
+
+    // use symmetry to get it into the first quadrant.
+    initialAngle = Math.abs(initialAngle);
+    if (initialAngle > 90) initialAngle = 90 - initialAngle;
+
+    context.touchDirections = {
+      x: (initialAngle < (90 - 45/2)),
+      y: (initialAngle > 45/2)
+    };
+  }
+
+  // save the full x & y ranges.
+  context.initialRange = {
+    x: g.xAxisRange(),
+    y: g.yAxisRange()
+  };
+};
+
+/**
+ * @private
+ */
+Dygraph.Interaction.moveTouch = function(event, g, context) {
+  // If the tap moves, then it's definitely not part of a double-tap.
+  context.startTimeForDoubleTapMs = null;
+
+  var i, touches = [];
+  for (i = 0; i < event.touches.length; i++) {
+    var t = event.touches[i];
+    touches.push({
+      pageX: t.pageX,
+      pageY: t.pageY
+    });
+  }
+  var initialTouches = context.initialTouches;
+
+  var c_now;
+
+  // old and new centers.
+  var c_init = context.initialPinchCenter;
+  if (touches.length == 1) {
+    c_now = touches[0];
+  } else {
+    c_now = {
+      pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
+      pageY: 0.5 * (touches[0].pageY + touches[1].pageY)
+    };
+  }
+
+  // this is the "swipe" component
+  // we toss it out for now, but could use it in the future.
+  var swipe = {
+    pageX: c_now.pageX - c_init.pageX,
+    pageY: c_now.pageY - c_init.pageY
+  };
+  var dataWidth = context.initialRange.x[1] - context.initialRange.x[0];
+  var dataHeight = context.initialRange.y[0] - context.initialRange.y[1];
+  swipe.dataX = (swipe.pageX / g.plotter_.area.w) * dataWidth;
+  swipe.dataY = (swipe.pageY / g.plotter_.area.h) * dataHeight;
+  var xScale, yScale;
+
+  // The residual bits are usually split into scale & rotate bits, but we split
+  // them into x-scale and y-scale bits.
+  if (touches.length == 1) {
+    xScale = 1.0;
+    yScale = 1.0;
+  } else if (touches.length >= 2) {
+    var initHalfWidth = (initialTouches[1].pageX - c_init.pageX);
+    xScale = (touches[1].pageX - c_now.pageX) / initHalfWidth;
+
+    var initHalfHeight = (initialTouches[1].pageY - c_init.pageY);
+    yScale = (touches[1].pageY - c_now.pageY) / initHalfHeight;
+  }
+
+  // Clip scaling to [1/8, 8] to prevent too much blowup.
+  xScale = Math.min(8, Math.max(0.125, xScale));
+  yScale = Math.min(8, Math.max(0.125, yScale));
+
+  var didZoom = false;
+  if (context.touchDirections.x) {
+    g.dateWindow_ = [
+      c_init.dataX - swipe.dataX + (context.initialRange.x[0] - c_init.dataX) / xScale,
+      c_init.dataX - swipe.dataX + (context.initialRange.x[1] - c_init.dataX) / xScale
+    ];
+    didZoom = true;
+  }
+  
+  if (context.touchDirections.y) {
+    for (i = 0; i < 1  /*g.axes_.length*/; i++) {
+      var axis = g.axes_[i];
+      var logscale = g.attributes_.getForAxis("logscale", i);
+      if (logscale) {
+        // TODO(danvk): implement
+      } else {
+        axis.valueWindow = [
+          c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale,
+          c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale
+        ];
+        didZoom = true;
+      }
+    }
+  }
+
+  g.drawGraph_(false);
+
+  // We only call zoomCallback on zooms, not pans, to mirror desktop behavior.
+  if (didZoom && touches.length > 1 && g.getFunctionOption('zoomCallback')) {
+    var viewWindow = g.xAxisRange();
+    g.getFunctionOption("zoomCallback").call(g, viewWindow[0], viewWindow[1], g.yAxisRanges());
+  }
+};
+
+/**
+ * @private
+ */
+Dygraph.Interaction.endTouch = function(event, g, context) {
+  if (event.touches.length !== 0) {
+    // this is effectively a "reset"
+    Dygraph.Interaction.startTouch(event, g, context);
+  } else if (event.changedTouches.length == 1) {
+    // Could be part of a "double tap"
+    // The heuristic here is that it's a double-tap if the two touchend events
+    // occur within 500ms and within a 50x50 pixel box.
+    var now = new Date().getTime();
+    var t = event.changedTouches[0];
+    if (context.startTimeForDoubleTapMs &&
+        now - context.startTimeForDoubleTapMs < 500 &&
+        context.doubleTapX && Math.abs(context.doubleTapX - t.screenX) < 50 &&
+        context.doubleTapY && Math.abs(context.doubleTapY - t.screenY) < 50) {
+      g.resetZoom();
+    } else {
+      context.startTimeForDoubleTapMs = now;
+      context.doubleTapX = t.screenX;
+      context.doubleTapY = t.screenY;
+    }
+  }
+};
+
+// Determine the distance from x to [left, right].
+var distanceFromInterval = function(x, left, right) {
+  if (x < left) {
+    return left - x;
+  } else if (x > right) {
+    return x - right;
+  } else {
+    return 0;
+  }
+};
+
+/**
+ * Returns the number of pixels by which the event happens from the nearest
+ * edge of the chart. For events in the interior of the chart, this returns zero.
+ */
+var distanceFromChart = function(event, g) {
+  var chartPos = Dygraph.findPos(g.canvas_);
+  var box = {
+    left: chartPos.x,
+    right: chartPos.x + g.canvas_.offsetWidth,
+    top: chartPos.y,
+    bottom: chartPos.y + g.canvas_.offsetHeight
+  };
+
+  var pt = {
+    x: Dygraph.pageX(event),
+    y: Dygraph.pageY(event)
+  };
+
+  var dx = distanceFromInterval(pt.x, box.left, box.right),
+      dy = distanceFromInterval(pt.y, box.top, box.bottom);
+  return Math.max(dx, dy);
+};
+
+/**
+ * Default interation model for dygraphs. You can refer to specific elements of
+ * this when constructing your own interaction model, e.g.:
+ * g.updateOptions( {
+ *   interactionModel: {
+ *     mousedown: Dygraph.defaultInteractionModel.mousedown
+ *   }
+ * } );
+ */
+Dygraph.Interaction.defaultModel = {
+  // Track the beginning of drag events
+  mousedown: function(event, g, context) {
+    // Right-click should not initiate a zoom.
+    if (event.button && event.button == 2) return;
+
+    context.initializeMouseDown(event, g, context);
+
+    if (event.altKey || event.shiftKey) {
+      Dygraph.startPan(event, g, context);
+    } else {
+      Dygraph.startZoom(event, g, context);
+    }
+
+    // Note: we register mousemove/mouseup on document to allow some leeway for
+    // events to move outside of the chart. Interaction model events get
+    // registered on the canvas, which is too small to allow this.
+    var mousemove = function(event) {
+      if (context.isZooming) {
+        // When the mouse moves >200px from the chart edge, cancel the zoom.
+        var d = distanceFromChart(event, g);
+        if (d < DRAG_EDGE_MARGIN) {
+          Dygraph.moveZoom(event, g, context);
+        } else {
+          if (context.dragEndX !== null) {
+            context.dragEndX = null;
+            context.dragEndY = null;
+            g.clearZoomRect_();
+          }
+        }
+      } else if (context.isPanning) {
+        Dygraph.movePan(event, g, context);
+      }
+    };
+    var mouseup = function(event) {
+      if (context.isZooming) {
+        if (context.dragEndX !== null) {
+          Dygraph.endZoom(event, g, context);
+        } else {
+          Dygraph.Interaction.maybeTreatMouseOpAsClick(event, g, context);
+        }
+      } else if (context.isPanning) {
+        Dygraph.endPan(event, g, context);
+      }
+
+      Dygraph.removeEvent(document, 'mousemove', mousemove);
+      Dygraph.removeEvent(document, 'mouseup', mouseup);
+      context.destroy();
+    };
+
+    g.addAndTrackEvent(document, 'mousemove', mousemove);
+    g.addAndTrackEvent(document, 'mouseup', mouseup);
+  },
+  willDestroyContextMyself: true,
+
+  touchstart: function(event, g, context) {
+    Dygraph.Interaction.startTouch(event, g, context);
+  },
+  touchmove: function(event, g, context) {
+    Dygraph.Interaction.moveTouch(event, g, context);
+  },
+  touchend: function(event, g, context) {
+    Dygraph.Interaction.endTouch(event, g, context);
+  },
+
+  // Disable zooming out if panning.
+  dblclick: function(event, g, context) {
+    if (context.cancelNextDblclick) {
+      context.cancelNextDblclick = false;
+      return;
+    }
+
+    // Give plugins a chance to grab this event.
+    var e = {
+      canvasx: context.dragEndX,
+      canvasy: context.dragEndY
+    };
+    if (g.cascadeEvents_('dblclick', e)) {
+      return;
+    }
+
+    if (event.altKey || event.shiftKey) {
+      return;
+    }
+    g.resetZoom();
+  }
+};
+
+Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
+
+// old ways of accessing these methods/properties
+Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
+Dygraph.endZoom = Dygraph.Interaction.endZoom;
+Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
+Dygraph.startZoom = Dygraph.Interaction.startZoom;
+Dygraph.endPan = Dygraph.Interaction.endPan;
+Dygraph.movePan = Dygraph.Interaction.movePan;
+Dygraph.startPan = Dygraph.Interaction.startPan;
+
+Dygraph.Interaction.nonInteractiveModel_ = {
+  mousedown: function(event, g, context) {
+    context.initializeMouseDown(event, g, context);
+  },
+  mouseup: Dygraph.Interaction.maybeTreatMouseOpAsClick
+};
+
+// Default interaction model when using the range selector.
+Dygraph.Interaction.dragIsPanInteractionModel = {
+  mousedown: function(event, g, context) {
+    context.initializeMouseDown(event, g, context);
+    Dygraph.startPan(event, g, context);
+  },
+  mousemove: function(event, g, context) {
+    if (context.isPanning) {
+      Dygraph.movePan(event, g, context);
+    }
+  },
+  mouseup: function(event, g, context) {
+    if (context.isPanning) {
+      Dygraph.endPan(event, g, context);
+    }
+  }
+};
+
+})();
diff --git a/src/dygraph-internal.externs.js b/src/dygraph-internal.externs.js
new file mode 100644 (file)
index 0000000..2108fce
--- /dev/null
@@ -0,0 +1,31 @@
+// This file:
+// - declares symbols that are provided outisde of dygraphs
+// - defines custom types used internally
+
+
+/**
+ * @typedef {function(
+ *   (number|Date),
+ *   number,
+ *   function(string):*,
+ *   (Dygraph|undefined)
+ * ):string}
+ */
+var AxisLabelFormatter;
+
+
+/**
+ * @typedef {function(number,function(string),Dygraph):string}
+ */
+var ValueFormatter;
+
+
+/**
+ * @typedef {Array.<Array.<string|number|Array.<number>>>}
+ */
+var DygraphDataArray;
+
+/**
+ * @constructor
+ */
+function GVizDataTable() {}
diff --git a/src/dygraph-layout.js b/src/dygraph-layout.js
new file mode 100644 (file)
index 0000000..ef1df91
--- /dev/null
@@ -0,0 +1,351 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview Based on PlotKitLayout, but modified to meet the needs of
+ * dygraphs.
+ */
+
+var DygraphLayout = (function() {
+
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * Creates a new DygraphLayout object.
+ *
+ * This class contains all the data to be charted.
+ * It uses data coordinates, but also records the chart range (in data
+ * coordinates) and hence is able to calculate percentage positions ('In this
+ * view, Point A lies 25% down the x-axis.')
+ *
+ * Two things that it does not do are:
+ * 1. Record pixel coordinates for anything.
+ * 2. (oddly) determine anything about the layout of chart elements.
+ *
+ * The naming is a vestige of Dygraph's original PlotKit roots.
+ *
+ * @constructor
+ */
+var DygraphLayout = function(dygraph) {
+  this.dygraph_ = dygraph;
+  /**
+   * Array of points for each series.
+   *
+   * [series index][row index in series] = |Point| structure,
+   * where series index refers to visible series only, and the
+   * point index is for the reduced set of points for the current
+   * zoom region (including one point just outside the window).
+   * All points in the same row index share the same X value.
+   *
+   * @type {Array.<Array.<Dygraph.PointType>>}
+   */
+  this.points = [];
+  this.setNames = [];
+  this.annotations = [];
+  this.yAxes_ = null;
+
+  // TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and
+  // yticks are outputs. Clean this up.
+  this.xTicks_ = null;
+  this.yTicks_ = null;
+};
+
+/**
+ * Add points for a single series.
+ *
+ * @param {string} setname Name of the series.
+ * @param {Array.<Dygraph.PointType>} set_xy Points for the series.
+ */
+DygraphLayout.prototype.addDataset = function(setname, set_xy) {
+  this.points.push(set_xy);
+  this.setNames.push(setname);
+};
+
+/**
+ * Returns the box which the chart should be drawn in. This is the canvas's
+ * box, less space needed for the axis and chart labels.
+ *
+ * @return {{x: number, y: number, w: number, h: number}}
+ */
+DygraphLayout.prototype.getPlotArea = function() {
+  return this.area_;
+};
+
+// Compute the box which the chart should be drawn in. This is the canvas's
+// box, less space needed for axis, chart labels, and other plug-ins.
+// NOTE: This should only be called by Dygraph.predraw_().
+DygraphLayout.prototype.computePlotArea = function() {
+  var area = {
+    // TODO(danvk): per-axis setting.
+    x: 0,
+    y: 0
+  };
+
+  area.w = this.dygraph_.width_ - area.x - this.dygraph_.getOption('rightGap');
+  area.h = this.dygraph_.height_;
+
+  // Let plugins reserve space.
+  var e = {
+    chart_div: this.dygraph_.graphDiv,
+    reserveSpaceLeft: function(px) {
+      var r = {
+        x: area.x,
+        y: area.y,
+        w: px,
+        h: area.h
+      };
+      area.x += px;
+      area.w -= px;
+      return r;
+    },
+    reserveSpaceRight: function(px) {
+      var r = {
+        x: area.x + area.w - px,
+        y: area.y,
+        w: px,
+        h: area.h
+      };
+      area.w -= px;
+      return r;
+    },
+    reserveSpaceTop: function(px) {
+      var r = {
+        x: area.x,
+        y: area.y,
+        w: area.w,
+        h: px
+      };
+      area.y += px;
+      area.h -= px;
+      return r;
+    },
+    reserveSpaceBottom: function(px) {
+      var r = {
+        x: area.x,
+        y: area.y + area.h - px,
+        w: area.w,
+        h: px
+      };
+      area.h -= px;
+      return r;
+    },
+    chartRect: function() {
+      return {x:area.x, y:area.y, w:area.w, h:area.h};
+    }
+  };
+  this.dygraph_.cascadeEvents_('layout', e);
+
+  this.area_ = area;
+};
+
+DygraphLayout.prototype.setAnnotations = function(ann) {
+  // The Dygraph object's annotations aren't parsed. We parse them here and
+  // save a copy. If there is no parser, then the user must be using raw format.
+  this.annotations = [];
+  var parse = this.dygraph_.getOption('xValueParser') || function(x) { return x; };
+  for (var i = 0; i < ann.length; i++) {
+    var a = {};
+    if (!ann[i].xval && ann[i].x === undefined) {
+      console.error("Annotations must have an 'x' property");
+      return;
+    }
+    if (ann[i].icon &&
+        !(ann[i].hasOwnProperty('width') &&
+          ann[i].hasOwnProperty('height'))) {
+      console.error("Must set width and height when setting " +
+                    "annotation.icon property");
+      return;
+    }
+    Dygraph.update(a, ann[i]);
+    if (!a.xval) a.xval = parse(a.x);
+    this.annotations.push(a);
+  }
+};
+
+DygraphLayout.prototype.setXTicks = function(xTicks) {
+  this.xTicks_ = xTicks;
+};
+
+// TODO(danvk): add this to the Dygraph object's API or move it into Layout.
+DygraphLayout.prototype.setYAxes = function (yAxes) {
+  this.yAxes_ = yAxes;
+};
+
+DygraphLayout.prototype.evaluate = function() {
+  this._xAxis = {};
+  this._evaluateLimits();
+  this._evaluateLineCharts();
+  this._evaluateLineTicks();
+  this._evaluateAnnotations();
+};
+
+DygraphLayout.prototype._evaluateLimits = function() {
+  var xlimits = this.dygraph_.xAxisRange();
+  this._xAxis.minval = xlimits[0];
+  this._xAxis.maxval = xlimits[1];
+  var xrange = xlimits[1] - xlimits[0];
+  this._xAxis.scale = (xrange !== 0 ? 1 / xrange : 1.0);
+
+  if (this.dygraph_.getOptionForAxis("logscale", 'x')) {
+    this._xAxis.xlogrange = Dygraph.log10(this._xAxis.maxval) - Dygraph.log10(this._xAxis.minval);
+    this._xAxis.xlogscale = (this._xAxis.xlogrange !== 0 ? 1.0 / this._xAxis.xlogrange : 1.0);
+  }
+  for (var i = 0; i < this.yAxes_.length; i++) {
+    var axis = this.yAxes_[i];
+    axis.minyval = axis.computedValueRange[0];
+    axis.maxyval = axis.computedValueRange[1];
+    axis.yrange = axis.maxyval - axis.minyval;
+    axis.yscale = (axis.yrange !== 0 ? 1.0 / axis.yrange : 1.0);
+
+    if (this.dygraph_.getOption("logscale")) {
+      axis.ylogrange = Dygraph.log10(axis.maxyval) - Dygraph.log10(axis.minyval);
+      axis.ylogscale = (axis.ylogrange !== 0 ? 1.0 / axis.ylogrange : 1.0);
+      if (!isFinite(axis.ylogrange) || isNaN(axis.ylogrange)) {
+        console.error('axis ' + i + ' of graph at ' + axis.g +
+                      ' can\'t be displayed in log scale for range [' +
+                      axis.minyval + ' - ' + axis.maxyval + ']');
+      }
+    }
+  }
+};
+
+DygraphLayout.calcXNormal_ = function(value, xAxis, logscale) {
+  if (logscale) {
+    return ((Dygraph.log10(value) - Dygraph.log10(xAxis.minval)) * xAxis.xlogscale);
+  } else {
+    return (value - xAxis.minval) * xAxis.scale;
+  }
+};
+
+/**
+ * @param {DygraphAxisType} axis
+ * @param {number} value
+ * @param {boolean} logscale
+ * @return {number}
+ */
+DygraphLayout.calcYNormal_ = function(axis, value, logscale) {
+  if (logscale) {
+    var x = 1.0 - ((Dygraph.log10(value) - Dygraph.log10(axis.minyval)) * axis.ylogscale);
+    return isFinite(x) ? x : NaN;  // shim for v8 issue; see pull request 276
+  } else {
+    return 1.0 - ((value - axis.minyval) * axis.yscale);
+  }
+};
+
+DygraphLayout.prototype._evaluateLineCharts = function() {
+  var isStacked = this.dygraph_.getOption("stackedGraph");
+  var isLogscaleForX = this.dygraph_.getOptionForAxis("logscale", 'x');
+
+  for (var setIdx = 0; setIdx < this.points.length; setIdx++) {
+    var points = this.points[setIdx];
+    var setName = this.setNames[setIdx];
+    var connectSeparated = this.dygraph_.getOption('connectSeparatedPoints', setName);
+    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+    // TODO (konigsberg): use optionsForAxis instead.
+    var logscale = this.dygraph_.attributes_.getForSeries("logscale", setName);
+
+    for (var j = 0; j < points.length; j++) {
+      var point = points[j];
+
+      // Range from 0-1 where 0 represents left and 1 represents right.
+      point.x = DygraphLayout.calcXNormal_(point.xval, this._xAxis, isLogscaleForX);
+      // Range from 0-1 where 0 represents top and 1 represents bottom
+      var yval = point.yval;
+      if (isStacked) {
+        point.y_stacked = DygraphLayout.calcYNormal_(
+            axis, point.yval_stacked, logscale);
+        if (yval !== null && !isNaN(yval)) {
+          yval = point.yval_stacked;
+        }
+      }
+      if (yval === null) {
+        yval = NaN;
+        if (!connectSeparated) {
+          point.yval = NaN;
+        }
+      }
+      point.y = DygraphLayout.calcYNormal_(axis, yval, logscale);
+    }
+
+    this.dygraph_.dataHandler_.onLineEvaluated(points, axis, logscale);
+  }
+};
+
+DygraphLayout.prototype._evaluateLineTicks = function() {
+  var i, tick, label, pos;
+  this.xticks = [];
+  for (i = 0; i < this.xTicks_.length; i++) {
+    tick = this.xTicks_[i];
+    label = tick.label;
+    pos = this.dygraph_.toPercentXCoord(tick.v);
+    if ((pos >= 0.0) && (pos < 1.0)) {
+      this.xticks.push([pos, label]);
+    }
+  }
+
+  this.yticks = [];
+  for (i = 0; i < this.yAxes_.length; i++ ) {
+    var axis = this.yAxes_[i];
+    for (var j = 0; j < axis.ticks.length; j++) {
+      tick = axis.ticks[j];
+      label = tick.label;
+      pos = this.dygraph_.toPercentYCoord(tick.v, i);
+      if ((pos > 0.0) && (pos <= 1.0)) {
+        this.yticks.push([i, pos, label]);
+      }
+    }
+  }
+};
+
+DygraphLayout.prototype._evaluateAnnotations = function() {
+  // Add the annotations to the point to which they belong.
+  // Make a map from (setName, xval) to annotation for quick lookups.
+  var i;
+  var annotations = {};
+  for (i = 0; i < this.annotations.length; i++) {
+    var a = this.annotations[i];
+    annotations[a.xval + "," + a.series] = a;
+  }
+
+  this.annotated_points = [];
+
+  // Exit the function early if there are no annotations.
+  if (!this.annotations || !this.annotations.length) {
+    return;
+  }
+
+  // TODO(antrob): loop through annotations not points.
+  for (var setIdx = 0; setIdx < this.points.length; setIdx++) {
+    var points = this.points[setIdx];
+    for (i = 0; i < points.length; i++) {
+      var p = points[i];
+      var k = p.xval + "," + p.name;
+      if (k in annotations) {
+        p.annotation = annotations[k];
+        this.annotated_points.push(p);
+      }
+    }
+  }
+};
+
+/**
+ * Convenience function to remove all the data sets from a graph
+ */
+DygraphLayout.prototype.removeAllDatasets = function() {
+  delete this.points;
+  delete this.setNames;
+  delete this.setPointsLengths;
+  delete this.setPointsOffsets;
+  this.points = [];
+  this.setNames = [];
+  this.setPointsLengths = [];
+  this.setPointsOffsets = [];
+};
+
+return DygraphLayout;
+
+})();
diff --git a/src/dygraph-options-reference.js b/src/dygraph-options-reference.js
new file mode 100644 (file)
index 0000000..ca94dc5
--- /dev/null
@@ -0,0 +1,887 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+// NOTE: in addition to parsing as JS, this snippet is expected to be valid
+// JSON. This assumption cannot be checked in JS, but it will be checked when
+// documentation is generated by the generate-documentation.py script. For the
+// most part, this just means that you should always use double quotes.
+Dygraph.OPTIONS_REFERENCE =  // <JSON>
+{
+  "xValueParser": {
+    "default": "parseFloat() or Date.parse()*",
+    "labels": ["CSV parsing"],
+    "type": "function(str) -> number",
+    "description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
+  },
+  "stackedGraph": {
+    "default": "false",
+    "labels": ["Data Line display"],
+    "type": "boolean",
+    "description": "If set, stack series on top of one another rather than drawing them independently. The first series specified in the input data will wind up on top of the chart and the last will be on bottom. NaN values are drawn as white areas without a line on top, see stackedGraphNaNFill for details."
+  },
+  "stackedGraphNaNFill": {
+    "default": "all",
+    "labels": ["Data Line display"],
+    "type": "string",
+    "description": "Controls handling of NaN values inside a stacked graph. NaN values are interpolated/extended for stacking purposes, but the actual point value remains NaN in the legend display. Valid option values are \"all\" (interpolate internally, repeat leftmost and rightmost value as needed), \"inside\" (interpolate internally only, use zero outside leftmost and rightmost value), and \"none\" (treat NaN as zero everywhere)."
+  },
+  "pointSize": {
+    "default": "1",
+    "labels": ["Data Line display"],
+    "type": "integer",
+    "description": "The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is \"isolated\", i.e. there is a missing point on either side of it. This also controls the size of those dots."
+  },
+  "labelsDivStyles": {
+    "default": "null",
+    "labels": ["Legend"],
+    "type": "{}",
+    "description": "Additional styles to apply to the currently-highlighted points div. For example, { 'fontWeight': 'bold' } will make the labels bold. In general, it is better to use CSS to style the .dygraph-legend class than to use this property."
+  },
+  "drawPoints": {
+    "default": "false",
+    "labels": ["Data Line display"],
+    "type": "boolean",
+    "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart. The small dot can be replaced with a custom rendering by supplying a <a href='#drawPointCallback'>drawPointCallback</a>."
+  },
+  "drawGapEdgePoints": {
+    "default": "false",
+    "labels": ["Data Line display"],
+    "type": "boolean",
+    "description": "Draw points at the edges of gaps in the data. This improves visibility of small data segments or other data irregularities."
+  },
+  "drawPointCallback": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
+    "parameters": [
+      [ "g" , "the reference graph" ],
+      [ "seriesName" , "the name of the series" ],
+      [ "canvasContext" , "the canvas to draw on" ],
+      [ "cx" , "center x coordinate" ],
+      [ "cy" , "center y coordinate" ],
+      [ "color" , "series color" ],
+      [ "pointSize" , "the radius of the image." ],
+      [ "idx" , "the row-index of the point in the data."]
+    ],
+    "description": "Draw a custom item when drawPoints is enabled. Default is a small dot matching the series color. This method should constrain drawing to within pointSize pixels from (cx, cy).  Also see <a href='#drawHighlightPointCallback'>drawHighlightPointCallback</a>"
+  },
+  "height": {
+    "default": "320",
+    "labels": ["Overall display"],
+    "type": "integer",
+    "description": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
+  },
+  "zoomCallback": {
+    "default": "null",
+    "labels": ["Callbacks"],
+    "type": "function(minDate, maxDate, yRanges)",
+    "parameters": [
+      [ "minDate" , "milliseconds since epoch" ],
+      [ "maxDate" , "milliseconds since epoch." ],
+      [ "yRanges" , "is an array of [bottom, top] pairs, one for each y-axis." ]
+    ],
+    "description": "A function to call when the zoom window is changed (either by zooming in or out)."
+  },
+  "pointClickCallback": {
+    "snippet": "function(e, point){<br>&nbsp;&nbsp;alert(point);<br>}",
+    "default": "null",
+    "labels": ["Callbacks", "Interactive Elements"],
+    "type": "function(e, point)",
+    "parameters": [
+      [ "e" , "the event object for the click" ],
+      [ "point" , "the point that was clicked See <a href='#point_properties'>Point properties</a> for details" ]
+    ],
+    "description": "A function to call when a data point is clicked. and the point that was clicked."
+  },
+  "color": {
+    "default": "(see description)",
+    "labels": ["Data Series Colors"],
+    "type": "string",
+    "example": "red",
+    "description": "A per-series color definition. Used in conjunction with, and overrides, the colors option."
+  },
+  "colors": {
+    "default": "(see description)",
+    "labels": ["Data Series Colors"],
+    "type": "array<string>",
+    "example": "['red', '#00FF00']",
+    "description": "List of colors for the data series. These can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\", etc. If not specified, equally-spaced points around a color wheel are used. Overridden by the 'color' option."
+  },
+  "connectSeparatedPoints": {
+    "default": "false",
+    "labels": ["Data Line display"],
+    "type": "boolean",
+    "description": "Usually, when Dygraphs encounters a missing value in a data series, it interprets this as a gap and draws it as such. If, instead, the missing values represents an x-value for which only a different series has data, then you'll want to connect the dots by setting this to true. To explicitly include a gap with this option set, use a value of NaN."
+  },
+  "highlightCallback": {
+    "default": "null",
+    "labels": ["Callbacks"],
+    "type": "function(event, x, points, row, seriesName)",
+    "description": "When set, this callback gets called every time a new point is highlighted.",
+    "parameters": [
+      ["event", "the JavaScript mousemove event"],
+      ["x", "the x-coordinate of the highlighted points"],
+      ["points", "an array of highlighted points: <code>[ {name: 'series', yval: y-value}, &hellip; ]</code>"],
+      ["row", "integer index of the highlighted row in the data table, starting from 0"],
+      ["seriesName", "name of the highlighted series, only present if highlightSeriesOpts is set."]
+    ]
+  },
+  "drawHighlightPointCallback": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
+    "parameters": [
+      [ "g" , "the reference graph" ],
+      [ "seriesName" , "the name of the series" ],
+      [ "canvasContext" , "the canvas to draw on" ],
+      [ "cx" , "center x coordinate" ],
+      [ "cy" , "center y coordinate" ],
+      [ "color" , "series color" ],
+      [ "pointSize" , "the radius of the image." ],
+      [ "idx" , "the row-index of the point in the data."]
+    ],
+    "description": "Draw a custom item when a point is highlighted.  Default is a small dot matching the series color. This method should constrain drawing to within pointSize pixels from (cx, cy) Also see <a href='#drawPointCallback'>drawPointCallback</a>"
+  },
+  "highlightSeriesOpts": {
+    "default": "null",
+    "labels": ["Interactive Elements"],
+    "type": "Object",
+    "description": "When set, the options from this object are applied to the timeseries closest to the mouse pointer for interactive highlighting. See also 'highlightCallback'. Example: highlightSeriesOpts: { strokeWidth: 3 }."
+  },
+  "highlightSeriesBackgroundAlpha": {
+    "default": "0.5",
+    "labels": ["Interactive Elements"],
+    "type": "float",
+    "description": "Fade the background while highlighting series. 1=fully visible background (disable fading), 0=hiddden background (show highlighted series only)."
+  },
+  "includeZero": {
+    "default": "false",
+    "labels": ["Axis display"],
+    "type": "boolean",
+    "description": "Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data"
+  },
+  "rollPeriod": {
+    "default": "1",
+    "labels": ["Error Bars", "Rolling Averages"],
+    "type": "integer &gt;= 1",
+    "description": "Number of days over which to average data. Discussed extensively above."
+  },
+  "unhighlightCallback": {
+    "default": "null",
+    "labels": ["Callbacks"],
+    "type": "function(event)",
+    "parameters": [
+      [ "event" , "the mouse event" ]
+    ],
+    "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph."
+  },
+  "axisTickSize": {
+    "default": "3.0",
+    "labels": ["Axis display"],
+    "type": "number",
+    "description": "The size of the line to display next to each tick mark on x- or y-axes."
+  },
+  "labelsSeparateLines": {
+    "default": "false",
+    "labels": ["Legend"],
+    "type": "boolean",
+    "description": "Put <code>&lt;br/&gt;</code> between lines in the label string. Often used in conjunction with <strong>labelsDiv</strong>."
+  },
+  "valueFormatter": {
+    "default": "Depends on the type of your data.",
+    "labels": ["Legend", "Value display/formatting"],
+    "type": "function(num or millis, opts, seriesName, dygraph, row, col)",
+    "description": "Function to provide a custom display format for the values displayed on mouseover. This does not affect the values that appear on tick marks next to the axes. To format those, see axisLabelFormatter. This is usually set on a <a href='per-axis.html'>per-axis</a> basis. .",
+    "parameters": [
+      ["num_or_millis", "The value to be formatted. This is always a number. For date axes, it's millis since epoch. You can call new Date(millis) to get a Date object."],
+      ["opts", "This is a function you can call to access various options (e.g. opts('labelsKMB')). It returns per-axis values for the option when available."],
+      ["seriesName", "The name of the series from which the point came, e.g. 'X', 'Y', 'A', etc."],
+      ["dygraph", "The dygraph object for which the formatting is being done"],
+      ["row", "The row of the data from which this point comes. g.getValue(row, 0) will return the x-value for this point."],
+      ["col", "The column of the data from which this point comes. g.getValue(row, col) will return the original y-value for this point. This can be used to get the full confidence interval for the point, or access un-rolled values for the point."]
+    ]
+  },
+  "annotationMouseOverHandler": {
+    "default": "null",
+    "labels": ["Annotations"],
+    "type": "function(annotation, point, dygraph, event)",
+    "description": "If provided, this function is called whenever the user mouses over an annotation."
+  },
+  "annotationMouseOutHandler": {
+    "default": "null",
+    "labels": ["Annotations"],
+    "type": "function(annotation, point, dygraph, event)",
+    "parameters": [
+      [ "annotation" , "the annotation left" ],
+      [ "point" , "the point associated with the annotation" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "event" , "the mouse event" ]
+    ],
+    "description": "If provided, this function is called whenever the user mouses out of an annotation."
+  },
+  "annotationClickHandler": {
+    "default": "null",
+    "labels": ["Annotations"],
+    "type": "function(annotation, point, dygraph, event)",
+    "parameters": [
+      [ "annotation" , "the annotation left" ],
+      [ "point" , "the point associated with the annotation" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "event" , "the mouse event" ]
+    ],
+    "description": "If provided, this function is called whenever the user clicks on an annotation."
+  },
+  "annotationDblClickHandler": {
+    "default": "null",
+    "labels": ["Annotations"],
+    "type": "function(annotation, point, dygraph, event)",
+    "parameters": [
+      [ "annotation" , "the annotation left" ],
+      [ "point" , "the point associated with the annotation" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "event" , "the mouse event" ]
+    ],
+    "description": "If provided, this function is called whenever the user double-clicks on an annotation."
+  },
+  "drawCallback": {
+    "default": "null",
+    "labels": ["Callbacks"],
+    "type": "function(dygraph, is_initial)",
+    "parameters": [
+      [ "dygraph" , "The graph being drawn" ],
+      [ "is_initial" , "True if this is the initial draw, false for subsequent draws." ]
+    ],
+    "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning."
+  },
+  "labelsKMG2": {
+    "default": "false",
+    "labels": ["Value display/formatting"],
+    "type": "boolean",
+    "description": "Show k/M/G for kilo/Mega/Giga on y-axis. This is different than <code>labelsKMB</code> in that it uses base 2, not 10."
+  },
+  "delimiter": {
+    "default": ",",
+    "labels": ["CSV parsing"],
+    "type": "string",
+    "description": "The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."
+  },
+  "axisLabelFontSize": {
+    "default": "14",
+    "labels": ["Axis display"],
+    "type": "integer",
+    "description": "Size of the font (in pixels) to use in the axis labels, both x- and y-axis."
+  },
+  "underlayCallback": {
+    "default": "null",
+    "labels": ["Callbacks"],
+    "type": "function(context, area, dygraph)",
+    "parameters": [
+      [ "context" , "the canvas drawing context on which to draw" ],
+      [ "area" , "An object with {x,y,w,h} properties describing the drawing area." ],
+      [ "dygraph" , "the reference graph" ]
+    ],
+    "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
+  },
+  "width": {
+    "default": "480",
+    "labels": ["Overall display"],
+    "type": "integer",
+    "description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
+  },
+  "interactionModel": {
+    "default": "...",
+    "labels": ["Interactive Elements"],
+    "type": "Object",
+    "description": "TODO(konigsberg): document this"
+  },
+  "ticker": {
+    "default": "Dygraph.dateTicker or Dygraph.numericTicks",
+    "labels": ["Axis display"],
+    "type": "function(min, max, pixels, opts, dygraph, vals) -> [{v: ..., label: ...}, ...]",
+    "parameters": [
+      [ "min" , "" ],
+      [ "max" , "" ],
+      [ "pixels" , "" ],
+      [ "opts" , "" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "vals" , "" ]
+    ],
+    "description": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result. See dygraph-tickers.js for an extensive discussion. This is set on a <a href='per-axis.html'>per-axis</a> basis."
+  },
+  "xAxisHeight": {
+    "default": "(null)",
+    "labels": ["Axis display"],
+    "type": "integer",
+    "description": "Height, in pixels, of the x-axis. If not set explicitly, this is computed based on axisLabelFontSize and axisTickSize."
+  },
+  "showLabelsOnHighlight": {
+    "default": "true",
+    "labels": ["Interactive Elements", "Legend"],
+    "type": "boolean",
+    "description": "Whether to show the legend upon mouseover."
+  },
+  "axis": {
+    "default": "(none)",
+    "labels": ["Axis display"],
+    "type": "string",
+    "description": "Set to either 'y1' or 'y2' to assign a series to a y-axis (primary or secondary). Must be set per-series."
+  },
+  "pixelsPerLabel": {
+    "default": "70 (x-axis) or 30 (y-axes)",
+    "labels": ["Axis display", "Grid"],
+    "type": "integer",
+    "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks. This is set on a <a href='per-axis.html'>per-axis</a> basis."
+  },
+  "labelsDiv": {
+    "default": "null",
+    "labels": ["Legend"],
+    "type": "DOM element or string",
+    "example": "<code style='font-size: small'>document.getElementById('foo')</code>or<code>'foo'",
+    "description": "Show data labels in an external div, rather than on the graph.  This value can either be a div element or a div id."
+  },
+  "fractions": {
+    "default": "false",
+    "labels": ["CSV parsing", "Error Bars"],
+    "type": "boolean",
+    "description": "When set, attempt to parse each cell in the CSV file as \"a/b\", where a and b are integers. The ratio will be plotted. This allows computation of Wilson confidence intervals (see below)."
+  },
+  "logscale": {
+    "default": "false",
+    "labels": ["Axis display"],
+    "type": "boolean",
+    "description": "When set for the y-axis or x-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed. Showing log scale with ranges that go below zero will result in an unviewable graph.\n\n Not compatible with showZero. connectSeparatedPoints is ignored. This is ignored for date-based x-axes."
+  },
+  "strokeWidth": {
+    "default": "1.0",
+    "labels": ["Data Line display"],
+    "type": "float",
+    "example": "0.5, 2.0",
+    "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
+  },
+  "strokePattern": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "array<integer>",
+    "example": "[10, 2, 5, 2]",
+    "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array. This is used to create dashed lines."
+  },
+  "strokeBorderWidth": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "float",
+    "example": "1.0",
+    "description": "Draw a border around graph lines to make crossing lines more easily distinguishable. Useful for graphs with many lines."
+  },
+  "strokeBorderColor": {
+    "default": "white",
+    "labels": ["Data Line display"],
+    "type": "string",
+    "example": "red, #ccffdd",
+    "description": "Color for the line border used if strokeBorderWidth is set."
+  },
+  "wilsonInterval": {
+    "default": "true",
+    "labels": ["Error Bars"],
+    "type": "boolean",
+    "description": "Use in conjunction with the \"fractions\" option. Instead of plotting +/- N standard deviations, dygraphs will compute a Wilson confidence interval and plot that. This has more reasonable behavior for ratios close to 0 or 1."
+  },
+  "fillGraph": {
+    "default": "false",
+    "labels": ["Data Line display"],
+    "type": "boolean",
+    "description": "Should the area underneath the graph be filled? This option is not compatible with error bars. This may be set on a <a href='per-axis.html'>per-series</a> basis."
+  },
+  "highlightCircleSize": {
+    "default": "3",
+    "labels": ["Interactive Elements"],
+    "type": "integer",
+    "description": "The size in pixels of the dot drawn over highlighted points."
+  },
+  "gridLineColor": {
+    "default": "rgb(128,128,128)",
+    "labels": ["Grid"],
+    "type": "red, blue",
+    "description": "The color of the gridlines. This may be set on a per-axis basis to define each axis' grid separately."
+  },
+  "gridLinePattern": {
+    "default": "null",
+    "labels": ["Grid"],
+    "type": "array<integer>",
+    "example": "[10, 2, 5, 2]",
+    "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array. This is used to create dashed gridlines."
+  },
+  "visibility": {
+    "default": "[true, true, ...]",
+    "labels": ["Data Line display"],
+    "type": "Array of booleans",
+    "description": "Which series should initially be visible? Once the Dygraph has been constructed, you can access and modify the visibility of each series using the <code>visibility</code> and <code>setVisibility</code> methods."
+  },
+  "valueRange": {
+    "default": "Full range of the input is shown",
+    "labels": ["Axis display"],
+    "type": "Array of two numbers",
+    "example": "[10, 110]",
+    "description": "Explicitly set the vertical range of the graph to [low, high]. This may be set on a per-axis basis to define each y-axis separately. If either limit is unspecified, it will be calculated automatically (e.g. [null, 30] to automatically calculate just the lower bound)"
+  },
+  "labelsDivWidth": {
+    "default": "250",
+    "labels": ["Legend"],
+    "type": "integer",
+    "description": "Width (in pixels) of the div which shows information on the currently-highlighted points."
+  },
+  "colorSaturation": {
+    "default": "1.0",
+    "labels": ["Data Series Colors"],
+    "type": "float (0.0 - 1.0)",
+    "description": "If <strong>colors</strong> is not specified, saturation of the automatically-generated data series colors."
+  },
+  "hideOverlayOnMouseOut": {
+    "default": "true",
+    "labels": ["Interactive Elements", "Legend"],
+    "type": "boolean",
+    "description": "Whether to hide the legend when the mouse leaves the chart area."
+  },
+  "legend": {
+    "default": "onmouseover",
+    "labels": ["Legend"],
+    "type": "string",
+    "description": "When to display the legend. By default, it only appears when a user mouses over the chart. Set it to \"always\" to always display a legend of some sort. When set to \"follow\", legend follows highlighted points."
+  },
+  "labelsShowZeroValues": {
+    "default": "true",
+    "labels": ["Legend"],
+    "type": "boolean",
+    "description": "Show zero value labels in the labelsDiv."
+  },
+  "stepPlot": {
+    "default": "false",
+    "labels": ["Data Line display"],
+    "type": "boolean",
+    "description": "When set, display the graph as a step plot instead of a line plot. This option may either be set for the whole graph or for single series."
+  },
+  "labelsUTC": {
+    "default": "false",
+    "labels": ["Value display/formatting", "Axis display"],
+    "type": "boolean",
+    "description": "Show date/time labels according to UTC (instead of local time)."
+  },
+  "labelsKMB": {
+    "default": "false",
+    "labels": ["Value display/formatting"],
+    "type": "boolean",
+    "description": "Show K/M/B for thousands/millions/billions on y-axis."
+  },
+  "rightGap": {
+    "default": "5",
+    "labels": ["Overall display"],
+    "type": "integer",
+    "description": "Number of pixels to leave blank at the right edge of the Dygraph. This makes it easier to highlight the right-most data point."
+  },
+  "avoidMinZero": {
+    "default": "false",
+    "labels": ["Deprecated"],
+    "type": "boolean",
+    "description": "Deprecated, please use yRangePad instead. When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
+  },
+  "drawAxesAtZero": {
+    "default": "false",
+    "labels": ["Axis display"],
+    "type": "boolean",
+    "description": "When set, draw the X axis at the Y=0 position and the Y axis at the X=0 position if those positions are inside the graph's visible area. Otherwise, draw the axes at the bottom or left graph edge as usual."
+  },
+  "xRangePad": {
+    "default": "0",
+    "labels": ["Axis display"],
+    "type": "float",
+    "description": "Add the specified amount of extra space (in pixels) around the X-axis value range to ensure points at the edges remain visible."
+  },
+  "yRangePad": {
+    "default": "null",
+    "labels": ["Axis display"],
+    "type": "float",
+    "description": "If set, add the specified amount of extra space (in pixels) around the Y-axis value range to ensure points at the edges remain visible. If unset, use the traditional Y padding algorithm."
+  },
+  "axisLabelFormatter": {
+    "default": "Depends on the data type",
+    "labels": ["Axis display"],
+    "type": "function(number or Date, granularity, opts, dygraph)",
+    "parameters": [
+      [ "number or date" , "Either a number (for a numeric axis) or a Date object (for a date axis)" ],
+      [ "granularity" , "specifies how fine-grained the axis is. For date axes, this is a reference to the time granularity enumeration, defined in dygraph-tickers.js, e.g. Dygraph.WEEKLY." ],
+      [ "opts" , "a function which provides access to various options on the dygraph, e.g. opts('labelsKMB')." ],
+      [ "dygraph" , "the referenced graph" ]
+    ],
+    "description": "Function to call to format the tick values that appear along an axis. This is usually set on a <a href='per-axis.html'>per-axis</a> basis."
+  },
+  "clickCallback": {
+    "snippet": "function(e, date_millis){<br>&nbsp;&nbsp;alert(new Date(date_millis));<br>}",
+    "default": "null",
+    "labels": ["Callbacks"],
+    "type": "function(e, x, points)",
+    "parameters": [
+      [ "e" , "The event object for the click" ],
+      [ "x" , "The x value that was clicked (for dates, this is milliseconds since epoch)" ],
+      [ "points" , "The closest points along that date. See <a href='#point_properties'>Point properties</a> for details." ]
+    ],
+    "description": "A function to call when the canvas is clicked."
+  },
+  "labels": {
+    "default": "[\"X\", \"Y1\", \"Y2\", ...]*",
+    "labels": ["Legend"],
+    "type": "array<string>",
+    "description": "A name for each data series, including the independent (X) series. For CSV files and DataTable objections, this is determined by context. For raw data, this must be specified. If it is not, default values are supplied and a warning is logged."
+  },
+  "dateWindow": {
+    "default": "Full range of the input is shown",
+    "labels": ["Axis display"],
+    "type": "Array of two numbers",
+    "example": "[<br>&nbsp;&nbsp;Date.parse('2006-01-01'),<br>&nbsp;&nbsp;(new Date()).valueOf()<br>]",
+    "description": "Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers."
+  },
+  "showRoller": {
+    "default": "false",
+    "labels": ["Interactive Elements", "Rolling Averages"],
+    "type": "boolean",
+    "description": "If the rolling average period text box should be shown."
+  },
+  "sigma": {
+    "default": "2.0",
+    "labels": ["Error Bars"],
+    "type": "float",
+    "description": "When errorBars is set, shade this many standard deviations above/below each point."
+  },
+  "customBars": {
+    "default": "false",
+    "labels": ["CSV parsing", "Error Bars"],
+    "type": "boolean",
+    "description": "When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle."
+  },
+  "colorValue": {
+    "default": "1.0",
+    "labels": ["Data Series Colors"],
+    "type": "float (0.0 - 1.0)",
+    "description": "If colors is not specified, value of the data series colors, as in hue/saturation/value. (0.0-1.0, default 0.5)"
+  },
+  "errorBars": {
+    "default": "false",
+    "labels": ["CSV parsing", "Error Bars"],
+    "type": "boolean",
+    "description": "Does the data contain standard deviations? Setting this to true alters the input format (see above)."
+  },
+  "displayAnnotations": {
+    "default": "false",
+    "labels": ["Annotations"],
+    "type": "boolean",
+    "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
+  },
+  "panEdgeFraction": {
+    "default": "null",
+    "labels": ["Axis display", "Interactive Elements"],
+    "type": "float",
+    "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
+  },
+  "title": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."
+  },
+  "titleHeight": {
+    "default": "18",
+    "labels": ["Chart labels"],
+    "type": "integer",
+    "description": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."
+  },
+  "xlabel": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."
+  },
+  "xLabelHeight": {
+    "labels": ["Chart labels"],
+    "type": "integer",
+    "default": "18",
+    "description": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."
+  },
+  "ylabel": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option."
+  },
+  "y2label": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display to the right of the chart's secondary y-axis. This label is only displayed if a secondary y-axis is present. See <a href='http://dygraphs.com/tests/two-axes.html'>this test</a> for an example of how to do this. The comments for the 'ylabel' option generally apply here as well. This label gets a 'dygraph-y2label' instead of a 'dygraph-ylabel' class."
+  },
+  "yLabelWidth": {
+    "labels": ["Chart labels"],
+    "type": "integer",
+    "default": "18",
+    "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
+  },
+  "isZoomedIgnoreProgrammaticZoom" : {
+    "default": "false",
+    "labels": ["Zooming"],
+    "type": "boolean",
+    "description" : "When this option is passed to updateOptions() along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
+  },
+  "drawGrid": {
+    "default": "true for x and y, false for y2",
+    "labels": ["Grid"],
+    "type": "boolean",
+    "description" : "Whether to display gridlines in the chart. This may be set on a per-axis basis to define the visibility of each axis' grid separately."
+  },
+  "independentTicks": {
+    "default": "true for y, false for y2",
+    "labels": ["Axis display", "Grid"],
+    "type": "boolean",
+    "description" : "Only valid for y and y2, has no effect on x: This option defines whether the y axes should align their ticks or if they should be independent. Possible combinations: 1.) y=true, y2=false (default): y is the primary axis and the y2 ticks are aligned to the the ones of y. (only 1 grid) 2.) y=false, y2=true: y2 is the primary axis and the y ticks are aligned to the the ones of y2. (only 1 grid) 3.) y=true, y2=true: Both axis are independent and have their own ticks. (2 grids) 4.) y=false, y2=false: Invalid configuration causes an error."
+  },
+  "drawAxis": {
+    "default": "true for x and y, false for y2",
+    "labels": ["Axis display"],
+    "type": "boolean",
+    "description" : "Whether to draw the specified axis. This may be set on a per-axis basis to define the visibility of each axis separately. Setting this to false also prevents axis ticks from being drawn and reclaims the space for the chart grid/lines."
+  },
+  "gridLineWidth": {
+    "default": "0.3",
+    "labels": ["Grid"],
+    "type": "float",
+    "description" : "Thickness (in pixels) of the gridlines drawn under the chart. The vertical/horizontal gridlines can be turned off entirely by using the drawGrid option. This may be set on a per-axis basis to define each axis' grid separately."
+  },
+  "axisLineWidth": {
+    "default": "0.3",
+    "labels": ["Axis display"],
+    "type": "float",
+    "description" : "Thickness (in pixels) of the x- and y-axis lines."
+  },
+  "axisLineColor": {
+    "default": "black",
+    "labels": ["Axis display"],
+    "type": "string",
+    "description" : "Color of the x- and y-axis lines. Accepts any value which the HTML canvas strokeStyle attribute understands, e.g. 'black' or 'rgb(0, 100, 255)'."
+  },
+  "fillAlpha": {
+    "default": "0.15",
+    "labels": ["Error Bars", "Data Series Colors"],
+    "type": "float (0.0 - 1.0)",
+    "description" : "Error bars (or custom bars) for each series are drawn in the same color as the series, but with partial transparency. This sets the transparency. A value of 0.0 means that the error bars will not be drawn, whereas a value of 1.0 means that the error bars will be as dark as the line for the series itself. This can be used to produce chart lines whose thickness varies at each point."
+  },
+  "axisLabelColor": {
+    "default": "black",
+    "labels": ["Axis display"],
+    "type": "string",
+    "description" : "Color for x- and y-axis labels. This is a CSS color string."
+  },
+  "axisLabelWidth": {
+    "default": "50 (y-axis), 60 (x-axis)",
+    "labels": ["Axis display", "Chart labels"],
+    "type": "integer",
+    "description" : "Width (in pixels) of the containing divs for x- and y-axis labels. For the y-axis, this also controls the width of the y-axis. Note that for the x-axis, this is independent from pixelsPerLabel, which controls the spacing between labels."
+  },
+  "sigFigs" : {
+    "default": "null",
+    "labels": ["Value display/formatting"],
+    "type": "integer",
+    "description": "By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3."
+  },
+  "digitsAfterDecimal" : {
+    "default": "2",
+    "labels": ["Value display/formatting"],
+    "type": "integer",
+    "description": "Unless it's run in scientific mode (see the <code>sigFigs</code> option), dygraphs displays numbers with <code>digitsAfterDecimal</code> digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation."
+  },
+  "maxNumberWidth" : {
+    "default": "6",
+    "labels": ["Value display/formatting"],
+    "type": "integer",
+    "description": "When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than <code>maxNumberWidth</code> digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30."
+  },
+  "file": {
+    "default": "(set when constructed)",
+    "labels": ["Data"],
+    "type": "string (URL of CSV or CSV), GViz DataTable or 2D Array",
+    "description": "Sets the data being displayed in the chart. This can only be set when calling updateOptions; it cannot be set from the constructor. For a full description of valid data formats, see the <a href='http://dygraphs.com/data.html'>Data Formats</a> page."
+  },
+  "timingName": {
+    "default": "null",
+    "labels": [ "Debugging" ],
+    "type": "string",
+    "description": "Set this option to log timing information. The value of the option will be logged along with the timimg, so that you can distinguish multiple dygraphs on the same page."
+  },
+  "showRangeSelector": {
+    "default": "false",
+    "labels": ["Range Selector"],
+    "type": "boolean",
+    "description": "Show or hide the range selector widget."
+  },
+  "rangeSelectorHeight": {
+    "default": "40",
+    "labels": ["Range Selector"],
+    "type": "integer",
+    "description": "Height, in pixels, of the range selector widget. This option can only be specified at Dygraph creation time."
+  },
+  "rangeSelectorPlotStrokeColor": {
+    "default": "#808FAB",
+    "labels": ["Range Selector"],
+    "type": "string",
+    "description": "The range selector mini plot stroke color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off stroke."
+  },
+  "rangeSelectorPlotFillColor": {
+    "default": "#A7B1C4",
+    "labels": ["Range Selector"],
+    "type": "string",
+    "description": "The range selector mini plot fill color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off fill."
+  },
+  "rangeSelectorPlotFillGradientColor": {
+    "default": "white",
+    "labels": ["Range Selector"],
+    "type": "string",
+    "description": "The top color for the range selector mini plot fill color gradient. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"rgba(255,100,200,42)\" or \"yellow\". You can also specify null or \"\" to disable the gradient and fill with one single color."
+  },
+  "rangeSelectorBackgroundStrokeColor": {
+    "default": "gray",
+    "labels": ["Range Selector"],
+    "type": "string",
+    "description": "The color of the lines below and on both sides of the range selector mini plot. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\"."
+  },
+  "rangeSelectorBackgroundLineWidth": {
+    "default": "1",
+    "labels": ["Range Selector"],
+    "type": "float",
+    "description": "The width of the lines below and on both sides of the range selector mini plot."
+  },
+  "rangeSelectorPlotLineWidth": {
+    "default": "1.5",
+    "labels": ["Range Selector"],
+    "type": "float",
+    "description": "The width of the range selector mini plot line."
+  },
+  "rangeSelectorForegroundStrokeColor": {
+    "default": "black",
+    "labels": ["Range Selector"],
+    "type": "string",
+    "description": "The color of the lines in the interactive layer of the range selector. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\"."
+  },
+  "rangeSelectorForegroundLineWidth": {
+    "default": "1",
+    "labels": ["Range Selector"],
+    "type": "float",
+    "description": "The width the lines in the interactive layer of the range selector."
+  },
+  "rangeSelectorAlpha": {
+    "default": "0.6",
+    "labels": ["Range Selector"],
+    "type": "float (0.0 - 1.0)",
+    "description": "The transparency of the veil that is drawn over the unselected portions of the range selector mini plot. A value of 0 represents full transparency and the unselected portions of the mini plot will appear as normal. A value of 1 represents full opacity and the unselected portions of the mini plot will be hidden."
+  },
+  "showInRangeSelector": {
+    "default": "null",
+    "labels": ["Range Selector"],
+    "type": "boolean",
+    "description": "Mark this series for inclusion in the range selector. The mini plot curve will be an average of all such series. If this is not specified for any series, the default behavior is to average all the series. Setting it for one series will result in that series being charted alone in the range selector."
+  },
+  "animatedZooms": {
+    "default": "false",
+    "labels": ["Interactive Elements"],
+    "type": "boolean",
+    "description": "Set this option to animate the transition between zoom windows. Applies to programmatic and interactive zooms. Note that if you also set a drawCallback, it will be called several times on each zoom. If you set a zoomCallback, it will only be called after the animation is complete."
+  },
+  "plotter": {
+    "default": "[DygraphCanvasRenderer.Plotters.fillPlotter, DygraphCanvasRenderer.Plotters.errorPlotter, DygraphCanvasRenderer.Plotters.linePlotter]",
+    "labels": ["Data Line display"],
+    "type": "array or function",
+    "description": "A function (or array of functions) which plot each data series on the chart. TODO(danvk): more details! May be set per-series."
+  },
+  "axes": {
+    "default": "null",
+    "labels": ["Configuration"],
+    "type": "Object",
+    "description": "Defines per-axis options. Valid keys are 'x', 'y' and 'y2'. Only some options may be set on a per-axis basis. If an option may be set in this way, it will be noted on this page. See also documentation on <a href='http://dygraphs.com/per-axis.html'>per-series and per-axis options</a>."
+  },
+  "series": {
+    "default": "null",
+    "labels": ["Series"],
+    "type": "Object",
+    "description": "Defines per-series options. Its keys match the y-axis label names, and the values are dictionaries themselves that contain options specific to that series."
+  },
+  "plugins": {
+    "default": "[]",
+    "labels": ["Configuration"],
+    "type": "Array<plugin>",
+    "description": "Defines per-graph plugins. Useful for per-graph customization"
+  },
+  "dataHandler": {
+    "default": "(depends on data)",
+    "labels": ["Data"],
+    "type": "Dygraph.DataHandler",
+    "description": "Custom DataHandler. This is an advanced customization. See http://bit.ly/151E7Aq."
+  }
+}
+;  // </JSON>
+// NOTE: in addition to parsing as JS, this snippet is expected to be valid
+// JSON. This assumption cannot be checked in JS, but it will be checked when
+// documentation is generated by the generate-documentation.py script. For the
+// most part, this just means that you should always use double quotes.
+
+// Do a quick sanity check on the options reference.
+(function() {
+  "use strict";
+  var warn = function(msg) { if (window.console) window.console.warn(msg); };
+  var flds = ['type', 'default', 'description'];
+  var valid_cats = [
+   'Annotations',
+   'Axis display',
+   'Chart labels',
+   'CSV parsing',
+   'Callbacks',
+   'Data',
+   'Data Line display',
+   'Data Series Colors',
+   'Error Bars',
+   'Grid',
+   'Interactive Elements',
+   'Range Selector',
+   'Legend',
+   'Overall display',
+   'Rolling Averages',
+   'Series',
+   'Value display/formatting',
+   'Zooming',
+   'Debugging',
+   'Configuration',
+   'Deprecated'
+  ];
+  var i;
+  var cats = {};
+  for (i = 0; i < valid_cats.length; i++) cats[valid_cats[i]] = true;
+
+  for (var k in Dygraph.OPTIONS_REFERENCE) {
+    if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(k)) continue;
+    var op = Dygraph.OPTIONS_REFERENCE[k];
+    for (i = 0; i < flds.length; i++) {
+      if (!op.hasOwnProperty(flds[i])) {
+        warn('Option ' + k + ' missing "' + flds[i] + '" property');
+      } else if (typeof(op[flds[i]]) != 'string') {
+        warn(k + '.' + flds[i] + ' must be of type string');
+      }
+    }
+    var labels = op.labels;
+    if (typeof(labels) !== 'object') {
+      warn('Option "' + k + '" is missing a "labels": [...] option');
+    } else {
+      for (i = 0; i < labels.length; i++) {
+        if (!cats.hasOwnProperty(labels[i])) {
+          warn('Option "' + k + '" has label "' + labels[i] +
+               '", which is invalid.');
+        }
+      }
+    }
+  }
+})();
diff --git a/src/dygraph-options.js b/src/dygraph-options.js
new file mode 100644 (file)
index 0000000..d21ccc1
--- /dev/null
@@ -0,0 +1,405 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview DygraphOptions is responsible for parsing and returning information about options.
+ *
+ * Still tightly coupled to Dygraphs, we could remove some of that, you know.
+ */
+
+var DygraphOptions = (function() {
+/*jshint strict:false */
+
+// For "production" code, this gets set to false by uglifyjs.
+// Need to define it outside of "use strict", hence the nested IIFEs.
+if (typeof(DEBUG) === 'undefined') DEBUG=true;
+
+return (function() {
+
+// TODO: remove this jshint directive & fix the warnings.
+/*jshint sub:true */
+/*global Dygraph:false */
+"use strict";
+
+/*
+ * Interesting member variables: (REMOVING THIS LIST AS I CLOSURIZE)
+ * global_ - global attributes (common among all graphs, AIUI)
+ * user - attributes set by the user
+ * series_ - { seriesName -> { idx, yAxis, options }}
+ */
+
+/**
+ * This parses attributes into an object that can be easily queried.
+ *
+ * It doesn't necessarily mean that all options are available, specifically
+ * if labels are not yet available, since those drive details of the per-series
+ * and per-axis options.
+ *
+ * @param {Dygraph} dygraph The chart to which these options belong.
+ * @constructor
+ */
+var DygraphOptions = function(dygraph) {
+  /**
+   * The dygraph.
+   * @type {!Dygraph}
+   */
+  this.dygraph_ = dygraph;
+
+  /**
+   * Array of axis index to { series : [ series names ] , options : { axis-specific options. }
+   * @type {Array.<{series : Array.<string>, options : Object}>} @private
+   */
+  this.yAxes_ = [];
+
+  /**
+   * Contains x-axis specific options, which are stored in the options key.
+   * This matches the yAxes_ object structure (by being a dictionary with an
+   * options element) allowing for shared code.
+   * @type {options: Object} @private
+   */
+  this.xAxis_ = {};
+  this.series_ = {};
+
+  // Once these two objects are initialized, you can call get();
+  this.global_ = this.dygraph_.attrs_;
+  this.user_ = this.dygraph_.user_attrs_ || {};
+
+  /**
+   * A list of series in columnar order.
+   * @type {Array.<string>}
+   */
+  this.labels_ = [];
+
+  this.highlightSeries_ = this.get("highlightSeriesOpts") || {};
+  this.reparseSeries();
+};
+
+/**
+ * Not optimal, but does the trick when you're only using two axes.
+ * If we move to more axes, this can just become a function.
+ *
+ * @type {Object.<number>}
+ * @private
+ */
+DygraphOptions.AXIS_STRING_MAPPINGS_ = {
+  'y' : 0,
+  'Y' : 0,
+  'y1' : 0,
+  'Y1' : 0,
+  'y2' : 1,
+  'Y2' : 1
+};
+
+/**
+ * @param {string|number} axis
+ * @private
+ */
+DygraphOptions.axisToIndex_ = function(axis) {
+  if (typeof(axis) == "string") {
+    if (DygraphOptions.AXIS_STRING_MAPPINGS_.hasOwnProperty(axis)) {
+      return DygraphOptions.AXIS_STRING_MAPPINGS_[axis];
+    }
+    throw "Unknown axis : " + axis;
+  }
+  if (typeof(axis) == "number") {
+    if (axis === 0 || axis === 1) {
+      return axis;
+    }
+    throw "Dygraphs only supports two y-axes, indexed from 0-1.";
+  }
+  if (axis) {
+    throw "Unknown axis : " + axis;
+  }
+  // No axis specification means axis 0.
+  return 0;
+};
+
+/**
+ * Reparses options that are all related to series. This typically occurs when
+ * options are either updated, or source data has been made available.
+ *
+ * TODO(konigsberg): The method name is kind of weak; fix.
+ */
+DygraphOptions.prototype.reparseSeries = function() {
+  var labels = this.get("labels");
+  if (!labels) {
+    return; // -- can't do more for now, will parse after getting the labels.
+  }
+
+  this.labels_ = labels.slice(1);
+
+  this.yAxes_ = [ { series : [], options : {}} ]; // Always one axis at least.
+  this.xAxis_ = { options : {} };
+  this.series_ = {};
+
+  // Series are specified in the series element:
+  //
+  // {
+  //   labels: [ "X", "foo", "bar" ],
+  //   pointSize: 3,
+  //   series : {
+  //     foo : {}, // options for foo
+  //     bar : {} // options for bar
+  //   }
+  // }
+  //
+  // So, if series is found, it's expected to contain per-series data, otherwise set a
+  // default.
+  var seriesDict = this.user_.series || {};
+  for (var idx = 0; idx < this.labels_.length; idx++) {
+    var seriesName = this.labels_[idx];
+    var optionsForSeries = seriesDict[seriesName] || {};
+    var yAxis = DygraphOptions.axisToIndex_(optionsForSeries["axis"]);
+
+    this.series_[seriesName] = {
+      idx: idx,
+      yAxis: yAxis,
+      options : optionsForSeries };
+
+    if (!this.yAxes_[yAxis]) {
+      this.yAxes_[yAxis] =  { series : [ seriesName ], options : {} };
+    } else {
+      this.yAxes_[yAxis].series.push(seriesName);
+    }
+  }
+
+  var axis_opts = this.user_["axes"] || {};
+  Dygraph.update(this.yAxes_[0].options, axis_opts["y"] || {});
+  if (this.yAxes_.length > 1) {
+    Dygraph.update(this.yAxes_[1].options, axis_opts["y2"] || {});
+  }
+  Dygraph.update(this.xAxis_.options, axis_opts["x"] || {});
+
+  if (DEBUG) this.validateOptions_();
+};
+
+/**
+ * Get a global value.
+ *
+ * @param {string} name the name of the option.
+ */
+DygraphOptions.prototype.get = function(name) {
+  var result = this.getGlobalUser_(name);
+  if (result !== null) {
+    return result;
+  }
+  return this.getGlobalDefault_(name);
+};
+
+DygraphOptions.prototype.getGlobalUser_ = function(name) {
+  if (this.user_.hasOwnProperty(name)) {
+    return this.user_[name];
+  }
+  return null;
+};
+
+DygraphOptions.prototype.getGlobalDefault_ = function(name) {
+  if (this.global_.hasOwnProperty(name)) {
+    return this.global_[name];
+  }
+  if (Dygraph.DEFAULT_ATTRS.hasOwnProperty(name)) {
+    return Dygraph.DEFAULT_ATTRS[name];
+  }
+  return null;
+};
+
+/**
+ * Get a value for a specific axis. If there is no specific value for the axis,
+ * the global value is returned.
+ *
+ * @param {string} name the name of the option.
+ * @param {string|number} axis the axis to search. Can be the string representation
+ * ("y", "y2") or the axis number (0, 1).
+ */
+DygraphOptions.prototype.getForAxis = function(name, axis) {
+  var axisIdx;
+  var axisString;
+
+  // Since axis can be a number or a string, straighten everything out here.
+  if (typeof(axis) == 'number') {
+    axisIdx = axis;
+    axisString = axisIdx === 0 ? "y" : "y2";
+  } else {
+    if (axis == "y1") { axis = "y"; } // Standardize on 'y'. Is this bad? I think so.
+    if (axis == "y") {
+      axisIdx = 0;
+    } else if (axis == "y2") {
+      axisIdx = 1;
+    } else if (axis == "x") {
+      axisIdx = -1; // simply a placeholder for below.
+    } else {
+      throw "Unknown axis " + axis;
+    }
+    axisString = axis;
+  }
+
+  var userAxis = (axisIdx == -1) ? this.xAxis_ : this.yAxes_[axisIdx];
+
+  // Search the user-specified axis option first.
+  if (userAxis) { // This condition could be removed if we always set up this.yAxes_ for y2.
+    var axisOptions = userAxis.options;
+    if (axisOptions.hasOwnProperty(name)) {
+      return axisOptions[name];
+    }
+  }
+
+  // User-specified global options second.
+  // But, hack, ignore globally-specified 'logscale' for 'x' axis declaration.
+  if (!(axis === 'x' && name === 'logscale')) {
+    var result = this.getGlobalUser_(name);
+    if (result !== null) {
+      return result;
+    }
+  }
+  // Default axis options third.
+  var defaultAxisOptions = Dygraph.DEFAULT_ATTRS.axes[axisString];
+  if (defaultAxisOptions.hasOwnProperty(name)) {
+    return defaultAxisOptions[name];
+  }
+
+  // Default global options last.
+  return this.getGlobalDefault_(name);
+};
+
+/**
+ * Get a value for a specific series. If there is no specific value for the series,
+ * the value for the axis is returned (and afterwards, the global value.)
+ *
+ * @param {string} name the name of the option.
+ * @param {string} series the series to search.
+ */
+DygraphOptions.prototype.getForSeries = function(name, series) {
+  // Honors indexes as series.
+  if (series === this.dygraph_.getHighlightSeries()) {
+    if (this.highlightSeries_.hasOwnProperty(name)) {
+      return this.highlightSeries_[name];
+    }
+  }
+
+  if (!this.series_.hasOwnProperty(series)) {
+    throw "Unknown series: " + series;
+  }
+
+  var seriesObj = this.series_[series];
+  var seriesOptions = seriesObj["options"];
+  if (seriesOptions.hasOwnProperty(name)) {
+    return seriesOptions[name];
+  }
+
+  return this.getForAxis(name, seriesObj["yAxis"]);
+};
+
+/**
+ * Returns the number of y-axes on the chart.
+ * @return {number} the number of axes.
+ */
+DygraphOptions.prototype.numAxes = function() {
+  return this.yAxes_.length;
+};
+
+/**
+ * Return the y-axis for a given series, specified by name.
+ */
+DygraphOptions.prototype.axisForSeries = function(series) {
+  return this.series_[series].yAxis;
+};
+
+/**
+ * Returns the options for the specified axis.
+ */
+// TODO(konigsberg): this is y-axis specific. Support the x axis.
+DygraphOptions.prototype.axisOptions = function(yAxis) {
+  return this.yAxes_[yAxis].options;
+};
+
+/**
+ * Return the series associated with an axis.
+ */
+DygraphOptions.prototype.seriesForAxis = function(yAxis) {
+  return this.yAxes_[yAxis].series;
+};
+
+/**
+ * Return the list of all series, in their columnar order.
+ */
+DygraphOptions.prototype.seriesNames = function() {
+  return this.labels_;
+};
+
+if (DEBUG) {
+
+/**
+ * Validate all options.
+ * This requires Dygraph.OPTIONS_REFERENCE, which is only available in debug builds.
+ * @private
+ */
+DygraphOptions.prototype.validateOptions_ = function() {
+  if (typeof Dygraph.OPTIONS_REFERENCE === 'undefined') {
+    throw 'Called validateOptions_ in prod build.';
+  }
+
+  var that = this;
+  var validateOption = function(optionName) {
+    if (!Dygraph.OPTIONS_REFERENCE[optionName]) {
+      that.warnInvalidOption_(optionName);
+    }
+  };
+
+  var optionsDicts = [this.xAxis_.options,
+                      this.yAxes_[0].options,
+                      this.yAxes_[1] && this.yAxes_[1].options,
+                      this.global_,
+                      this.user_,
+                      this.highlightSeries_];
+  var names = this.seriesNames();
+  for (var i = 0; i < names.length; i++) {
+    var name = names[i];
+    if (this.series_.hasOwnProperty(name)) {
+      optionsDicts.push(this.series_[name].options);
+    }
+  }
+  for (var i = 0; i < optionsDicts.length; i++) {
+    var dict = optionsDicts[i];
+    if (!dict) continue;
+    for (var optionName in dict) {
+      if (dict.hasOwnProperty(optionName)) {
+        validateOption(optionName);
+      }
+    }
+  }
+};
+
+var WARNINGS = {};  // Only show any particular warning once.
+
+/**
+ * Logs a warning about invalid options.
+ * TODO: make this throw for testing
+ * @private
+ */
+DygraphOptions.prototype.warnInvalidOption_ = function(optionName) {
+  if (!WARNINGS[optionName]) {
+    WARNINGS[optionName] = true;
+    var isSeries = (this.labels_.indexOf(optionName) >= 0);
+    if (isSeries) {
+      console.warn('Use new-style per-series options (saw ' + optionName + ' as top-level options key). See http://bit.ly/1tceaJs');
+    } else {
+      console.warn('Unknown option ' + optionName + ' (full list of options at dygraphs.com/options.html');
+      throw "invalid option " + optionName;
+    }
+  }
+};
+
+// Reset list of previously-shown warnings. Used for testing.
+DygraphOptions.resetWarnings_ = function() {
+  WARNINGS = {};
+};
+
+}
+
+return DygraphOptions;
+
+})();
+})();
diff --git a/src/dygraph-plugin-base.js b/src/dygraph-plugin-base.js
new file mode 100644 (file)
index 0000000..7f758a9
--- /dev/null
@@ -0,0 +1,4 @@
+/*global Dygraph:false */
+
+// Namespace for plugins. Load this before plugins/*.js files.
+Dygraph.Plugins = {};
diff --git a/src/dygraph-plugin-install.js b/src/dygraph-plugin-install.js
new file mode 100644 (file)
index 0000000..806a927
--- /dev/null
@@ -0,0 +1,19 @@
+/*global Dygraph:false */
+
+// This file defines the ordering of the plugins.
+//
+// The ordering is from most-general to most-specific.
+// This means that, in an event cascade, plugins which have registered for that
+// event will be called in reverse order.
+//
+// This is most relevant for plugins which register a layout event, e.g.
+// Axes, Legend and ChartLabels.
+
+Dygraph.PLUGINS.push(
+  Dygraph.Plugins.Legend,
+  Dygraph.Plugins.Axes,
+  Dygraph.Plugins.RangeSelector, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
+  Dygraph.Plugins.ChartLabels,
+  Dygraph.Plugins.Annotations,
+  Dygraph.Plugins.Grid
+);
diff --git a/src/dygraph-tickers.js b/src/dygraph-tickers.js
new file mode 100644 (file)
index 0000000..0f6b1ab
--- /dev/null
@@ -0,0 +1,456 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview Description of this file.
+ * @author danvk@google.com (Dan Vanderkam)
+ *
+ * A ticker is a function with the following interface:
+ *
+ * function(a, b, pixels, options_view, dygraph, forced_values);
+ * -> [ { v: tick1_v, label: tick1_label[, label_v: label_v1] },
+ *      { v: tick2_v, label: tick2_label[, label_v: label_v2] },
+ *      ...
+ *    ]
+ *
+ * The returned value is called a "tick list".
+ *
+ * Arguments
+ * ---------
+ *
+ * [a, b] is the range of the axis for which ticks are being generated. For a
+ * numeric axis, these will simply be numbers. For a date axis, these will be
+ * millis since epoch (convertable to Date objects using "new Date(a)" and "new
+ * Date(b)").
+ *
+ * opts provides access to chart- and axis-specific options. It can be used to
+ * access number/date formatting code/options, check for a log scale, etc.
+ *
+ * pixels is the length of the axis in pixels. opts('pixelsPerLabel') is the
+ * minimum amount of space to be allotted to each label. For instance, if
+ * pixels=400 and opts('pixelsPerLabel')=40 then the ticker should return
+ * between zero and ten (400/40) ticks.
+ *
+ * dygraph is the Dygraph object for which an axis is being constructed.
+ *
+ * forced_values is used for secondary y-axes. The tick positions are typically
+ * set by the primary y-axis, so the secondary y-axis has no choice in where to
+ * put these. It simply has to generate labels for these data values.
+ *
+ * Tick lists
+ * ----------
+ * Typically a tick will have both a grid/tick line and a label at one end of
+ * that line (at the bottom for an x-axis, at left or right for the y-axis).
+ *
+ * A tick may be missing one of these two components:
+ * - If "label_v" is specified instead of "v", then there will be no tick or
+ *   gridline, just a label.
+ * - Similarly, if "label" is not specified, then there will be a gridline
+ *   without a label.
+ *
+ * This flexibility is useful in a few situations:
+ * - For log scales, some of the tick lines may be too close to all have labels.
+ * - For date scales where years are being displayed, it is desirable to display
+ *   tick marks at the beginnings of years but labels (e.g. "2006") in the
+ *   middle of the years.
+ */
+
+/*jshint sub:true */
+/*global Dygraph:false */
+(function() {
+"use strict";
+
+/** @typedef {Array.<{v:number, label:string, label_v:(string|undefined)}>} */
+Dygraph.TickList = undefined;  // the ' = undefined' keeps jshint happy.
+
+/** @typedef {function(
+ *    number,
+ *    number,
+ *    number,
+ *    function(string):*,
+ *    Dygraph=,
+ *    Array.<number>=
+ *  ): Dygraph.TickList}
+ */
+Dygraph.Ticker = undefined;  // the ' = undefined' keeps jshint happy.
+
+/** @type {Dygraph.Ticker} */
+Dygraph.numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) {
+  var nonLogscaleOpts = function(opt) {
+    if (opt === 'logscale') return false;
+    return opts(opt);
+  };
+  return Dygraph.numericTicks(a, b, pixels, nonLogscaleOpts, dygraph, vals);
+};
+
+/** @type {Dygraph.Ticker} */
+Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) {
+  var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
+  var ticks = [];
+  var i, j, tickV, nTicks;
+  if (vals) {
+    for (i = 0; i < vals.length; i++) {
+      ticks.push({v: vals[i]});
+    }
+  } else {
+    // TODO(danvk): factor this log-scale block out into a separate function.
+    if (opts("logscale")) {
+      nTicks  = Math.floor(pixels / pixels_per_tick);
+      var minIdx = Dygraph.binarySearch(a, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
+      var maxIdx = Dygraph.binarySearch(b, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
+      if (minIdx == -1) {
+        minIdx = 0;
+      }
+      if (maxIdx == -1) {
+        maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
+      }
+      // Count the number of tick values would appear, if we can get at least
+      // nTicks / 4 accept them.
+      var lastDisplayed = null;
+      if (maxIdx - minIdx >= nTicks / 4) {
+        for (var idx = maxIdx; idx >= minIdx; idx--) {
+          var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
+          var pixel_coord = Math.log(tickValue / a) / Math.log(b / a) * pixels;
+          var tick = { v: tickValue };
+          if (lastDisplayed === null) {
+            lastDisplayed = {
+              tickValue : tickValue,
+              pixel_coord : pixel_coord
+            };
+          } else {
+            if (Math.abs(pixel_coord - lastDisplayed.pixel_coord) >= pixels_per_tick) {
+              lastDisplayed = {
+                tickValue : tickValue,
+                pixel_coord : pixel_coord
+              };
+            } else {
+              tick.label = "";
+            }
+          }
+          ticks.push(tick);
+        }
+        // Since we went in backwards order.
+        ticks.reverse();
+      }
+    }
+
+    // ticks.length won't be 0 if the log scale function finds values to insert.
+    if (ticks.length === 0) {
+      // Basic idea:
+      // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
+      // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
+      // The first spacing greater than pixelsPerYLabel is what we use.
+      // TODO(danvk): version that works on a log scale.
+      var kmg2 = opts("labelsKMG2");
+      var mults, base;
+      if (kmg2) {
+        mults = [1, 2, 4, 8, 16, 32, 64, 128, 256];
+        base = 16;
+      } else {
+        mults = [1, 2, 5, 10, 20, 50, 100];
+        base = 10;
+      }
+
+      // Get the maximum number of permitted ticks based on the
+      // graph's pixel size and pixels_per_tick setting.
+      var max_ticks = Math.ceil(pixels / pixels_per_tick);
+
+      // Now calculate the data unit equivalent of this tick spacing.
+      // Use abs() since graphs may have a reversed Y axis.
+      var units_per_tick = Math.abs(b - a) / max_ticks;
+
+      // Based on this, get a starting scale which is the largest
+      // integer power of the chosen base (10 or 16) that still remains
+      // below the requested pixels_per_tick spacing.
+      var base_power = Math.floor(Math.log(units_per_tick) / Math.log(base));
+      var base_scale = Math.pow(base, base_power);
+
+      // Now try multiples of the starting scale until we find one
+      // that results in tick marks spaced sufficiently far apart.
+      // The "mults" array should cover the range 1 .. base^2 to
+      // adjust for rounding and edge effects.
+      var scale, low_val, high_val, spacing;
+      for (j = 0; j < mults.length; j++) {
+        scale = base_scale * mults[j];
+        low_val = Math.floor(a / scale) * scale;
+        high_val = Math.ceil(b / scale) * scale;
+        nTicks = Math.abs(high_val - low_val) / scale;
+        spacing = pixels / nTicks;
+        if (spacing > pixels_per_tick) break;
+      }
+
+      // Construct the set of ticks.
+      // Allow reverse y-axis if it's explicitly requested.
+      if (low_val > high_val) scale *= -1;
+      for (i = 0; i <= nTicks; i++) {
+        tickV = low_val + i * scale;
+        ticks.push( {v: tickV} );
+      }
+    }
+  }
+
+  var formatter = /**@type{AxisLabelFormatter}*/(opts('axisLabelFormatter'));
+
+  // Add labels to the ticks.
+  for (i = 0; i < ticks.length; i++) {
+    if (ticks[i].label !== undefined) continue;  // Use current label.
+    // TODO(danvk): set granularity to something appropriate here.
+    ticks[i].label = formatter.call(dygraph, ticks[i].v, 0, opts, dygraph);
+  }
+
+  return ticks;
+};
+
+
+/** @type {Dygraph.Ticker} */
+Dygraph.dateTicker = function(a, b, pixels, opts, dygraph, vals) {
+  var chosen = Dygraph.pickDateTickGranularity(a, b, pixels, opts);
+
+  if (chosen >= 0) {
+    return Dygraph.getDateAxis(a, b, chosen, opts, dygraph);
+  } else {
+    // this can happen if self.width_ is zero.
+    return [];
+  }
+};
+
+// Time granularity enumeration
+// TODO(danvk): make this an @enum
+Dygraph.SECONDLY = 0;
+Dygraph.TWO_SECONDLY = 1;
+Dygraph.FIVE_SECONDLY = 2;
+Dygraph.TEN_SECONDLY = 3;
+Dygraph.THIRTY_SECONDLY  = 4;
+Dygraph.MINUTELY = 5;
+Dygraph.TWO_MINUTELY = 6;
+Dygraph.FIVE_MINUTELY = 7;
+Dygraph.TEN_MINUTELY = 8;
+Dygraph.THIRTY_MINUTELY = 9;
+Dygraph.HOURLY = 10;
+Dygraph.TWO_HOURLY = 11;
+Dygraph.SIX_HOURLY = 12;
+Dygraph.DAILY = 13;
+Dygraph.TWO_DAILY = 14;
+Dygraph.WEEKLY = 15;
+Dygraph.MONTHLY = 16;
+Dygraph.QUARTERLY = 17;
+Dygraph.BIANNUAL = 18;
+Dygraph.ANNUAL = 19;
+Dygraph.DECADAL = 20;
+Dygraph.CENTENNIAL = 21;
+Dygraph.NUM_GRANULARITIES = 22;
+
+// Date components enumeration (in the order of the arguments in Date)
+// TODO: make this an @enum
+Dygraph.DATEFIELD_Y = 0;
+Dygraph.DATEFIELD_M = 1;
+Dygraph.DATEFIELD_D = 2;
+Dygraph.DATEFIELD_HH = 3;
+Dygraph.DATEFIELD_MM = 4;
+Dygraph.DATEFIELD_SS = 5;
+Dygraph.DATEFIELD_MS = 6;
+Dygraph.NUM_DATEFIELDS = 7;
+
+
+/**
+ * The value of datefield will start at an even multiple of "step", i.e.
+ *   if datefield=SS and step=5 then the first tick will be on a multiple of 5s.
+ *
+ * For granularities <= HOURLY, ticks are generated every `spacing` ms.
+ *
+ * At coarser granularities, ticks are generated by incrementing `datefield` by
+ *   `step`. In this case, the `spacing` value is only used to estimate the
+ *   number of ticks. It should roughly correspond to the spacing between
+ *   adjacent ticks.
+ *
+ * @type {Array.<{datefield:number, step:number, spacing:number}>}
+ */
+Dygraph.TICK_PLACEMENT = [];
+Dygraph.TICK_PLACEMENT[Dygraph.SECONDLY]        = {datefield: Dygraph.DATEFIELD_SS, step:   1, spacing: 1000 * 1};
+Dygraph.TICK_PLACEMENT[Dygraph.TWO_SECONDLY]    = {datefield: Dygraph.DATEFIELD_SS, step:   2, spacing: 1000 * 2};
+Dygraph.TICK_PLACEMENT[Dygraph.FIVE_SECONDLY]   = {datefield: Dygraph.DATEFIELD_SS, step:   5, spacing: 1000 * 5};
+Dygraph.TICK_PLACEMENT[Dygraph.TEN_SECONDLY]    = {datefield: Dygraph.DATEFIELD_SS, step:  10, spacing: 1000 * 10};
+Dygraph.TICK_PLACEMENT[Dygraph.THIRTY_SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step:  30, spacing: 1000 * 30};
+Dygraph.TICK_PLACEMENT[Dygraph.MINUTELY]        = {datefield: Dygraph.DATEFIELD_MM, step:   1, spacing: 1000 * 60};
+Dygraph.TICK_PLACEMENT[Dygraph.TWO_MINUTELY]    = {datefield: Dygraph.DATEFIELD_MM, step:   2, spacing: 1000 * 60 * 2};
+Dygraph.TICK_PLACEMENT[Dygraph.FIVE_MINUTELY]   = {datefield: Dygraph.DATEFIELD_MM, step:   5, spacing: 1000 * 60 * 5};
+Dygraph.TICK_PLACEMENT[Dygraph.TEN_MINUTELY]    = {datefield: Dygraph.DATEFIELD_MM, step:  10, spacing: 1000 * 60 * 10};
+Dygraph.TICK_PLACEMENT[Dygraph.THIRTY_MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step:  30, spacing: 1000 * 60 * 30};
+Dygraph.TICK_PLACEMENT[Dygraph.HOURLY]          = {datefield: Dygraph.DATEFIELD_HH, step:   1, spacing: 1000 * 3600};
+Dygraph.TICK_PLACEMENT[Dygraph.TWO_HOURLY]      = {datefield: Dygraph.DATEFIELD_HH, step:   2, spacing: 1000 * 3600 * 2};
+Dygraph.TICK_PLACEMENT[Dygraph.SIX_HOURLY]      = {datefield: Dygraph.DATEFIELD_HH, step:   6, spacing: 1000 * 3600 * 6};
+Dygraph.TICK_PLACEMENT[Dygraph.DAILY]           = {datefield: Dygraph.DATEFIELD_D,  step:   1, spacing: 1000 * 86400};
+Dygraph.TICK_PLACEMENT[Dygraph.TWO_DAILY]       = {datefield: Dygraph.DATEFIELD_D,  step:   2, spacing: 1000 * 86400 * 2};
+Dygraph.TICK_PLACEMENT[Dygraph.WEEKLY]          = {datefield: Dygraph.DATEFIELD_D,  step:   7, spacing: 1000 * 604800};
+Dygraph.TICK_PLACEMENT[Dygraph.MONTHLY]         = {datefield: Dygraph.DATEFIELD_M,  step:   1, spacing: 1000 * 7200  * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 12
+Dygraph.TICK_PLACEMENT[Dygraph.QUARTERLY]       = {datefield: Dygraph.DATEFIELD_M,  step:   3, spacing: 1000 * 21600 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 4
+Dygraph.TICK_PLACEMENT[Dygraph.BIANNUAL]        = {datefield: Dygraph.DATEFIELD_M,  step:   6, spacing: 1000 * 43200 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 2
+Dygraph.TICK_PLACEMENT[Dygraph.ANNUAL]          = {datefield: Dygraph.DATEFIELD_Y,  step:   1, spacing: 1000 * 86400   * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 1
+Dygraph.TICK_PLACEMENT[Dygraph.DECADAL]         = {datefield: Dygraph.DATEFIELD_Y,  step:  10, spacing: 1000 * 864000  * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 10
+Dygraph.TICK_PLACEMENT[Dygraph.CENTENNIAL]      = {datefield: Dygraph.DATEFIELD_Y,  step: 100, spacing: 1000 * 8640000 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 100
+
+
+/**
+ * This is a list of human-friendly values at which to show tick marks on a log
+ * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
+ * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
+ * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
+ * @type {Array.<number>}
+ */
+Dygraph.PREFERRED_LOG_TICK_VALUES = (function() {
+  var vals = [];
+  for (var power = -39; power <= 39; power++) {
+    var range = Math.pow(10, power);
+    for (var mult = 1; mult <= 9; mult++) {
+      var val = range * mult;
+      vals.push(val);
+    }
+  }
+  return vals;
+})();
+
+/**
+ * Determine the correct granularity of ticks on a date axis.
+ *
+ * @param {number} a Left edge of the chart (ms)
+ * @param {number} b Right edge of the chart (ms)
+ * @param {number} pixels Size of the chart in the relevant dimension (width).
+ * @param {function(string):*} opts Function mapping from option name -&gt; value.
+ * @return {number} The appropriate axis granularity for this chart. See the
+ *     enumeration of possible values in dygraph-tickers.js.
+ */
+Dygraph.pickDateTickGranularity = function(a, b, pixels, opts) {
+  var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
+  for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
+    var num_ticks = Dygraph.numDateTicks(a, b, i);
+    if (pixels / num_ticks >= pixels_per_tick) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+/**
+ * Compute the number of ticks on a date axis for a given granularity.
+ * @param {number} start_time
+ * @param {number} end_time
+ * @param {number} granularity (one of the granularities enumerated above)
+ * @return {number} (Approximate) number of ticks that would result.
+ */
+Dygraph.numDateTicks = function(start_time, end_time, granularity) {
+  var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing;
+  return Math.round(1.0 * (end_time - start_time) / spacing);
+};
+
+/**
+ * Compute the positions and labels of ticks on a date axis for a given granularity.
+ * @param {number} start_time
+ * @param {number} end_time
+ * @param {number} granularity (one of the granularities enumerated above)
+ * @param {function(string):*} opts Function mapping from option name -&gt; value.
+ * @param {Dygraph=} dg
+ * @return {!Dygraph.TickList}
+ */
+Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
+  var formatter = /** @type{AxisLabelFormatter} */(
+      opts("axisLabelFormatter"));
+  var utc = opts("labelsUTC");
+  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
+
+  var datefield = Dygraph.TICK_PLACEMENT[granularity].datefield;
+  var step = Dygraph.TICK_PLACEMENT[granularity].step;
+  var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing;
+
+  // Choose a nice tick position before the initial instant.
+  // Currently, this code deals properly with the existent daily granularities:
+  // DAILY (with step of 1) and WEEKLY (with step of 7 but specially handled).
+  // Other daily granularities (say TWO_DAILY) should also be handled specially
+  // by setting the start_date_offset to 0.
+  var start_date = new Date(start_time);
+  var date_array = [];
+  date_array[Dygraph.DATEFIELD_Y]  = accessors.getFullYear(start_date);
+  date_array[Dygraph.DATEFIELD_M]  = accessors.getMonth(start_date);
+  date_array[Dygraph.DATEFIELD_D]  = accessors.getDate(start_date);
+  date_array[Dygraph.DATEFIELD_HH] = accessors.getHours(start_date);
+  date_array[Dygraph.DATEFIELD_MM] = accessors.getMinutes(start_date);
+  date_array[Dygraph.DATEFIELD_SS] = accessors.getSeconds(start_date);
+  date_array[Dygraph.DATEFIELD_MS] = accessors.getMilliseconds(start_date);
+
+  var start_date_offset = date_array[datefield] % step;
+  if (granularity == Dygraph.WEEKLY) {
+    // This will put the ticks on Sundays.
+    start_date_offset = accessors.getDay(start_date);
+  }
+  
+  date_array[datefield] -= start_date_offset;
+  for (var df = datefield + 1; df < Dygraph.NUM_DATEFIELDS; df++) {
+    // The minimum value is 1 for the day of month, and 0 for all other fields.
+    date_array[df] = (df === Dygraph.DATEFIELD_D) ? 1 : 0;
+  }
+
+  // Generate the ticks.
+  // For granularities not coarser than HOURLY we use the fact that:
+  //   the number of milliseconds between ticks is constant
+  //   and equal to the defined spacing.
+  // Otherwise we rely on the 'roll over' property of the Date functions:
+  //   when some date field is set to a value outside of its logical range,
+  //   the excess 'rolls over' the next (more significant) field.
+  // However, when using local time with DST transitions,
+  // there are dates that do not represent any time value at all
+  // (those in the hour skipped at the 'spring forward'),
+  // and the JavaScript engines usually return an equivalent value.
+  // Hence we have to check that the date is properly increased at each step,
+  // returning a date at a nice tick position.
+  var ticks = [];
+  var tick_date = accessors.makeDate.apply(null, date_array);
+  var tick_time = tick_date.getTime();
+  if (granularity <= Dygraph.HOURLY) {
+    if (tick_time < start_time) {
+      tick_time += spacing;
+      tick_date = new Date(tick_time);
+    }
+    while (tick_time <= end_time) {
+      ticks.push({ v: tick_time,
+                   label: formatter.call(dg, tick_date, granularity, opts, dg)
+                 });
+      tick_time += spacing;
+      tick_date = new Date(tick_time);
+    }
+  } else {
+    if (tick_time < start_time) {
+      date_array[datefield] += step;
+      tick_date = accessors.makeDate.apply(null, date_array);
+      tick_time = tick_date.getTime();
+    }
+    while (tick_time <= end_time) {
+      if (granularity >= Dygraph.DAILY ||
+          accessors.getHours(tick_date) % step === 0) {
+        ticks.push({ v: tick_time,
+                     label: formatter.call(dg, tick_date, granularity, opts, dg)
+                   });
+      }
+      date_array[datefield] += step;
+      tick_date = accessors.makeDate.apply(null, date_array);
+      tick_time = tick_date.getTime();
+    }
+  }
+  return ticks;
+};
+
+// These are set here so that this file can be included after dygraph.js
+// or independently.
+if (Dygraph &&
+    Dygraph.DEFAULT_ATTRS &&
+    Dygraph.DEFAULT_ATTRS['axes'] &&
+    Dygraph.DEFAULT_ATTRS['axes']['x'] &&
+    Dygraph.DEFAULT_ATTRS['axes']['y'] &&
+    Dygraph.DEFAULT_ATTRS['axes']['y2']) {
+  Dygraph.DEFAULT_ATTRS['axes']['x']['ticker'] = Dygraph.dateTicker;
+  Dygraph.DEFAULT_ATTRS['axes']['y']['ticker'] = Dygraph.numericTicks;
+  Dygraph.DEFAULT_ATTRS['axes']['y2']['ticker'] = Dygraph.numericTicks;
+}
+
+})();
diff --git a/src/dygraph-types.js b/src/dygraph-types.js
new file mode 100644 (file)
index 0000000..4434758
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+// This file contains typedefs and externs that are needed by the Closure Compiler.
+
+/**
+ * @typedef {{
+ *   px: number,
+ *   py: number,
+ *   isZooming: boolean,
+ *   isPanning: boolean,
+ *   is2DPan: boolean,
+ *   cancelNextDblclick: boolean,
+ *   initializeMouseDown:
+ *       function(!Event, !Dygraph, !DygraphInteractionContext)
+ * }}
+ */
+var DygraphInteractionContext;
+
+/**
+ * Point structure.
+ *
+ * xval_* and yval_* are the original unscaled data values,
+ * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
+ * yval_stacked is the cumulative Y value used for stacking graphs,
+ * and bottom/top/minus/plus are used for error bar graphs.
+ *
+ * @typedef {{
+ *     idx: number,
+ *     name: string,
+ *     x: ?number,
+ *     xval: ?number,
+ *     y_bottom: ?number,
+ *     y: ?number,
+ *     y_stacked: ?number,
+ *     y_top: ?number,
+ *     yval_minus: ?number,
+ *     yval: ?number,
+ *     yval_plus: ?number,
+ *     yval_stacked
+ * }}
+ */
+Dygraph.PointType;
diff --git a/src/dygraph-utils.js b/src/dygraph-utils.js
new file mode 100644 (file)
index 0000000..eb08fef
--- /dev/null
@@ -0,0 +1,1185 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview This file contains utility functions used by dygraphs. These
+ * are typically static (i.e. not related to any particular dygraph). Examples
+ * include date/time formatting functions, basic algorithms (e.g. binary
+ * search) and generic DOM-manipulation functions.
+ */
+
+(function() {
+
+/*global Dygraph:false, Node:false */
+"use strict";
+
+Dygraph.LOG_SCALE = 10;
+Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
+
+/**
+ * @private
+ * @param {number} x
+ * @return {number}
+ */
+Dygraph.log10 = function(x) {
+  return Math.log(x) / Dygraph.LN_TEN;
+};
+
+/** A dotted line stroke pattern. */
+Dygraph.DOTTED_LINE = [2, 2];
+/** A dashed line stroke pattern. */
+Dygraph.DASHED_LINE = [7, 3];
+/** A dot dash stroke pattern. */
+Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
+
+/**
+ * Return the 2d context for a dygraph canvas.
+ *
+ * This method is only exposed for the sake of replacing the function in
+ * automated tests, e.g.
+ *
+ * var oldFunc = Dygraph.getContext();
+ * Dygraph.getContext = function(canvas) {
+ *   var realContext = oldFunc(canvas);
+ *   return new Proxy(realContext);
+ * };
+ * @param {!HTMLCanvasElement} canvas
+ * @return {!CanvasRenderingContext2D}
+ * @private
+ */
+Dygraph.getContext = function(canvas) {
+  return /** @type{!CanvasRenderingContext2D}*/(canvas.getContext("2d"));
+};
+
+/**
+ * Add an event handler.
+ * @param {!Node} elem The element to add the event to.
+ * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param {function(Event):(boolean|undefined)} fn The function to call
+ *     on the event. The function takes one parameter: the event object.
+ * @private
+ */
+Dygraph.addEvent = function addEvent(elem, type, fn) {
+  elem.addEventListener(type, fn, false);
+};
+
+/**
+ * Add an event handler. This event handler is kept until the graph is
+ * destroyed with a call to graph.destroy().
+ *
+ * @param {!Node} elem The element to add the event to.
+ * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param {function(Event):(boolean|undefined)} fn The function to call
+ *     on the event. The function takes one parameter: the event object.
+ * @private
+ */
+Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) {
+  Dygraph.addEvent(elem, type, fn);
+  this.registeredEvents_.push({ elem : elem, type : type, fn : fn });
+};
+
+/**
+ * Remove an event handler.
+ * @param {!Node} elem The element to remove the event from.
+ * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param {function(Event):(boolean|undefined)} fn The function to call
+ *     on the event. The function takes one parameter: the event object.
+ * @private
+ */
+Dygraph.removeEvent = function(elem, type, fn) {
+  elem.removeEventListener(type, fn, false);
+};
+
+Dygraph.prototype.removeTrackedEvents_ = function() {
+  if (this.registeredEvents_) {
+    for (var idx = 0; idx < this.registeredEvents_.length; idx++) {
+      var reg = this.registeredEvents_[idx];
+      Dygraph.removeEvent(reg.elem, reg.type, reg.fn);
+    }
+  }
+
+  this.registeredEvents_ = [];
+};
+
+/**
+ * Cancels further processing of an event. This is useful to prevent default
+ * browser actions, e.g. highlighting text on a double-click.
+ * Based on the article at
+ * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
+ * @param {!Event} e The event whose normal behavior should be canceled.
+ * @private
+ */
+Dygraph.cancelEvent = function(e) {
+  e = e ? e : window.event;
+  if (e.stopPropagation) {
+    e.stopPropagation();
+  }
+  if (e.preventDefault) {
+    e.preventDefault();
+  }
+  e.cancelBubble = true;
+  e.cancel = true;
+  e.returnValue = false;
+  return false;
+};
+
+/**
+ * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
+ * is used to generate default series colors which are evenly spaced on the
+ * color wheel.
+ * @param { number } hue Range is 0.0-1.0.
+ * @param { number } saturation Range is 0.0-1.0.
+ * @param { number } value Range is 0.0-1.0.
+ * @return { string } "rgb(r,g,b)" where r, g and b range from 0-255.
+ * @private
+ */
+Dygraph.hsvToRGB = function (hue, saturation, value) {
+  var red;
+  var green;
+  var blue;
+  if (saturation === 0) {
+    red = value;
+    green = value;
+    blue = value;
+  } else {
+    var i = Math.floor(hue * 6);
+    var f = (hue * 6) - i;
+    var p = value * (1 - saturation);
+    var q = value * (1 - (saturation * f));
+    var t = value * (1 - (saturation * (1 - f)));
+    switch (i) {
+      case 1: red = q; green = value; blue = p; break;
+      case 2: red = p; green = value; blue = t; break;
+      case 3: red = p; green = q; blue = value; break;
+      case 4: red = t; green = p; blue = value; break;
+      case 5: red = value; green = p; blue = q; break;
+      case 6: // fall through
+      case 0: red = value; green = t; blue = p; break;
+    }
+  }
+  red = Math.floor(255 * red + 0.5);
+  green = Math.floor(255 * green + 0.5);
+  blue = Math.floor(255 * blue + 0.5);
+  return 'rgb(' + red + ',' + green + ',' + blue + ')';
+};
+
+// The following functions are from quirksmode.org with a modification for Safari from
+// http://blog.firetree.net/2005/07/04/javascript-find-position/
+// http://www.quirksmode.org/js/findpos.html
+// ... and modifications to support scrolling divs.
+
+/**
+ * Find the coordinates of an object relative to the top left of the page.
+ *
+ * TODO(danvk): change obj type from Node -&gt; !Node
+ * @param {Node} obj
+ * @return {{x:number,y:number}}
+ * @private
+ */
+Dygraph.findPos = function(obj) {
+  var curleft = 0, curtop = 0;
+  if (obj.offsetParent) {
+    var copyObj = obj;
+    while (1) {
+      var borderLeft = "0", borderTop = "0";
+      var computedStyle = window.getComputedStyle(copyObj, null);
+      borderLeft = computedStyle.borderLeft || "0";
+      borderTop = computedStyle.borderTop || "0";
+      curleft += parseInt(borderLeft, 10) ;
+      curtop += parseInt(borderTop, 10) ;
+      curleft += copyObj.offsetLeft;
+      curtop += copyObj.offsetTop;
+      if (!copyObj.offsetParent) {
+        break;
+      }
+      copyObj = copyObj.offsetParent;
+    }
+  } else {
+    // TODO(danvk): why would obj ever have these properties?
+    if (obj.x) curleft += obj.x;
+    if (obj.y) curtop += obj.y;
+  }
+
+  // This handles the case where the object is inside a scrolled div.
+  while (obj && obj != document.body) {
+    curleft -= obj.scrollLeft;
+    curtop -= obj.scrollTop;
+    obj = obj.parentNode;
+  }
+  return {x: curleft, y: curtop};
+};
+
+/**
+ * Returns the x-coordinate of the event in a coordinate system where the
+ * top-left corner of the page (not the window) is (0,0).
+ * Taken from MochiKit.Signal
+ * @param {!Event} e
+ * @return {number}
+ * @private
+ */
+Dygraph.pageX = function(e) {
+  if (e.pageX) {
+    return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
+  } else {
+    var de = document.documentElement;
+    var b = document.body;
+    return e.clientX +
+        (de.scrollLeft || b.scrollLeft) -
+        (de.clientLeft || 0);
+  }
+};
+
+/**
+ * Returns the y-coordinate of the event in a coordinate system where the
+ * top-left corner of the page (not the window) is (0,0).
+ * Taken from MochiKit.Signal
+ * @param {!Event} e
+ * @return {number}
+ * @private
+ */
+Dygraph.pageY = function(e) {
+  if (e.pageY) {
+    return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
+  } else {
+    var de = document.documentElement;
+    var b = document.body;
+    return e.clientY +
+        (de.scrollTop || b.scrollTop) -
+        (de.clientTop || 0);
+  }
+};
+
+/**
+ * Converts page the x-coordinate of the event to pixel x-coordinates on the
+ * canvas (i.e. DOM Coords).
+ * @param {!Event} e Drag event.
+ * @param {!DygraphInteractionContext} context Interaction context object.
+ * @return {number} The amount by which the drag has moved to the right.
+ */
+Dygraph.dragGetX_ = function(e, context) {
+  return Dygraph.pageX(e) - context.px;
+};
+
+/**
+ * Converts page the y-coordinate of the event to pixel y-coordinates on the
+ * canvas (i.e. DOM Coords).
+ * @param {!Event} e Drag event.
+ * @param {!DygraphInteractionContext} context Interaction context object.
+ * @return {number} The amount by which the drag has moved down.
+ */
+Dygraph.dragGetY_ = function(e, context) {
+  return Dygraph.pageY(e) - context.py;
+};
+
+/**
+ * This returns true unless the parameter is 0, null, undefined or NaN.
+ * TODO(danvk): rename this function to something like 'isNonZeroNan'.
+ *
+ * @param {number} x The number to consider.
+ * @return {boolean} Whether the number is zero or NaN.
+ * @private
+ */
+Dygraph.isOK = function(x) {
+  return !!x && !isNaN(x);
+};
+
+/**
+ * @param {{x:?number,y:?number,yval:?number}} p The point to consider, valid
+ *     points are {x, y} objects
+ * @param {boolean=} opt_allowNaNY Treat point with y=NaN as valid
+ * @return {boolean} Whether the point has numeric x and y.
+ * @private
+ */
+Dygraph.isValidPoint = function(p, opt_allowNaNY) {
+  if (!p) return false;  // null or undefined object
+  if (p.yval === null) return false;  // missing point
+  if (p.x === null || p.x === undefined) return false;
+  if (p.y === null || p.y === undefined) return false;
+  if (isNaN(p.x) || (!opt_allowNaNY && isNaN(p.y))) return false;
+  return true;
+};
+
+/**
+ * Number formatting function which mimicks the behavior of %g in printf, i.e.
+ * either exponential or fixed format (without trailing 0s) is used depending on
+ * the length of the generated string.  The advantage of this format is that
+ * there is a predictable upper bound on the resulting string length,
+ * significant figures are not dropped, and normal numbers are not displayed in
+ * exponential notation.
+ *
+ * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
+ * It creates strings which are too long for absolute values between 10^-4 and
+ * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
+ * output examples.
+ *
+ * @param {number} x The number to format
+ * @param {number=} opt_precision The precision to use, default 2.
+ * @return {string} A string formatted like %g in printf.  The max generated
+ *                  string length should be precision + 6 (e.g 1.123e+300).
+ */
+Dygraph.floatFormat = function(x, opt_precision) {
+  // Avoid invalid precision values; [1, 21] is the valid range.
+  var p = Math.min(Math.max(1, opt_precision || 2), 21);
+
+  // This is deceptively simple.  The actual algorithm comes from:
+  //
+  // Max allowed length = p + 4
+  // where 4 comes from 'e+n' and '.'.
+  //
+  // Length of fixed format = 2 + y + p
+  // where 2 comes from '0.' and y = # of leading zeroes.
+  //
+  // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
+  // 1.0e-3.
+  //
+  // Since the behavior of toPrecision() is identical for larger numbers, we
+  // don't have to worry about the other bound.
+  //
+  // Finally, the argument for toExponential() is the number of trailing digits,
+  // so we take off 1 for the value before the '.'.
+  return (Math.abs(x) < 1.0e-3 && x !== 0.0) ?
+      x.toExponential(p - 1) : x.toPrecision(p);
+};
+
+/**
+ * Converts '9' to '09' (useful for dates)
+ * @param {number} x
+ * @return {string}
+ * @private
+ */
+Dygraph.zeropad = function(x) {
+  if (x < 10) return "0" + x; else return "" + x;
+};
+
+/**
+ * Date accessors to get the parts of a calendar date (year, month,
+ * day, hour, minute, second and millisecond) according to local time,
+ * and factory method to call the Date constructor with an array of arguments.
+ */
+Dygraph.DateAccessorsLocal = {
+  getFullYear:     function(d) {return d.getFullYear();},
+  getMonth:        function(d) {return d.getMonth();},
+  getDate:         function(d) {return d.getDate();},
+  getHours:        function(d) {return d.getHours();},
+  getMinutes:      function(d) {return d.getMinutes();},
+  getSeconds:      function(d) {return d.getSeconds();},
+  getMilliseconds: function(d) {return d.getMilliseconds();},
+  getDay:          function(d) {return d.getDay();},
+  makeDate:        function(y, m, d, hh, mm, ss, ms) {
+    return new Date(y, m, d, hh, mm, ss, ms);
+  }
+};
+
+/**
+ * Date accessors to get the parts of a calendar date (year, month,
+ * day of month, hour, minute, second and millisecond) according to UTC time,
+ * and factory method to call the Date constructor with an array of arguments.
+ */
+Dygraph.DateAccessorsUTC = {
+  getFullYear:     function(d) {return d.getUTCFullYear();},
+  getMonth:        function(d) {return d.getUTCMonth();},
+  getDate:         function(d) {return d.getUTCDate();},
+  getHours:        function(d) {return d.getUTCHours();},
+  getMinutes:      function(d) {return d.getUTCMinutes();},
+  getSeconds:      function(d) {return d.getUTCSeconds();},
+  getMilliseconds: function(d) {return d.getUTCMilliseconds();},
+  getDay:          function(d) {return d.getUTCDay();},
+  makeDate:        function(y, m, d, hh, mm, ss, ms) {
+    return new Date(Date.UTC(y, m, d, hh, mm, ss, ms));
+  }
+};
+
+/**
+ * Return a string version of the hours, minutes and seconds portion of a date.
+ * @param {number} hh The hours (from 0-23)
+ * @param {number} mm The minutes (from 0-59)
+ * @param {number} ss The seconds (from 0-59)
+ * @return {string} A time of the form "HH:MM" or "HH:MM:SS"
+ * @private
+ */
+Dygraph.hmsString_ = function(hh, mm, ss) {
+  var zeropad = Dygraph.zeropad;
+  var ret = zeropad(hh) + ":" + zeropad(mm);
+  if (ss) {
+    ret += ":" + zeropad(ss);
+  }
+  return ret;
+};
+
+/**
+ * Convert a JS date (millis since epoch) to a formatted string.
+ * @param {number} time The JavaScript time value (ms since epoch)
+ * @param {boolean} utc Wether output UTC or local time
+ * @return {string} A date of one of these forms:
+ *     "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS"
+ * @private
+ */
+Dygraph.dateString_ = function(time, utc) {
+  var zeropad = Dygraph.zeropad;
+  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
+  var date = new Date(time);
+  var y = accessors.getFullYear(date);
+  var m = accessors.getMonth(date);
+  var d = accessors.getDate(date);
+  var hh = accessors.getHours(date);
+  var mm = accessors.getMinutes(date);
+  var ss = accessors.getSeconds(date);
+  // Get a year string:
+  var year = "" + y;
+  // Get a 0 padded month string
+  var month = zeropad(m + 1);  //months are 0-offset, sigh
+  // Get a 0 padded day string
+  var day = zeropad(d);
+  var frac = hh * 3600 + mm * 60 + ss;
+  var ret = year + "/" + month + "/" + day;
+  if (frac) {
+    ret += " " + Dygraph.hmsString_(hh, mm, ss);
+  }
+  return ret;
+};
+
+/**
+ * Round a number to the specified number of digits past the decimal point.
+ * @param {number} num The number to round
+ * @param {number} places The number of decimals to which to round
+ * @return {number} The rounded number
+ * @private
+ */
+Dygraph.round_ = function(num, places) {
+  var shift = Math.pow(10, places);
+  return Math.round(num * shift)/shift;
+};
+
+/**
+ * Implementation of binary search over an array.
+ * Currently does not work when val is outside the range of arry's values.
+ * @param {number} val the value to search for
+ * @param {Array.<number>} arry is the value over which to search
+ * @param {number} abs If abs > 0, find the lowest entry greater than val
+ *     If abs < 0, find the highest entry less than val.
+ *     If abs == 0, find the entry that equals val.
+ * @param {number=} low The first index in arry to consider (optional)
+ * @param {number=} high The last index in arry to consider (optional)
+ * @return {number} Index of the element, or -1 if it isn't found.
+ * @private
+ */
+Dygraph.binarySearch = function(val, arry, abs, low, high) {
+  if (low === null || low === undefined ||
+      high === null || high === undefined) {
+    low = 0;
+    high = arry.length - 1;
+  }
+  if (low > high) {
+    return -1;
+  }
+  if (abs === null || abs === undefined) {
+    abs = 0;
+  }
+  var validIndex = function(idx) {
+    return idx >= 0 && idx < arry.length;
+  };
+  var mid = parseInt((low + high) / 2, 10);
+  var element = arry[mid];
+  var idx;
+  if (element == val) {
+    return mid;
+  } else if (element > val) {
+    if (abs > 0) {
+      // Accept if element > val, but also if prior element < val.
+      idx = mid - 1;
+      if (validIndex(idx) && arry[idx] < val) {
+        return mid;
+      }
+    }
+    return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
+  } else if (element < val) {
+    if (abs < 0) {
+      // Accept if element < val, but also if prior element > val.
+      idx = mid + 1;
+      if (validIndex(idx) && arry[idx] > val) {
+        return mid;
+      }
+    }
+    return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
+  }
+  return -1;  // can't actually happen, but makes closure compiler happy
+};
+
+/**
+ * Parses a date, returning the number of milliseconds since epoch. This can be
+ * passed in as an xValueParser in the Dygraph constructor.
+ * TODO(danvk): enumerate formats that this understands.
+ *
+ * @param {string} dateStr A date in a variety of possible string formats.
+ * @return {number} Milliseconds since epoch.
+ * @private
+ */
+Dygraph.dateParser = function(dateStr) {
+  var dateStrSlashed;
+  var d;
+
+  // Let the system try the format first, with one caveat:
+  // YYYY-MM-DD[ HH:MM:SS] is interpreted as UTC by a variety of browsers.
+  // dygraphs displays dates in local time, so this will result in surprising
+  // inconsistencies. But if you specify "T" or "Z" (i.e. YYYY-MM-DDTHH:MM:SS),
+  // then you probably know what you're doing, so we'll let you go ahead.
+  // Issue: http://code.google.com/p/dygraphs/issues/detail?id=255
+  if (dateStr.search("-") == -1 ||
+      dateStr.search("T") != -1 || dateStr.search("Z") != -1) {
+    d = Dygraph.dateStrToMillis(dateStr);
+    if (d && !isNaN(d)) return d;
+  }
+
+  if (dateStr.search("-") != -1) {  // e.g. '2009-7-12' or '2009-07-12'
+    dateStrSlashed = dateStr.replace("-", "/", "g");
+    while (dateStrSlashed.search("-") != -1) {
+      dateStrSlashed = dateStrSlashed.replace("-", "/");
+    }
+    d = Dygraph.dateStrToMillis(dateStrSlashed);
+  } else if (dateStr.length == 8) {  // e.g. '20090712'
+    // TODO(danvk): remove support for this format. It's confusing.
+    dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) + "/" +
+        dateStr.substr(6,2);
+    d = Dygraph.dateStrToMillis(dateStrSlashed);
+  } else {
+    // Any format that Date.parse will accept, e.g. "2009/07/12" or
+    // "2009/07/12 12:34:56"
+    d = Dygraph.dateStrToMillis(dateStr);
+  }
+
+  if (!d || isNaN(d)) {
+    console.error("Couldn't parse " + dateStr + " as a date");
+  }
+  return d;
+};
+
+/**
+ * This is identical to JavaScript's built-in Date.parse() method, except that
+ * it doesn't get replaced with an incompatible method by aggressive JS
+ * libraries like MooTools or Joomla.
+ * @param {string} str The date string, e.g. "2011/05/06"
+ * @return {number} millis since epoch
+ * @private
+ */
+Dygraph.dateStrToMillis = function(str) {
+  return new Date(str).getTime();
+};
+
+// These functions are all based on MochiKit.
+/**
+ * Copies all the properties from o to self.
+ *
+ * @param {!Object} self
+ * @param {!Object} o
+ * @return {!Object}
+ */
+Dygraph.update = function(self, o) {
+  if (typeof(o) != 'undefined' && o !== null) {
+    for (var k in o) {
+      if (o.hasOwnProperty(k)) {
+        self[k] = o[k];
+      }
+    }
+  }
+  return self;
+};
+
+/**
+ * Copies all the properties from o to self.
+ *
+ * @param {!Object} self
+ * @param {!Object} o
+ * @return {!Object}
+ * @private
+ */
+Dygraph.updateDeep = function (self, o) {
+  // Taken from http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
+  function isNode(o) {
+    return (
+      typeof Node === "object" ? o instanceof Node :
+      typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
+    );
+  }
+
+  if (typeof(o) != 'undefined' && o !== null) {
+    for (var k in o) {
+      if (o.hasOwnProperty(k)) {
+        if (o[k] === null) {
+          self[k] = null;
+        } else if (Dygraph.isArrayLike(o[k])) {
+          self[k] = o[k].slice();
+        } else if (isNode(o[k])) {
+          // DOM objects are shallowly-copied.
+          self[k] = o[k];
+        } else if (typeof(o[k]) == 'object') {
+          if (typeof(self[k]) != 'object' || self[k] === null) {
+            self[k] = {};
+          }
+          Dygraph.updateDeep(self[k], o[k]);
+        } else {
+          self[k] = o[k];
+        }
+      }
+    }
+  }
+  return self;
+};
+
+/**
+ * @param {*} o
+ * @return {boolean}
+ * @private
+ */
+Dygraph.isArrayLike = function(o) {
+  var typ = typeof(o);
+  if (
+      (typ != 'object' && !(typ == 'function' &&
+        typeof(o.item) == 'function')) ||
+      o === null ||
+      typeof(o.length) != 'number' ||
+      o.nodeType === 3
+     ) {
+    return false;
+  }
+  return true;
+};
+
+/**
+ * @param {Object} o
+ * @return {boolean}
+ * @private
+ */
+Dygraph.isDateLike = function (o) {
+  if (typeof(o) != "object" || o === null ||
+      typeof(o.getTime) != 'function') {
+    return false;
+  }
+  return true;
+};
+
+/**
+ * Note: this only seems to work for arrays.
+ * @param {!Array} o
+ * @return {!Array}
+ * @private
+ */
+Dygraph.clone = function(o) {
+  // TODO(danvk): figure out how MochiKit's version works
+  var r = [];
+  for (var i = 0; i < o.length; i++) {
+    if (Dygraph.isArrayLike(o[i])) {
+      r.push(Dygraph.clone(o[i]));
+    } else {
+      r.push(o[i]);
+    }
+  }
+  return r;
+};
+
+/**
+ * Create a new canvas element.
+ *
+ * @return {!HTMLCanvasElement}
+ * @private
+ */
+Dygraph.createCanvas = function() {
+  return document.createElement('canvas');
+};
+
+/**
+ * Returns the context's pixel ratio, which is the ratio between the device
+ * pixel ratio and the backing store ratio. Typically this is 1 for conventional
+ * displays, and > 1 for HiDPI displays (such as the Retina MBP).
+ * See http://www.html5rocks.com/en/tutorials/canvas/hidpi/ for more details.
+ *
+ * @param {!CanvasRenderingContext2D} context The canvas's 2d context.
+ * @return {number} The ratio of the device pixel ratio and the backing store
+ * ratio for the specified context.
+ */
+Dygraph.getContextPixelRatio = function(context) {
+  try {
+    var devicePixelRatio = window.devicePixelRatio;
+    var backingStoreRatio = context.webkitBackingStorePixelRatio ||
+                            context.mozBackingStorePixelRatio ||
+                            context.msBackingStorePixelRatio ||
+                            context.oBackingStorePixelRatio ||
+                            context.backingStorePixelRatio || 1;
+    if (devicePixelRatio !== undefined) {
+      return devicePixelRatio / backingStoreRatio;
+    } else {
+      // At least devicePixelRatio must be defined for this ratio to make sense.
+      // We default backingStoreRatio to 1: this does not exist on some browsers
+      // (i.e. desktop Chrome).
+      return 1;
+    }
+  } catch (e) {
+    return 1;
+  }
+};
+
+/**
+ * Checks whether the user is on an Android browser.
+ * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
+ * @return {boolean}
+ * @private
+ */
+Dygraph.isAndroid = function() {
+  return (/Android/).test(navigator.userAgent);
+};
+
+
+/**
+ * TODO(danvk): use @template here when it's better supported for classes.
+ * @param {!Array} array
+ * @param {number} start
+ * @param {number} length
+ * @param {function(!Array,?):boolean=} predicate
+ * @constructor
+ */
+Dygraph.Iterator = function(array, start, length, predicate) {
+  start = start || 0;
+  length = length || array.length;
+  this.hasNext = true; // Use to identify if there's another element.
+  this.peek = null; // Use for look-ahead
+  this.start_ = start;
+  this.array_ = array;
+  this.predicate_ = predicate;
+  this.end_ = Math.min(array.length, start + length);
+  this.nextIdx_ = start - 1; // use -1 so initial advance works.
+  this.next(); // ignoring result.
+};
+
+/**
+ * @return {Object}
+ */
+Dygraph.Iterator.prototype.next = function() {
+  if (!this.hasNext) {
+    return null;
+  }
+  var obj = this.peek;
+
+  var nextIdx = this.nextIdx_ + 1;
+  var found = false;
+  while (nextIdx < this.end_) {
+    if (!this.predicate_ || this.predicate_(this.array_, nextIdx)) {
+      this.peek = this.array_[nextIdx];
+      found = true;
+      break;
+    }
+    nextIdx++;
+  }
+  this.nextIdx_ = nextIdx;
+  if (!found) {
+    this.hasNext = false;
+    this.peek = null;
+  }
+  return obj;
+};
+
+/**
+ * Returns a new iterator over array, between indexes start and
+ * start + length, and only returns entries that pass the accept function
+ *
+ * @param {!Array} array the array to iterate over.
+ * @param {number} start the first index to iterate over, 0 if absent.
+ * @param {number} length the number of elements in the array to iterate over.
+ *     This, along with start, defines a slice of the array, and so length
+ *     doesn't imply the number of elements in the iterator when accept doesn't
+ *     always accept all values. array.length when absent.
+ * @param {function(?):boolean=} opt_predicate a function that takes
+ *     parameters array and idx, which returns true when the element should be
+ *     returned.  If omitted, all elements are accepted.
+ * @private
+ */
+Dygraph.createIterator = function(array, start, length, opt_predicate) {
+  return new Dygraph.Iterator(array, start, length, opt_predicate);
+};
+
+// Shim layer with setTimeout fallback.
+// From: http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+// Should be called with the window context:
+//   Dygraph.requestAnimFrame.call(window, function() {})
+Dygraph.requestAnimFrame = (function() {
+  return window.requestAnimationFrame       ||
+          window.webkitRequestAnimationFrame ||
+          window.mozRequestAnimationFrame    ||
+          window.oRequestAnimationFrame      ||
+          window.msRequestAnimationFrame     ||
+          function (callback) {
+            window.setTimeout(callback, 1000 / 60);
+          };
+})();
+
+/**
+ * Call a function at most maxFrames times at an attempted interval of
+ * framePeriodInMillis, then call a cleanup function once. repeatFn is called
+ * once immediately, then at most (maxFrames - 1) times asynchronously. If
+ * maxFrames==1, then cleanup_fn() is also called synchronously.  This function
+ * is used to sequence animation.
+ * @param {function(number)} repeatFn Called repeatedly -- takes the frame
+ *     number (from 0 to maxFrames-1) as an argument.
+ * @param {number} maxFrames The max number of times to call repeatFn
+ * @param {number} framePeriodInMillis Max requested time between frames.
+ * @param {function()} cleanupFn A function to call after all repeatFn calls.
+ * @private
+ */
+Dygraph.repeatAndCleanup = function(repeatFn, maxFrames, framePeriodInMillis,
+    cleanupFn) {
+  var frameNumber = 0;
+  var previousFrameNumber;
+  var startTime = new Date().getTime();
+  repeatFn(frameNumber);
+  if (maxFrames == 1) {
+    cleanupFn();
+    return;
+  }
+  var maxFrameArg = maxFrames - 1;
+
+  (function loop() {
+    if (frameNumber >= maxFrames) return;
+    Dygraph.requestAnimFrame.call(window, function() {
+      // Determine which frame to draw based on the delay so far.  Will skip
+      // frames if necessary.
+      var currentTime = new Date().getTime();
+      var delayInMillis = currentTime - startTime;
+      previousFrameNumber = frameNumber;
+      frameNumber = Math.floor(delayInMillis / framePeriodInMillis);
+      var frameDelta = frameNumber - previousFrameNumber;
+      // If we predict that the subsequent repeatFn call will overshoot our
+      // total frame target, so our last call will cause a stutter, then jump to
+      // the last call immediately.  If we're going to cause a stutter, better
+      // to do it faster than slower.
+      var predictOvershootStutter = (frameNumber + frameDelta) > maxFrameArg;
+      if (predictOvershootStutter || (frameNumber >= maxFrameArg)) {
+        repeatFn(maxFrameArg);  // Ensure final call with maxFrameArg.
+        cleanupFn();
+      } else {
+        if (frameDelta !== 0) {  // Don't call repeatFn with duplicate frames.
+          repeatFn(frameNumber);
+        }
+        loop();
+      }
+    });
+  })();
+};
+
+// A whitelist of options that do not change pixel positions.
+var pixelSafeOptions = {
+  'annotationClickHandler': true,
+  'annotationDblClickHandler': true,
+  'annotationMouseOutHandler': true,
+  'annotationMouseOverHandler': true,
+  'axisLabelColor': true,
+  'axisLineColor': true,
+  'axisLineWidth': true,
+  'clickCallback': true,
+  'drawCallback': true,
+  'drawHighlightPointCallback': true,
+  'drawPoints': true,
+  'drawPointCallback': true,
+  'drawGrid': true,
+  'fillAlpha': true,
+  'gridLineColor': true,
+  'gridLineWidth': true,
+  'hideOverlayOnMouseOut': true,
+  'highlightCallback': true,
+  'highlightCircleSize': true,
+  'interactionModel': true,
+  'isZoomedIgnoreProgrammaticZoom': true,
+  'labelsDiv': true,
+  'labelsDivStyles': true,
+  'labelsDivWidth': true,
+  'labelsKMB': true,
+  'labelsKMG2': true,
+  'labelsSeparateLines': true,
+  'labelsShowZeroValues': true,
+  'legend': true,
+  'panEdgeFraction': true,
+  'pixelsPerYLabel': true,
+  'pointClickCallback': true,
+  'pointSize': true,
+  'rangeSelectorPlotFillColor': true,
+  'rangeSelectorPlotFillGradientColor': true,
+  'rangeSelectorPlotStrokeColor': true,
+  'rangeSelectorBackgroundStrokeColor': true,
+  'rangeSelectorBackgroundLineWidth': true,
+  'rangeSelectorPlotLineWidth': true,
+  'rangeSelectorForegroundStrokeColor': true,
+  'rangeSelectorForegroundLineWidth': true,
+  'rangeSelectorAlpha': true,
+  'showLabelsOnHighlight': true,
+  'showRoller': true,
+  'strokeWidth': true,
+  'underlayCallback': true,
+  'unhighlightCallback': true,
+  'zoomCallback': true
+};
+
+/**
+ * This function will scan the option list and determine if they
+ * require us to recalculate the pixel positions of each point.
+ * TODO: move this into dygraph-options.js
+ * @param {!Array.<string>} labels a list of options to check.
+ * @param {!Object} attrs
+ * @return {boolean} true if the graph needs new points else false.
+ * @private
+ */
+Dygraph.isPixelChangingOptionList = function(labels, attrs) {
+  // Assume that we do not require new points.
+  // This will change to true if we actually do need new points.
+
+  // Create a dictionary of series names for faster lookup.
+  // If there are no labels, then the dictionary stays empty.
+  var seriesNamesDictionary = { };
+  if (labels) {
+    for (var i = 1; i < labels.length; i++) {
+      seriesNamesDictionary[labels[i]] = true;
+    }
+  }
+
+  // Scan through a flat (i.e. non-nested) object of options.
+  // Returns true/false depending on whether new points are needed.
+  var scanFlatOptions = function(options) {
+    for (var property in options) {
+      if (options.hasOwnProperty(property) &&
+          !pixelSafeOptions[property]) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  // Iterate through the list of updated options.
+  for (var property in attrs) {
+    if (!attrs.hasOwnProperty(property)) continue;
+
+    // Find out of this field is actually a series specific options list.
+    if (property == 'highlightSeriesOpts' ||
+        (seriesNamesDictionary[property] && !attrs.series)) {
+      // This property value is a list of options for this series.
+      if (scanFlatOptions(attrs[property])) return true;
+    } else if (property == 'series' || property == 'axes') {
+      // This is twice-nested options list.
+      var perSeries = attrs[property];
+      for (var series in perSeries) {
+        if (perSeries.hasOwnProperty(series) &&
+            scanFlatOptions(perSeries[series])) {
+          return true;
+        }
+      }
+    } else {
+      // If this was not a series specific option list, check if it's a pixel
+      // changing property.
+      if (!pixelSafeOptions[property]) return true;
+    }
+  }
+
+  return false;
+};
+
+Dygraph.Circles = {
+  DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
+    ctx.beginPath();
+    ctx.fillStyle = color;
+    ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false);
+    ctx.fill();
+  }
+  // For more shapes, include extras/shapes.js
+};
+
+/**
+ * To create a "drag" interaction, you typically register a mousedown event
+ * handler on the element where the drag begins. In that handler, you register a
+ * mouseup handler on the window to determine when the mouse is released,
+ * wherever that release happens. This works well, except when the user releases
+ * the mouse over an off-domain iframe. In that case, the mouseup event is
+ * handled by the iframe and never bubbles up to the window handler.
+ *
+ * To deal with this issue, we cover iframes with high z-index divs to make sure
+ * they don't capture mouseup.
+ *
+ * Usage:
+ * element.addEventListener('mousedown', function() {
+ *   var tarper = new Dygraph.IFrameTarp();
+ *   tarper.cover();
+ *   var mouseUpHandler = function() {
+ *     ...
+ *     window.removeEventListener(mouseUpHandler);
+ *     tarper.uncover();
+ *   };
+ *   window.addEventListener('mouseup', mouseUpHandler);
+ * };
+ *
+ * @constructor
+ */
+Dygraph.IFrameTarp = function() {
+  /** @type {Array.<!HTMLDivElement>} */
+  this.tarps = [];
+};
+
+/**
+ * Find all the iframes in the document and cover them with high z-index
+ * transparent divs.
+ */
+Dygraph.IFrameTarp.prototype.cover = function() {
+  var iframes = document.getElementsByTagName("iframe");
+  for (var i = 0; i < iframes.length; i++) {
+    var iframe = iframes[i];
+    var pos = Dygraph.findPos(iframe),
+        x = pos.x,
+        y = pos.y,
+        width = iframe.offsetWidth,
+        height = iframe.offsetHeight;
+
+    var div = document.createElement("div");
+    div.style.position = "absolute";
+    div.style.left = x + 'px';
+    div.style.top = y + 'px';
+    div.style.width = width + 'px';
+    div.style.height = height + 'px';
+    div.style.zIndex = 999;
+    document.body.appendChild(div);
+    this.tarps.push(div);
+  }
+};
+
+/**
+ * Remove all the iframe covers. You should call this in a mouseup handler.
+ */
+Dygraph.IFrameTarp.prototype.uncover = function() {
+  for (var i = 0; i < this.tarps.length; i++) {
+    this.tarps[i].parentNode.removeChild(this.tarps[i]);
+  }
+  this.tarps = [];
+};
+
+/**
+ * Determine whether |data| is delimited by CR, CRLF, LF, LFCR.
+ * @param {string} data
+ * @return {?string} the delimiter that was detected (or null on failure).
+ */
+Dygraph.detectLineDelimiter = function(data) {
+  for (var i = 0; i < data.length; i++) {
+    var code = data.charAt(i);
+    if (code === '\r') {
+      // Might actually be "\r\n".
+      if (((i + 1) < data.length) && (data.charAt(i + 1) === '\n')) {
+        return '\r\n';
+      }
+      return code;
+    }
+    if (code === '\n') {
+      // Might actually be "\n\r".
+      if (((i + 1) < data.length) && (data.charAt(i + 1) === '\r')) {
+        return '\n\r';
+      }
+      return code;
+    }
+  }
+
+  return null;
+};
+
+/**
+ * Is one node contained by another?
+ * @param {Node} containee The contained node.
+ * @param {Node} container The container node.
+ * @return {boolean} Whether containee is inside (or equal to) container.
+ * @private
+ */
+Dygraph.isNodeContainedBy = function(containee, container) {
+  if (container === null || containee === null) {
+    return false;
+  }
+  var containeeNode = /** @type {Node} */ (containee);
+  while (containeeNode && containeeNode !== container) {
+    containeeNode = containeeNode.parentNode;
+  }
+  return (containeeNode === container);
+};
+
+
+// This masks some numeric issues in older versions of Firefox,
+// where 1.0/Math.pow(10,2) != Math.pow(10,-2).
+/** @type {function(number,number):number} */
+Dygraph.pow = function(base, exp) {
+  if (exp < 0) {
+    return 1.0 / Math.pow(base, -exp);
+  }
+  return Math.pow(base, exp);
+};
+
+/**
+ * Converts any valid CSS color (hex, rgb(), named color) to an RGB tuple.
+ *
+ * @param {!string} colorStr Any valid CSS color string.
+ * @return {{r:number,g:number,b:number}} Parsed RGB tuple.
+ * @private
+ */
+Dygraph.toRGB_ = function(colorStr) {
+  // TODO(danvk): cache color parses to avoid repeated DOM manipulation.
+  var div = document.createElement('div');
+  div.style.backgroundColor = colorStr;
+  div.style.visibility = 'hidden';
+  document.body.appendChild(div);
+  var rgbStr = window.getComputedStyle(div, null).backgroundColor;
+  document.body.removeChild(div);
+  var bits = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(rgbStr);
+  return {
+    r: parseInt(bits[1], 10),
+    g: parseInt(bits[2], 10),
+    b: parseInt(bits[3], 10)
+  };
+};
+
+/**
+ * Checks whether the browser supports the &lt;canvas&gt; tag.
+ * @param {HTMLCanvasElement=} opt_canvasElement Pass a canvas element as an
+ *     optimization if you have one.
+ * @return {boolean} Whether the browser supports canvas.
+ */
+Dygraph.isCanvasSupported = function(opt_canvasElement) {
+  try {
+    var canvas = opt_canvasElement || document.createElement("canvas");
+    canvas.getContext("2d");
+  } catch (e) {
+    return false;
+  }
+  return true;
+};
+
+/**
+ * Parses the value as a floating point number. This is like the parseFloat()
+ * built-in, but with a few differences:
+ * - the empty string is parsed as null, rather than NaN.
+ * - if the string cannot be parsed at all, an error is logged.
+ * If the string can't be parsed, this method returns null.
+ * @param {string} x The string to be parsed
+ * @param {number=} opt_line_no The line number from which the string comes.
+ * @param {string=} opt_line The text of the line from which the string comes.
+ */
+Dygraph.parseFloat_ = function(x, opt_line_no, opt_line) {
+  var val = parseFloat(x);
+  if (!isNaN(val)) return val;
+
+  // Try to figure out what happeend.
+  // If the value is the empty string, parse it as null.
+  if (/^ *$/.test(x)) return null;
+
+  // If it was actually "NaN", return it as NaN.
+  if (/^ *nan *$/i.test(x)) return NaN;
+
+  // Looks like a parsing error.
+  var msg = "Unable to parse '" + x + "' as a number";
+  if (opt_line !== undefined && opt_line_no !== undefined) {
+    msg += " on line " + (1+(opt_line_no||0)) + " ('" + opt_line + "') of CSV.";
+  }
+  console.error(msg);
+
+  return null;
+};
+
+})();
diff --git a/src/dygraph.js b/src/dygraph.js
new file mode 100644 (file)
index 0000000..aaf4b39
--- /dev/null
@@ -0,0 +1,3785 @@
+/**
+ * @license
+ * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
+ * string. Dygraph can handle multiple series with or without error bars. The
+ * date/value ranges will be automatically set. Dygraph uses the
+ * &lt;canvas&gt; tag, so it only works in FF1.5+.
+ * @author danvdk@gmail.com (Dan Vanderkam)
+
+  Usage:
+   <div id="graphdiv" style="width:800px; height:500px;"></div>
+   <script type="text/javascript">
+     new Dygraph(document.getElementById("graphdiv"),
+                 "datafile.csv",  // CSV file with headers
+                 { }); // options
+   </script>
+
+ The CSV file is of the form
+
+   Date,SeriesA,SeriesB,SeriesC
+   YYYYMMDD,A1,B1,C1
+   YYYYMMDD,A2,B2,C2
+
+ If the 'errorBars' option is set in the constructor, the input should be of
+ the form
+   Date,SeriesA,SeriesB,...
+   YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
+   YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
+
+ If the 'fractions' option is set, the input should be of the form:
+
+   Date,SeriesA,SeriesB,...
+   YYYYMMDD,A1/B1,A2/B2,...
+   YYYYMMDD,A1/B1,A2/B2,...
+
+ And error bars will be calculated automatically using a binomial distribution.
+
+ For further documentation and examples, see http://dygraphs.com/
+
+ */
+
+// For "production" code, this gets set to false by uglifyjs.
+if (typeof(DEBUG) === 'undefined') DEBUG=true;
+
+var Dygraph = (function() {
+/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */
+"use strict";
+
+/**
+ * Creates an interactive, zoomable chart.
+ *
+ * @constructor
+ * @param {div | String} div A div or the id of a div into which to construct
+ * the chart.
+ * @param {String | Function} file A file containing CSV data or a function
+ * that returns this data. The most basic expected format for each line is
+ * "YYYY/MM/DD,val1,val2,...". For more information, see
+ * http://dygraphs.com/data.html.
+ * @param {Object} attrs Various other attributes, e.g. errorBars determines
+ * whether the input data contains error ranges. For a complete list of
+ * options, see http://dygraphs.com/options.html.
+ */
+var Dygraph = function(div, data, opts, opt_fourth_param) {
+  // These have to go above the "Hack for IE" in __init__ since .ready() can be
+  // called as soon as the constructor returns. Once support for OldIE is
+  // dropped, this can go down with the rest of the initializers.
+  this.is_initial_draw_ = true;
+  this.readyFns_ = [];
+
+  if (opt_fourth_param !== undefined) {
+    // Old versions of dygraphs took in the series labels as a constructor
+    // parameter. This doesn't make sense anymore, but it's easy to continue
+    // to support this usage.
+    console.warn("Using deprecated four-argument dygraph constructor");
+    this.__old_init__(div, data, opts, opt_fourth_param);
+  } else {
+    this.__init__(div, data, opts);
+  }
+};
+
+Dygraph.NAME = "Dygraph";
+Dygraph.VERSION = "1.1.0";
+Dygraph.__repr__ = function() {
+  return "[" + Dygraph.NAME + " " + Dygraph.VERSION + "]";
+};
+
+/**
+ * Returns information about the Dygraph class.
+ */
+Dygraph.toString = function() {
+  return Dygraph.__repr__();
+};
+
+// Various default values
+Dygraph.DEFAULT_ROLL_PERIOD = 1;
+Dygraph.DEFAULT_WIDTH = 480;
+Dygraph.DEFAULT_HEIGHT = 320;
+
+// For max 60 Hz. animation:
+Dygraph.ANIMATION_STEPS = 12;
+Dygraph.ANIMATION_DURATION = 200;
+
+// Label constants for the labelsKMB and labelsKMG2 options.
+// (i.e. '100000' -> '100K')
+Dygraph.KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ];
+Dygraph.KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ];
+Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ];
+
+// These are defined before DEFAULT_ATTRS so that it can refer to them.
+/**
+ * @private
+ * Return a string version of a number. This respects the digitsAfterDecimal
+ * and maxNumberWidth options.
+ * @param {number} x The number to be formatted
+ * @param {Dygraph} opts An options view
+ */
+Dygraph.numberValueFormatter = function(x, opts) {
+  var sigFigs = opts('sigFigs');
+
+  if (sigFigs !== null) {
+    // User has opted for a fixed number of significant figures.
+    return Dygraph.floatFormat(x, sigFigs);
+  }
+
+  var digits = opts('digitsAfterDecimal');
+  var maxNumberWidth = opts('maxNumberWidth');
+
+  var kmb = opts('labelsKMB');
+  var kmg2 = opts('labelsKMG2');
+
+  var label;
+
+  // switch to scientific notation if we underflow or overflow fixed display.
+  if (x !== 0.0 &&
+      (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
+       Math.abs(x) < Math.pow(10, -digits))) {
+    label = x.toExponential(digits);
+  } else {
+    label = '' + Dygraph.round_(x, digits);
+  }
+
+  if (kmb || kmg2) {
+    var k;
+    var k_labels = [];
+    var m_labels = [];
+    if (kmb) {
+      k = 1000;
+      k_labels = Dygraph.KMB_LABELS;
+    }
+    if (kmg2) {
+      if (kmb) console.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+      k = 1024;
+      k_labels = Dygraph.KMG2_BIG_LABELS;
+      m_labels = Dygraph.KMG2_SMALL_LABELS;
+    }
+
+    var absx = Math.abs(x);
+    var n = Dygraph.pow(k, k_labels.length);
+    for (var j = k_labels.length - 1; j >= 0; j--, n /= k) {
+      if (absx >= n) {
+        label = Dygraph.round_(x / n, digits) + k_labels[j];
+        break;
+      }
+    }
+    if (kmg2) {
+      // TODO(danvk): clean up this logic. Why so different than kmb?
+      var x_parts = String(x.toExponential()).split('e-');
+      if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) {
+        if (x_parts[1] % 3 > 0) {
+          label = Dygraph.round_(x_parts[0] /
+              Dygraph.pow(10, (x_parts[1] % 3)),
+              digits);
+        } else {
+          label = Number(x_parts[0]).toFixed(2);
+        }
+        label += m_labels[Math.floor(x_parts[1] / 3) - 1];
+      }
+    }
+  }
+
+  return label;
+};
+
+/**
+ * variant for use as an axisLabelFormatter.
+ * @private
+ */
+Dygraph.numberAxisLabelFormatter = function(x, granularity, opts) {
+  return Dygraph.numberValueFormatter.call(this, x, opts);
+};
+
+/**
+ * @type {!Array.<string>}
+ * @private
+ * @constant
+ */
+Dygraph.SHORT_MONTH_NAMES_ = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+
+
+/**
+ * Convert a JS date to a string appropriate to display on an axis that
+ * is displaying values at the stated granularity. This respects the
+ * labelsUTC option.
+ * @param {Date} date The date to format
+ * @param {number} granularity One of the Dygraph granularity constants
+ * @param {Dygraph} opts An options view
+ * @return {string} The date formatted as local time
+ * @private
+ */
+Dygraph.dateAxisLabelFormatter = function(date, granularity, opts) {
+  var utc = opts('labelsUTC');
+  var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
+
+  var year = accessors.getFullYear(date),
+      month = accessors.getMonth(date),
+      day = accessors.getDate(date),
+      hours = accessors.getHours(date),
+      mins = accessors.getMinutes(date),
+      secs = accessors.getSeconds(date),
+      millis = accessors.getSeconds(date);
+
+  if (granularity >= Dygraph.DECADAL) {
+    return '' + year;
+  } else if (granularity >= Dygraph.MONTHLY) {
+    return Dygraph.SHORT_MONTH_NAMES_[month] + '&#160;' + year;
+  } else {
+    var frac = hours * 3600 + mins * 60 + secs + 1e-3 * millis;
+    if (frac === 0 || granularity >= Dygraph.DAILY) {
+      // e.g. '21 Jan' (%d%b)
+      return Dygraph.zeropad(day) + '&#160;' + Dygraph.SHORT_MONTH_NAMES_[month];
+    } else {
+      return Dygraph.hmsString_(hours, mins, secs);
+    }
+  }
+};
+// alias in case anyone is referencing the old method.
+Dygraph.dateAxisFormatter = Dygraph.dateAxisLabelFormatter;
+
+/**
+ * Return a string version of a JS date for a value label. This respects the
+ * labelsUTC option.
+ * @param {Date} date The date to be formatted
+ * @param {Dygraph} opts An options view
+ * @private
+ */
+Dygraph.dateValueFormatter = function(d, opts) {
+  return Dygraph.dateString_(d, opts('labelsUTC'));
+};
+
+/**
+ * Standard plotters. These may be used by clients.
+ * Available plotters are:
+ * - Dygraph.Plotters.linePlotter: draws central lines (most common)
+ * - Dygraph.Plotters.errorPlotter: draws error bars
+ * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
+ *
+ * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
+ * This causes all the lines to be drawn over all the fills/error bars.
+ */
+Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
+
+
+// Default attribute values.
+Dygraph.DEFAULT_ATTRS = {
+  highlightCircleSize: 3,
+  highlightSeriesOpts: null,
+  highlightSeriesBackgroundAlpha: 0.5,
+
+  labelsDivWidth: 250,
+  labelsDivStyles: {
+    // TODO(danvk): move defaults from createStatusMessage_ here.
+  },
+  labelsSeparateLines: false,
+  labelsShowZeroValues: true,
+  labelsKMB: false,
+  labelsKMG2: false,
+  showLabelsOnHighlight: true,
+
+  digitsAfterDecimal: 2,
+  maxNumberWidth: 6,
+  sigFigs: null,
+
+  strokeWidth: 1.0,
+  strokeBorderWidth: 0,
+  strokeBorderColor: "white",
+
+  axisTickSize: 3,
+  axisLabelFontSize: 14,
+  rightGap: 5,
+
+  showRoller: false,
+  xValueParser: Dygraph.dateParser,
+
+  delimiter: ',',
+
+  sigma: 2.0,
+  errorBars: false,
+  fractions: false,
+  wilsonInterval: true,  // only relevant if fractions is true
+  customBars: false,
+  fillGraph: false,
+  fillAlpha: 0.15,
+  connectSeparatedPoints: false,
+
+  stackedGraph: false,
+  stackedGraphNaNFill: 'all',
+  hideOverlayOnMouseOut: true,
+
+  legend: 'onmouseover',
+  stepPlot: false,
+  avoidMinZero: false,
+  xRangePad: 0,
+  yRangePad: null,
+  drawAxesAtZero: false,
+
+  // Sizes of the various chart labels.
+  titleHeight: 28,
+  xLabelHeight: 18,
+  yLabelWidth: 18,
+
+  axisLineColor: "black",
+  axisLineWidth: 0.3,
+  gridLineWidth: 0.3,
+  axisLabelColor: "black",
+  axisLabelWidth: 50,
+  gridLineColor: "rgb(128,128,128)",
+
+  interactionModel: null,  // will be set to Dygraph.Interaction.defaultModel
+  animatedZooms: false,  // (for now)
+
+  // Range selector options
+  showRangeSelector: false,
+  rangeSelectorHeight: 40,
+  rangeSelectorPlotStrokeColor: "#808FAB",
+  rangeSelectorPlotFillGradientColor: "white",
+  rangeSelectorPlotFillColor: "#A7B1C4",
+  rangeSelectorBackgroundStrokeColor: "gray",
+  rangeSelectorBackgroundLineWidth: 1,
+  rangeSelectorPlotLineWidth:1.5,
+  rangeSelectorForegroundStrokeColor: "black",
+  rangeSelectorForegroundLineWidth: 1,
+  rangeSelectorAlpha: 0.6,
+  showInRangeSelector: null,
+
+  // The ordering here ensures that central lines always appear above any
+  // fill bars/error bars.
+  plotter: [
+    Dygraph.Plotters.fillPlotter,
+    Dygraph.Plotters.errorPlotter,
+    Dygraph.Plotters.linePlotter
+  ],
+
+  plugins: [ ],
+
+  // per-axis options
+  axes: {
+    x: {
+      pixelsPerLabel: 70,
+      axisLabelWidth: 60,
+      axisLabelFormatter: Dygraph.dateAxisLabelFormatter,
+      valueFormatter: Dygraph.dateValueFormatter,
+      drawGrid: true,
+      drawAxis: true,
+      independentTicks: true,
+      ticker: null  // will be set in dygraph-tickers.js
+    },
+    y: {
+      axisLabelWidth: 50,
+      pixelsPerLabel: 30,
+      valueFormatter: Dygraph.numberValueFormatter,
+      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+      drawGrid: true,
+      drawAxis: true,
+      independentTicks: true,
+      ticker: null  // will be set in dygraph-tickers.js
+    },
+    y2: {
+      axisLabelWidth: 50,
+      pixelsPerLabel: 30,
+      valueFormatter: Dygraph.numberValueFormatter,
+      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+      drawAxis: true,  // only applies when there are two axes of data.
+      drawGrid: false,
+      independentTicks: false,
+      ticker: null  // will be set in dygraph-tickers.js
+    }
+  }
+};
+
+// Directions for panning and zooming. Use bit operations when combined
+// values are possible.
+Dygraph.HORIZONTAL = 1;
+Dygraph.VERTICAL = 2;
+
+// Installed plugins, in order of precedence (most-general to most-specific).
+// Plugins are installed after they are defined, in plugins/install.js.
+Dygraph.PLUGINS = [
+];
+
+// Used for initializing annotation CSS rules only once.
+Dygraph.addedAnnotationCSS = false;
+
+Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
+  // Labels is no longer a constructor parameter, since it's typically set
+  // directly from the data source. It also conains a name for the x-axis,
+  // which the previous constructor form did not.
+  if (labels !== null) {
+    var new_labels = ["Date"];
+    for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
+    Dygraph.update(attrs, { 'labels': new_labels });
+  }
+  this.__init__(div, file, attrs);
+};
+
+/**
+ * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
+ * and context &lt;canvas&gt; inside of it. See the constructor for details.
+ * on the parameters.
+ * @param {Element} div the Element to render the graph into.
+ * @param {string | Function} file Source data
+ * @param {Object} attrs Miscellaneous other options
+ * @private
+ */
+Dygraph.prototype.__init__ = function(div, file, attrs) {
+  // Support two-argument constructor
+  if (attrs === null || attrs === undefined) { attrs = {}; }
+
+  attrs = Dygraph.copyUserAttrs_(attrs);
+
+  if (typeof(div) == 'string') {
+    div = document.getElementById(div);
+  }
+
+  if (!div) {
+    console.error("Constructing dygraph with a non-existent div!");
+    return;
+  }
+
+  // Copy the important bits into the object
+  // TODO(danvk): most of these should just stay in the attrs_ dictionary.
+  this.maindiv_ = div;
+  this.file_ = file;
+  this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
+  this.previousVerticalX_ = -1;
+  this.fractions_ = attrs.fractions || false;
+  this.dateWindow_ = attrs.dateWindow || null;
+
+  this.annotations_ = [];
+
+  // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
+  this.zoomed_x_ = false;
+  this.zoomed_y_ = false;
+
+  // Clear the div. This ensure that, if multiple dygraphs are passed the same
+  // div, then only one will be drawn.
+  div.innerHTML = "";
+
+  // For historical reasons, the 'width' and 'height' options trump all CSS
+  // rules _except_ for an explicit 'width' or 'height' on the div.
+  // As an added convenience, if the div has zero height (like <div></div> does
+  // without any styles), then we use a default height/width.
+  if (div.style.width === '' && attrs.width) {
+    div.style.width = attrs.width + "px";
+  }
+  if (div.style.height === '' && attrs.height) {
+    div.style.height = attrs.height + "px";
+  }
+  if (div.style.height === '' && div.clientHeight === 0) {
+    div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
+    if (div.style.width === '') {
+      div.style.width = Dygraph.DEFAULT_WIDTH + "px";
+    }
+  }
+  // These will be zero if the dygraph's div is hidden. In that case,
+  // use the user-specified attributes if present. If not, use zero
+  // and assume the user will call resize to fix things later.
+  this.width_ = div.clientWidth || attrs.width || 0;
+  this.height_ = div.clientHeight || attrs.height || 0;
+
+  // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
+  if (attrs.stackedGraph) {
+    attrs.fillGraph = true;
+    // TODO(nikhilk): Add any other stackedGraph checks here.
+  }
+
+  // DEPRECATION WARNING: All option processing should be moved from
+  // attrs_ and user_attrs_ to options_, which holds all this information.
+  //
+  // Dygraphs has many options, some of which interact with one another.
+  // To keep track of everything, we maintain two sets of options:
+  //
+  //  this.user_attrs_   only options explicitly set by the user.
+  //  this.attrs_        defaults, options derived from user_attrs_, data.
+  //
+  // Options are then accessed this.attr_('attr'), which first looks at
+  // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
+  // defaults without overriding behavior that the user specifically asks for.
+  this.user_attrs_ = {};
+  Dygraph.update(this.user_attrs_, attrs);
+
+  // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
+  this.attrs_ = {};
+  Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
+
+  this.boundaryIds_ = [];
+  this.setIndexByName_ = {};
+  this.datasetIndex_ = [];
+
+  this.registeredEvents_ = [];
+  this.eventListeners_ = {};
+
+  this.attributes_ = new DygraphOptions(this);
+
+  // Create the containing DIV and other interactive elements
+  this.createInterface_();
+
+  // Activate plugins.
+  this.plugins_ = [];
+  var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
+  for (var i = 0; i < plugins.length; i++) {
+    // the plugins option may contain either plugin classes or instances.
+    // Plugin instances contain an activate method.
+    var Plugin = plugins[i];  // either a constructor or an instance.
+    var pluginInstance;
+    if (typeof(Plugin.activate) !== 'undefined') {
+      pluginInstance = Plugin;
+    } else {
+      pluginInstance = new Plugin();
+    }
+
+    var pluginDict = {
+      plugin: pluginInstance,
+      events: {},
+      options: {},
+      pluginOptions: {}
+    };
+
+    var handlers = pluginInstance.activate(this);
+    for (var eventName in handlers) {
+      if (!handlers.hasOwnProperty(eventName)) continue;
+      // TODO(danvk): validate eventName.
+      pluginDict.events[eventName] = handlers[eventName];
+    }
+
+    this.plugins_.push(pluginDict);
+  }
+
+  // At this point, plugins can no longer register event handlers.
+  // Construct a map from event -> ordered list of [callback, plugin].
+  for (var i = 0; i < this.plugins_.length; i++) {
+    var plugin_dict = this.plugins_[i];
+    for (var eventName in plugin_dict.events) {
+      if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
+      var callback = plugin_dict.events[eventName];
+
+      var pair = [plugin_dict.plugin, callback];
+      if (!(eventName in this.eventListeners_)) {
+        this.eventListeners_[eventName] = [pair];
+      } else {
+        this.eventListeners_[eventName].push(pair);
+      }
+    }
+  }
+
+  this.createDragInterface_();
+
+  this.start_();
+};
+
+/**
+ * Triggers a cascade of events to the various plugins which are interested in them.
+ * Returns true if the "default behavior" should be prevented, i.e. if one
+ * of the event listeners called event.preventDefault().
+ * @private
+ */
+Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
+  if (!(name in this.eventListeners_)) return false;
+
+  // QUESTION: can we use objects & prototypes to speed this up?
+  var e = {
+    dygraph: this,
+    cancelable: false,
+    defaultPrevented: false,
+    preventDefault: function() {
+      if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
+      e.defaultPrevented = true;
+    },
+    propagationStopped: false,
+    stopPropagation: function() {
+      e.propagationStopped = true;
+    }
+  };
+  Dygraph.update(e, extra_props);
+
+  var callback_plugin_pairs = this.eventListeners_[name];
+  if (callback_plugin_pairs) {
+    for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
+      var plugin = callback_plugin_pairs[i][0];
+      var callback = callback_plugin_pairs[i][1];
+      callback.call(plugin, e);
+      if (e.propagationStopped) break;
+    }
+  }
+  return e.defaultPrevented;
+};
+
+/**
+ * Fetch a plugin instance of a particular class. Only for testing.
+ * @private
+ * @param {!Class} type The type of the plugin.
+ * @return {Object} Instance of the plugin, or null if there is none.
+ */
+Dygraph.prototype.getPluginInstance_ = function(type) {
+  for (var i = 0; i < this.plugins_.length; i++) {
+    var p = this.plugins_[i];
+    if (p.plugin instanceof type) {
+      return p.plugin;
+    }
+  }
+  return null;
+};
+
+/**
+ * Returns the zoomed status of the chart for one or both axes.
+ *
+ * Axis is an optional parameter. Can be set to 'x' or 'y'.
+ *
+ * The zoomed status for an axis is set whenever a user zooms using the mouse
+ * or when the dateWindow or valueRange are updated (unless the
+ * isZoomedIgnoreProgrammaticZoom option is also specified).
+ */
+Dygraph.prototype.isZoomed = function(axis) {
+  if (axis === null || axis === undefined) {
+    return this.zoomed_x_ || this.zoomed_y_;
+  }
+  if (axis === 'x') return this.zoomed_x_;
+  if (axis === 'y') return this.zoomed_y_;
+  throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'.";
+};
+
+/**
+ * Returns information about the Dygraph object, including its containing ID.
+ */
+Dygraph.prototype.toString = function() {
+  var maindiv = this.maindiv_;
+  var id = (maindiv && maindiv.id) ? maindiv.id : maindiv;
+  return "[Dygraph " + id + "]";
+};
+
+/**
+ * @private
+ * Returns the value of an option. This may be set by the user (either in the
+ * constructor or by calling updateOptions) or by dygraphs, and may be set to a
+ * per-series value.
+ * @param {string} name The name of the option, e.g. 'rollPeriod'.
+ * @param {string} [seriesName] The name of the series to which the option
+ * will be applied. If no per-series value of this option is available, then
+ * the global value is returned. This is optional.
+ * @return { ... } The value of the option.
+ */
+Dygraph.prototype.attr_ = function(name, seriesName) {
+  if (DEBUG) {
+    if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
+      console.error('Must include options reference JS for testing');
+    } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
+      console.error('Dygraphs is using property ' + name + ', which has no ' +
+                    'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
+      // Only log this error once.
+      Dygraph.OPTIONS_REFERENCE[name] = true;
+    }
+  }
+  return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name);
+};
+
+/**
+ * Returns the current value for an option, as set in the constructor or via
+ * updateOptions. You may pass in an (optional) series name to get per-series
+ * values for the option.
+ *
+ * All values returned by this method should be considered immutable. If you
+ * modify them, there is no guarantee that the changes will be honored or that
+ * dygraphs will remain in a consistent state. If you want to modify an option,
+ * use updateOptions() instead.
+ *
+ * @param {string} name The name of the option (e.g. 'strokeWidth')
+ * @param {string=} opt_seriesName Series name to get per-series values.
+ * @return {*} The value of the option.
+ */
+Dygraph.prototype.getOption = function(name, opt_seriesName) {
+  return this.attr_(name, opt_seriesName);
+};
+
+/**
+ * Like getOption(), but specifically returns a number.
+ * This is a convenience function for working with the Closure Compiler.
+ * @param {string} name The name of the option (e.g. 'strokeWidth')
+ * @param {string=} opt_seriesName Series name to get per-series values.
+ * @return {number} The value of the option.
+ * @private
+ */
+Dygraph.prototype.getNumericOption = function(name, opt_seriesName) {
+  return /** @type{number} */(this.getOption(name, opt_seriesName));
+};
+
+/**
+ * Like getOption(), but specifically returns a string.
+ * This is a convenience function for working with the Closure Compiler.
+ * @param {string} name The name of the option (e.g. 'strokeWidth')
+ * @param {string=} opt_seriesName Series name to get per-series values.
+ * @return {string} The value of the option.
+ * @private
+ */
+Dygraph.prototype.getStringOption = function(name, opt_seriesName) {
+  return /** @type{string} */(this.getOption(name, opt_seriesName));
+};
+
+/**
+ * Like getOption(), but specifically returns a boolean.
+ * This is a convenience function for working with the Closure Compiler.
+ * @param {string} name The name of the option (e.g. 'strokeWidth')
+ * @param {string=} opt_seriesName Series name to get per-series values.
+ * @return {boolean} The value of the option.
+ * @private
+ */
+Dygraph.prototype.getBooleanOption = function(name, opt_seriesName) {
+  return /** @type{boolean} */(this.getOption(name, opt_seriesName));
+};
+
+/**
+ * Like getOption(), but specifically returns a function.
+ * This is a convenience function for working with the Closure Compiler.
+ * @param {string} name The name of the option (e.g. 'strokeWidth')
+ * @param {string=} opt_seriesName Series name to get per-series values.
+ * @return {function(...)} The value of the option.
+ * @private
+ */
+Dygraph.prototype.getFunctionOption = function(name, opt_seriesName) {
+  return /** @type{function(...)} */(this.getOption(name, opt_seriesName));
+};
+
+Dygraph.prototype.getOptionForAxis = function(name, axis) {
+  return this.attributes_.getForAxis(name, axis);
+};
+
+/**
+ * @private
+ * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2')
+ * @return { ... } A function mapping string -> option value
+ */
+Dygraph.prototype.optionsViewForAxis_ = function(axis) {
+  var self = this;
+  return function(opt) {
+    var axis_opts = self.user_attrs_.axes;
+    if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
+      return axis_opts[axis][opt];
+    }
+
+    // I don't like that this is in a second spot.
+    if (axis === 'x' && opt === 'logscale') {
+      // return the default value.
+      // TODO(konigsberg): pull the default from a global default.
+      return false;
+    }
+
+    // user-specified attributes always trump defaults, even if they're less
+    // specific.
+    if (typeof(self.user_attrs_[opt]) != 'undefined') {
+      return self.user_attrs_[opt];
+    }
+
+    axis_opts = self.attrs_.axes;
+    if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
+      return axis_opts[axis][opt];
+    }
+    // check old-style axis options
+    // TODO(danvk): add a deprecation warning if either of these match.
+    if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) {
+      return self.axes_[0][opt];
+    } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) {
+      return self.axes_[1][opt];
+    }
+    return self.attr_(opt);
+  };
+};
+
+/**
+ * Returns the current rolling period, as set by the user or an option.
+ * @return {number} The number of points in the rolling window
+ */
+Dygraph.prototype.rollPeriod = function() {
+  return this.rollPeriod_;
+};
+
+/**
+ * Returns the currently-visible x-range. This can be affected by zooming,
+ * panning or a call to updateOptions.
+ * Returns a two-element array: [left, right].
+ * If the Dygraph has dates on the x-axis, these will be millis since epoch.
+ */
+Dygraph.prototype.xAxisRange = function() {
+  return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
+};
+
+/**
+ * Returns the lower- and upper-bound x-axis values of the
+ * data set.
+ */
+Dygraph.prototype.xAxisExtremes = function() {
+  var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w;
+  if (this.numRows() === 0) {
+    return [0 - pad, 1 + pad];
+  }
+  var left = this.rawData_[0][0];
+  var right = this.rawData_[this.rawData_.length - 1][0];
+  if (pad) {
+    // Must keep this in sync with dygraph-layout _evaluateLimits()
+    var range = right - left;
+    left -= range * pad;
+    right += range * pad;
+  }
+  return [left, right];
+};
+
+/**
+ * Returns the currently-visible y-range for an axis. This can be affected by
+ * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
+ * called with no arguments, returns the range of the first axis.
+ * Returns a two-element array: [bottom, top].
+ */
+Dygraph.prototype.yAxisRange = function(idx) {
+  if (typeof(idx) == "undefined") idx = 0;
+  if (idx < 0 || idx >= this.axes_.length) {
+    return null;
+  }
+  var axis = this.axes_[idx];
+  return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
+};
+
+/**
+ * Returns the currently-visible y-ranges for each axis. This can be affected by
+ * zooming, panning, calls to updateOptions, etc.
+ * Returns an array of [bottom, top] pairs, one for each y-axis.
+ */
+Dygraph.prototype.yAxisRanges = function() {
+  var ret = [];
+  for (var i = 0; i < this.axes_.length; i++) {
+    ret.push(this.yAxisRange(i));
+  }
+  return ret;
+};
+
+// TODO(danvk): use these functions throughout dygraphs.
+/**
+ * Convert from data coordinates to canvas/div X/Y coordinates.
+ * If specified, do this conversion for the coordinate system of a particular
+ * axis. Uses the first axis by default.
+ * Returns a two-element array: [X, Y]
+ *
+ * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
+ * instead of toDomCoords(null, y, axis).
+ */
+Dygraph.prototype.toDomCoords = function(x, y, axis) {
+  return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
+};
+
+/**
+ * Convert from data x coordinates to canvas/div X coordinate.
+ * If specified, do this conversion for the coordinate system of a particular
+ * axis.
+ * Returns a single value or null if x is null.
+ */
+Dygraph.prototype.toDomXCoord = function(x) {
+  if (x === null) {
+    return null;
+  }
+
+  var area = this.plotter_.area;
+  var xRange = this.xAxisRange();
+  return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
+};
+
+/**
+ * Convert from data x coordinates to canvas/div Y coordinate and optional
+ * axis. Uses the first axis by default.
+ *
+ * returns a single value or null if y is null.
+ */
+Dygraph.prototype.toDomYCoord = function(y, axis) {
+  var pct = this.toPercentYCoord(y, axis);
+
+  if (pct === null) {
+    return null;
+  }
+  var area = this.plotter_.area;
+  return area.y + pct * area.h;
+};
+
+/**
+ * Convert from canvas/div coords to data coordinates.
+ * If specified, do this conversion for the coordinate system of a particular
+ * axis. Uses the first axis by default.
+ * Returns a two-element array: [X, Y].
+ *
+ * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
+ * instead of toDataCoords(null, y, axis).
+ */
+Dygraph.prototype.toDataCoords = function(x, y, axis) {
+  return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
+};
+
+/**
+ * Convert from canvas/div x coordinate to data coordinate.
+ *
+ * If x is null, this returns null.
+ */
+Dygraph.prototype.toDataXCoord = function(x) {
+  if (x === null) {
+    return null;
+  }
+
+  var area = this.plotter_.area;
+  var xRange = this.xAxisRange();
+
+  if (!this.attributes_.getForAxis("logscale", 'x')) {
+    return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+  } else {
+    // TODO: remove duplicate code?
+    // Computing the inverse of toDomCoord.
+    var pct = (x - area.x) / area.w;
+
+    // Computing the inverse of toPercentXCoord. The function was arrived at with
+    // the following steps:
+    //
+    // Original calcuation:
+    // pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0])));
+    //
+    // Multiply both sides by the right-side demoninator.
+    // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
+    //
+    // add log(xRange[0]) to both sides
+    // log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) = log(x);
+    //
+    // Swap both sides of the equation,
+    // log(x) = log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0]))
+    //
+    // Use both sides as the exponent in 10^exp and we're done.
+    // x = 10 ^ (log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])))
+    var logr0 = Dygraph.log10(xRange[0]);
+    var logr1 = Dygraph.log10(xRange[1]);
+    var exponent = logr0 + (pct * (logr1 - logr0));
+    var value = Math.pow(Dygraph.LOG_SCALE, exponent);
+    return value;
+  }
+};
+
+/**
+ * Convert from canvas/div y coord to value.
+ *
+ * If y is null, this returns null.
+ * if axis is null, this uses the first axis.
+ */
+Dygraph.prototype.toDataYCoord = function(y, axis) {
+  if (y === null) {
+    return null;
+  }
+
+  var area = this.plotter_.area;
+  var yRange = this.yAxisRange(axis);
+
+  if (typeof(axis) == "undefined") axis = 0;
+  if (!this.attributes_.getForAxis("logscale", axis)) {
+    return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]);
+  } else {
+    // Computing the inverse of toDomCoord.
+    var pct = (y - area.y) / area.h;
+
+    // Computing the inverse of toPercentYCoord. The function was arrived at with
+    // the following steps:
+    //
+    // Original calcuation:
+    // pct = (log(yRange[1]) - log(y)) / (log(yRange[1]) - log(yRange[0]));
+    //
+    // Multiply both sides by the right-side demoninator.
+    // pct * (log(yRange[1]) - log(yRange[0])) = log(yRange[1]) - log(y);
+    //
+    // subtract log(yRange[1]) from both sides.
+    // (pct * (log(yRange[1]) - log(yRange[0]))) - log(yRange[1]) = -log(y);
+    //
+    // and multiply both sides by -1.
+    // log(yRange[1]) - (pct * (logr1 - log(yRange[0])) = log(y);
+    //
+    // Swap both sides of the equation,
+    // log(y) = log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0])));
+    //
+    // Use both sides as the exponent in 10^exp and we're done.
+    // y = 10 ^ (log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0]))));
+    var logr0 = Dygraph.log10(yRange[0]);
+    var logr1 = Dygraph.log10(yRange[1]);
+    var exponent = logr1 - (pct * (logr1 - logr0));
+    var value = Math.pow(Dygraph.LOG_SCALE, exponent);
+    return value;
+  }
+};
+
+/**
+ * Converts a y for an axis to a percentage from the top to the
+ * bottom of the drawing area.
+ *
+ * If the coordinate represents a value visible on the canvas, then
+ * the value will be between 0 and 1, where 0 is the top of the canvas.
+ * However, this method will return values outside the range, as
+ * values can fall outside the canvas.
+ *
+ * If y is null, this returns null.
+ * if axis is null, this uses the first axis.
+ *
+ * @param {number} y The data y-coordinate.
+ * @param {number} [axis] The axis number on which the data coordinate lives.
+ * @return {number} A fraction in [0, 1] where 0 = the top edge.
+ */
+Dygraph.prototype.toPercentYCoord = function(y, axis) {
+  if (y === null) {
+    return null;
+  }
+  if (typeof(axis) == "undefined") axis = 0;
+
+  var yRange = this.yAxisRange(axis);
+
+  var pct;
+  var logscale = this.attributes_.getForAxis("logscale", axis);
+  if (logscale) {
+    var logr0 = Dygraph.log10(yRange[0]);
+    var logr1 = Dygraph.log10(yRange[1]);
+    pct = (logr1 - Dygraph.log10(y)) / (logr1 - logr0);
+  } else {
+    // yRange[1] - y is unit distance from the bottom.
+    // yRange[1] - yRange[0] is the scale of the range.
+    // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
+    pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
+  }
+  return pct;
+};
+
+/**
+ * Converts an x value to a percentage from the left to the right of
+ * the drawing area.
+ *
+ * If the coordinate represents a value visible on the canvas, then
+ * the value will be between 0 and 1, where 0 is the left of the canvas.
+ * However, this method will return values outside the range, as
+ * values can fall outside the canvas.
+ *
+ * If x is null, this returns null.
+ * @param {number} x The data x-coordinate.
+ * @return {number} A fraction in [0, 1] where 0 = the left edge.
+ */
+Dygraph.prototype.toPercentXCoord = function(x) {
+  if (x === null) {
+    return null;
+  }
+
+  var xRange = this.xAxisRange();
+  var pct;
+  var logscale = this.attributes_.getForAxis("logscale", 'x') ;
+  if (logscale === true) {  // logscale can be null so we test for true explicitly.
+    var logr0 = Dygraph.log10(xRange[0]);
+    var logr1 = Dygraph.log10(xRange[1]);
+    pct = (Dygraph.log10(x) - logr0) / (logr1 - logr0);
+  } else {
+    // x - xRange[0] is unit distance from the left.
+    // xRange[1] - xRange[0] is the scale of the range.
+    // The full expression below is the % from the left.
+    pct = (x - xRange[0]) / (xRange[1] - xRange[0]);
+  }
+  return pct;
+};
+
+/**
+ * Returns the number of columns (including the independent variable).
+ * @return {number} The number of columns.
+ */
+Dygraph.prototype.numColumns = function() {
+  if (!this.rawData_) return 0;
+  return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ * @return {number} The number of rows, less any header.
+ */
+Dygraph.prototype.numRows = function() {
+  if (!this.rawData_) return 0;
+  return this.rawData_.length;
+};
+
+/**
+ * Returns the value in the given row and column. If the row and column exceed
+ * the bounds on the data, returns null. Also returns null if the value is
+ * missing.
+ * @param {number} row The row number of the data (0-based). Row 0 is the
+ *     first row of data, not a header row.
+ * @param {number} col The column number of the data (0-based)
+ * @return {number} The value in the specified cell or null if the row/col
+ *     were out of range.
+ */
+Dygraph.prototype.getValue = function(row, col) {
+  if (row < 0 || row > this.rawData_.length) return null;
+  if (col < 0 || col > this.rawData_[row].length) return null;
+
+  return this.rawData_[row][col];
+};
+
+/**
+ * Generates interface elements for the Dygraph: a containing div, a div to
+ * display the current point, and a textbox to adjust the rolling average
+ * period. Also creates the Renderer/Layout elements.
+ * @private
+ */
+Dygraph.prototype.createInterface_ = function() {
+  // Create the all-enclosing graph div
+  var enclosing = this.maindiv_;
+
+  this.graphDiv = document.createElement("div");
+
+  // TODO(danvk): any other styles that are useful to set here?
+  this.graphDiv.style.textAlign = 'left';  // This is a CSS "reset"
+  this.graphDiv.style.position = 'relative';
+  enclosing.appendChild(this.graphDiv);
+
+  // Create the canvas for interactive parts of the chart.
+  this.canvas_ = Dygraph.createCanvas();
+  this.canvas_.style.position = "absolute";
+
+  // ... and for static parts of the chart.
+  this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
+
+  this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
+  this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
+
+  this.resizeElements_();
+
+  // The interactive parts of the graph are drawn on top of the chart.
+  this.graphDiv.appendChild(this.hidden_);
+  this.graphDiv.appendChild(this.canvas_);
+  this.mouseEventElement_ = this.createMouseEventElement_();
+
+  // Create the grapher
+  this.layout_ = new DygraphLayout(this);
+
+  var dygraph = this;
+
+  this.mouseMoveHandler_ = function(e) {
+    dygraph.mouseMove_(e);
+  };
+
+  this.mouseOutHandler_ = function(e) {
+    // The mouse has left the chart if:
+    // 1. e.target is inside the chart
+    // 2. e.relatedTarget is outside the chart
+    var target = e.target || e.fromElement;
+    var relatedTarget = e.relatedTarget || e.toElement;
+    if (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) &&
+        !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) {
+      dygraph.mouseOut_(e);
+    }
+  };
+
+  this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_);
+  this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
+
+  // Don't recreate and register the resize handler on subsequent calls.
+  // This happens when the graph is resized.
+  if (!this.resizeHandler_) {
+    this.resizeHandler_ = function(e) {
+      dygraph.resize();
+    };
+
+    // Update when the window is resized.
+    // TODO(danvk): drop frames depending on complexity of the chart.
+    this.addAndTrackEvent(window, 'resize', this.resizeHandler_);
+  }
+};
+
+Dygraph.prototype.resizeElements_ = function() {
+  this.graphDiv.style.width = this.width_ + "px";
+  this.graphDiv.style.height = this.height_ + "px";
+
+  var canvasScale = Dygraph.getContextPixelRatio(this.canvas_ctx_);
+  this.canvas_.width = this.width_ * canvasScale;
+  this.canvas_.height = this.height_ * canvasScale;
+  this.canvas_.style.width = this.width_ + "px";    // for IE
+  this.canvas_.style.height = this.height_ + "px";  // for IE
+  if (canvasScale !== 1) {
+    this.canvas_ctx_.scale(canvasScale, canvasScale);
+  }
+
+  var hiddenScale = Dygraph.getContextPixelRatio(this.hidden_ctx_);
+  this.hidden_.width = this.width_ * hiddenScale;
+  this.hidden_.height = this.height_ * hiddenScale;
+  this.hidden_.style.width = this.width_ + "px";    // for IE
+  this.hidden_.style.height = this.height_ + "px";  // for IE
+  if (hiddenScale !== 1) {
+    this.hidden_ctx_.scale(hiddenScale, hiddenScale);
+  }
+};
+
+/**
+ * Detach DOM elements in the dygraph and null out all data references.
+ * Calling this when you're done with a dygraph can dramatically reduce memory
+ * usage. See, e.g., the tests/perf.html example.
+ */
+Dygraph.prototype.destroy = function() {
+  this.canvas_ctx_.restore();
+  this.hidden_ctx_.restore();
+
+  // Destroy any plugins, in the reverse order that they were registered.
+  for (var i = this.plugins_.length - 1; i >= 0; i--) {
+    var p = this.plugins_.pop();
+    if (p.plugin.destroy) p.plugin.destroy();
+  }
+
+  var removeRecursive = function(node) {
+    while (node.hasChildNodes()) {
+      removeRecursive(node.firstChild);
+      node.removeChild(node.firstChild);
+    }
+  };
+
+  this.removeTrackedEvents_();
+
+  // remove mouse event handlers (This may not be necessary anymore)
+  Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_);
+  Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
+
+  // remove window handlers
+  Dygraph.removeEvent(window,'resize', this.resizeHandler_);
+  this.resizeHandler_ = null;
+
+  removeRecursive(this.maindiv_);
+
+  var nullOut = function(obj) {
+    for (var n in obj) {
+      if (typeof(obj[n]) === 'object') {
+        obj[n] = null;
+      }
+    }
+  };
+  // These may not all be necessary, but it can't hurt...
+  nullOut(this.layout_);
+  nullOut(this.plotter_);
+  nullOut(this);
+};
+
+/**
+ * Creates the canvas on which the chart will be drawn. Only the Renderer ever
+ * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
+ * or the zoom rectangles) is done on this.canvas_.
+ * @param {Object} canvas The Dygraph canvas over which to overlay the plot
+ * @return {Object} The newly-created canvas
+ * @private
+ */
+Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
+  var h = Dygraph.createCanvas();
+  h.style.position = "absolute";
+  // TODO(danvk): h should be offset from canvas. canvas needs to include
+  // some extra area to make it easier to zoom in on the far left and far
+  // right. h needs to be precisely the plot area, so that clipping occurs.
+  h.style.top = canvas.style.top;
+  h.style.left = canvas.style.left;
+  h.width = this.width_;
+  h.height = this.height_;
+  h.style.width = this.width_ + "px";    // for IE
+  h.style.height = this.height_ + "px";  // for IE
+  return h;
+};
+
+/**
+ * Creates an overlay element used to handle mouse events.
+ * @return {Object} The mouse event element.
+ * @private
+ */
+Dygraph.prototype.createMouseEventElement_ = function() {
+  return this.canvas_;
+};
+
+/**
+ * Generate a set of distinct colors for the data series. This is done with a
+ * color wheel. Saturation/Value are customizable, and the hue is
+ * equally-spaced around the color wheel. If a custom set of colors is
+ * specified, that is used instead.
+ * @private
+ */
+Dygraph.prototype.setColors_ = function() {
+  var labels = this.getLabels();
+  var num = labels.length - 1;
+  this.colors_ = [];
+  this.colorsMap_ = {};
+
+  // These are used for when no custom colors are specified.
+  var sat = this.getNumericOption('colorSaturation') || 1.0;
+  var val = this.getNumericOption('colorValue') || 0.5;
+  var half = Math.ceil(num / 2);
+
+  var colors = this.getOption('colors');
+  var visibility = this.visibility();
+  for (var i = 0; i < num; i++) {
+    if (!visibility[i]) {
+      continue;
+    }
+    var label = labels[i + 1];
+    var colorStr = this.attributes_.getForSeries('color', label);
+    if (!colorStr) {
+      if (colors) {
+        colorStr = colors[i % colors.length];
+      } else {
+        // alternate colors for high contrast.
+        var idx = i % 2 ? (half + (i + 1)/ 2) : Math.ceil((i + 1) / 2);
+        var hue = (1.0 * idx / (1 + num));
+        colorStr = Dygraph.hsvToRGB(hue, sat, val);
+      }
+    }
+    this.colors_.push(colorStr);
+    this.colorsMap_[label] = colorStr;
+  }
+};
+
+/**
+ * Return the list of colors. This is either the list of colors passed in the
+ * attributes or the autogenerated list of rgb(r,g,b) strings.
+ * This does not return colors for invisible series.
+ * @return {Array.<string>} The list of colors.
+ */
+Dygraph.prototype.getColors = function() {
+  return this.colors_;
+};
+
+/**
+ * Returns a few attributes of a series, i.e. its color, its visibility, which
+ * axis it's assigned to, and its column in the original data.
+ * Returns null if the series does not exist.
+ * Otherwise, returns an object with column, visibility, color and axis properties.
+ * The "axis" property will be set to 1 for y1 and 2 for y2.
+ * The "column" property can be fed back into getValue(row, column) to get
+ * values for this series.
+ */
+Dygraph.prototype.getPropertiesForSeries = function(series_name) {
+  var idx = -1;
+  var labels = this.getLabels();
+  for (var i = 1; i < labels.length; i++) {
+    if (labels[i] == series_name) {
+      idx = i;
+      break;
+    }
+  }
+  if (idx == -1) return null;
+
+  return {
+    name: series_name,
+    column: idx,
+    visible: this.visibility()[idx - 1],
+    color: this.colorsMap_[series_name],
+    axis: 1 + this.attributes_.axisForSeries(series_name)
+  };
+};
+
+/**
+ * Create the text box to adjust the averaging period
+ * @private
+ */
+Dygraph.prototype.createRollInterface_ = function() {
+  // Create a roller if one doesn't exist already.
+  if (!this.roller_) {
+    this.roller_ = document.createElement("input");
+    this.roller_.type = "text";
+    this.roller_.style.display = "none";
+    this.graphDiv.appendChild(this.roller_);
+  }
+
+  var display = this.getBooleanOption('showRoller') ? 'block' : 'none';
+
+  var area = this.plotter_.area;
+  var textAttr = { "position": "absolute",
+                   "zIndex": 10,
+                   "top": (area.y + area.h - 25) + "px",
+                   "left": (area.x + 1) + "px",
+                   "display": display
+                  };
+  this.roller_.size = "2";
+  this.roller_.value = this.rollPeriod_;
+  for (var name in textAttr) {
+    if (textAttr.hasOwnProperty(name)) {
+      this.roller_.style[name] = textAttr[name];
+    }
+  }
+
+  var dygraph = this;
+  this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
+};
+
+/**
+ * Set up all the mouse handlers needed to capture dragging behavior for zoom
+ * events.
+ * @private
+ */
+Dygraph.prototype.createDragInterface_ = function() {
+  var context = {
+    // Tracks whether the mouse is down right now
+    isZooming: false,
+    isPanning: false,  // is this drag part of a pan?
+    is2DPan: false,    // if so, is that pan 1- or 2-dimensional?
+    dragStartX: null, // pixel coordinates
+    dragStartY: null, // pixel coordinates
+    dragEndX: null, // pixel coordinates
+    dragEndY: null, // pixel coordinates
+    dragDirection: null,
+    prevEndX: null, // pixel coordinates
+    prevEndY: null, // pixel coordinates
+    prevDragDirection: null,
+    cancelNextDblclick: false,  // see comment in dygraph-interaction-model.js
+
+    // The value on the left side of the graph when a pan operation starts.
+    initialLeftmostDate: null,
+
+    // The number of units each pixel spans. (This won't be valid for log
+    // scales)
+    xUnitsPerPixel: null,
+
+    // TODO(danvk): update this comment
+    // The range in second/value units that the viewport encompasses during a
+    // panning operation.
+    dateRange: null,
+
+    // Top-left corner of the canvas, in DOM coords
+    // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
+    px: 0,
+    py: 0,
+
+    // Values for use with panEdgeFraction, which limit how far outside the
+    // graph's data boundaries it can be panned.
+    boundedDates: null, // [minDate, maxDate]
+    boundedValues: null, // [[minValue, maxValue] ...]
+
+    // We cover iframes during mouse interactions. See comments in
+    // dygraph-utils.js for more info on why this is a good idea.
+    tarp: new Dygraph.IFrameTarp(),
+
+    // contextB is the same thing as this context object but renamed.
+    initializeMouseDown: function(event, g, contextB) {
+      // prevents mouse drags from selecting page text.
+      if (event.preventDefault) {
+        event.preventDefault();  // Firefox, Chrome, etc.
+      } else {
+        event.returnValue = false;  // IE
+        event.cancelBubble = true;
+      }
+
+      var canvasPos = Dygraph.findPos(g.canvas_);
+      contextB.px = canvasPos.x;
+      contextB.py = canvasPos.y;
+      contextB.dragStartX = Dygraph.dragGetX_(event, contextB);
+      contextB.dragStartY = Dygraph.dragGetY_(event, contextB);
+      contextB.cancelNextDblclick = false;
+      contextB.tarp.cover();
+    },
+    destroy: function() {
+      var context = this;
+      if (context.isZooming || context.isPanning) {
+        context.isZooming = false;
+        context.dragStartX = null;
+        context.dragStartY = null;
+      }
+
+      if (context.isPanning) {
+        context.isPanning = false;
+        context.draggingDate = null;
+        context.dateRange = null;
+        for (var i = 0; i < self.axes_.length; i++) {
+          delete self.axes_[i].draggingValue;
+          delete self.axes_[i].dragValueRange;
+        }
+      }
+
+      context.tarp.uncover();
+    }
+  };
+
+  var interactionModel = this.getOption("interactionModel");
+
+  // Self is the graph.
+  var self = this;
+
+  // Function that binds the graph and context to the handler.
+  var bindHandler = function(handler) {
+    return function(event) {
+      handler(event, self, context);
+    };
+  };
+
+  for (var eventName in interactionModel) {
+    if (!interactionModel.hasOwnProperty(eventName)) continue;
+    this.addAndTrackEvent(this.mouseEventElement_, eventName,
+        bindHandler(interactionModel[eventName]));
+  }
+
+  // If the user releases the mouse button during a drag, but not over the
+  // canvas, then it doesn't count as a zooming action.
+  if (!interactionModel.willDestroyContextMyself) {
+    var mouseUpHandler = function(event) {
+      context.destroy();
+    };
+
+    this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
+  }
+};
+
+/**
+ * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
+ * up any previous zoom rectangles that were drawn. This could be optimized to
+ * avoid extra redrawing, but it's tricky to avoid interactions with the status
+ * dots.
+ *
+ * @param {number} direction the direction of the zoom rectangle. Acceptable
+ *     values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
+ * @param {number} startX The X position where the drag started, in canvas
+ *     coordinates.
+ * @param {number} endX The current X position of the drag, in canvas coords.
+ * @param {number} startY The Y position where the drag started, in canvas
+ *     coordinates.
+ * @param {number} endY The current Y position of the drag, in canvas coords.
+ * @param {number} prevDirection the value of direction on the previous call to
+ *     this function. Used to avoid excess redrawing
+ * @param {number} prevEndX The value of endX on the previous call to this
+ *     function. Used to avoid excess redrawing
+ * @param {number} prevEndY The value of endY on the previous call to this
+ *     function. Used to avoid excess redrawing
+ * @private
+ */
+Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
+                                           endY, prevDirection, prevEndX,
+                                           prevEndY) {
+  var ctx = this.canvas_ctx_;
+
+  // Clean up from the previous rect if necessary
+  if (prevDirection == Dygraph.HORIZONTAL) {
+    ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y,
+                  Math.abs(startX - prevEndX), this.layout_.getPlotArea().h);
+  } else if (prevDirection == Dygraph.VERTICAL) {
+    ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY),
+                  this.layout_.getPlotArea().w, Math.abs(startY - prevEndY));
+  }
+
+  // Draw a light-grey rectangle to show the new viewing area
+  if (direction == Dygraph.HORIZONTAL) {
+    if (endX && startX) {
+      ctx.fillStyle = "rgba(128,128,128,0.33)";
+      ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y,
+                   Math.abs(endX - startX), this.layout_.getPlotArea().h);
+    }
+  } else if (direction == Dygraph.VERTICAL) {
+    if (endY && startY) {
+      ctx.fillStyle = "rgba(128,128,128,0.33)";
+      ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY),
+                   this.layout_.getPlotArea().w, Math.abs(endY - startY));
+    }
+  }
+};
+
+/**
+ * Clear the zoom rectangle (and perform no zoom).
+ * @private
+ */
+Dygraph.prototype.clearZoomRect_ = function() {
+  this.currentZoomRectArgs_ = null;
+  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
+};
+
+/**
+ * Zoom to something containing [lowX, highX]. These are pixel coordinates in
+ * the canvas. The exact zoom window may be slightly larger if there are no data
+ * points near lowX or highX. Don't confuse this function with doZoomXDates,
+ * which accepts dates that match the raw data. This function redraws the graph.
+ *
+ * @param {number} lowX The leftmost pixel value that should be visible.
+ * @param {number} highX The rightmost pixel value that should be visible.
+ * @private
+ */
+Dygraph.prototype.doZoomX_ = function(lowX, highX) {
+  this.currentZoomRectArgs_ = null;
+  // Find the earliest and latest dates contained in this canvasx range.
+  // Convert the call to date ranges of the raw data.
+  var minDate = this.toDataXCoord(lowX);
+  var maxDate = this.toDataXCoord(highX);
+  this.doZoomXDates_(minDate, maxDate);
+};
+
+/**
+ * Zoom to something containing [minDate, maxDate] values. Don't confuse this
+ * method with doZoomX which accepts pixel coordinates. This function redraws
+ * the graph.
+ *
+ * @param {number} minDate The minimum date that should be visible.
+ * @param {number} maxDate The maximum date that should be visible.
+ * @private
+ */
+Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
+  // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation
+  // can produce strange effects. Rather than the x-axis transitioning slowly
+  // between values, it can jerk around.)
+  var old_window = this.xAxisRange();
+  var new_window = [minDate, maxDate];
+  this.zoomed_x_ = true;
+  var that = this;
+  this.doAnimatedZoom(old_window, new_window, null, null, function() {
+    if (that.getFunctionOption("zoomCallback")) {
+      that.getFunctionOption("zoomCallback").call(that,
+          minDate, maxDate, that.yAxisRanges());
+    }
+  });
+};
+
+/**
+ * Zoom to something containing [lowY, highY]. These are pixel coordinates in
+ * the canvas. This function redraws the graph.
+ *
+ * @param {number} lowY The topmost pixel value that should be visible.
+ * @param {number} highY The lowest pixel value that should be visible.
+ * @private
+ */
+Dygraph.prototype.doZoomY_ = function(lowY, highY) {
+  this.currentZoomRectArgs_ = null;
+  // Find the highest and lowest values in pixel range for each axis.
+  // Note that lowY (in pixels) corresponds to the max Value (in data coords).
+  // This is because pixels increase as you go down on the screen, whereas data
+  // coordinates increase as you go up the screen.
+  var oldValueRanges = this.yAxisRanges();
+  var newValueRanges = [];
+  for (var i = 0; i < this.axes_.length; i++) {
+    var hi = this.toDataYCoord(lowY, i);
+    var low = this.toDataYCoord(highY, i);
+    newValueRanges.push([low, hi]);
+  }
+
+  this.zoomed_y_ = true;
+  var that = this;
+  this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() {
+    if (that.getFunctionOption("zoomCallback")) {
+      var xRange = that.xAxisRange();
+      that.getFunctionOption("zoomCallback").call(that,
+          xRange[0], xRange[1], that.yAxisRanges());
+    }
+  });
+};
+
+/**
+ * Transition function to use in animations. Returns values between 0.0
+ * (totally old values) and 1.0 (totally new values) for each frame.
+ * @private
+ */
+Dygraph.zoomAnimationFunction = function(frame, numFrames) {
+  var k = 1.5;
+  return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames));
+};
+
+/**
+ * Reset the zoom to the original view coordinates. This is the same as
+ * double-clicking on the graph.
+ */
+Dygraph.prototype.resetZoom = function() {
+  var dirty = false, dirtyX = false, dirtyY = false;
+  if (this.dateWindow_ !== null) {
+    dirty = true;
+    dirtyX = true;
+  }
+
+  for (var i = 0; i < this.axes_.length; i++) {
+    if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) {
+      dirty = true;
+      dirtyY = true;
+    }
+  }
+
+  // Clear any selection, since it's likely to be drawn in the wrong place.
+  this.clearSelection();
+
+  if (dirty) {
+    this.zoomed_x_ = false;
+    this.zoomed_y_ = false;
+
+    var minDate = this.rawData_[0][0];
+    var maxDate = this.rawData_[this.rawData_.length - 1][0];
+
+    // With only one frame, don't bother calculating extreme ranges.
+    // TODO(danvk): merge this block w/ the code below.
+    if (!this.getBooleanOption("animatedZooms")) {
+      this.dateWindow_ = null;
+      for (i = 0; i < this.axes_.length; i++) {
+        if (this.axes_[i].valueWindow !== null) {
+          delete this.axes_[i].valueWindow;
+        }
+      }
+      this.drawGraph_();
+      if (this.getFunctionOption("zoomCallback")) {
+        this.getFunctionOption("zoomCallback").call(this,
+            minDate, maxDate, this.yAxisRanges());
+      }
+      return;
+    }
+
+    var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null;
+    if (dirtyX) {
+      oldWindow = this.xAxisRange();
+      newWindow = [minDate, maxDate];
+    }
+
+    if (dirtyY) {
+      oldValueRanges = this.yAxisRanges();
+      // TODO(danvk): this is pretty inefficient
+      var packed = this.gatherDatasets_(this.rolledSeries_, null);
+      var extremes = packed.extremes;
+
+      // this has the side-effect of modifying this.axes_.
+      // this doesn't make much sense in this context, but it's convenient (we
+      // need this.axes_[*].extremeValues) and not harmful since we'll be
+      // calling drawGraph_ shortly, which clobbers these values.
+      this.computeYAxisRanges_(extremes);
+
+      newValueRanges = [];
+      for (i = 0; i < this.axes_.length; i++) {
+        var axis = this.axes_[i];
+        newValueRanges.push((axis.valueRange !== null &&
+                             axis.valueRange !== undefined) ?
+                            axis.valueRange : axis.extremeRange);
+      }
+    }
+
+    var that = this;
+    this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges,
+        function() {
+          that.dateWindow_ = null;
+          for (var i = 0; i < that.axes_.length; i++) {
+            if (that.axes_[i].valueWindow !== null) {
+              delete that.axes_[i].valueWindow;
+            }
+          }
+          if (that.getFunctionOption("zoomCallback")) {
+            that.getFunctionOption("zoomCallback").call(that,
+                minDate, maxDate, that.yAxisRanges());
+          }
+        });
+  }
+};
+
+/**
+ * Combined animation logic for all zoom functions.
+ * either the x parameters or y parameters may be null.
+ * @private
+ */
+Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) {
+  var steps = this.getBooleanOption("animatedZooms") ?
+      Dygraph.ANIMATION_STEPS : 1;
+
+  var windows = [];
+  var valueRanges = [];
+  var step, frac;
+
+  if (oldXRange !== null && newXRange !== null) {
+    for (step = 1; step <= steps; step++) {
+      frac = Dygraph.zoomAnimationFunction(step, steps);
+      windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0],
+                         oldXRange[1]*(1-frac) + frac*newXRange[1]];
+    }
+  }
+
+  if (oldYRanges !== null && newYRanges !== null) {
+    for (step = 1; step <= steps; step++) {
+      frac = Dygraph.zoomAnimationFunction(step, steps);
+      var thisRange = [];
+      for (var j = 0; j < this.axes_.length; j++) {
+        thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0],
+                        oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]);
+      }
+      valueRanges[step-1] = thisRange;
+    }
+  }
+
+  var that = this;
+  Dygraph.repeatAndCleanup(function(step) {
+    if (valueRanges.length) {
+      for (var i = 0; i < that.axes_.length; i++) {
+        var w = valueRanges[step][i];
+        that.axes_[i].valueWindow = [w[0], w[1]];
+      }
+    }
+    if (windows.length) {
+      that.dateWindow_ = windows[step];
+    }
+    that.drawGraph_();
+  }, steps, Dygraph.ANIMATION_DURATION / steps, callback);
+};
+
+/**
+ * Get the current graph's area object.
+ *
+ * Returns: {x, y, w, h}
+ */
+Dygraph.prototype.getArea = function() {
+  return this.plotter_.area;
+};
+
+/**
+ * Convert a mouse event to DOM coordinates relative to the graph origin.
+ *
+ * Returns a two-element array: [X, Y].
+ */
+Dygraph.prototype.eventToDomCoords = function(event) {
+  if (event.offsetX && event.offsetY) {
+    return [ event.offsetX, event.offsetY ];
+  } else {
+    var eventElementPos = Dygraph.findPos(this.mouseEventElement_);
+    var canvasx = Dygraph.pageX(event) - eventElementPos.x;
+    var canvasy = Dygraph.pageY(event) - eventElementPos.y;
+    return [canvasx, canvasy];
+  }
+};
+
+/**
+ * Given a canvas X coordinate, find the closest row.
+ * @param {number} domX graph-relative DOM X coordinate
+ * Returns {number} row number.
+ * @private
+ */
+Dygraph.prototype.findClosestRow = function(domX) {
+  var minDistX = Infinity;
+  var closestRow = -1;
+  var sets = this.layout_.points;
+  for (var i = 0; i < sets.length; i++) {
+    var points = sets[i];
+    var len = points.length;
+    for (var j = 0; j < len; j++) {
+      var point = points[j];
+      if (!Dygraph.isValidPoint(point, true)) continue;
+      var dist = Math.abs(point.canvasx - domX);
+      if (dist < minDistX) {
+        minDistX = dist;
+        closestRow = point.idx;
+      }
+    }
+  }
+
+  return closestRow;
+};
+
+/**
+ * Given canvas X,Y coordinates, find the closest point.
+ *
+ * This finds the individual data point across all visible series
+ * that's closest to the supplied DOM coordinates using the standard
+ * Euclidean X,Y distance.
+ *
+ * @param {number} domX graph-relative DOM X coordinate
+ * @param {number} domY graph-relative DOM Y coordinate
+ * Returns: {row, seriesName, point}
+ * @private
+ */
+Dygraph.prototype.findClosestPoint = function(domX, domY) {
+  var minDist = Infinity;
+  var dist, dx, dy, point, closestPoint, closestSeries, closestRow;
+  for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) {
+    var points = this.layout_.points[setIdx];
+    for (var i = 0; i < points.length; ++i) {
+      point = points[i];
+      if (!Dygraph.isValidPoint(point)) continue;
+      dx = point.canvasx - domX;
+      dy = point.canvasy - domY;
+      dist = dx * dx + dy * dy;
+      if (dist < minDist) {
+        minDist = dist;
+        closestPoint = point;
+        closestSeries = setIdx;
+        closestRow = point.idx;
+      }
+    }
+  }
+  var name = this.layout_.setNames[closestSeries];
+  return {
+    row: closestRow,
+    seriesName: name,
+    point: closestPoint
+  };
+};
+
+/**
+ * Given canvas X,Y coordinates, find the touched area in a stacked graph.
+ *
+ * This first finds the X data point closest to the supplied DOM X coordinate,
+ * then finds the series which puts the Y coordinate on top of its filled area,
+ * using linear interpolation between adjacent point pairs.
+ *
+ * @param {number} domX graph-relative DOM X coordinate
+ * @param {number} domY graph-relative DOM Y coordinate
+ * Returns: {row, seriesName, point}
+ * @private
+ */
+Dygraph.prototype.findStackedPoint = function(domX, domY) {
+  var row = this.findClosestRow(domX);
+  var closestPoint, closestSeries;
+  for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
+    var boundary = this.getLeftBoundary_(setIdx);
+    var rowIdx = row - boundary;
+    var points = this.layout_.points[setIdx];
+    if (rowIdx >= points.length) continue;
+    var p1 = points[rowIdx];
+    if (!Dygraph.isValidPoint(p1)) continue;
+    var py = p1.canvasy;
+    if (domX > p1.canvasx && rowIdx + 1 < points.length) {
+      // interpolate series Y value using next point
+      var p2 = points[rowIdx + 1];
+      if (Dygraph.isValidPoint(p2)) {
+        var dx = p2.canvasx - p1.canvasx;
+        if (dx > 0) {
+          var r = (domX - p1.canvasx) / dx;
+          py += r * (p2.canvasy - p1.canvasy);
+        }
+      }
+    } else if (domX < p1.canvasx && rowIdx > 0) {
+      // interpolate series Y value using previous point
+      var p0 = points[rowIdx - 1];
+      if (Dygraph.isValidPoint(p0)) {
+        var dx = p1.canvasx - p0.canvasx;
+        if (dx > 0) {
+          var r = (p1.canvasx - domX) / dx;
+          py += r * (p0.canvasy - p1.canvasy);
+        }
+      }
+    }
+    // Stop if the point (domX, py) is above this series' upper edge
+    if (setIdx === 0 || py < domY) {
+      closestPoint = p1;
+      closestSeries = setIdx;
+    }
+  }
+  var name = this.layout_.setNames[closestSeries];
+  return {
+    row: row,
+    seriesName: name,
+    point: closestPoint
+  };
+};
+
+/**
+ * When the mouse moves in the canvas, display information about a nearby data
+ * point and draw dots over those points in the data series. This function
+ * takes care of cleanup of previously-drawn dots.
+ * @param {Object} event The mousemove event from the browser.
+ * @private
+ */
+Dygraph.prototype.mouseMove_ = function(event) {
+  // This prevents JS errors when mousing over the canvas before data loads.
+  var points = this.layout_.points;
+  if (points === undefined || points === null) return;
+
+  var canvasCoords = this.eventToDomCoords(event);
+  var canvasx = canvasCoords[0];
+  var canvasy = canvasCoords[1];
+
+  var highlightSeriesOpts = this.getOption("highlightSeriesOpts");
+  var selectionChanged = false;
+  if (highlightSeriesOpts && !this.isSeriesLocked()) {
+    var closest;
+    if (this.getBooleanOption("stackedGraph")) {
+      closest = this.findStackedPoint(canvasx, canvasy);
+    } else {
+      closest = this.findClosestPoint(canvasx, canvasy);
+    }
+    selectionChanged = this.setSelection(closest.row, closest.seriesName);
+  } else {
+    var idx = this.findClosestRow(canvasx);
+    selectionChanged = this.setSelection(idx);
+  }
+
+  var callback = this.getFunctionOption("highlightCallback");
+  if (callback && selectionChanged) {
+    callback.call(this, event,
+        this.lastx_,
+        this.selPoints_,
+        this.lastRow_,
+        this.highlightSet_);
+  }
+};
+
+/**
+ * Fetch left offset from the specified set index or if not passed, the
+ * first defined boundaryIds record (see bug #236).
+ * @private
+ */
+Dygraph.prototype.getLeftBoundary_ = function(setIdx) {
+  if (this.boundaryIds_[setIdx]) {
+      return this.boundaryIds_[setIdx][0];
+  } else {
+    for (var i = 0; i < this.boundaryIds_.length; i++) {
+      if (this.boundaryIds_[i] !== undefined) {
+        return this.boundaryIds_[i][0];
+      }
+    }
+    return 0;
+  }
+};
+
+Dygraph.prototype.animateSelection_ = function(direction) {
+  var totalSteps = 10;
+  var millis = 30;
+  if (this.fadeLevel === undefined) this.fadeLevel = 0;
+  if (this.animateId === undefined) this.animateId = 0;
+  var start = this.fadeLevel;
+  var steps = direction < 0 ? start : totalSteps - start;
+  if (steps <= 0) {
+    if (this.fadeLevel) {
+      this.updateSelection_(1.0);
+    }
+    return;
+  }
+
+  var thisId = ++this.animateId;
+  var that = this;
+  var cleanupIfClearing = function() {
+    // if we haven't reached fadeLevel 0 in the max frame time,
+    // ensure that the clear happens and just go to 0
+    if (that.fadeLevel !== 0 && direction < 0) {
+      that.fadeLevel = 0;
+      that.clearSelection();
+    }
+  };
+  Dygraph.repeatAndCleanup(
+    function(n) {
+      // ignore simultaneous animations
+      if (that.animateId != thisId) return;
+
+      that.fadeLevel += direction;
+      if (that.fadeLevel === 0) {
+        that.clearSelection();
+      } else {
+        that.updateSelection_(that.fadeLevel / totalSteps);
+      }
+    },
+    steps, millis, cleanupIfClearing);
+};
+
+/**
+ * Draw dots over the selectied points in the data series. This function
+ * takes care of cleanup of previously-drawn dots.
+ * @private
+ */
+Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
+  /*var defaultPrevented = */
+  this.cascadeEvents_('select', {
+    selectedRow: this.lastRow_,
+    selectedX: this.lastx_,
+    selectedPoints: this.selPoints_
+  });
+  // TODO(danvk): use defaultPrevented here?
+
+  // Clear the previously drawn vertical, if there is one
+  var i;
+  var ctx = this.canvas_ctx_;
+  if (this.getOption('highlightSeriesOpts')) {
+    ctx.clearRect(0, 0, this.width_, this.height_);
+    var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
+    if (alpha) {
+      // Activating background fade includes an animation effect for a gradual
+      // fade. TODO(klausw): make this independently configurable if it causes
+      // issues? Use a shared preference to control animations?
+      var animateBackgroundFade = true;
+      if (animateBackgroundFade) {
+        if (opt_animFraction === undefined) {
+          // start a new animation
+          this.animateSelection_(1);
+          return;
+        }
+        alpha *= opt_animFraction;
+      }
+      ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
+      ctx.fillRect(0, 0, this.width_, this.height_);
+    }
+
+    // Redraw only the highlighted series in the interactive canvas (not the
+    // static plot canvas, which is where series are usually drawn).
+    this.plotter_._renderLineChart(this.highlightSet_, ctx);
+  } else if (this.previousVerticalX_ >= 0) {
+    // Determine the maximum highlight circle size.
+    var maxCircleSize = 0;
+    var labels = this.attr_('labels');
+    for (i = 1; i < labels.length; i++) {
+      var r = this.getNumericOption('highlightCircleSize', labels[i]);
+      if (r > maxCircleSize) maxCircleSize = r;
+    }
+    var px = this.previousVerticalX_;
+    ctx.clearRect(px - maxCircleSize - 1, 0,
+                  2 * maxCircleSize + 2, this.height_);
+  }
+
+  if (this.selPoints_.length > 0) {
+    // Draw colored circles over the center of each selected point
+    var canvasx = this.selPoints_[0].canvasx;
+    ctx.save();
+    for (i = 0; i < this.selPoints_.length; i++) {
+      var pt = this.selPoints_[i];
+      if (!Dygraph.isOK(pt.canvasy)) continue;
+
+      var circleSize = this.getNumericOption('highlightCircleSize', pt.name);
+      var callback = this.getFunctionOption("drawHighlightPointCallback", pt.name);
+      var color = this.plotter_.colors[pt.name];
+      if (!callback) {
+        callback = Dygraph.Circles.DEFAULT;
+      }
+      ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name);
+      ctx.strokeStyle = color;
+      ctx.fillStyle = color;
+      callback.call(this, this, pt.name, ctx, canvasx, pt.canvasy,
+          color, circleSize, pt.idx);
+    }
+    ctx.restore();
+
+    this.previousVerticalX_ = canvasx;
+  }
+};
+
+/**
+ * Manually set the selected points and display information about them in the
+ * legend. The selection can be cleared using clearSelection() and queried
+ * using getSelection().
+ * @param {number} row Row number that should be highlighted (i.e. appear with
+ * hover dots on the chart).
+ * @param {seriesName} optional series name to highlight that series with the
+ * the highlightSeriesOpts setting.
+ * @param { locked } optional If true, keep seriesName selected when mousing
+ * over the graph, disabling closest-series highlighting. Call clearSelection()
+ * to unlock it.
+ */
+Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) {
+  // Extract the points we've selected
+  this.selPoints_ = [];
+
+  var changed = false;
+  if (row !== false && row >= 0) {
+    if (row != this.lastRow_) changed = true;
+    this.lastRow_ = row;
+    for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
+      var points = this.layout_.points[setIdx];
+      // Check if the point at the appropriate index is the point we're looking
+      // for.  If it is, just use it, otherwise search the array for a point
+      // in the proper place.
+      var setRow = row - this.getLeftBoundary_(setIdx);
+      if (setRow < points.length && points[setRow].idx == row) {
+        var point = points[setRow];
+        if (point.yval !== null) this.selPoints_.push(point);
+      } else {
+        for (var pointIdx = 0; pointIdx < points.length; ++pointIdx) {
+          var point = points[pointIdx];
+          if (point.idx == row) {
+            if (point.yval !== null) {
+              this.selPoints_.push(point);
+            }
+            break;
+          }
+        }
+      }
+    }
+  } else {
+    if (this.lastRow_ >= 0) changed = true;
+    this.lastRow_ = -1;
+  }
+
+  if (this.selPoints_.length) {
+    this.lastx_ = this.selPoints_[0].xval;
+  } else {
+    this.lastx_ = -1;
+  }
+
+  if (opt_seriesName !== undefined) {
+    if (this.highlightSet_ !== opt_seriesName) changed = true;
+    this.highlightSet_ = opt_seriesName;
+  }
+
+  if (opt_locked !== undefined) {
+    this.lockedSet_ = opt_locked;
+  }
+
+  if (changed) {
+    this.updateSelection_(undefined);
+  }
+  return changed;
+};
+
+/**
+ * The mouse has left the canvas. Clear out whatever artifacts remain
+ * @param {Object} event the mouseout event from the browser.
+ * @private
+ */
+Dygraph.prototype.mouseOut_ = function(event) {
+  if (this.getFunctionOption("unhighlightCallback")) {
+    this.getFunctionOption("unhighlightCallback").call(this, event);
+  }
+
+  if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_) {
+    this.clearSelection();
+  }
+};
+
+/**
+ * Clears the current selection (i.e. points that were highlighted by moving
+ * the mouse over the chart).
+ */
+Dygraph.prototype.clearSelection = function() {
+  this.cascadeEvents_('deselect', {});
+
+  this.lockedSet_ = false;
+  // Get rid of the overlay data
+  if (this.fadeLevel) {
+    this.animateSelection_(-1);
+    return;
+  }
+  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
+  this.fadeLevel = 0;
+  this.selPoints_ = [];
+  this.lastx_ = -1;
+  this.lastRow_ = -1;
+  this.highlightSet_ = null;
+};
+
+/**
+ * Returns the number of the currently selected row. To get data for this row,
+ * you can use the getValue method.
+ * @return {number} row number, or -1 if nothing is selected
+ */
+Dygraph.prototype.getSelection = function() {
+  if (!this.selPoints_ || this.selPoints_.length < 1) {
+    return -1;
+  }
+
+  for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) {
+    var points = this.layout_.points[setIdx];
+    for (var row = 0; row < points.length; row++) {
+      if (points[row].x == this.selPoints_[0].x) {
+        return points[row].idx;
+      }
+    }
+  }
+  return -1;
+};
+
+/**
+ * Returns the name of the currently-highlighted series.
+ * Only available when the highlightSeriesOpts option is in use.
+ */
+Dygraph.prototype.getHighlightSeries = function() {
+  return this.highlightSet_;
+};
+
+/**
+ * Returns true if the currently-highlighted series was locked
+ * via setSelection(..., seriesName, true).
+ */
+Dygraph.prototype.isSeriesLocked = function() {
+  return this.lockedSet_;
+};
+
+/**
+ * Fires when there's data available to be graphed.
+ * @param {string} data Raw CSV data to be plotted
+ * @private
+ */
+Dygraph.prototype.loadedEvent_ = function(data) {
+  this.rawData_ = this.parseCSV_(data);
+  this.cascadeDataDidUpdateEvent_();
+  this.predraw_();
+};
+
+/**
+ * Add ticks on the x-axis representing years, months, quarters, weeks, or days
+ * @private
+ */
+Dygraph.prototype.addXTicks_ = function() {
+  // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
+  var range;
+  if (this.dateWindow_) {
+    range = [this.dateWindow_[0], this.dateWindow_[1]];
+  } else {
+    range = this.xAxisExtremes();
+  }
+
+  var xAxisOptionsView = this.optionsViewForAxis_('x');
+  var xTicks = xAxisOptionsView('ticker')(
+      range[0],
+      range[1],
+      this.plotter_.area.w,  // TODO(danvk): should be area.width
+      xAxisOptionsView,
+      this);
+  // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
+  // console.log(msg);
+  this.layout_.setXTicks(xTicks);
+};
+
+/**
+ * Returns the correct handler class for the currently set options.
+ * @private
+ */
+Dygraph.prototype.getHandlerClass_ = function() {
+  var handlerClass;
+  if (this.attr_('dataHandler')) {
+    handlerClass =  this.attr_('dataHandler');
+  } else if (this.fractions_) {
+    if (this.getBooleanOption('errorBars')) {
+      handlerClass = Dygraph.DataHandlers.FractionsBarsHandler;
+    } else {
+      handlerClass = Dygraph.DataHandlers.DefaultFractionHandler;
+    }
+  } else if (this.getBooleanOption('customBars')) {
+    handlerClass = Dygraph.DataHandlers.CustomBarsHandler;
+  } else if (this.getBooleanOption('errorBars')) {
+    handlerClass = Dygraph.DataHandlers.ErrorBarsHandler;
+  } else {
+    handlerClass = Dygraph.DataHandlers.DefaultHandler;
+  }
+  return handlerClass;
+};
+
+/**
+ * @private
+ * This function is called once when the chart's data is changed or the options
+ * dictionary is updated. It is _not_ called when the user pans or zooms. The
+ * idea is that values derived from the chart's data can be computed here,
+ * rather than every time the chart is drawn. This includes things like the
+ * number of axes, rolling averages, etc.
+ */
+Dygraph.prototype.predraw_ = function() {
+  var start = new Date();
+
+  // Create the correct dataHandler
+  this.dataHandler_ = new (this.getHandlerClass_())();
+
+  this.layout_.computePlotArea();
+
+  // TODO(danvk): move more computations out of drawGraph_ and into here.
+  this.computeYAxes_();
+
+  if (!this.is_initial_draw_) {
+    this.canvas_ctx_.restore();
+    this.hidden_ctx_.restore();
+  }
+
+  this.canvas_ctx_.save();
+  this.hidden_ctx_.save();
+
+  // Create a new plotter.
+  this.plotter_ = new DygraphCanvasRenderer(this,
+                                            this.hidden_,
+                                            this.hidden_ctx_,
+                                            this.layout_);
+
+  // The roller sits in the bottom left corner of the chart. We don't know where
+  // this will be until the options are available, so it's positioned here.
+  this.createRollInterface_();
+
+  this.cascadeEvents_('predraw');
+
+  // Convert the raw data (a 2D array) into the internal format and compute
+  // rolling averages.
+  this.rolledSeries_ = [null];  // x-axis is the first series and it's special
+  for (var i = 1; i < this.numColumns(); i++) {
+    // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too.
+    var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_);
+    if (this.rollPeriod_ > 1) {
+      series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_);
+    }
+
+    this.rolledSeries_.push(series);
+  }
+
+  // If the data or options have changed, then we'd better redraw.
+  this.drawGraph_();
+
+  // This is used to determine whether to do various animations.
+  var end = new Date();
+  this.drawingTimeMs_ = (end - start);
+};
+
+/**
+ * Point structure.
+ *
+ * xval_* and yval_* are the original unscaled data values,
+ * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
+ * yval_stacked is the cumulative Y value used for stacking graphs,
+ * and bottom/top/minus/plus are used for error bar graphs.
+ *
+ * @typedef {{
+ *     idx: number,
+ *     name: string,
+ *     x: ?number,
+ *     xval: ?number,
+ *     y_bottom: ?number,
+ *     y: ?number,
+ *     y_stacked: ?number,
+ *     y_top: ?number,
+ *     yval_minus: ?number,
+ *     yval: ?number,
+ *     yval_plus: ?number,
+ *     yval_stacked
+ * }}
+ */
+Dygraph.PointType = undefined;
+
+/**
+ * Calculates point stacking for stackedGraph=true.
+ *
+ * For stacking purposes, interpolate or extend neighboring data across
+ * NaN values based on stackedGraphNaNFill settings. This is for display
+ * only, the underlying data value as shown in the legend remains NaN.
+ *
+ * @param {Array.<Dygraph.PointType>} points Point array for a single series.
+ *     Updates each Point's yval_stacked property.
+ * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
+ *     values for the series seen so far. Index is the row number. Updated
+ *     based on the current series's values.
+ * @param {Array.<number>} seriesExtremes Min and max values, updated
+ *     to reflect the stacked values.
+ * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
+ *     'none'.
+ * @private
+ */
+Dygraph.stackPoints_ = function(
+    points, cumulativeYval, seriesExtremes, fillMethod) {
+  var lastXval = null;
+  var prevPoint = null;
+  var nextPoint = null;
+  var nextPointIdx = -1;
+
+  // Find the next stackable point starting from the given index.
+  var updateNextPoint = function(idx) {
+    // If we've previously found a non-NaN point and haven't gone past it yet,
+    // just use that.
+    if (nextPointIdx >= idx) return;
+
+    // We haven't found a non-NaN point yet or have moved past it,
+    // look towards the right to find a non-NaN point.
+    for (var j = idx; j < points.length; ++j) {
+      // Clear out a previously-found point (if any) since it's no longer
+      // valid, we shouldn't use it for interpolation anymore.
+      nextPoint = null;
+      if (!isNaN(points[j].yval) && points[j].yval !== null) {
+        nextPointIdx = j;
+        nextPoint = points[j];
+        break;
+      }
+    }
+  };
+
+  for (var i = 0; i < points.length; ++i) {
+    var point = points[i];
+    var xval = point.xval;
+    if (cumulativeYval[xval] === undefined) {
+      cumulativeYval[xval] = 0;
+    }
+
+    var actualYval = point.yval;
+    if (isNaN(actualYval) || actualYval === null) {
+      if(fillMethod == 'none') {
+        actualYval = 0;
+      } else {
+        // Interpolate/extend for stacking purposes if possible.
+        updateNextPoint(i);
+        if (prevPoint && nextPoint && fillMethod != 'none') {
+          // Use linear interpolation between prevPoint and nextPoint.
+          actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) *
+              ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval));
+        } else if (prevPoint && fillMethod == 'all') {
+          actualYval = prevPoint.yval;
+        } else if (nextPoint && fillMethod == 'all') {
+          actualYval = nextPoint.yval;
+        } else {
+          actualYval = 0;
+        }
+      }
+    } else {
+      prevPoint = point;
+    }
+
+    var stackedYval = cumulativeYval[xval];
+    if (lastXval != xval) {
+      // If an x-value is repeated, we ignore the duplicates.
+      stackedYval += actualYval;
+      cumulativeYval[xval] = stackedYval;
+    }
+    lastXval = xval;
+
+    point.yval_stacked = stackedYval;
+
+    if (stackedYval > seriesExtremes[1]) {
+      seriesExtremes[1] = stackedYval;
+    }
+    if (stackedYval < seriesExtremes[0]) {
+      seriesExtremes[0] = stackedYval;
+    }
+  }
+};
+
+
+/**
+ * Loop over all fields and create datasets, calculating extreme y-values for
+ * each series and extreme x-indices as we go.
+ *
+ * dateWindow is passed in as an explicit parameter so that we can compute
+ * extreme values "speculatively", i.e. without actually setting state on the
+ * dygraph.
+ *
+ * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
+ *     rolledSeries[seriesIndex][row] = raw point, where
+ *     seriesIndex is the column number starting with 1, and
+ *     rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
+ * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
+ * @return {{
+ *     points: Array.<Array.<Dygraph.PointType>>,
+ *     seriesExtremes: Array.<Array.<number>>,
+ *     boundaryIds: Array.<number>}}
+ * @private
+ */
+Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
+  var boundaryIds = [];
+  var points = [];
+  var cumulativeYval = [];  // For stacked series.
+  var extremes = {};  // series name -> [low, high]
+  var seriesIdx, sampleIdx;
+  var firstIdx, lastIdx;
+  var axisIdx;
+
+  // Loop over the fields (series).  Go from the last to the first,
+  // because if they're stacked that's how we accumulate the values.
+  var num_series = rolledSeries.length - 1;
+  var series;
+  for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) {
+    if (!this.visibility()[seriesIdx - 1]) continue;
+
+    // Prune down to the desired range, if necessary (for zooming)
+    // Because there can be lines going to points outside of the visible area,
+    // we actually prune to visible points, plus one on either side.
+    if (dateWindow) {
+      series = rolledSeries[seriesIdx];
+      var low = dateWindow[0];
+      var high = dateWindow[1];
+
+      // TODO(danvk): do binary search instead of linear search.
+      // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+      firstIdx = null;
+      lastIdx = null;
+      for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) {
+        if (series[sampleIdx][0] >= low && firstIdx === null) {
+          firstIdx = sampleIdx;
+        }
+        if (series[sampleIdx][0] <= high) {
+          lastIdx = sampleIdx;
+        }
+      }
+
+      if (firstIdx === null) firstIdx = 0;
+      var correctedFirstIdx = firstIdx;
+      var isInvalidValue = true;
+      while (isInvalidValue && correctedFirstIdx > 0) {
+        correctedFirstIdx--;
+        // check if the y value is null.
+        isInvalidValue = series[correctedFirstIdx][1] === null;
+      }
+
+      if (lastIdx === null) lastIdx = series.length - 1;
+      var correctedLastIdx = lastIdx;
+      isInvalidValue = true;
+      while (isInvalidValue && correctedLastIdx < series.length - 1) {
+        correctedLastIdx++;
+        isInvalidValue = series[correctedLastIdx][1] === null;
+      }
+
+      if (correctedFirstIdx!==firstIdx) {
+        firstIdx = correctedFirstIdx;
+      }
+      if (correctedLastIdx !== lastIdx) {
+        lastIdx = correctedLastIdx;
+      }
+
+      boundaryIds[seriesIdx-1] = [firstIdx, lastIdx];
+
+      // .slice's end is exclusive, we want to include lastIdx.
+      series = series.slice(firstIdx, lastIdx + 1);
+    } else {
+      series = rolledSeries[seriesIdx];
+      boundaryIds[seriesIdx-1] = [0, series.length-1];
+    }
+
+    var seriesName = this.attr_("labels")[seriesIdx];
+    var seriesExtremes = this.dataHandler_.getExtremeYValues(series,
+        dateWindow, this.getBooleanOption("stepPlot",seriesName));
+
+    var seriesPoints = this.dataHandler_.seriesToPoints(series,
+        seriesName, boundaryIds[seriesIdx-1][0]);
+
+    if (this.getBooleanOption("stackedGraph")) {
+      axisIdx = this.attributes_.axisForSeries(seriesName);
+      if (cumulativeYval[axisIdx] === undefined) {
+        cumulativeYval[axisIdx] = [];
+      }
+      Dygraph.stackPoints_(seriesPoints, cumulativeYval[axisIdx], seriesExtremes,
+                           this.getBooleanOption("stackedGraphNaNFill"));
+    }
+
+    extremes[seriesName] = seriesExtremes;
+    points[seriesIdx] = seriesPoints;
+  }
+
+  return { points: points, extremes: extremes, boundaryIds: boundaryIds };
+};
+
+/**
+ * Update the graph with new data. This method is called when the viewing area
+ * has changed. If the underlying data or options have changed, predraw_ will
+ * be called before drawGraph_ is called.
+ *
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function() {
+  var start = new Date();
+
+  // This is used to set the second parameter to drawCallback, below.
+  var is_initial_draw = this.is_initial_draw_;
+  this.is_initial_draw_ = false;
+
+  this.layout_.removeAllDatasets();
+  this.setColors_();
+  this.attrs_.pointSize = 0.5 * this.getNumericOption('highlightCircleSize');
+
+  var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
+  var points = packed.points;
+  var extremes = packed.extremes;
+  this.boundaryIds_ = packed.boundaryIds;
+
+  this.setIndexByName_ = {};
+  var labels = this.attr_("labels");
+  if (labels.length > 0) {
+    this.setIndexByName_[labels[0]] = 0;
+  }
+  var dataIdx = 0;
+  for (var i = 1; i < points.length; i++) {
+    this.setIndexByName_[labels[i]] = i;
+    if (!this.visibility()[i - 1]) continue;
+    this.layout_.addDataset(labels[i], points[i]);
+    this.datasetIndex_[i] = dataIdx++;
+  }
+
+  this.computeYAxisRanges_(extremes);
+  this.layout_.setYAxes(this.axes_);
+
+  this.addXTicks_();
+
+  // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
+  var tmp_zoomed_x = this.zoomed_x_;
+  // Tell PlotKit to use this new data and render itself
+  this.zoomed_x_ = tmp_zoomed_x;
+  this.layout_.evaluate();
+  this.renderGraph_(is_initial_draw);
+
+  if (this.getStringOption("timingName")) {
+    var end = new Date();
+    console.log(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms");
+  }
+};
+
+/**
+ * This does the work of drawing the chart. It assumes that the layout and axis
+ * scales have already been set (e.g. by predraw_).
+ *
+ * @private
+ */
+Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
+  this.cascadeEvents_('clearChart');
+  this.plotter_.clear();
+
+  if (this.getFunctionOption('underlayCallback')) {
+    // NOTE: we pass the dygraph object to this callback twice to avoid breaking
+    // users who expect a deprecated form of this callback.
+    this.getFunctionOption('underlayCallback').call(this,
+        this.hidden_ctx_, this.layout_.getPlotArea(), this, this);
+  }
+
+  var e = {
+    canvas: this.hidden_,
+    drawingContext: this.hidden_ctx_
+  };
+  this.cascadeEvents_('willDrawChart', e);
+  this.plotter_.render();
+  this.cascadeEvents_('didDrawChart', e);
+  this.lastRow_ = -1;  // because plugins/legend.js clears the legend
+
+  // TODO(danvk): is this a performance bottleneck when panning?
+  // The interaction canvas should already be empty in that situation.
+  this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_);
+
+  if (this.getFunctionOption("drawCallback") !== null) {
+    this.getFunctionOption("drawCallback")(this, is_initial_draw);
+  }
+  if (is_initial_draw) {
+    this.readyFired_ = true;
+    while (this.readyFns_.length > 0) {
+      var fn = this.readyFns_.pop();
+      fn(this);
+    }
+  }
+};
+
+/**
+ * @private
+ * Determine properties of the y-axes which are independent of the data
+ * currently being displayed. This includes things like the number of axes and
+ * the style of the axes. It does not include the range of each axis and its
+ * tick marks.
+ * This fills in this.axes_.
+ * axes_ = [ { options } ]
+ *   indices are into the axes_ array.
+ */
+Dygraph.prototype.computeYAxes_ = function() {
+  // Preserve valueWindow settings if they exist, and if the user hasn't
+  // specified a new valueRange.
+  var valueWindows, axis, index, opts, v;
+  if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
+    valueWindows = [];
+    for (index = 0; index < this.axes_.length; index++) {
+      valueWindows.push(this.axes_[index].valueWindow);
+    }
+  }
+
+  // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
+  // data computation as well as options storage.
+  // Go through once and add all the axes.
+  this.axes_ = [];
+
+  for (axis = 0; axis < this.attributes_.numAxes(); axis++) {
+    // Add a new axis, making a copy of its per-axis options.
+    opts = { g : this };
+    Dygraph.update(opts, this.attributes_.axisOptions(axis));
+    this.axes_[axis] = opts;
+  }
+
+
+  // Copy global valueRange option over to the first axis.
+  // NOTE(konigsberg): Are these two statements necessary?
+  // I tried removing it. The automated tests pass, and manually
+  // messing with tests/zoom.html showed no trouble.
+  v = this.attr_('valueRange');
+  if (v) this.axes_[0].valueRange = v;
+
+  if (valueWindows !== undefined) {
+    // Restore valueWindow settings.
+
+    // When going from two axes back to one, we only restore
+    // one axis.
+    var idxCount = Math.min(valueWindows.length, this.axes_.length);
+
+    for (index = 0; index < idxCount; index++) {
+      this.axes_[index].valueWindow = valueWindows[index];
+    }
+  }
+
+  for (axis = 0; axis < this.axes_.length; axis++) {
+    if (axis === 0) {
+      opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
+      v = opts("valueRange");
+      if (v) this.axes_[axis].valueRange = v;
+    } else {  // To keep old behavior
+      var axes = this.user_attrs_.axes;
+      if (axes && axes.y2) {
+        v = axes.y2.valueRange;
+        if (v) this.axes_[axis].valueRange = v;
+      }
+    }
+  }
+};
+
+/**
+ * Returns the number of y-axes on the chart.
+ * @return {number} the number of axes.
+ */
+Dygraph.prototype.numAxes = function() {
+  return this.attributes_.numAxes();
+};
+
+/**
+ * @private
+ * Returns axis properties for the given series.
+ * @param {string} setName The name of the series for which to get axis
+ * properties, e.g. 'Y1'.
+ * @return {Object} The axis properties.
+ */
+Dygraph.prototype.axisPropertiesForSeries = function(series) {
+  // TODO(danvk): handle errors.
+  return this.axes_[this.attributes_.axisForSeries(series)];
+};
+
+/**
+ * @private
+ * Determine the value range and tick marks for each axis.
+ * @param {Object} extremes A mapping from seriesName -> [low, high]
+ * This fills in the valueRange and ticks fields in each entry of this.axes_.
+ */
+Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
+  var isNullUndefinedOrNaN = function(num) {
+    return isNaN(parseFloat(num));
+  };
+  var numAxes = this.attributes_.numAxes();
+  var ypadCompat, span, series, ypad;
+
+  var p_axis;
+
+  // Compute extreme values, a span and tick marks for each axis.
+  for (var i = 0; i < numAxes; i++) {
+    var axis = this.axes_[i];
+    var logscale = this.attributes_.getForAxis("logscale", i);
+    var includeZero = this.attributes_.getForAxis("includeZero", i);
+    var independentTicks = this.attributes_.getForAxis("independentTicks", i);
+    series = this.attributes_.seriesForAxis(i);
+
+    // Add some padding. This supports two Y padding operation modes:
+    //
+    // - backwards compatible (yRangePad not set):
+    //   10% padding for automatic Y ranges, but not for user-supplied
+    //   ranges, and move a close-to-zero edge to zero except if
+    //   avoidMinZero is set, since drawing at the edge results in
+    //   invisible lines. Unfortunately lines drawn at the edge of a
+    //   user-supplied range will still be invisible. If logscale is
+    //   set, add a variable amount of padding at the top but
+    //   none at the bottom.
+    //
+    // - new-style (yRangePad set by the user):
+    //   always add the specified Y padding.
+    //
+    ypadCompat = true;
+    ypad = 0.1; // add 10%
+    if (this.getNumericOption('yRangePad') !== null) {
+      ypadCompat = false;
+      // Convert pixel padding to ratio
+      ypad = this.getNumericOption('yRangePad') / this.plotter_.area.h;
+    }
+
+    if (series.length === 0) {
+      // If no series are defined or visible then use a reasonable default
+      axis.extremeRange = [0, 1];
+    } else {
+      // Calculate the extremes of extremes.
+      var minY = Infinity;  // extremes[series[0]][0];
+      var maxY = -Infinity;  // extremes[series[0]][1];
+      var extremeMinY, extremeMaxY;
+
+      for (var j = 0; j < series.length; j++) {
+        // this skips invisible series
+        if (!extremes.hasOwnProperty(series[j])) continue;
+
+        // Only use valid extremes to stop null data series' from corrupting the scale.
+        extremeMinY = extremes[series[j]][0];
+        if (extremeMinY !== null) {
+          minY = Math.min(extremeMinY, minY);
+        }
+        extremeMaxY = extremes[series[j]][1];
+        if (extremeMaxY !== null) {
+          maxY = Math.max(extremeMaxY, maxY);
+        }
+      }
+
+      // Include zero if requested by the user.
+      if (includeZero && !logscale) {
+        if (minY > 0) minY = 0;
+        if (maxY < 0) maxY = 0;
+      }
+
+      // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
+      if (minY == Infinity) minY = 0;
+      if (maxY == -Infinity) maxY = 1;
+
+      span = maxY - minY;
+      // special case: if we have no sense of scale, center on the sole value.
+      if (span === 0) {
+        if (maxY !== 0) {
+          span = Math.abs(maxY);
+        } else {
+          // ... and if the sole value is zero, use range 0-1.
+          maxY = 1;
+          span = 1;
+        }
+      }
+
+      var maxAxisY, minAxisY;
+      if (logscale) {
+        if (ypadCompat) {
+          maxAxisY = maxY + ypad * span;
+          minAxisY = minY;
+        } else {
+          var logpad = Math.exp(Math.log(span) * ypad);
+          maxAxisY = maxY * logpad;
+          minAxisY = minY / logpad;
+        }
+      } else {
+        maxAxisY = maxY + ypad * span;
+        minAxisY = minY - ypad * span;
+
+        // Backwards-compatible behavior: Move the span to start or end at zero if it's
+        // close to zero, but not if avoidMinZero is set.
+        if (ypadCompat && !this.getBooleanOption("avoidMinZero")) {
+          if (minAxisY < 0 && minY >= 0) minAxisY = 0;
+          if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
+        }
+      }
+      axis.extremeRange = [minAxisY, maxAxisY];
+    }
+    if (axis.valueWindow) {
+      // This is only set if the user has zoomed on the y-axis. It is never set
+      // by a user. It takes precedence over axis.valueRange because, if you set
+      // valueRange, you'd still expect to be able to pan.
+      axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
+    } else if (axis.valueRange) {
+      // This is a user-set value range for this axis.
+      var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
+      var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
+      if (!ypadCompat) {
+        if (axis.logscale) {
+          var logpad = Math.exp(Math.log(span) * ypad);
+          y0 *= logpad;
+          y1 /= logpad;
+        } else {
+          span = y1 - y0;
+          y0 -= span * ypad;
+          y1 += span * ypad;
+        }
+      }
+      axis.computedValueRange = [y0, y1];
+    } else {
+      axis.computedValueRange = axis.extremeRange;
+    }
+
+
+    if (independentTicks) {
+      axis.independentTicks = independentTicks;
+      var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
+      var ticker = opts('ticker');
+      axis.ticks = ticker(axis.computedValueRange[0],
+              axis.computedValueRange[1],
+              this.plotter_.area.h,
+              opts,
+              this);
+      // Define the first independent axis as primary axis.
+      if (!p_axis) p_axis = axis;
+    }
+  }
+  if (p_axis === undefined) {
+    throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
+  }
+  // Add ticks. By default, all axes inherit the tick positions of the
+  // primary axis. However, if an axis is specifically marked as having
+  // independent ticks, then that is permissible as well.
+  for (var i = 0; i < numAxes; i++) {
+    var axis = this.axes_[i];
+
+    if (!axis.independentTicks) {
+      var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
+      var ticker = opts('ticker');
+      var p_ticks = p_axis.ticks;
+      var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
+      var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
+      var tick_values = [];
+      for (var k = 0; k < p_ticks.length; k++) {
+        var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale;
+        var y_val = axis.computedValueRange[0] + y_frac * scale;
+        tick_values.push(y_val);
+      }
+
+      axis.ticks = ticker(axis.computedValueRange[0],
+                          axis.computedValueRange[1],
+                          this.plotter_.area.h,
+                          opts,
+                          this,
+                          tick_values);
+    }
+  }
+};
+
+/**
+ * Detects the type of the str (date or numeric) and sets the various
+ * formatting attributes in this.attrs_ based on this type.
+ * @param {string} str An x value.
+ * @private
+ */
+Dygraph.prototype.detectTypeFromString_ = function(str) {
+  var isDate = false;
+  var dashPos = str.indexOf('-');  // could be 2006-01-01 _or_ 1.0e-2
+  if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) ||
+      str.indexOf('/') >= 0 ||
+      isNaN(parseFloat(str))) {
+    isDate = true;
+  } else if (str.length == 8 && str > '19700101' && str < '20371231') {
+    // TODO(danvk): remove support for this format.
+    isDate = true;
+  }
+
+  this.setXAxisOptions_(isDate);
+};
+
+Dygraph.prototype.setXAxisOptions_ = function(isDate) {
+  if (isDate) {
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter;
+  } else {
+    /** @private (shut up, jsdoc!) */
+    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
+    // TODO(danvk): use Dygraph.numberValueFormatter here?
+    /** @private (shut up, jsdoc!) */
+    this.attrs_.axes.x.valueFormatter = function(x) { return x; };
+    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+    this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
+  }
+};
+
+/**
+ * @private
+ * Parses a string in a special csv format.  We expect a csv file where each
+ * line is a date point, and the first field in each line is the date string.
+ * We also expect that all remaining fields represent series.
+ * if the errorBars attribute is set, then interpret the fields as:
+ * date, series1, stddev1, series2, stddev2, ...
+ * @param {[Object]} data See above.
+ *
+ * @return [Object] An array with one entry for each row. These entries
+ * are an array of cells in that row. The first entry is the parsed x-value for
+ * the row. The second, third, etc. are the y-values. These can take on one of
+ * three forms, depending on the CSV and constructor parameters:
+ * 1. numeric value
+ * 2. [ value, stddev ]
+ * 3. [ low value, center value, high value ]
+ */
+Dygraph.prototype.parseCSV_ = function(data) {
+  var ret = [];
+  var line_delimiter = Dygraph.detectLineDelimiter(data);
+  var lines = data.split(line_delimiter || "\n");
+  var vals, j;
+
+  // Use the default delimiter or fall back to a tab if that makes sense.
+  var delim = this.getStringOption('delimiter');
+  if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
+    delim = '\t';
+  }
+
+  var start = 0;
+  if (!('labels' in this.user_attrs_)) {
+    // User hasn't explicitly set labels, so they're (presumably) in the CSV.
+    start = 1;
+    this.attrs_.labels = lines[0].split(delim);  // NOTE: _not_ user_attrs_.
+    this.attributes_.reparseSeries();
+  }
+  var line_no = 0;
+
+  var xParser;
+  var defaultParserSet = false;  // attempt to auto-detect x value type
+  var expectedCols = this.attr_("labels").length;
+  var outOfOrder = false;
+  for (var i = start; i < lines.length; i++) {
+    var line = lines[i];
+    line_no = i;
+    if (line.length === 0) continue;  // skip blank lines
+    if (line[0] == '#') continue;    // skip comment lines
+    var inFields = line.split(delim);
+    if (inFields.length < 2) continue;
+
+    var fields = [];
+    if (!defaultParserSet) {
+      this.detectTypeFromString_(inFields[0]);
+      xParser = this.getFunctionOption("xValueParser");
+      defaultParserSet = true;
+    }
+    fields[0] = xParser(inFields[0], this);
+
+    // If fractions are expected, parse the numbers as "A/B"
+    if (this.fractions_) {
+      for (j = 1; j < inFields.length; j++) {
+        // TODO(danvk): figure out an appropriate way to flag parse errors.
+        vals = inFields[j].split("/");
+        if (vals.length != 2) {
+          console.error('Expected fractional "num/den" values in CSV data ' +
+                        "but found a value '" + inFields[j] + "' on line " +
+                        (1 + i) + " ('" + line + "') which is not of this form.");
+          fields[j] = [0, 0];
+        } else {
+          fields[j] = [Dygraph.parseFloat_(vals[0], i, line),
+                       Dygraph.parseFloat_(vals[1], i, line)];
+        }
+      }
+    } else if (this.getBooleanOption("errorBars")) {
+      // If there are error bars, values are (value, stddev) pairs
+      if (inFields.length % 2 != 1) {
+        console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
+                      'but line ' + (1 + i) + ' has an odd number of values (' +
+                      (inFields.length - 1) + "): '" + line + "'");
+      }
+      for (j = 1; j < inFields.length; j += 2) {
+        fields[(j + 1) / 2] = [Dygraph.parseFloat_(inFields[j], i, line),
+                               Dygraph.parseFloat_(inFields[j + 1], i, line)];
+      }
+    } else if (this.getBooleanOption("customBars")) {
+      // Bars are a low;center;high tuple
+      for (j = 1; j < inFields.length; j++) {
+        var val = inFields[j];
+        if (/^ *$/.test(val)) {
+          fields[j] = [null, null, null];
+        } else {
+          vals = val.split(";");
+          if (vals.length == 3) {
+            fields[j] = [ Dygraph.parseFloat_(vals[0], i, line),
+                          Dygraph.parseFloat_(vals[1], i, line),
+                          Dygraph.parseFloat_(vals[2], i, line) ];
+          } else {
+            console.warn('When using customBars, values must be either blank ' +
+                         'or "low;center;high" tuples (got "' + val +
+                         '" on line ' + (1+i));
+          }
+        }
+      }
+    } else {
+      // Values are just numbers
+      for (j = 1; j < inFields.length; j++) {
+        fields[j] = Dygraph.parseFloat_(inFields[j], i, line);
+      }
+    }
+    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
+
+    if (fields.length != expectedCols) {
+      console.error("Number of columns in line " + i + " (" + fields.length +
+                    ") does not agree with number of labels (" + expectedCols +
+                    ") " + line);
+    }
+
+    // If the user specified the 'labels' option and none of the cells of the
+    // first row parsed correctly, then they probably double-specified the
+    // labels. We go with the values set in the option, discard this row and
+    // log a warning to the JS console.
+    if (i === 0 && this.attr_('labels')) {
+      var all_null = true;
+      for (j = 0; all_null && j < fields.length; j++) {
+        if (fields[j]) all_null = false;
+      }
+      if (all_null) {
+        console.warn("The dygraphs 'labels' option is set, but the first row " +
+                     "of CSV data ('" + line + "') appears to also contain " +
+                     "labels. Will drop the CSV labels and use the option " +
+                     "labels.");
+        continue;
+      }
+    }
+    ret.push(fields);
+  }
+
+  if (outOfOrder) {
+    console.warn("CSV is out of order; order it correctly to speed loading.");
+    ret.sort(function(a,b) { return a[0] - b[0]; });
+  }
+
+  return ret;
+};
+
+/**
+ * The user has provided their data as a pre-packaged JS array. If the x values
+ * are numeric, this is the same as dygraphs' internal format. If the x values
+ * are dates, we need to convert them from Date objects to ms since epoch.
+ * @param {!Array} data
+ * @return {Object} data with numeric x values.
+ * @private
+ */
+Dygraph.prototype.parseArray_ = function(data) {
+  // Peek at the first x value to see if it's numeric.
+  if (data.length === 0) {
+    console.error("Can't plot empty data set");
+    return null;
+  }
+  if (data[0].length === 0) {
+    console.error("Data set cannot contain an empty row");
+    return null;
+  }
+
+  var i;
+  if (this.attr_("labels") === null) {
+    console.warn("Using default labels. Set labels explicitly via 'labels' " +
+                 "in the options parameter");
+    this.attrs_.labels = [ "X" ];
+    for (i = 1; i < data[0].length; i++) {
+      this.attrs_.labels.push("Y" + i); // Not user_attrs_.
+    }
+    this.attributes_.reparseSeries();
+  } else {
+    var num_labels = this.attr_("labels");
+    if (num_labels.length != data[0].length) {
+      console.error("Mismatch between number of labels (" + num_labels + ")" +
+                    " and number of columns in array (" + data[0].length + ")");
+      return null;
+    }
+  }
+
+  if (Dygraph.isDateLike(data[0][0])) {
+    // Some intelligent defaults for a date x-axis.
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter;
+
+    // Assume they're all dates.
+    var parsedData = Dygraph.clone(data);
+    for (i = 0; i < data.length; i++) {
+      if (parsedData[i].length === 0) {
+        console.error("Row " + (1 + i) + " of data is empty");
+        return null;
+      }
+      if (parsedData[i][0] === null ||
+          typeof(parsedData[i][0].getTime) != 'function' ||
+          isNaN(parsedData[i][0].getTime())) {
+        console.error("x value in row " + (1 + i) + " is not a Date");
+        return null;
+      }
+      parsedData[i][0] = parsedData[i][0].getTime();
+    }
+    return parsedData;
+  } else {
+    // Some intelligent defaults for a numeric x-axis.
+    /** @private (shut up, jsdoc!) */
+    this.attrs_.axes.x.valueFormatter = function(x) { return x; };
+    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
+    return data;
+  }
+};
+
+/**
+ * Parses a DataTable object from gviz.
+ * The data is expected to have a first column that is either a date or a
+ * number. All subsequent columns must be numbers. If there is a clear mismatch
+ * between this.xValueParser_ and the type of the first column, it will be
+ * fixed. Fills out rawData_.
+ * @param {!google.visualization.DataTable} data See above.
+ * @private
+ */
+Dygraph.prototype.parseDataTable_ = function(data) {
+  var shortTextForAnnotationNum = function(num) {
+    // converts [0-9]+ [A-Z][a-z]*
+    // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
+    // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
+    var shortText = String.fromCharCode(65 /* A */ + num % 26);
+    num = Math.floor(num / 26);
+    while ( num > 0 ) {
+      shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
+      num = Math.floor((num - 1) / 26);
+    }
+    return shortText;
+  };
+
+  var cols = data.getNumberOfColumns();
+  var rows = data.getNumberOfRows();
+
+  var indepType = data.getColumnType(0);
+  if (indepType == 'date' || indepType == 'datetime') {
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter;
+  } else if (indepType == 'number') {
+    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
+    this.attrs_.axes.x.valueFormatter = function(x) { return x; };
+    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+    this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
+  } else {
+    console.error("only 'date', 'datetime' and 'number' types are supported " +
+                  "for column 1 of DataTable input (Got '" + indepType + "')");
+    return null;
+  }
+
+  // Array of the column indices which contain data (and not annotations).
+  var colIdx = [];
+  var annotationCols = {};  // data index -> [annotation cols]
+  var hasAnnotations = false;
+  var i, j;
+  for (i = 1; i < cols; i++) {
+    var type = data.getColumnType(i);
+    if (type == 'number') {
+      colIdx.push(i);
+    } else if (type == 'string' && this.getBooleanOption('displayAnnotations')) {
+      // This is OK -- it's an annotation column.
+      var dataIdx = colIdx[colIdx.length - 1];
+      if (!annotationCols.hasOwnProperty(dataIdx)) {
+        annotationCols[dataIdx] = [i];
+      } else {
+        annotationCols[dataIdx].push(i);
+      }
+      hasAnnotations = true;
+    } else {
+      console.error("Only 'number' is supported as a dependent type with Gviz." +
+                    " 'string' is only supported if displayAnnotations is true");
+    }
+  }
+
+  // Read column labels
+  // TODO(danvk): add support back for errorBars
+  var labels = [data.getColumnLabel(0)];
+  for (i = 0; i < colIdx.length; i++) {
+    labels.push(data.getColumnLabel(colIdx[i]));
+    if (this.getBooleanOption("errorBars")) i += 1;
+  }
+  this.attrs_.labels = labels;
+  cols = labels.length;
+
+  var ret = [];
+  var outOfOrder = false;
+  var annotations = [];
+  for (i = 0; i < rows; i++) {
+    var row = [];
+    if (typeof(data.getValue(i, 0)) === 'undefined' ||
+        data.getValue(i, 0) === null) {
+      console.warn("Ignoring row " + i +
+                   " of DataTable because of undefined or null first column.");
+      continue;
+    }
+
+    if (indepType == 'date' || indepType == 'datetime') {
+      row.push(data.getValue(i, 0).getTime());
+    } else {
+      row.push(data.getValue(i, 0));
+    }
+    if (!this.getBooleanOption("errorBars")) {
+      for (j = 0; j < colIdx.length; j++) {
+        var col = colIdx[j];
+        row.push(data.getValue(i, col));
+        if (hasAnnotations &&
+            annotationCols.hasOwnProperty(col) &&
+            data.getValue(i, annotationCols[col][0]) !== null) {
+          var ann = {};
+          ann.series = data.getColumnLabel(col);
+          ann.xval = row[0];
+          ann.shortText = shortTextForAnnotationNum(annotations.length);
+          ann.text = '';
+          for (var k = 0; k < annotationCols[col].length; k++) {
+            if (k) ann.text += "\n";
+            ann.text += data.getValue(i, annotationCols[col][k]);
+          }
+          annotations.push(ann);
+        }
+      }
+
+      // Strip out infinities, which give dygraphs problems later on.
+      for (j = 0; j < row.length; j++) {
+        if (!isFinite(row[j])) row[j] = null;
+      }
+    } else {
+      for (j = 0; j < cols - 1; j++) {
+        row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
+      }
+    }
+    if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
+    ret.push(row);
+  }
+
+  if (outOfOrder) {
+    console.warn("DataTable is out of order; order it correctly to speed loading.");
+    ret.sort(function(a,b) { return a[0] - b[0]; });
+  }
+  this.rawData_ = ret;
+
+  if (annotations.length > 0) {
+    this.setAnnotations(annotations, true);
+  }
+  this.attributes_.reparseSeries();
+};
+
+/**
+ * Signals to plugins that the chart data has updated.
+ * This happens after the data has updated but before the chart has redrawn.
+ */
+Dygraph.prototype.cascadeDataDidUpdateEvent_ = function() {
+  // TODO(danvk): there are some issues checking xAxisRange() and using
+  // toDomCoords from handlers of this event. The visible range should be set
+  // when the chart is drawn, not derived from the data.
+  this.cascadeEvents_('dataDidUpdate', {});
+};
+
+/**
+ * Get the CSV data. If it's in a function, call that function. If it's in a
+ * file, do an XMLHttpRequest to get it.
+ * @private
+ */
+Dygraph.prototype.start_ = function() {
+  var data = this.file_;
+
+  // Functions can return references of all other types.
+  if (typeof data == 'function') {
+    data = data();
+  }
+
+  if (Dygraph.isArrayLike(data)) {
+    this.rawData_ = this.parseArray_(data);
+    this.cascadeDataDidUpdateEvent_();
+    this.predraw_();
+  } else if (typeof data == 'object' &&
+             typeof data.getColumnRange == 'function') {
+    // must be a DataTable from gviz.
+    this.parseDataTable_(data);
+    this.cascadeDataDidUpdateEvent_();
+    this.predraw_();
+  } else if (typeof data == 'string') {
+    // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
+    var line_delimiter = Dygraph.detectLineDelimiter(data);
+    if (line_delimiter) {
+      this.loadedEvent_(data);
+    } else {
+      // REMOVE_FOR_IE
+      var req;
+      if (window.XMLHttpRequest) {
+        // Firefox, Opera, IE7, and other browsers will use the native object
+        req = new XMLHttpRequest();
+      } else {
+        // IE 5 and 6 will use the ActiveX control
+        req = new ActiveXObject("Microsoft.XMLHTTP");
+      }
+
+      var caller = this;
+      req.onreadystatechange = function () {
+        if (req.readyState == 4) {
+          if (req.status === 200 ||  // Normal http
+              req.status === 0) {    // Chrome w/ --allow-file-access-from-files
+            caller.loadedEvent_(req.responseText);
+          }
+        }
+      };
+
+      req.open("GET", data, true);
+      req.send(null);
+    }
+  } else {
+    console.error("Unknown data format: " + (typeof data));
+  }
+};
+
+/**
+ * Changes various properties of the graph. These can include:
+ * <ul>
+ * <li>file: changes the source data for the graph</li>
+ * <li>errorBars: changes whether the data contains stddev</li>
+ * </ul>
+ *
+ * There's a huge variety of options that can be passed to this method. For a
+ * full list, see http://dygraphs.com/options.html.
+ *
+ * @param {Object} input_attrs The new properties and values
+ * @param {boolean} block_redraw Usually the chart is redrawn after every
+ *     call to updateOptions(). If you know better, you can pass true to
+ *     explicitly block the redraw. This can be useful for chaining
+ *     updateOptions() calls, avoiding the occasional infinite loop and
+ *     preventing redraws when it's not necessary (e.g. when updating a
+ *     callback).
+ */
+Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
+  if (typeof(block_redraw) == 'undefined') block_redraw = false;
+
+  // copyUserAttrs_ drops the "file" parameter as a convenience to us.
+  var file = input_attrs.file;
+  var attrs = Dygraph.copyUserAttrs_(input_attrs);
+
+  // TODO(danvk): this is a mess. Move these options into attr_.
+  if ('rollPeriod' in attrs) {
+    this.rollPeriod_ = attrs.rollPeriod;
+  }
+  if ('dateWindow' in attrs) {
+    this.dateWindow_ = attrs.dateWindow;
+    if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
+      this.zoomed_x_ = (attrs.dateWindow !== null);
+    }
+  }
+  if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
+    this.zoomed_y_ = (attrs.valueRange !== null);
+  }
+
+  // TODO(danvk): validate per-series options.
+  // Supported:
+  // strokeWidth
+  // pointSize
+  // drawPoints
+  // highlightCircleSize
+
+  // Check if this set options will require new points.
+  var requiresNewPoints = Dygraph.isPixelChangingOptionList(this.attr_("labels"), attrs);
+
+  Dygraph.updateDeep(this.user_attrs_, attrs);
+
+  this.attributes_.reparseSeries();
+
+  if (file) {
+    // This event indicates that the data is about to change, but hasn't yet.
+    // TODO(danvk): support cancelation of the update via this event.
+    this.cascadeEvents_('dataWillUpdate', {});
+
+    this.file_ = file;
+    if (!block_redraw) this.start_();
+  } else {
+    if (!block_redraw) {
+      if (requiresNewPoints) {
+        this.predraw_();
+      } else {
+        this.renderGraph_(false);
+      }
+    }
+  }
+};
+
+/**
+ * Make a copy of input attributes, removing file as a convenience.
+ */
+Dygraph.copyUserAttrs_ = function(attrs) {
+  var my_attrs = {};
+  for (var k in attrs) {
+    if (!attrs.hasOwnProperty(k)) continue;
+    if (k == 'file') continue;
+    if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
+  }
+  return my_attrs;
+};
+
+/**
+ * Resizes the dygraph. If no parameters are specified, resizes to fill the
+ * containing div (which has presumably changed size since the dygraph was
+ * instantiated. If the width/height are specified, the div will be resized.
+ *
+ * This is far more efficient than destroying and re-instantiating a
+ * Dygraph, since it doesn't have to reparse the underlying data.
+ *
+ * @param {number} width Width (in pixels)
+ * @param {number} height Height (in pixels)
+ */
+Dygraph.prototype.resize = function(width, height) {
+  if (this.resize_lock) {
+    return;
+  }
+  this.resize_lock = true;
+
+  if ((width === null) != (height === null)) {
+    console.warn("Dygraph.resize() should be called with zero parameters or " +
+                 "two non-NULL parameters. Pretending it was zero.");
+    width = height = null;
+  }
+
+  var old_width = this.width_;
+  var old_height = this.height_;
+
+  if (width) {
+    this.maindiv_.style.width = width + "px";
+    this.maindiv_.style.height = height + "px";
+    this.width_ = width;
+    this.height_ = height;
+  } else {
+    this.width_ = this.maindiv_.clientWidth;
+    this.height_ = this.maindiv_.clientHeight;
+  }
+
+  if (old_width != this.width_ || old_height != this.height_) {
+    // Resizing a canvas erases it, even when the size doesn't change, so
+    // any resize needs to be followed by a redraw.
+    this.resizeElements_();
+    this.predraw_();
+  }
+
+  this.resize_lock = false;
+};
+
+/**
+ * Adjusts the number of points in the rolling average. Updates the graph to
+ * reflect the new averaging period.
+ * @param {number} length Number of points over which to average the data.
+ */
+Dygraph.prototype.adjustRoll = function(length) {
+  this.rollPeriod_ = length;
+  this.predraw_();
+};
+
+/**
+ * Returns a boolean array of visibility statuses.
+ */
+Dygraph.prototype.visibility = function() {
+  // Do lazy-initialization, so that this happens after we know the number of
+  // data series.
+  if (!this.getOption("visibility")) {
+    this.attrs_.visibility = [];
+  }
+  // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
+  while (this.getOption("visibility").length < this.numColumns() - 1) {
+    this.attrs_.visibility.push(true);
+  }
+  return this.getOption("visibility");
+};
+
+/**
+ * Changes the visibility of one or more series.
+ *
+ * @param {number|number[]} num the series index or an array of series indices
+ * @param {boolean} value true or false, identifying the visibility.
+ */
+Dygraph.prototype.setVisibility = function(num, value) {
+  var x = this.visibility();
+
+  if (num.constructor !== Array) num = [num];
+
+  for (var i = 0; i < num.length; i++) {
+    if (num[i] < 0 || num[i] >= x.length) {
+      console.warn("invalid series number in setVisibility: " + num[i]);
+    } else {
+      x[num[i]] = value;
+    }
+  }
+
+  this.predraw_();
+};
+
+/**
+ * How large of an area will the dygraph render itself in?
+ * This is used for testing.
+ * @return A {width: w, height: h} object.
+ * @private
+ */
+Dygraph.prototype.size = function() {
+  return { width: this.width_, height: this.height_ };
+};
+
+/**
+ * Update the list of annotations and redraw the chart.
+ * See dygraphs.com/annotations.html for more info on how to use annotations.
+ * @param ann {Array} An array of annotation objects.
+ * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
+ */
+Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
+  // Only add the annotation CSS rule once we know it will be used.
+  Dygraph.addAnnotationRule();
+  this.annotations_ = ann;
+  if (!this.layout_) {
+    console.warn("Tried to setAnnotations before dygraph was ready. " +
+                 "Try setting them in a ready() block. See " +
+                 "dygraphs.com/tests/annotation.html");
+    return;
+  }
+
+  this.layout_.setAnnotations(this.annotations_);
+  if (!suppressDraw) {
+    this.predraw_();
+  }
+};
+
+/**
+ * Return the list of annotations.
+ */
+Dygraph.prototype.annotations = function() {
+  return this.annotations_;
+};
+
+/**
+ * Get the list of label names for this graph. The first column is the
+ * x-axis, so the data series names start at index 1.
+ *
+ * Returns null when labels have not yet been defined.
+ */
+Dygraph.prototype.getLabels = function() {
+  var labels = this.attr_("labels");
+  return labels ? labels.slice() : null;
+};
+
+/**
+ * Get the index of a series (column) given its name. The first column is the
+ * x-axis, so the data series start with index 1.
+ */
+Dygraph.prototype.indexFromSetName = function(name) {
+  return this.setIndexByName_[name];
+};
+
+/**
+ * Find the row number corresponding to the given x-value.
+ * Returns null if there is no such x-value in the data.
+ * If there are multiple rows with the same x-value, this will return the
+ * first one.
+ * @param {number} xVal The x-value to look for (e.g. millis since epoch).
+ * @return {?number} The row number, which you can pass to getValue(), or null.
+ */
+Dygraph.prototype.getRowForX = function(xVal) {
+  var low = 0,
+      high = this.numRows() - 1;
+
+  while (low <= high) {
+    var idx = (high + low) >> 1;
+    var x = this.getValue(idx, 0);
+    if (x < xVal) {
+      low = idx + 1;
+    } else if (x > xVal) {
+      high = idx - 1;
+    } else if (low != idx) {  // equal, but there may be an earlier match.
+      high = idx;
+    } else {
+      return idx;
+    }
+  }
+
+  return null;
+};
+
+/**
+ * Trigger a callback when the dygraph has drawn itself and is ready to be
+ * manipulated. This is primarily useful when dygraphs has to do an XHR for the
+ * data (i.e. a URL is passed as the data source) and the chart is drawn
+ * asynchronously. If the chart has already drawn, the callback will fire
+ * immediately.
+ *
+ * This is a good place to call setAnnotation().
+ *
+ * @param {function(!Dygraph)} callback The callback to trigger when the chart
+ *     is ready.
+ */
+Dygraph.prototype.ready = function(callback) {
+  if (this.is_initial_draw_) {
+    this.readyFns_.push(callback);
+  } else {
+    callback.call(this, this);
+  }
+};
+
+/**
+ * @private
+ * Adds a default style for the annotation CSS classes to the document. This is
+ * only executed when annotations are actually used. It is designed to only be
+ * called once -- all calls after the first will return immediately.
+ */
+Dygraph.addAnnotationRule = function() {
+  // TODO(danvk): move this function into plugins/annotations.js?
+  if (Dygraph.addedAnnotationCSS) return;
+
+  var rule = "border: 1px solid black; " +
+             "background-color: white; " +
+             "text-align: center;";
+
+  var styleSheetElement = document.createElement("style");
+  styleSheetElement.type = "text/css";
+  document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
+
+  // Find the first style sheet that we can access.
+  // We may not add a rule to a style sheet from another domain for security
+  // reasons. This sometimes comes up when using gviz, since the Google gviz JS
+  // adds its own style sheets from google.com.
+  for (var i = 0; i < document.styleSheets.length; i++) {
+    if (document.styleSheets[i].disabled) continue;
+    var mysheet = document.styleSheets[i];
+    try {
+      if (mysheet.insertRule) {  // Firefox
+        var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
+        mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
+      } else if (mysheet.addRule) {  // IE
+        mysheet.addRule(".dygraphDefaultAnnotation", rule);
+      }
+      Dygraph.addedAnnotationCSS = true;
+      return;
+    } catch(err) {
+      // Was likely a security exception.
+    }
+  }
+
+  console.warn("Unable to add default annotation CSS rule; display may be off.");
+};
+
+return Dygraph;
+
+})();
diff --git a/src/extras/hairlines.js b/src/extras/hairlines.js
new file mode 100644 (file)
index 0000000..904e432
--- /dev/null
@@ -0,0 +1,459 @@
+/**
+ * @license
+ * Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ *
+ * Note: This plugin requires jQuery and jQuery UI Draggable.
+ *
+ * See high-level documentation at
+ * https://docs.google.com/document/d/1OHNE8BNNmMtFlRQ969DACIYIJ9VVJ7w3dSPRJDEeIew/edit#
+ */
+
+/*global Dygraph:false */
+
+Dygraph.Plugins.Hairlines = (function() {
+
+"use strict";
+
+/**
+ * @typedef {
+ *   xval:  number,      // x-value (i.e. millis or a raw number)
+ *   interpolated: bool,  // alternative is to snap to closest
+ *   lineDiv: !Element    // vertical hairline div
+ *   infoDiv: !Element    // div containing info about the nearest points
+ *   selected: boolean    // whether this hairline is selected
+ * } Hairline
+ */
+
+// We have to wait a few ms after clicks to give the user a chance to
+// double-click to unzoom. This sets that delay period.
+var CLICK_DELAY_MS = 300;
+
+var hairlines = function(opt_options) {
+  /* @type {!Array.<!Hairline>} */
+  this.hairlines_ = [];
+
+  // Used to detect resizes (which require the divs to be repositioned).
+  this.lastWidth_ = -1;
+  this.lastHeight = -1;
+  this.dygraph_ = null;
+
+  this.addTimer_ = null;
+  opt_options = opt_options || {};
+
+  this.divFiller_ = opt_options['divFiller'] || null;
+};
+
+hairlines.prototype.toString = function() {
+  return "Hairlines Plugin";
+};
+
+hairlines.prototype.activate = function(g) {
+  this.dygraph_ = g;
+  this.hairlines_ = [];
+
+  return {
+    didDrawChart: this.didDrawChart,
+    click: this.click,
+    dblclick: this.dblclick,
+    dataDidUpdate: this.dataDidUpdate
+  };
+};
+
+hairlines.prototype.detachLabels = function() {
+  for (var i = 0; i < this.hairlines_.length; i++) {
+    var h = this.hairlines_[i];
+    $(h.lineDiv).remove();
+    $(h.infoDiv).remove();
+    this.hairlines_[i] = null;
+  }
+  this.hairlines_ = [];
+};
+
+hairlines.prototype.hairlineWasDragged = function(h, event, ui) {
+  var area = this.dygraph_.getArea();
+  var oldXVal = h.xval;
+  h.xval = this.dygraph_.toDataXCoord(ui.position.left);
+  this.moveHairlineToTop(h);
+  this.updateHairlineDivPositions();
+  this.updateHairlineInfo();
+  this.updateHairlineStyles();
+  $(this).triggerHandler('hairlineMoved', {
+    oldXVal: oldXVal,
+    newXVal: h.xval
+  });
+  $(this).triggerHandler('hairlinesChanged', {});
+};
+
+// This creates the hairline object and returns it.
+// It does not position it and does not attach it to the chart.
+hairlines.prototype.createHairline = function(props) {
+  var h;
+  var self = this;
+
+  var $lineContainerDiv = $('<div/>').css({
+      'width': '6px',
+      'margin-left': '-3px',
+      'position': 'absolute',
+      'z-index': '10'
+    })
+    .addClass('dygraph-hairline');
+
+  var $lineDiv = $('<div/>').css({
+    'width': '1px',
+    'position': 'relative',
+    'left': '3px',
+    'background': 'black',
+    'height': '100%'
+  });
+  $lineDiv.appendTo($lineContainerDiv);
+
+  var $infoDiv = $('#hairline-template').clone().removeAttr('id').css({
+      'position': 'absolute'
+    })
+    .show();
+
+  // Surely there's a more jQuery-ish way to do this!
+  $([$infoDiv.get(0), $lineContainerDiv.get(0)])
+    .draggable({
+      'axis': 'x',
+      'drag': function(event, ui) {
+        self.hairlineWasDragged(h, event, ui);
+      }
+      // TODO(danvk): set cursor here
+    });
+
+  h = $.extend({
+    interpolated: true,
+    selected: false,
+    lineDiv: $lineContainerDiv.get(0),
+    infoDiv: $infoDiv.get(0)
+  }, props);
+
+  var that = this;
+  $infoDiv.on('click', '.hairline-kill-button', function(e) {
+    that.removeHairline(h);
+    $(that).triggerHandler('hairlineDeleted', {
+      xval: h.xval
+    });
+    $(that).triggerHandler('hairlinesChanged', {});
+    e.stopPropagation();  // don't want .click() to trigger, below.
+  }).on('click', function() {
+    that.moveHairlineToTop(h);
+  });
+
+  return h;
+};
+
+// Moves a hairline's divs to the top of the z-ordering.
+hairlines.prototype.moveHairlineToTop = function(h) {
+  var div = this.dygraph_.graphDiv;
+  $(h.infoDiv).appendTo(div);
+  $(h.lineDiv).appendTo(div);
+
+  var idx = this.hairlines_.indexOf(h);
+  this.hairlines_.splice(idx, 1);
+  this.hairlines_.push(h);
+};
+
+// Positions existing hairline divs.
+hairlines.prototype.updateHairlineDivPositions = function() {
+  var g = this.dygraph_;
+  var layout = this.dygraph_.getArea();
+  var chartLeft = layout.x, chartRight = layout.x + layout.w;
+  var div = this.dygraph_.graphDiv;
+  var pos = Dygraph.findPos(div);
+  var box = [layout.x + pos.x, layout.y + pos.y];
+  box.push(box[0] + layout.w);
+  box.push(box[1] + layout.h);
+
+  $.each(this.hairlines_, function(idx, h) {
+    var left = g.toDomXCoord(h.xval);
+    h.domX = left;  // See comments in this.dataDidUpdate
+    $(h.lineDiv).css({
+      'left': left + 'px',
+      'top': layout.y + 'px',
+      'height': layout.h + 'px'
+    });  // .draggable("option", "containment", box);
+    $(h.infoDiv).css({
+      'left': left + 'px',
+      'top': layout.y + 'px',
+    }).draggable("option", "containment", box);
+
+    var visible = (left >= chartLeft && left <= chartRight);
+    $([h.infoDiv, h.lineDiv]).toggle(visible);
+  });
+};
+
+// Sets styles on the hairline (i.e. "selected")
+hairlines.prototype.updateHairlineStyles = function() {
+  $.each(this.hairlines_, function(idx, h) {
+    $([h.infoDiv, h.lineDiv]).toggleClass('selected', h.selected);
+  });
+};
+
+// Find prevRow and nextRow such that
+// g.getValue(prevRow, 0) <= xval
+// g.getValue(nextRow, 0) >= xval
+// g.getValue({prev,next}Row, col) != null, NaN or undefined
+// and there's no other row such that:
+//   g.getValue(prevRow, 0) < g.getValue(row, 0) < g.getValue(nextRow, 0)
+//   g.getValue(row, col) != null, NaN or undefined.
+// Returns [prevRow, nextRow]. Either can be null (but not both).
+hairlines.findPrevNextRows = function(g, xval, col) {
+  var prevRow = null, nextRow = null;
+  var numRows = g.numRows();
+  for (var row = 0; row < numRows; row++) {
+    var yval = g.getValue(row, col);
+    if (yval === null || yval === undefined || isNaN(yval)) continue;
+
+    var rowXval = g.getValue(row, 0);
+    if (rowXval <= xval) prevRow = row;
+
+    if (rowXval >= xval) {
+      nextRow = row;
+      break;
+    }
+  }
+
+  return [prevRow, nextRow];
+};
+
+// Fills out the info div based on current coordinates.
+hairlines.prototype.updateHairlineInfo = function() {
+  var mode = 'closest';
+
+  var g = this.dygraph_;
+  var xRange = g.xAxisRange();
+  var that = this;
+  $.each(this.hairlines_, function(idx, h) {
+    // To use generateLegendHTML, we synthesize an array of selected points.
+    var selPoints = [];
+    var labels = g.getLabels();
+    var row, prevRow, nextRow;
+
+    if (!h.interpolated) {
+      // "closest point" mode.
+      // TODO(danvk): make findClosestRow method public
+      row = g.findClosestRow(g.toDomXCoord(h.xval));
+      for (var i = 1; i < g.numColumns(); i++) {
+        selPoints.push({
+          canvasx: 1,  // TODO(danvk): real coordinate
+          canvasy: 1,  // TODO(danvk): real coordinate
+          xval: h.xval,
+          yval: g.getValue(row, i),
+          name: labels[i]
+        });
+      }
+    } else {
+      // "interpolated" mode.
+      for (var i = 1; i < g.numColumns(); i++) {
+        var prevNextRow = hairlines.findPrevNextRows(g, h.xval, i);
+        prevRow = prevNextRow[0], nextRow = prevNextRow[1];
+
+        // For x-values outside the domain, interpolate "between" the extreme
+        // point and itself.
+        if (prevRow === null) prevRow = nextRow;
+        if (nextRow === null) nextRow = prevRow;
+
+        // linear interpolation
+        var prevX = g.getValue(prevRow, 0),
+            nextX = g.getValue(nextRow, 0),
+            prevY = g.getValue(prevRow, i),
+            nextY = g.getValue(nextRow, i),
+            frac = prevRow == nextRow ? 0 : (h.xval - prevX) / (nextX - prevX),
+            yval = frac * nextY + (1 - frac) * prevY;
+
+        selPoints.push({
+          canvasx: 1,  // TODO(danvk): real coordinate
+          canvasy: 1,  // TODO(danvk): real coordinate
+          xval: h.xval,
+          yval: yval,
+          prevRow: prevRow,
+          nextRow: nextRow,
+          name: labels[i]
+        });
+      }
+    }
+
+    if (that.divFiller_) {
+      that.divFiller_(h.infoDiv, {
+        closestRow: row,
+        points: selPoints,
+        hairline: that.createPublicHairline_(h),
+        dygraph: g
+      });
+    } else {
+      var html = Dygraph.Plugins.Legend.generateLegendHTML(g, h.xval, selPoints, 10);
+      $('.hairline-legend', h.infoDiv).html(html);
+    }
+  });
+};
+
+// After a resize, the hairline divs can get dettached from the chart.
+// This reattaches them.
+hairlines.prototype.attachHairlinesToChart_ = function() {
+  var div = this.dygraph_.graphDiv;
+  $.each(this.hairlines_, function(idx, h) {
+    $([h.lineDiv, h.infoDiv]).appendTo(div);
+  });
+};
+
+// Deletes a hairline and removes it from the chart.
+hairlines.prototype.removeHairline = function(h) {
+  var idx = this.hairlines_.indexOf(h);
+  if (idx >= 0) {
+    this.hairlines_.splice(idx, 1);
+    $([h.lineDiv, h.infoDiv]).remove();
+  } else {
+    Dygraph.warn('Tried to remove non-existent hairline.');
+  }
+};
+
+hairlines.prototype.didDrawChart = function(e) {
+  var g = e.dygraph;
+
+  // Early out in the (common) case of zero hairlines.
+  if (this.hairlines_.length === 0) return;
+
+  this.updateHairlineDivPositions();
+  this.attachHairlinesToChart_();
+  this.updateHairlineInfo();
+  this.updateHairlineStyles();
+};
+
+hairlines.prototype.dataDidUpdate = function(e) {
+  // When the data in the chart updates, the hairlines should stay in the same
+  // position on the screen. didDrawChart stores a domX parameter for each
+  // hairline. We use that to reposition them on data updates.
+  var g = this.dygraph_;
+  $.each(this.hairlines_, function(idx, h) {
+    if (h.hasOwnProperty('domX')) {
+      h.xval = g.toDataXCoord(h.domX);
+    }
+  });
+};
+
+hairlines.prototype.click = function(e) {
+  if (this.addTimer_) {
+    // Another click is in progress; ignore this one.
+    return;
+  }
+
+  var area = e.dygraph.getArea();
+  var xval = this.dygraph_.toDataXCoord(e.canvasx);
+
+  var that = this;
+  this.addTimer_ = setTimeout(function() {
+    that.addTimer_ = null;
+    that.hairlines_.push(that.createHairline({xval: xval}));
+
+    that.updateHairlineDivPositions();
+    that.updateHairlineInfo();
+    that.updateHairlineStyles();
+    that.attachHairlinesToChart_();
+
+    $(that).triggerHandler('hairlineCreated', {
+      xval: xval
+    });
+    $(that).triggerHandler('hairlinesChanged', {});
+  }, CLICK_DELAY_MS);
+};
+
+hairlines.prototype.dblclick = function(e) {
+  if (this.addTimer_) {
+    clearTimeout(this.addTimer_);
+    this.addTimer_ = null;
+  }
+};
+
+hairlines.prototype.destroy = function() {
+  this.detachLabels();
+};
+
+
+// Public API
+
+/**
+ * This is a restricted view of this.hairlines_ which doesn't expose
+ * implementation details like the handle divs.
+ *
+ * @typedef {
+ *   xval:  number,       // x-value (i.e. millis or a raw number)
+ *   interpolated: bool,  // alternative is to snap to closest
+ *   selected: bool       // whether the hairline is selected.
+ * } PublicHairline
+ */
+
+/**
+ * @param {!Hairline} h Internal hairline.
+ * @return {!PublicHairline} Restricted public view of the hairline.
+ */
+hairlines.prototype.createPublicHairline_ = function(h) {
+  return {
+    xval: h.xval,
+    interpolated: h.interpolated,
+    selected: h.selected
+  };
+};
+
+/**
+ * @return {!Array.<!PublicHairline>} The current set of hairlines, ordered
+ *     from back to front.
+ */
+hairlines.prototype.get = function() {
+  var result = [];
+  for (var i = 0; i < this.hairlines_.length; i++) {
+    var h = this.hairlines_[i];
+    result.push(this.createPublicHairline_(h));
+  }
+  return result;
+};
+
+/**
+ * Calling this will result in a hairlinesChanged event being triggered, no
+ * matter whether it consists of additions, deletions, moves or no changes at
+ * all.
+ *
+ * @param {!Array.<!PublicHairline>} hairlines The new set of hairlines,
+ *     ordered from back to front.
+ */
+hairlines.prototype.set = function(hairlines) {
+  // Re-use divs from the old hairlines array so far as we can.
+  // They're already correctly z-ordered.
+  var anyCreated = false;
+  for (var i = 0; i < hairlines.length; i++) {
+    var h = hairlines[i];
+
+    if (this.hairlines_.length > i) {
+      this.hairlines_[i].xval = h.xval;
+      this.hairlines_[i].interpolated = h.interpolated;
+      this.hairlines_[i].selected = h.selected;
+    } else {
+      this.hairlines_.push(this.createHairline({
+        xval: h.xval,
+        interpolated: h.interpolated,
+        selected: h.selected
+      }));
+      anyCreated = true;
+    }
+  }
+
+  // If there are any remaining hairlines, destroy them.
+  while (hairlines.length < this.hairlines_.length) {
+    this.removeHairline(this.hairlines_[hairlines.length]);
+  }
+
+  this.updateHairlineDivPositions();
+  this.updateHairlineInfo();
+  this.updateHairlineStyles();
+  if (anyCreated) {
+    this.attachHairlinesToChart_();
+  }
+
+  $(this).triggerHandler('hairlinesChanged', {});
+};
+
+return hairlines;
+
+})();
diff --git a/src/extras/shapes.js b/src/extras/shapes.js
new file mode 100644 (file)
index 0000000..a453177
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/**
+ * @fileoverview
+ * Including this file will add several additional shapes to Dygraph.Circles
+ * which can be passed to drawPointCallback.
+ * See tests/custom-circles.html for usage.
+ */
+
+(function() {
+
+/**
+ * @param {!CanvasRenderingContext2D} ctx the canvas context
+ * @param {number} sides the number of sides in the shape.
+ * @param {number} radius the radius of the image.
+ * @param {number} cx center x coordate
+ * @param {number} cy center y coordinate
+ * @param {number=} rotationRadians the shift of the initial angle, in radians.
+ * @param {number=} delta the angle shift for each line. If missing, creates a
+ *     regular polygon.
+ */
+var regularShape = function(
+    ctx, sides, radius, cx, cy, rotationRadians, delta) {
+  rotationRadians = rotationRadians || 0;
+  delta = delta || Math.PI * 2 / sides;
+
+  ctx.beginPath();
+  var initialAngle = rotationRadians;
+  var angle = initialAngle;
+
+  var computeCoordinates = function() {
+    var x = cx + (Math.sin(angle) * radius);
+    var y = cy + (-Math.cos(angle) * radius);
+    return [x, y];
+  };
+
+  var initialCoordinates = computeCoordinates();
+  var x = initialCoordinates[0];
+  var y = initialCoordinates[1];
+  ctx.moveTo(x, y);
+
+  for (var idx = 0; idx < sides; idx++) {
+    angle = (idx == sides - 1) ? initialAngle : (angle + delta);
+    var coords = computeCoordinates();
+    ctx.lineTo(coords[0], coords[1]);
+  }
+  ctx.fill();
+  ctx.stroke();
+};
+
+/**
+ * TODO(danvk): be more specific on the return type.
+ * @param {number} sides
+ * @param {number=} rotationRadians
+ * @param {number=} delta
+ * @return {Function}
+ * @private
+ */
+var shapeFunction = function(sides, rotationRadians, delta) {
+  return function(g, name, ctx, cx, cy, color, radius) {
+    ctx.strokeStyle = color;
+    ctx.fillStyle = "white";
+    regularShape(ctx, sides, radius, cx, cy, rotationRadians, delta);
+  };
+};
+
+Dygraph.update(Dygraph.Circles, {
+  TRIANGLE : shapeFunction(3),
+  SQUARE : shapeFunction(4, Math.PI / 4),
+  DIAMOND : shapeFunction(4),
+  PENTAGON : shapeFunction(5),
+  HEXAGON : shapeFunction(6),
+  CIRCLE : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.beginPath();
+    ctx.strokeStyle = color;
+    ctx.fillStyle = "white";
+    ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
+    ctx.fill();
+    ctx.stroke();
+  },
+  STAR : shapeFunction(5, 0, 4 * Math.PI / 5),
+  PLUS : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.strokeStyle = color;
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy);
+    ctx.lineTo(cx - radius, cy);
+    ctx.closePath();
+    ctx.stroke();
+
+    ctx.beginPath();
+    ctx.moveTo(cx, cy + radius);
+    ctx.lineTo(cx, cy - radius);
+    ctx.closePath();
+    ctx.stroke();
+  },
+  EX : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.strokeStyle = color;
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy + radius);
+    ctx.lineTo(cx - radius, cy - radius);
+    ctx.closePath();
+    ctx.stroke();
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy - radius);
+    ctx.lineTo(cx - radius, cy + radius);
+    ctx.closePath();
+    ctx.stroke();
+  }
+});
+
+})();
diff --git a/src/extras/smooth-plotter.js b/src/extras/smooth-plotter.js
new file mode 100644 (file)
index 0000000..119bf3f
--- /dev/null
@@ -0,0 +1,126 @@
+var smoothPlotter = (function() {
+"use strict";
+
+/**
+ * Given three sequential points, p0, p1 and p2, find the left and right
+ * control points for p1.
+ *
+ * The three points are expected to have x and y properties.
+ *
+ * The alpha parameter controls the amount of smoothing.
+ * If Î±=0, then both control points will be the same as p1 (i.e. no smoothing).
+ *
+ * Returns [l1x, l1y, r1x, r1y]
+ *
+ * It's guaranteed that the line from (l1x, l1y)-(r1x, r1y) passes through p1.
+ * Unless allowFalseExtrema is set, then it's also guaranteed that:
+ *   l1y âˆˆ [p0.y, p1.y]
+ *   r1y âˆˆ [p1.y, p2.y]
+ *
+ * The basic algorithm is:
+ * 1. Put the control points l1 and r1 Î± of the way down (p0, p1) and (p1, p2).
+ * 2. Shift l1 and r2 so that the line l1–r1 passes through p1
+ * 3. Adjust to prevent false extrema while keeping p1 on the l1–r1 line.
+ *
+ * This is loosely based on the HighCharts algorithm.
+ */
+function getControlPoints(p0, p1, p2, opt_alpha, opt_allowFalseExtrema) {
+  var alpha = (opt_alpha !== undefined) ? opt_alpha : 1/3;  // 0=no smoothing, 1=crazy smoothing
+  var allowFalseExtrema = opt_allowFalseExtrema || false;
+
+  if (!p2) {
+    return [p1.x, p1.y, null, null];
+  }
+
+  // Step 1: Position the control points along each line segment.
+  var l1x = (1 - alpha) * p1.x + alpha * p0.x,
+      l1y = (1 - alpha) * p1.y + alpha * p0.y,
+      r1x = (1 - alpha) * p1.x + alpha * p2.x,
+      r1y = (1 - alpha) * p1.y + alpha * p2.y;
+
+  // Step 2: shift the points up so that p1 is on the l1–r1 line.
+  if (l1x != r1x) {
+    // This can be derived w/ some basic algebra.
+    var deltaY = p1.y - r1y - (p1.x - r1x) * (l1y - r1y) / (l1x - r1x);
+    l1y += deltaY;
+    r1y += deltaY;
+  }
+
+  // Step 3: correct to avoid false extrema.
+  if (!allowFalseExtrema) {
+    if (l1y > p0.y && l1y > p1.y) {
+      l1y = Math.max(p0.y, p1.y);
+      r1y = 2 * p1.y - l1y;
+    } else if (l1y < p0.y && l1y < p1.y) {
+      l1y = Math.min(p0.y, p1.y);
+      r1y = 2 * p1.y - l1y;
+    }
+
+    if (r1y > p1.y && r1y > p2.y) {
+      r1y = Math.max(p1.y, p2.y);
+      l1y = 2 * p1.y - r1y;
+    } else if (r1y < p1.y && r1y < p2.y) {
+      r1y = Math.min(p1.y, p2.y);
+      l1y = 2 * p1.y - r1y;
+    }
+  }
+
+  return [l1x, l1y, r1x, r1y];
+}
+
+
+// A plotter which uses splines to create a smooth curve.
+// See tests/plotters.html for a demo.
+// Can be controlled via smoothPlotter.smoothing
+function smoothPlotter(e) {
+  var ctx = e.drawingContext,
+      points = e.points;
+
+  ctx.beginPath();
+  ctx.moveTo(points[0].canvasx, points[0].canvasy);
+
+  // right control point for previous point
+  var lastRightX = points[0].canvasx, lastRightY = points[0].canvasy;
+  var isOK = Dygraph.isOK;  // i.e. is none of (null, undefined, NaN)
+
+  for (var i = 1; i < points.length; i++) {
+    var p0 = points[i - 1],
+        p1 = points[i],
+        p2 = points[i + 1];
+    p0 = p0 && isOK(p0.canvasy) ? p0 : null;
+    p1 = p1 && isOK(p1.canvasy) ? p1 : null;
+    p2 = p2 && isOK(p2.canvasy) ? p2 : null;
+    if (p0 && p1) {
+      var controls = getControlPoints({x: p0.canvasx, y: p0.canvasy},
+                                      {x: p1.canvasx, y: p1.canvasy},
+                                      p2 && {x: p2.canvasx, y: p2.canvasy},
+                                      smoothPlotter.smoothing);
+      // Uncomment to show the control points:
+      // ctx.lineTo(lastRightX, lastRightY);
+      // ctx.lineTo(controls[0], controls[1]);
+      // ctx.lineTo(p1.canvasx, p1.canvasy);
+      lastRightX = (lastRightX !== null) ? lastRightX : p0.canvasx;
+      lastRightY = (lastRightY !== null) ? lastRightY : p0.canvasy;
+      ctx.bezierCurveTo(lastRightX, lastRightY,
+                        controls[0], controls[1],
+                        p1.canvasx, p1.canvasy);
+      lastRightX = controls[2];
+      lastRightY = controls[3];
+    } else if (p1) {
+      // We're starting again after a missing point.
+      ctx.moveTo(p1.canvasx, p1.canvasy);
+      lastRightX = p1.canvasx;
+      lastRightY = p1.canvasy;
+    } else {
+      lastRightX = lastRightY = null;
+    }
+  }
+
+  ctx.stroke();
+}
+smoothPlotter.smoothing = 1/3;
+smoothPlotter._getControlPoints = getControlPoints;  // for testing
+
+return smoothPlotter;
+
+})();
diff --git a/src/extras/super-annotations.js b/src/extras/super-annotations.js
new file mode 100644 (file)
index 0000000..b8b10c6
--- /dev/null
@@ -0,0 +1,474 @@
+/**
+ * @license
+ * Copyright 2013 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ *
+ * Note: This plugin requires jQuery and jQuery UI Draggable.
+ *
+ * See high-level documentation at
+ * https://docs.google.com/document/d/1OHNE8BNNmMtFlRQ969DACIYIJ9VVJ7w3dSPRJDEeIew/edit#
+ */
+
+/*global Dygraph:false */
+
+Dygraph.Plugins.SuperAnnotations = (function() {
+
+"use strict";
+
+/**
+ * These are just the basic requirements -- annotations can have whatever other
+ * properties the code that displays them wants them to have.
+ *
+ * @typedef {
+ *   xval:  number,      // x-value (i.e. millis or a raw number)
+ *   series: string,     // series name
+ *   yFrac: ?number,     // y-positioning. Default is a few px above the point.
+ *   lineDiv: !Element   // vertical div connecting point to info div.
+ *   infoDiv: !Element   // div containing info about the annotation.
+ * } Annotation
+ */
+
+var annotations = function(opt_options) {
+  /* @type {!Array.<!Annotation>} */
+  this.annotations_ = [];
+  // Used to detect resizes (which require the divs to be repositioned).
+  this.lastWidth_ = -1;
+  this.lastHeight = -1;
+  this.dygraph_ = null;
+
+  opt_options = opt_options || {};
+  this.defaultAnnotationProperties_ = $.extend({
+    'text': 'Description'
+  }, opt_options['defaultAnnotationProperties']);
+};
+
+annotations.prototype.toString = function() {
+  return "SuperAnnotations Plugin";
+};
+
+annotations.prototype.activate = function(g) {
+  this.dygraph_ = g;
+  this.annotations_ = [];
+
+  return {
+    didDrawChart: this.didDrawChart,
+    pointClick: this.pointClick  // TODO(danvk): implement in dygraphs
+  };
+};
+
+annotations.prototype.detachLabels = function() {
+  for (var i = 0; i < this.annotations_.length; i++) {
+    var a = this.annotations_[i];
+    $(a.lineDiv).remove();
+    $(a.infoDiv).remove();
+    this.annotations_[i] = null;
+  }
+  this.annotations_ = [];
+};
+
+annotations.prototype.annotationWasDragged = function(a, event, ui) {
+  var g = this.dygraph_;
+  var area = g.getArea();
+  var oldYFrac = a.yFrac;
+
+  var infoDiv = a.infoDiv;
+  var newYFrac = ((infoDiv.offsetTop + infoDiv.offsetHeight) - area.y) / area.h;
+  if (newYFrac == oldYFrac) return;
+
+  a.yFrac = newYFrac;
+
+  this.moveAnnotationToTop(a);
+  this.updateAnnotationDivPositions();
+  this.updateAnnotationInfo();
+  $(this).triggerHandler('annotationMoved', {
+    annotation: a,
+    oldYFrac: oldYFrac,
+    newYFrac: a.yFrac
+  });
+  $(this).triggerHandler('annotationsChanged', {});
+};
+
+annotations.prototype.makeAnnotationEditable = function(a) {
+  if (a.editable == true) return;
+  this.moveAnnotationToTop(a);
+
+  // Note: we have to fill out the HTML ourselves because
+  // updateAnnotationInfo() won't touch editable annotations.
+  a.editable = true;
+  var editableTemplateDiv = $('#annotation-editable-template').get(0);
+  a.infoDiv.innerHTML = this.getTemplateHTML(editableTemplateDiv, a);
+  $(a.infoDiv).toggleClass('editable', !!a.editable);
+  $(this).triggerHandler('beganEditAnnotation', a);
+};
+
+// This creates the hairline object and returns it.
+// It does not position it and does not attach it to the chart.
+annotations.prototype.createAnnotation = function(a) {
+  var self = this;
+
+  var color = this.getColorForSeries_(a.series);
+
+  var $lineDiv = $('<div/>').css({
+    'width': '1px',
+    'left': '3px',
+    'background': 'black',
+    'height': '100%',
+    'position': 'absolute',
+    // TODO(danvk): use border-color here for consistency?
+    'background-color': color,
+    'z-index': 10
+  }).addClass('dygraph-annotation-line');
+
+  var $infoDiv = $('#annotation-template').clone().removeAttr('id').css({
+      'position': 'absolute',
+      'border-color': color,
+      'z-index': 10
+    })
+    .show();
+
+  $.extend(a, {
+    lineDiv: $lineDiv.get(0),
+    infoDiv: $infoDiv.get(0)
+  });
+
+  var that = this;
+
+  $infoDiv.draggable({
+    'start': function(event, ui) {
+      $(this).css({'bottom': ''});
+      a.isDragging = true;
+    },
+    'drag': function(event, ui) {
+      self.annotationWasDragged(a, event, ui);
+    },
+    'stop': function(event, ui) {
+      $(this).css({'top': ''});
+      a.isDragging = false;
+      self.updateAnnotationDivPositions();
+    },
+    'axis': 'y',
+    'containment': 'parent'
+  });
+
+  // TODO(danvk): use 'on' instead of delegate/dblclick
+  $infoDiv.on('click', '.annotation-kill-button', function() {
+    that.removeAnnotation(a);
+    $(that).triggerHandler('annotationDeleted', a);
+    $(that).triggerHandler('annotationsChanged', {});
+  });
+
+  $infoDiv.on('dblclick', function() {
+    that.makeAnnotationEditable(a);
+  });
+  $infoDiv.on('click', '.annotation-update', function() {
+    self.extractUpdatedProperties_($infoDiv.get(0), a);
+    a.editable = false;
+    self.updateAnnotationInfo();
+    $(that).triggerHandler('annotationEdited', a);
+    $(that).triggerHandler('annotationsChanged', {});
+  });
+  $infoDiv.on('click', '.annotation-cancel', function() {
+    a.editable = false;
+    self.updateAnnotationInfo();
+    $(that).triggerHandler('cancelEditAnnotation', a);
+  });
+
+  return a;
+};
+
+// Find the index of a point in a series.
+// Returns a 2-element array, [row, col], which can be used with
+// dygraph.getValue() to get the value at this point.
+// Returns null if there's no match.
+annotations.prototype.findPointIndex_ = function(series, xval) {
+  var col = this.dygraph_.getLabels().indexOf(series);
+  if (col == -1) return null;
+
+  var lowIdx = 0, highIdx = this.dygraph_.numRows() - 1;
+  while (lowIdx <= highIdx) {
+    var idx = Math.floor((lowIdx + highIdx) / 2);
+    var xAtIdx = this.dygraph_.getValue(idx, 0);
+    if (xAtIdx == xval) {
+      return [idx, col];
+    } else if (xAtIdx < xval) {
+      lowIdx = idx + 1;
+    } else {
+      highIdx = idx - 1;
+    }
+  }
+  return null;
+};
+
+annotations.prototype.getColorForSeries_ = function(series) {
+  var colors = this.dygraph_.getColors();
+  var col = this.dygraph_.getLabels().indexOf(series);
+  if (col == -1) return null;
+
+  return colors[(col - 1) % colors.length];
+};
+
+// Moves a hairline's divs to the top of the z-ordering.
+annotations.prototype.moveAnnotationToTop = function(a) {
+  var div = this.dygraph_.graphDiv;
+  $(a.infoDiv).appendTo(div);
+  $(a.lineDiv).appendTo(div);
+
+  var idx = this.annotations_.indexOf(a);
+  this.annotations_.splice(idx, 1);
+  this.annotations_.push(a);
+};
+
+// Positions existing hairline divs.
+annotations.prototype.updateAnnotationDivPositions = function() {
+  var layout = this.dygraph_.getArea();
+  var chartLeft = layout.x, chartRight = layout.x + layout.w;
+  var chartTop = layout.y, chartBottom = layout.y + layout.h;
+  var div = this.dygraph_.graphDiv;
+  var pos = Dygraph.findPos(div);
+  var box = [layout.x + pos.x, layout.y + pos.y];
+  box.push(box[0] + layout.w);
+  box.push(box[1] + layout.h);
+
+  var g = this.dygraph_;
+
+  var that = this;
+  $.each(this.annotations_, function(idx, a) {
+    var row_col = that.findPointIndex_(a.series, a.xval);
+    if (row_col == null) {
+      $([a.lineDiv, a.infoDiv]).hide();
+      return;
+    } else {
+      // TODO(danvk): only do this if they're invisible?
+      $([a.lineDiv, a.infoDiv]).show();
+    }
+    var xy = g.toDomCoords(a.xval, g.getValue(row_col[0], row_col[1]));
+    var x = xy[0], pointY = xy[1];
+
+    var lineHeight = 6;  // TODO(danvk): option?
+
+    var y = pointY;
+    if (a.yFrac !== undefined) {
+      y = layout.y + layout.h * a.yFrac;
+    } else {
+      y -= lineHeight;
+    }
+
+    var lineHeight = y < pointY ? (pointY - y) : (y - pointY - a.infoDiv.offsetHeight);
+    $(a.lineDiv).css({
+      'left': x + 'px',
+      'top': Math.min(y, pointY) + 'px',
+      'height': lineHeight + 'px'
+    });
+    $(a.infoDiv).css({
+      'left': x + 'px',
+    });
+    if (!a.isDragging) {
+      // jQuery UI draggable likes to set 'top', whereas superannotations sets
+      // 'bottom'. Setting both will make the annotation grow and contract as
+      // the user drags it, which looks bad.
+      $(a.infoDiv).css({
+        'bottom': (div.offsetHeight - y) + 'px'
+      })  //.draggable("option", "containment", box);
+
+      var visible = (x >= chartLeft && x <= chartRight) &&
+                    (pointY >= chartTop && pointY <= chartBottom);
+      $([a.infoDiv, a.lineDiv]).toggle(visible);
+    }
+  });
+};
+
+// Fills out the info div based on current coordinates.
+annotations.prototype.updateAnnotationInfo = function() {
+  var g = this.dygraph_;
+
+  var that = this;
+  var templateDiv = $('#annotation-template').get(0);
+  $.each(this.annotations_, function(idx, a) {
+    // We should never update an editable div -- doing so may kill unsaved
+    // edits to an annotation.
+    $(a.infoDiv).toggleClass('editable', !!a.editable);
+    if (a.editable) return;
+    a.infoDiv.innerHTML = that.getTemplateHTML(templateDiv, a);
+  });
+};
+
+/**
+ * @param {!Annotation} a Internal annotation
+ * @return {!PublicAnnotation} a view of the annotation for the public API.
+ */
+annotations.prototype.createPublicAnnotation_ = function(a, opt_props) {
+  var displayAnnotation = $.extend({}, a, opt_props);
+  delete displayAnnotation['infoDiv'];
+  delete displayAnnotation['lineDiv'];
+  delete displayAnnotation['isDragging'];
+  delete displayAnnotation['editable'];
+  return displayAnnotation;
+};
+
+// Fill out a div using the values in the annotation object.
+// The div's html is expected to have text of the form "{{key}}"
+annotations.prototype.getTemplateHTML = function(div, a) {
+  var g = this.dygraph_;
+  var row_col = this.findPointIndex_(a.series, a.xval);
+  if (row_col == null) return;  // perhaps it's no longer a real point?
+  var row = row_col[0];
+  var col = row_col[1];
+
+  var yOptView = g.optionsViewForAxis_('y1');  // TODO: support secondary, too
+  var xvf = g.getOptionForAxis('valueFormatter', 'x');
+
+  var x = xvf.call(g, a.xval);
+  var y = g.getOption('valueFormatter', a.series).call(
+      g, g.getValue(row, col), yOptView);
+
+  var displayAnnotation = this.createPublicAnnotation_(a, {x:x, y:y});
+  var html = div.innerHTML;
+  for (var k in displayAnnotation) {
+    var v = displayAnnotation[k];
+    if (typeof(v) == 'object') continue;  // e.g. infoDiv or lineDiv
+    html = html.replace(new RegExp('\{\{' + k + '\}\}', 'g'), v);
+  }
+  return html;
+};
+
+// Update the annotation object by looking for elements with a 'dg-ann-field'
+// attribute. For example, <input type='text' dg-ann-field='text' /> will have
+// its value placed in the 'text' attribute of the annotation.
+annotations.prototype.extractUpdatedProperties_ = function(div, a) {
+  $(div).find('[dg-ann-field]').each(function(idx, el) {
+    var k = $(el).attr('dg-ann-field');
+    var v = $(el).val();
+    a[k] = v;
+  });
+};
+
+// After a resize, the hairline divs can get dettached from the chart.
+// This reattaches them.
+annotations.prototype.attachAnnotationsToChart_ = function() {
+  var div = this.dygraph_.graphDiv;
+  $.each(this.annotations_, function(idx, a) {
+    // Re-attaching an editable div to the DOM can clear its focus.
+    // This makes typing really difficult!
+    if (a.editable) return;
+
+    $([a.lineDiv, a.infoDiv]).appendTo(div);
+  });
+};
+
+// Deletes a hairline and removes it from the chart.
+annotations.prototype.removeAnnotation = function(a) {
+  var idx = this.annotations_.indexOf(a);
+  if (idx >= 0) {
+    this.annotations_.splice(idx, 1);
+    $([a.lineDiv, a.infoDiv]).remove();
+  } else {
+    Dygraph.warn('Tried to remove non-existent annotation.');
+  }
+};
+
+annotations.prototype.didDrawChart = function(e) {
+  var g = e.dygraph;
+
+  // Early out in the (common) case of zero annotations.
+  if (this.annotations_.length === 0) return;
+
+  this.updateAnnotationDivPositions();
+  this.attachAnnotationsToChart_();
+  this.updateAnnotationInfo();
+};
+
+annotations.prototype.pointClick = function(e) {
+  // Prevent any other behavior based on this click, e.g. creation of a hairline.
+  e.preventDefault();
+
+  var a = $.extend({}, this.defaultAnnotationProperties_, {
+    series: e.point.name,
+    xval: e.point.xval
+  });
+  this.annotations_.push(this.createAnnotation(a));
+
+  this.updateAnnotationDivPositions();
+  this.updateAnnotationInfo();
+  this.attachAnnotationsToChart_();
+
+  $(this).triggerHandler('annotationCreated', a);
+  $(this).triggerHandler('annotationsChanged', {});
+
+  // Annotations should begin life editable.
+  this.makeAnnotationEditable(a);
+};
+
+annotations.prototype.destroy = function() {
+  this.detachLabels();
+};
+
+
+// Public API
+
+/**
+ * This is a restricted view of this.annotations_ which doesn't expose
+ * implementation details like the line / info divs.
+ *
+ * @typedef {
+ *   xval:  number,      // x-value (i.e. millis or a raw number)
+ *   series: string,     // series name
+ * } PublicAnnotation
+ */
+
+/**
+ * @return {!Array.<!PublicAnnotation>} The current set of annotations, ordered
+ *     from back to front.
+ */
+annotations.prototype.get = function() {
+  var result = [];
+  for (var i = 0; i < this.annotations_.length; i++) {
+    result.push(this.createPublicAnnotation_(this.annotations_[i]));
+  }
+  return result;
+};
+
+/**
+ * Calling this will result in an annotationsChanged event being triggered, no
+ * matter whether it consists of additions, deletions, moves or no changes at
+ * all.
+ *
+ * @param {!Array.<!PublicAnnotation>} annotations The new set of annotations,
+ *     ordered from back to front.
+ */
+annotations.prototype.set = function(annotations) {
+  // Re-use divs from the old annotations array so far as we can.
+  // They're already correctly z-ordered.
+  var anyCreated = false;
+  for (var i = 0; i < annotations.length; i++) {
+    var a = annotations[i];
+
+    if (this.annotations_.length > i) {
+      // Only the divs need to be preserved.
+      var oldA = this.annotations_[i];
+      this.annotations_[i] = $.extend({
+        infoDiv: oldA.infoDiv,
+        lineDiv: oldA.lineDiv
+      }, a);
+    } else {
+      this.annotations_.push(this.createAnnotation(a));
+      anyCreated = true;
+    }
+  }
+
+  // If there are any remaining annotations, destroy them.
+  while (annotations.length < this.annotations_.length) {
+    this.removeAnnotation(this.annotations_[annotations.length]);
+  }
+
+  this.updateAnnotationDivPositions();
+  this.updateAnnotationInfo();
+  if (anyCreated) {
+    this.attachAnnotationsToChart_();
+  }
+
+  $(this).triggerHandler('annotationsChanged', {});
+};
+
+return annotations;
+
+})();
diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js
new file mode 100644 (file)
index 0000000..16e4d96
--- /dev/null
@@ -0,0 +1,206 @@
+/**
+ * Synchronize zooming and/or selections between a set of dygraphs.
+ *
+ * Usage:
+ *
+ *   var g1 = new Dygraph(...),
+ *       g2 = new Dygraph(...),
+ *       ...;
+ *   var sync = Dygraph.synchronize(g1, g2, ...);
+ *   // charts are now synchronized
+ *   sync.detach();
+ *   // charts are no longer synchronized
+ *
+ * You can set options using the last parameter, for example:
+ *
+ *   var sync = Dygraph.synchronize(g1, g2, g3, {
+ *      selection: true,
+ *      zoom: true
+ *   });
+ *
+ * The default is to synchronize both of these.
+ *
+ * Instead of passing one Dygraph object as each parameter, you may also pass an
+ * array of dygraphs:
+ *
+ *   var sync = Dygraph.synchronize([g1, g2, g3], {
+ *      selection: false,
+ *      zoom: true
+ *   });
+ *
+ * You may also set `range: false` if you wish to only sync the x-axis.
+ * The `range` option has no effect unless `zoom` is true (the default).
+ */
+(function() {
+/* global Dygraph:false */
+'use strict';
+
+Dygraph.synchronize = function(/* dygraphs..., opts */) {
+  if (arguments.length === 0) {
+    throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
+  }
+
+  var OPTIONS = ['selection', 'zoom', 'range'];
+  var opts = {
+    selection: true,
+    zoom: true,
+    range: true
+  };
+  var dygraphs = [];
+
+  var prevCallbacks = {
+    draw: null,
+    highlight: null,
+    unhighlight: null
+  };
+
+  var parseOpts = function(obj) {
+    if (!(obj instanceof Object)) {
+      throw 'Last argument must be either Dygraph or Object.';
+    } else {
+      for (var i = 0; i < OPTIONS.length; i++) {
+        var optName = OPTIONS[i];
+        if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
+      }
+    }
+  };
+
+  if (arguments[0] instanceof Dygraph) {
+    // Arguments are Dygraph objects.
+    for (var i = 0; i < arguments.length; i++) {
+      if (arguments[i] instanceof Dygraph) {
+        dygraphs.push(arguments[i]);
+      } else {
+        break;
+      }
+    }
+    if (i < arguments.length - 1) {
+      throw 'Invalid invocation of Dygraph.synchronize(). ' +
+            'All but the last argument must be Dygraph objects.';
+    } else if (i == arguments.length - 1) {
+      parseOpts(arguments[arguments.length - 1]);
+    }
+  } else if (arguments[0].length) {
+    // Invoked w/ list of dygraphs, options
+    for (var i = 0; i < arguments[0].length; i++) {
+      dygraphs.push(arguments[0][i]);
+    }
+    if (arguments.length == 2) {
+      parseOpts(arguments[1]);
+    } else if (arguments.length > 2) {
+      throw 'Invalid invocation of Dygraph.synchronize(). ' +
+            'Expected two arguments: array and optional options argument.';
+    }  // otherwise arguments.length == 1, which is fine.
+  } else {
+    throw 'Invalid invocation of Dygraph.synchronize(). ' +
+          'First parameter must be either Dygraph or list of Dygraphs.';
+  }
+
+  if (dygraphs.length < 2) {
+    throw 'Invalid invocation of Dygraph.synchronize(). ' +
+          'Need two or more dygraphs to synchronize.';
+  }
+  
+  var readycount = dygraphs.length;
+  for (var i = 0; i < dygraphs.length; i++) {
+    var g = dygraphs[i];
+    g.ready( function() {
+      if (--readycount == 0) {
+        // Listen for draw, highlight, unhighlight callbacks.
+        if (opts.zoom) {
+          attachZoomHandlers(dygraphs, opts, prevCallbacks);
+        }
+
+        if (opts.selection) {
+          attachSelectionHandlers(dygraphs, prevCallbacks);
+        }
+      }
+    });
+  }
+  return {
+    detach: function() {
+      for (var i = 0; i < dygraphs.length; i++) {
+        var g = dygraphs[i];
+        if (opts.zoom) {
+          g.updateOptions({drawCallback: prevCallbacks.draw});
+        }
+        if (opts.selection) {
+          g.updateOptions({
+            highlightCallback: prevCallbacks.highlight,
+            unhighlightCallback: prevCallbacks.unhighlight
+          });
+        }
+      }
+      // release references & make subsequent calls throw.
+      dygraphs = null;
+      opts = null;
+      prevCallbacks = null;
+    }
+  };
+};
+
+function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
+  var block = false;
+  for (var i = 0; i < gs.length; i++) {
+    var g = gs[i];
+    prevCallbacks.draw = g.getFunctionOption('drawCallback');
+    g.updateOptions({
+      drawCallback: function(me, initial) {
+        if (prevCallbacks.draw) prevCallbacks.draw(me, initial);
+        if (block || initial) return;
+        block = true;
+        var opts = {
+          dateWindow: me.xAxisRange()
+        };
+        if (syncOpts.range) opts.valueRange = me.yAxisRange();
+
+        for (var j = 0; j < gs.length; j++) {
+          if (gs[j] == me) continue;
+          gs[j].updateOptions(opts);
+        }
+        block = false;
+      }
+    }, false /* no need to redraw */);
+  }
+}
+
+function attachSelectionHandlers(gs, prevCallbacks) {
+  var block = false;
+  for (var i = 0; i < gs.length; i++) {
+    var g = gs[i];
+    prevCallbacks.highlight = g.getFunctionOption('highlightCallback');
+    prevCallbacks.unhighlight = g.getFunctionOption('unhighlightCallback');
+    g.updateOptions({
+      highlightCallback: function(event, x, points, row, seriesName) {
+        if (prevCallbacks.highlight) {
+            prevCallbacks.highlight(event, x, points, row, seriesName);
+        }
+        if (block) return;
+        block = true;
+        var me = this;
+        for (var i = 0; i < gs.length; i++) {
+          if (me == gs[i]) continue;
+          var idx = gs[i].getRowForX(x);
+          if (idx !== null) {
+            gs[i].setSelection(idx, seriesName);
+          }
+        }
+        block = false;
+      },
+      unhighlightCallback: function(event) {
+        if (prevCallbacks.unhighlight) prevCallbacks.unhighlight(event);
+        if (block) return;
+        block = true;
+        var me = this;
+        for (var i = 0; i < gs.length; i++) {
+          if (me == gs[i]) continue;
+          gs[i].clearSelection();
+        }
+        block = false;
+      }
+    });
+  }
+}
+
+})();
diff --git a/src/extras/unzoom.js b/src/extras/unzoom.js
new file mode 100644 (file)
index 0000000..fac1bbc
--- /dev/null
@@ -0,0 +1,105 @@
+// Copyright (c) 2013 Google, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/*global Dygraph:false */
+
+/**
+ * @fileoverview Plug-in for providing unzoom-on-hover.
+ *
+ * @author konigsberg@google.com (Robert Konigsberg)
+ */
+Dygraph.Plugins.Unzoom = (function() {
+
+  "use strict";
+
+  /**
+   * Create a new instance.
+   *
+   * @constructor
+   */
+  var unzoom = function() {
+    this.button_ = null;
+
+    // True when the mouse is over the canvas. Must be tracked
+    // because the unzoom button state can change even when the
+    // mouse-over state hasn't.
+    this.over_ = false;
+  };
+
+  unzoom.prototype.toString = function() {
+    return 'Unzoom Plugin';
+  };
+
+  unzoom.prototype.activate = function(g) {
+    return {
+      willDrawChart: this.willDrawChart
+    };
+  };
+
+  unzoom.prototype.willDrawChart = function(e) {
+    var g = e.dygraph;
+
+    if (this.button_ !== null) {
+      // short-circuit: show the button only when we're moused over, and zoomed in.
+      var showButton = g.isZoomed() && this.over_;
+      this.show(showButton);
+      return;
+    }
+
+    this.button_ = document.createElement('button');
+    this.button_.innerHTML = 'Reset Zoom';
+    this.button_.style.display = 'none';
+    this.button_.style.position = 'absolute';
+    var area = g.plotter_.area;
+    this.button_.style.top = (area.y + 4) + 'px';
+    this.button_.style.left = (area.x + 4) + 'px';
+    this.button_.style.zIndex = 11;
+    var parent = g.graphDiv;
+    parent.insertBefore(this.button_, parent.firstChild);
+
+    var self = this;
+    this.button_.onclick = function() {
+      g.resetZoom();
+    };
+
+    g.addAndTrackEvent(parent, 'mouseover', function() {
+      if (g.isZoomed()) {
+        self.show(true);
+      }
+      self.over_ = true;
+    });
+
+    g.addAndTrackEvent(parent, 'mouseout', function() {
+      self.show(false);
+      self.over_ = false;
+    });
+  };
+
+  unzoom.prototype.show = function(enabled) {
+    this.button_.style.display = enabled ? '' : 'none';
+  };
+
+  unzoom.prototype.destroy = function() {
+    this.button_.parentElement.removeChild(this.button_);
+  };
+
+  return unzoom;
+
+})();
diff --git a/src/gviz-api.js b/src/gviz-api.js
new file mode 100644 (file)
index 0000000..b29fb26
--- /dev/null
@@ -0,0 +1,461 @@
+// Copyright 2009 Google Inc.
+// All Rights Reserved.
+
+/**
+ * This file exposes the external Google Visualization API.
+ *
+ * The file can be used to enable auto complete of objects and methods provided by the
+ * Google Visualization API, and for easier exploration of the API.
+ *
+ * To enable auto complete in a development environment - copy the file into the project
+ * you are working on where the development tool you are using can index the file.
+ *
+ * Disclaimer: there may be missing classes and methods and the file may
+ * be updated and/or changed. For the most up to date API reference please visit:
+ * {@link http://code.google.com/intl/iw/apis/visualization/documentation/reference.html}
+ */
+
+var google = {};
+google.visualization = {};
+
+/** @constructor */
+google.visualization.DataTable = function(opt_data, opt_version) {};
+google.visualization.DataTable.prototype.getNumberOfRows = function() {};
+google.visualization.DataTable.prototype.getNumberOfColumns = function() {};
+google.visualization.DataTable.prototype.clone = function() {};
+google.visualization.DataTable.prototype.getColumnId = function(columnIndex) {};
+google.visualization.DataTable.prototype.getColumnIndex = function(columnId) {};
+google.visualization.DataTable.prototype.getColumnLabel = function(columnIndex) {};
+google.visualization.DataTable.prototype.getColumnPattern = function(columnIndex) {};
+google.visualization.DataTable.prototype.getColumnRole = function(columnIndex) {};
+google.visualization.DataTable.prototype.getColumnType = function(columnIndex) {};
+google.visualization.DataTable.prototype.getValue = function(rowIndex, columnIndex) {};
+google.visualization.DataTable.prototype.getFormattedValue = function(rowIndex, columnIndex) {};
+google.visualization.DataTable.prototype.getProperty = function(rowIndex, columnIndex, property) {};
+google.visualization.DataTable.prototype.getProperties = function(rowIndex, columnIndex) {};
+google.visualization.DataTable.prototype.getTableProperties = function() {};
+google.visualization.DataTable.prototype.getTableProperty = function(property) {};
+google.visualization.DataTable.prototype.setTableProperties = function(properties) {};
+google.visualization.DataTable.prototype.setTableProperty = function(property, value) {};
+google.visualization.DataTable.prototype.setValue = function(rowIndex, columnIndex, value) {};
+google.visualization.DataTable.prototype.setFormattedValue = function(rowIndex, columnIndex, formattedValue) {};
+google.visualization.DataTable.prototype.setProperties = function(rowIndex, columnIndex, properties) {};
+google.visualization.DataTable.prototype.setProperty = function(rowIndex, columnIndex, property, value) {};
+google.visualization.DataTable.prototype.setCell = function(rowIndex, columnIndex, opt_value, opt_formattedValue, opt_properties) {};
+google.visualization.DataTable.prototype.setRowProperties = function(rowIndex, properties) {};
+google.visualization.DataTable.prototype.setRowProperty = function(rowIndex, property, value) {};
+google.visualization.DataTable.prototype.getRowProperty = function(rowIndex, property) {};
+google.visualization.DataTable.prototype.getRowProperties = function(rowIndex) {};
+google.visualization.DataTable.prototype.setColumnLabel = function(columnIndex, newLabel) {};
+google.visualization.DataTable.prototype.setColumnProperties = function(columnIndex, properties) {};
+google.visualization.DataTable.prototype.setColumnProperty = function(columnIndex, property, value) {};
+google.visualization.DataTable.prototype.getColumnProperty = function(columnIndex, property) {};
+google.visualization.DataTable.prototype.getColumnProperties = function(columnIndex) {};
+google.visualization.DataTable.prototype.insertColumn = function(atColIndex, type, opt_label, opt_id) {};
+google.visualization.DataTable.prototype.addColumn = function(type, opt_label, opt_id) {};
+google.visualization.DataTable.prototype.insertRows = function(atRowIndex, numOrArray) {};
+google.visualization.DataTable.prototype.addRows = function(numOrArray) {};
+google.visualization.DataTable.prototype.addRow = function(opt_cellArray) {};
+google.visualization.DataTable.prototype.getColumnRange = function(columnIndex) {};
+google.visualization.DataTable.prototype.getSortedRows = function(sortColumns) {};
+google.visualization.DataTable.prototype.sort = function(sortColumns) {};
+google.visualization.DataTable.prototype.getDistinctValues = function(column) {};
+google.visualization.DataTable.prototype.getFilteredRows = function(columnFilters) {};
+google.visualization.DataTable.prototype.removeRows = function(fromRowIndex, numRows) {};
+google.visualization.DataTable.prototype.removeRow = function(rowIndex) {};
+google.visualization.DataTable.prototype.removeColumns = function(fromColIndex, numCols) {};
+google.visualization.DataTable.prototype.removeColumn = function(colIndex) {};
+
+/** @return {string} JSON representation. */
+google.visualization.DataTable.prototype.toJSON = function() {};
+
+/** @constructor */
+google.visualization.QueryResponse = function(responseObj) {};
+google.visualization.QueryResponse.getVersionFromResponse = function(responseObj) {};
+google.visualization.QueryResponse.prototype.getVersion = function() {};
+google.visualization.QueryResponse.prototype.getExecutionStatus = function() {};
+google.visualization.QueryResponse.prototype.isError = function() {};
+google.visualization.QueryResponse.prototype.hasWarning = function() {};
+google.visualization.QueryResponse.prototype.containsReason = function(reason) {};
+google.visualization.QueryResponse.prototype.getDataSignature = function() {};
+google.visualization.QueryResponse.prototype.getDataTable = function() {};
+google.visualization.QueryResponse.prototype.getReasons = function() {};
+google.visualization.QueryResponse.prototype.getMessage = function() {};
+google.visualization.QueryResponse.prototype.getDetailedMessage = function() {};
+
+/** @constructor */
+google.visualization.Query = function(dataSourceUrl, opt_options) {};
+google.visualization.Query.refreshAllQueries = function() {};
+google.visualization.Query.setResponse = function(response) {};
+google.visualization.Query.prototype.setRefreshInterval = function(intervalSeconds) {};
+google.visualization.Query.prototype.send = function(responseHandler) {};
+google.visualization.Query.prototype.makeRequest = function(responseHandler, opt_params) {};
+google.visualization.Query.prototype.abort = function() {};
+google.visualization.Query.prototype.setTimeout = function(timeoutSeconds) {};
+google.visualization.Query.prototype.setRefreshable = function(refreshable) {};
+google.visualization.Query.prototype.setQuery = function(queryString) {};
+
+google.visualization.errors = {};
+google.visualization.errors.addError = function(container, message, opt_detailedMessage, opt_options) {};
+google.visualization.errors.removeAll = function(container) {};
+google.visualization.errors.addErrorFromQueryResponse = function(container, response) {};
+google.visualization.errors.removeError = function(id) {};
+google.visualization.errors.getContainer = function(errorId) {};
+
+google.visualization.events = {};
+google.visualization.events.addListener = function(eventSource, eventName, eventHandler) {};
+google.visualization.events.trigger = function(eventSource, eventName, eventDetails) {};
+google.visualization.events.removeListener = function(listener) {};
+google.visualization.events.removeAllListeners = function(eventSource) {};
+
+/** @constructor */
+google.visualization.DataView = function(dataTable) {};
+google.visualization.DataView.fromJSON = function(dataTable, view) {};
+google.visualization.DataView.prototype.setColumns = function(colIndices) {};
+google.visualization.DataView.prototype.setRows = function(arg0, opt_arg1) {};
+google.visualization.DataView.prototype.getViewColumns = function() {};
+google.visualization.DataView.prototype.getViewRows = function() {};
+google.visualization.DataView.prototype.hideColumns = function(colIndices) {};
+google.visualization.DataView.prototype.hideRows = function(arg0, opt_arg1) {};
+google.visualization.DataView.prototype.getViewColumnIndex = function(tableColumnIndex) {};
+google.visualization.DataView.prototype.getViewRowIndex = function(tableRowIndex) {};
+google.visualization.DataView.prototype.getTableColumnIndex = function(viewColumnIndex) {};
+google.visualization.DataView.prototype.getUnderlyingTableColumnIndex = function(viewColumnIndex) {};
+google.visualization.DataView.prototype.getTableRowIndex = function(viewRowIndex) {};
+google.visualization.DataView.prototype.getUnderlyingTableRowIndex = function(viewRowIndex) {};
+google.visualization.DataView.prototype.getNumberOfRows = function() {};
+google.visualization.DataView.prototype.getNumberOfColumns = function() {};
+google.visualization.DataView.prototype.getColumnId = function(columnIndex) {};
+google.visualization.DataView.prototype.getColumnIndex = function(columnId) {};
+google.visualization.DataView.prototype.getColumnLabel = function(columnIndex) {};
+google.visualization.DataView.prototype.getColumnPattern = function(columnIndex) {};
+google.visualization.DataView.prototype.getColumnRole = function(columnIndex) {};
+google.visualization.DataView.prototype.getColumnType = function(columnIndex) {};
+google.visualization.DataView.prototype.getValue = function(rowIndex, columnIndex) {};
+google.visualization.DataView.prototype.getFormattedValue = function(rowIndex, columnIndex) {};
+google.visualization.DataView.prototype.getProperty = function(rowIndex, columnIndex, property) {};
+google.visualization.DataView.prototype.getColumnProperty = function(columnIndex, property) {};
+google.visualization.DataView.prototype.getColumnProperties = function(columnIndex) {};
+google.visualization.DataView.prototype.getTableProperty = function(property) {};
+google.visualization.DataView.prototype.getTableProperties = function() {};
+google.visualization.DataView.prototype.getRowProperty = function(rowIndex, property) {};
+google.visualization.DataView.prototype.getRowProperties = function(rowIndex) {};
+google.visualization.DataView.prototype.getColumnRange = function(columnIndex) {};
+google.visualization.DataView.prototype.getDistinctValues = function(columnIndex) {};
+google.visualization.DataView.prototype.getSortedRows = function(sortColumns) {};
+google.visualization.DataView.prototype.getFilteredRows = function(columnFilters) {};
+google.visualization.DataView.prototype.toDataTable = function() {};
+
+/** @return {string} JSON representation. */
+google.visualization.DataView.prototype.toJSON = function() {};
+
+/** @constructor */
+google.visualization.ArrowFormat = function(opt_options) {};
+google.visualization.ArrowFormat.prototype.format = function(dataTable, columnIndex) {};
+
+/** @constructor */
+google.visualization.BarFormat = function(opt_options) {};
+google.visualization.BarFormat.prototype.format = function(dataTable, columnIndex) {};
+
+/** @constructor */
+google.visualization.ColorFormat = function() {};
+google.visualization.ColorFormat.prototype.addRange = function(from, to, color, bgcolor) {};
+google.visualization.ColorFormat.prototype.addGradientRange = function(from, to, color, fromBgColor, toBgColor) {};
+google.visualization.ColorFormat.prototype.format = function(dataTable, columnIndex) {};
+
+/** @constructor */
+google.visualization.DateFormat = function(opt_options) {};
+google.visualization.DateFormat.prototype.format = function(dataTable, columnIndex) {};
+google.visualization.DateFormat.prototype.formatValue = function(value) {};
+
+/** @constructor */
+google.visualization.NumberFormat = function(opt_options) {};
+google.visualization.NumberFormat.prototype.format = function(dataTable, columnIndex) {};
+google.visualization.NumberFormat.prototype.formatValue = function(value) {};
+google.visualization.NumberFormat.DECIMAL_SEP;
+google.visualization.NumberFormat.GROUP_SEP;
+google.visualization.NumberFormat.DECIMAL_PATTERN;
+
+/** @constructor */
+google.visualization.PatternFormat = function(pattern) {};
+google.visualization.PatternFormat.prototype.format = function(dataTable, srcColumnIndices, opt_dstColumnIndex) {};
+
+/** @constructor */
+google.visualization.GadgetHelper = function() {};
+google.visualization.GadgetHelper.prototype.createQueryFromPrefs = function(prefs) {};
+google.visualization.GadgetHelper.prototype.validateResponse = function(response) {};
+
+/** @constructor */
+google.visualization.AnnotatedTimeLine = function(container) {};
+google.visualization.AnnotatedTimeLine.prototype.draw = function(data, opt_options) {};
+google.visualization.AnnotatedTimeLine.prototype.getSelection = function() {};
+google.visualization.AnnotatedTimeLine.prototype.getVisibleChartRange = function() {};
+google.visualization.AnnotatedTimeLine.prototype.setVisibleChartRange = function(firstDate, lastDate, opt_animate) {};
+google.visualization.AnnotatedTimeLine.prototype.showDataColumns = function(columnIndexes) {};
+google.visualization.AnnotatedTimeLine.prototype.hideDataColumns = function(columnIndexes) {};
+
+/** @constructor */
+google.visualization.AreaChart = function(container) {};
+google.visualization.AreaChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.AreaChart.prototype.clearChart = function() {};
+google.visualization.AreaChart.prototype.getSelection = function() {};
+google.visualization.AreaChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.BarChart = function(container) {};
+google.visualization.BarChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.BarChart.prototype.clearChart = function() {};
+google.visualization.BarChart.prototype.getSelection = function() {};
+google.visualization.BarChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.BubbleChart = function(container) {};
+google.visualization.BubbleChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.BubbleChart.prototype.clearChart = function() {};
+google.visualization.BubbleChart.prototype.getSelection = function() {};
+google.visualization.BubbleChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.CandlestickChart = function(container) {};
+google.visualization.CandlestickChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.CandlestickChart.prototype.clearChart = function() {};
+google.visualization.CandlestickChart.prototype.getSelection = function() {};
+google.visualization.CandlestickChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.ColumnChart = function(container) {};
+google.visualization.ColumnChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.ColumnChart.prototype.clearChart = function() {};
+google.visualization.ColumnChart.prototype.getSelection = function() {};
+google.visualization.ColumnChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.ComboChart = function(container) {};
+google.visualization.ComboChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.ComboChart.prototype.clearChart = function() {};
+google.visualization.ComboChart.prototype.getSelection = function() {};
+google.visualization.ComboChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.Gauge = function(container) {};
+google.visualization.Gauge.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.Gauge.prototype.clearChart = function() {};
+
+/** @constructor */
+google.visualization.GeoChart = function(container) {};
+google.visualization.GeoChart.mapExists = function(userOptions) {};
+google.visualization.GeoChart.prototype.clearChart = function() {};
+google.visualization.GeoChart.prototype.draw = function(dataTable, userOptions, opt_state) {};
+google.visualization.GeoChart.prototype.getSelection = function() {};
+google.visualization.GeoChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.GeoMap = function(container) {};
+google.visualization.GeoMap.clickOnRegion = function(id, zoomLevel, segmentBy, instanceIndex) {};
+google.visualization.GeoMap.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.GeoMap.prototype.getSelection = function() {};
+google.visualization.GeoMap.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.Map = function(container) {};
+google.visualization.Map.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.Map.prototype.getSelection = function() {};
+google.visualization.Map.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.ImageAreaChart = function(container) {};
+google.visualization.ImageAreaChart.prototype.draw = function(data, opt_options) {};
+
+/** @constructor */
+google.visualization.ImageBarChart = function(container) {};
+google.visualization.ImageBarChart.prototype.draw = function(data, opt_options) {};
+
+/** @constructor */
+google.visualization.ImageCandlestickChart = function(container) {};
+google.visualization.ImageCandlestickChart.prototype.draw = function(data, opt_options) {};
+
+/** @constructor */
+google.visualization.ImageChart = function(container) {};
+google.visualization.ImageChart.prototype.draw = function(data, opt_options) {};
+
+/** @constructor */
+google.visualization.ImageLineChart = function(container) {};
+google.visualization.ImageLineChart.prototype.draw = function(data, opt_options) {};
+
+/** @constructor */
+google.visualization.ImagePieChart = function(container) {};
+google.visualization.ImagePieChart.prototype.draw = function(data, opt_options) {};
+
+/** @constructor */
+google.visualization.ImageSparkLine = function(container, opt_domHelper) {};
+google.visualization.ImageSparkLine.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.ImageSparkLine.prototype.getSelection = function() {};
+google.visualization.ImageSparkLine.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.IntensityMap = function(container) {};
+google.visualization.IntensityMap.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.IntensityMap.prototype.getSelection = function() {};
+google.visualization.IntensityMap.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.LineChart = function(container) {};
+google.visualization.LineChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.LineChart.prototype.clearChart = function() {};
+google.visualization.LineChart.prototype.getSelection = function() {};
+google.visualization.LineChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.MotionChart = function(container) {};
+google.visualization.MotionChart.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.MotionChart.prototype.getState = function() {};
+
+/** @constructor */
+google.visualization.OrgChart = function(container) {};
+google.visualization.OrgChart.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.OrgChart.prototype.getSelection = function() {};
+google.visualization.OrgChart.prototype.setSelection = function(selection) {};
+google.visualization.OrgChart.prototype.getCollapsedNodes = function() {};
+google.visualization.OrgChart.prototype.getChildrenIndexes = function(rowInd) {};
+google.visualization.OrgChart.prototype.collapse = function(rowInd, collapse) {};
+
+/** @constructor */
+google.visualization.PieChart = function(container) {};
+google.visualization.PieChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.PieChart.prototype.clearChart = function() {};
+google.visualization.PieChart.prototype.getSelection = function() {};
+google.visualization.PieChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.ScatterChart = function(container) {};
+google.visualization.ScatterChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.ScatterChart.prototype.clearChart = function() {};
+google.visualization.ScatterChart.prototype.getSelection = function() {};
+google.visualization.ScatterChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.SparklineChart = function(container) {};
+google.visualization.SparklineChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.SparklineChart.prototype.clearChart = function() {};
+google.visualization.SparklineChart.prototype.getSelection = function() {};
+google.visualization.SparklineChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.SteppedAreaChart = function(container) {};
+google.visualization.SteppedAreaChart.prototype.draw = function(data, opt_options, opt_state) {};
+google.visualization.SteppedAreaChart.prototype.clearChart = function() {};
+google.visualization.SteppedAreaChart.prototype.getSelection = function() {};
+google.visualization.SteppedAreaChart.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.Table = function(container) {};
+google.visualization.Table.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.Table.prototype.clearChart = function() {};
+google.visualization.Table.prototype.getSortInfo = function() {};
+google.visualization.Table.prototype.getSelection = function() {};
+google.visualization.Table.prototype.setSelection = function(selection) {};
+
+/** @constructor */
+google.visualization.TreeMap = function(container) {};
+google.visualization.TreeMap.prototype.draw = function(dataTable, opt_options) {};
+google.visualization.TreeMap.prototype.clearChart = function() {};
+google.visualization.TreeMap.prototype.getSelection = function() {};
+google.visualization.TreeMap.prototype.setSelection = function(selection) {};
+
+google.visualization.drawToolbar = function(container, components) {};
+
+/** @constructor */
+google.visualization.ChartWrapper = function(opt_specification) {};
+google.visualization.ChartWrapper.prototype.draw = function(opt_container) {};
+google.visualization.ChartWrapper.prototype.getDataSourceUrl = function() {};
+google.visualization.ChartWrapper.prototype.getDataTable = function() {};
+google.visualization.ChartWrapper.prototype.getChartName = function() {};
+google.visualization.ChartWrapper.prototype.getChartType = function() {};
+google.visualization.ChartWrapper.prototype.getContainerId = function() {};
+google.visualization.ChartWrapper.prototype.getQuery = function() {};
+google.visualization.ChartWrapper.prototype.getRefreshInterval = function() {};
+google.visualization.ChartWrapper.prototype.getView = function() {};
+google.visualization.ChartWrapper.prototype.getOption = function(key, opt_default) {};
+google.visualization.ChartWrapper.prototype.getOptions = function() {};
+google.visualization.ChartWrapper.prototype.setDataSourceUrl = function(dataSourceUrl) {};
+google.visualization.ChartWrapper.prototype.setDataTable = function(dataTable) {};
+google.visualization.ChartWrapper.prototype.setChartName = function(chartName) {};
+google.visualization.ChartWrapper.prototype.setChartType = function(chartType) {};
+google.visualization.ChartWrapper.prototype.setContainerId = function(containerId) {};
+google.visualization.ChartWrapper.prototype.setQuery = function(query) {};
+google.visualization.ChartWrapper.prototype.setRefreshInterval = function(refreshInterval) {};
+google.visualization.ChartWrapper.prototype.setView = function(view) {};
+google.visualization.ChartWrapper.prototype.setOption = function(key, value) {};
+google.visualization.ChartWrapper.prototype.setOptions = function(options) {};
+
+/** @return {string} JSON representation. */
+google.visualization.ChartWrapper.prototype.toJSON = function() {};
+
+/** @constructor */
+google.visualization.ControlWrapper = function(opt_specification) {};
+google.visualization.ControlWrapper.prototype.draw = function(opt_container) {};
+google.visualization.ControlWrapper.prototype.toJSON = function() {};
+google.visualization.ControlWrapper.prototype.getDataSourceUrl = function() {};
+google.visualization.ControlWrapper.prototype.getDataTable = function() {};
+google.visualization.ControlWrapper.prototype.getControlName = function() {};
+google.visualization.ControlWrapper.prototype.getControlType = function() {};
+google.visualization.ControlWrapper.prototype.getContainerId = function() {};
+google.visualization.ControlWrapper.prototype.getQuery = function() {};
+google.visualization.ControlWrapper.prototype.getRefreshInterval = function() {};
+google.visualization.ControlWrapper.prototype.getView = function() {};
+google.visualization.ControlWrapper.prototype.getOption = function(key, opt_default) {};
+google.visualization.ControlWrapper.prototype.getOptions = function() {};
+google.visualization.ControlWrapper.prototype.setDataSourceUrl = function(dataSourceUrl) {};
+google.visualization.ControlWrapper.prototype.setDataTable = function(dataTable) {};
+google.visualization.ControlWrapper.prototype.setControlName = function(controlName) {};
+google.visualization.ControlWrapper.prototype.setControlType = function(controlType) {};
+google.visualization.ControlWrapper.prototype.setContainerId = function(containerId) {};
+google.visualization.ControlWrapper.prototype.setQuery = function(query) {};
+google.visualization.ControlWrapper.prototype.setRefreshInterval = function(refreshInterval) {};
+google.visualization.ControlWrapper.prototype.setView = function(view) {};
+google.visualization.ControlWrapper.prototype.setOption = function(key, value) {};
+google.visualization.ControlWrapper.prototype.setOptions = function(options) {};
+
+// NOTE: I (danvk): commented this out because of compiler warnings.
+/** @return {string} JSON representation. */
+// google.visualization.ChartWrapper.prototype.toJSON = function() {};
+
+/** @constructor */
+google.visualization.ChartEditor = function(opt_config) {};
+google.visualization.ChartEditor.prototype.openDialog = function(specification, opt_options) {};
+google.visualization.ChartEditor.prototype.getChartWrapper = function() {};
+google.visualization.ChartEditor.prototype.setChartWrapper = function(chartWrapper) {};
+google.visualization.ChartEditor.prototype.closeDialog = function() {};
+
+/** @constructor */
+google.visualization.Dashboard = function(container) {};
+google.visualization.Dashboard.prototype.bind = function(controls, participants) {};
+google.visualization.Dashboard.prototype.draw = function(dataTable) {};
+
+/** @constructor */
+google.visualization.StringFilter = function(container) {};
+google.visualization.StringFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
+google.visualization.StringFilter.prototype.applyFilter = function() {};
+google.visualization.StringFilter.prototype.getState = function() {};
+google.visualization.StringFilter.prototype.resetControl = function() {};
+
+/** @constructor */
+google.visualization.NumberRangeFilter = function(container) {};
+google.visualization.NumberRangeFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
+google.visualization.NumberRangeFilter.prototype.applyFilter = function() {};
+google.visualization.NumberRangeFilter.prototype.getState = function() {};
+google.visualization.NumberRangeFilter.prototype.resetControl = function() {};
+
+/** @constructor */
+google.visualization.CategoryFilter = function(container) {};
+google.visualization.CategoryFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
+google.visualization.CategoryFilter.prototype.applyFilter = function() {};
+google.visualization.CategoryFilter.prototype.getState = function() {};
+google.visualization.CategoryFilter.prototype.resetControl = function() {};
+
+/** @constructor */
+google.visualization.ChartRangeFilter = function(container) {};
+google.visualization.ChartRangeFilter.prototype.draw = function(dataTable, opt_options, opt_state) {};
+google.visualization.ChartRangeFilter.prototype.applyFilter = function() {};
+google.visualization.ChartRangeFilter.prototype.getState = function() {};
+google.visualization.ChartRangeFilter.prototype.resetControl = function() {};
diff --git a/src/plugins/README b/src/plugins/README
new file mode 100644 (file)
index 0000000..f216545
--- /dev/null
@@ -0,0 +1,113 @@
+dygraphs plugins
+----------------
+
+A single dygraph is actually a collection of dygraphs plugins, each responsible
+for some portion of the chart: the plot area, the axes, the legend, the labels,
+etc.
+
+This forces the dygraphs code to be more modular and encourages better APIs.
+
+The "legend" plugin (plugins/legend.js) can be used as a template for new
+plugins.
+
+Here is a simplified version of it, with comments to explain the plugin
+lifecycle and APIs:
+
+----------------
+
+// (standard JavaScript library wrapper; prevents polluting global namespace)
+Dygraph.Plugins.Legend = (function() {
+
+// Plugin constructor. This is invoked once for every chart which uses the
+// plugin. You can't actually access the Dygraph object at this point, so the
+// initialization you do here shouldn't be chart-specific. (For that, use
+// the activate() method).
+var legend = function() {
+  this.div_ = null;
+};
+
+// Plugins are expected to implement this method for debugging purposes.
+legend.toString = function() {
+  return "Legend";
+};
+
+// This is called once the dygraph is ready. The chart data may not be
+// available yet, but the options specified in the constructor are.
+// 
+// Proper tasks to do here include:
+// - Reading your own options
+// - DOM manipulation
+// - Registering event listeners
+//
+// "dygraph" is the Dygraph object for which this instance is being activated.
+// "registerer" allows you to register event listeners.
+legend.prototype.activate = function(dygraph, registerer) {
+  // Create the legend div and attach it to the chart DOM.
+  this.div_ = document.createElement("div");
+  dygraph.graphDiv.appendChild(this.div_);
+
+  // Add event listeners. These will be called whenever points are selected
+  // (i.e. you hover over them) or deselected (i.e. you mouse away from the
+  // chart). This is your only chance to register event listeners! Once this
+  // method returns, the gig is up.
+  registerer.addEventListener('select', legend.select);
+  registerer.addEventListener('deselect', legend.deselect);
+};
+
+// The functions called by event listeners all take a single parameter, an
+// event object. This contains properties relevant to the particular event, but
+// you can always assume that it has:
+//
+// 1. A "dygraph" parameter which is a reference to the chart on which the
+//    event took place.
+// 2. A "stopPropagation" method, which you can call to prevent the event from
+//    being seen by any other plugins after you. This effectively cancels the
+//    event.
+// 3. A "preventDefault" method, which prevents dygraphs from performing the
+//    default action associated with this event.
+//
+legend.select = function(e) {
+  // These are two of the properties specific to the "select" event object:
+  var xValue = e.selectedX;
+  var points = e.selectedPoints;
+
+  var html = xValue + ':';
+  for (var i = 0; i < points.length; i++) {
+    var point = points[i];
+    html += ' ' + point.name + ':' + point.yval;
+  }
+
+  // In an event listener, "this" refers to your plugin object.
+  this.div_.innerHTML = html;
+};
+
+// This clears out the legend when the user mouses away from the chart.
+legend.deselect = function(e) {
+  this.div_.innerHTML = '';
+};
+
+return legend;
+})();
+
+----------------
+
+Plugin Events Reference:
+
+- predraw
+- clearChart
+- drawChart
+- select
+- deselect
+
+TODO(danvk): document all event properties for each event.
+
+
+Special methods:
+- (constructor)
+- activate
+- destroy
+
+
+----------------
+
+Notes on plugin registration and event cascade ordering/behavior.
diff --git a/src/plugins/annotations.js b/src/plugins/annotations.js
new file mode 100644 (file)
index 0000000..8576104
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * @license
+ * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/*global Dygraph:false */
+
+Dygraph.Plugins.Annotations = (function() {
+
+"use strict";
+
+/**
+Current bits of jankiness:
+- Uses dygraph.layout_ to get the parsed annotations.
+- Uses dygraph.plotter_.area
+
+It would be nice if the plugin didn't require so much special support inside
+the core dygraphs classes, but annotations involve quite a bit of parsing and
+layout.
+
+TODO(danvk): cache DOM elements.
+
+*/
+
+var annotations = function() {
+  this.annotations_ = [];
+};
+
+annotations.prototype.toString = function() {
+  return "Annotations Plugin";
+};
+
+annotations.prototype.activate = function(g) {
+  return {
+    clearChart: this.clearChart,
+    didDrawChart: this.didDrawChart
+  };
+};
+
+annotations.prototype.detachLabels = function() {
+  for (var i = 0; i < this.annotations_.length; i++) {
+    var a = this.annotations_[i];
+    if (a.parentNode) a.parentNode.removeChild(a);
+    this.annotations_[i] = null;
+  }
+  this.annotations_ = [];
+};
+
+annotations.prototype.clearChart = function(e) {
+  this.detachLabels();
+};
+
+annotations.prototype.didDrawChart = function(e) {
+  var g = e.dygraph;
+
+  // Early out in the (common) case of zero annotations.
+  var points = g.layout_.annotated_points;
+  if (!points || points.length === 0) return;
+
+  var containerDiv = e.canvas.parentNode;
+  var annotationStyle = {
+    "position": "absolute",
+    "fontSize": g.getOption('axisLabelFontSize') + "px",
+    "zIndex": 10,
+    "overflow": "hidden"
+  };
+
+  var bindEvt = function(eventName, classEventName, pt) {
+    return function(annotation_event) {
+      var a = pt.annotation;
+      if (a.hasOwnProperty(eventName)) {
+        a[eventName](a, pt, g, annotation_event);
+      } else if (g.getOption(classEventName)) {
+        g.getOption(classEventName)(a, pt, g, annotation_event );
+      }
+    };
+  };
+
+  // Add the annotations one-by-one.
+  var area = e.dygraph.plotter_.area;
+
+  // x-coord to sum of previous annotation's heights (used for stacking).
+  var xToUsedHeight = {};
+
+  for (var i = 0; i < points.length; i++) {
+    var p = points[i];
+    if (p.canvasx < area.x || p.canvasx > area.x + area.w ||
+        p.canvasy < area.y || p.canvasy > area.y + area.h) {
+      continue;
+    }
+
+    var a = p.annotation;
+    var tick_height = 6;
+    if (a.hasOwnProperty("tickHeight")) {
+      tick_height = a.tickHeight;
+    }
+
+    var div = document.createElement("div");
+    for (var name in annotationStyle) {
+      if (annotationStyle.hasOwnProperty(name)) {
+        div.style[name] = annotationStyle[name];
+      }
+    }
+    if (!a.hasOwnProperty('icon')) {
+      div.className = "dygraphDefaultAnnotation";
+    }
+    if (a.hasOwnProperty('cssClass')) {
+      div.className += " " + a.cssClass;
+    }
+
+    var width = a.hasOwnProperty('width') ? a.width : 16;
+    var height = a.hasOwnProperty('height') ? a.height : 16;
+    if (a.hasOwnProperty('icon')) {
+      var img = document.createElement("img");
+      img.src = a.icon;
+      img.width = width;
+      img.height = height;
+      div.appendChild(img);
+    } else if (p.annotation.hasOwnProperty('shortText')) {
+      div.appendChild(document.createTextNode(p.annotation.shortText));
+    }
+    var left = p.canvasx - width / 2;
+    div.style.left = left + "px";
+    var divTop = 0;
+    if (a.attachAtBottom) {
+      var y = (area.y + area.h - height - tick_height);
+      if (xToUsedHeight[left]) {
+        y -= xToUsedHeight[left];
+      } else {
+        xToUsedHeight[left] = 0;
+      }
+      xToUsedHeight[left] += (tick_height + height);
+      divTop = y;
+    } else {
+      divTop = p.canvasy - height - tick_height;
+    }
+    div.style.top = divTop + "px";
+    div.style.width = width + "px";
+    div.style.height = height + "px";
+    div.title = p.annotation.text;
+    div.style.color = g.colorsMap_[p.name];
+    div.style.borderColor = g.colorsMap_[p.name];
+    a.div = div;
+
+    g.addAndTrackEvent(div, 'click',
+        bindEvt('clickHandler', 'annotationClickHandler', p, this));
+    g.addAndTrackEvent(div, 'mouseover',
+        bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
+    g.addAndTrackEvent(div, 'mouseout',
+        bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
+    g.addAndTrackEvent(div, 'dblclick',
+        bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
+
+    containerDiv.appendChild(div);
+    this.annotations_.push(div);
+
+    var ctx = e.drawingContext;
+    ctx.save();
+    ctx.strokeStyle = g.colorsMap_[p.name];
+    ctx.beginPath();
+    if (!a.attachAtBottom) {
+      ctx.moveTo(p.canvasx, p.canvasy);
+      ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height);
+    } else {
+      var y = divTop + height;
+      ctx.moveTo(p.canvasx, y);
+      ctx.lineTo(p.canvasx, y + tick_height);
+    }
+    ctx.closePath();
+    ctx.stroke();
+    ctx.restore();
+  }
+};
+
+annotations.prototype.destroy = function() {
+  this.detachLabels();
+};
+
+return annotations;
+
+})();
diff --git a/src/plugins/axes.js b/src/plugins/axes.js
new file mode 100644 (file)
index 0000000..aa142ce
--- /dev/null
@@ -0,0 +1,323 @@
+/**
+ * @license
+ * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+/*global Dygraph:false */
+
+Dygraph.Plugins.Axes = (function() {
+
+'use strict';
+
+/*
+Bits of jankiness:
+- Direct layout access
+- Direct area access
+- Should include calculation of ticks, not just the drawing.
+
+Options left to make axis-friendly.
+  ('drawAxesAtZero')
+  ('xAxisHeight')
+*/
+
+/**
+ * Draws the axes. This includes the labels on the x- and y-axes, as well
+ * as the tick marks on the axes.
+ * It does _not_ draw the grid lines which span the entire chart.
+ */
+var axes = function() {
+  this.xlabels_ = [];
+  this.ylabels_ = [];
+};
+
+axes.prototype.toString = function() {
+  return 'Axes Plugin';
+};
+
+axes.prototype.activate = function(g) {
+  return {
+    layout: this.layout,
+    clearChart: this.clearChart,
+    willDrawChart: this.willDrawChart
+  };
+};
+
+axes.prototype.layout = function(e) {
+  var g = e.dygraph;
+
+  if (g.getOptionForAxis('drawAxis', 'y')) {
+    var w = g.getOptionForAxis('axisLabelWidth', 'y') + 2 * g.getOptionForAxis('axisTickSize', 'y');
+    e.reserveSpaceLeft(w);
+  }
+
+  if (g.getOptionForAxis('drawAxis', 'x')) {
+    var h;
+    // NOTE: I think this is probably broken now, since g.getOption() now
+    // hits the dictionary. (That is, g.getOption('xAxisHeight') now always
+    // has a value.)
+    if (g.getOption('xAxisHeight')) {
+      h = g.getOption('xAxisHeight');
+    } else {
+      h = g.getOptionForAxis('axisLabelFontSize', 'x') + 2 * g.getOptionForAxis('axisTickSize', 'x');
+    }
+    e.reserveSpaceBottom(h);
+  }
+
+  if (g.numAxes() == 2) {
+    if (g.getOptionForAxis('drawAxis', 'y2')) {
+      var w = g.getOptionForAxis('axisLabelWidth', 'y2') + 2 * g.getOptionForAxis('axisTickSize', 'y2');
+      e.reserveSpaceRight(w);
+    }
+  } else if (g.numAxes() > 2) {
+    g.error('Only two y-axes are supported at this time. (Trying ' +
+            'to use ' + g.numAxes() + ')');
+  }
+};
+
+axes.prototype.detachLabels = function() {
+  function removeArray(ary) {
+    for (var i = 0; i < ary.length; i++) {
+      var el = ary[i];
+      if (el.parentNode) el.parentNode.removeChild(el);
+    }
+  }
+
+  removeArray(this.xlabels_);
+  removeArray(this.ylabels_);
+  this.xlabels_ = [];
+  this.ylabels_ = [];
+};
+
+axes.prototype.clearChart = function(e) {
+  this.detachLabels();
+};
+
+axes.prototype.willDrawChart = function(e) {
+  var g = e.dygraph;
+
+  if (!g.getOptionForAxis('drawAxis', 'x') &&
+      !g.getOptionForAxis('drawAxis', 'y') &&
+      !g.getOptionForAxis('drawAxis', 'y2')) {
+    return;
+  }
+  
+  // Round pixels to half-integer boundaries for crisper drawing.
+  function halfUp(x)  { return Math.round(x) + 0.5; }
+  function halfDown(y){ return Math.round(y) - 0.5; }
+
+  var context = e.drawingContext;
+  var containerDiv = e.canvas.parentNode;
+  var canvasWidth = g.width_;  // e.canvas.width is affected by pixel ratio.
+  var canvasHeight = g.height_;
+
+  var label, x, y, tick, i;
+
+  var makeLabelStyle = function(axis) {
+    return {
+      position: 'absolute',
+      fontSize: g.getOptionForAxis('axisLabelFontSize', axis) + 'px',
+      zIndex: 10,
+      color: g.getOptionForAxis('axisLabelColor', axis),
+      width: g.getOptionForAxis('axisLabelWidth', axis) + 'px',
+      // height: g.getOptionForAxis('axisLabelFontSize', 'x') + 2 + "px",
+      lineHeight: 'normal',  // Something other than "normal" line-height screws up label positioning.
+      overflow: 'hidden'
+    };
+  };
+
+  var labelStyles = {
+    x : makeLabelStyle('x'),
+    y : makeLabelStyle('y'),
+    y2 : makeLabelStyle('y2')
+  };
+
+  var makeDiv = function(txt, axis, prec_axis) {
+    /*
+     * This seems to be called with the following three sets of axis/prec_axis:
+     * x: undefined
+     * y: y1
+     * y: y2
+     */
+    var div = document.createElement('div');
+    var labelStyle = labelStyles[prec_axis == 'y2' ? 'y2' : axis];
+    for (var name in labelStyle) {
+      if (labelStyle.hasOwnProperty(name)) {
+        div.style[name] = labelStyle[name];
+      }
+    }
+    var inner_div = document.createElement('div');
+    inner_div.className = 'dygraph-axis-label' +
+                          ' dygraph-axis-label-' + axis +
+                          (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
+    inner_div.innerHTML = txt;
+    div.appendChild(inner_div);
+    return div;
+  };
+
+  // axis lines
+  context.save();
+
+  var layout = g.layout_;
+  var area = e.dygraph.plotter_.area;
+
+  // Helper for repeated axis-option accesses.
+  var makeOptionGetter = function(axis) {
+    return function(option) {
+      return g.getOptionForAxis(option, axis);
+    };
+  };
+
+  if (g.getOptionForAxis('drawAxis', 'y')) {
+    if (layout.yticks && layout.yticks.length > 0) {
+      var num_axes = g.numAxes();
+      var getOptions = [makeOptionGetter('y'), makeOptionGetter('y2')];
+      for (i = 0; i < layout.yticks.length; i++) {
+        tick = layout.yticks[i];
+        if (typeof(tick) == 'function') return;  // <-- when would this happen?
+        x = area.x;
+        var sgn = 1;
+        var prec_axis = 'y1';
+        var getAxisOption = getOptions[0];
+        if (tick[0] == 1) {  // right-side y-axis
+          x = area.x + area.w;
+          sgn = -1;
+          prec_axis = 'y2';
+          getAxisOption = getOptions[1];
+        }
+        var fontSize = getAxisOption('axisLabelFontSize');
+        y = area.y + tick[1] * area.h;
+
+        /* Tick marks are currently clipped, so don't bother drawing them.
+        context.beginPath();
+        context.moveTo(halfUp(x), halfDown(y));
+        context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
+        context.closePath();
+        context.stroke();
+        */
+
+        label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
+        var top = (y - fontSize / 2);
+        if (top < 0) top = 0;
+
+        if (top + fontSize + 3 > canvasHeight) {
+          label.style.bottom = '0';
+        } else {
+          label.style.top = top + 'px';
+        }
+        if (tick[0] === 0) {
+          label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
+          label.style.textAlign = 'right';
+        } else if (tick[0] == 1) {
+          label.style.left = (area.x + area.w +
+                              getAxisOption('axisTickSize')) + 'px';
+          label.style.textAlign = 'left';
+        }
+        label.style.width = getAxisOption('axisLabelWidth') + 'px';
+        containerDiv.appendChild(label);
+        this.ylabels_.push(label);
+      }
+
+      // The lowest tick on the y-axis often overlaps with the leftmost
+      // tick on the x-axis. Shift the bottom tick up a little bit to
+      // compensate if necessary.
+      var bottomTick = this.ylabels_[0];
+      // Interested in the y2 axis also?
+      var fontSize = g.getOptionForAxis('axisLabelFontSize', 'y');
+      var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
+      if (bottom > canvasHeight - fontSize) {
+        bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
+            fontSize / 2) + 'px';
+      }
+    }
+
+    // draw a vertical line on the left to separate the chart from the labels.
+    var axisX;
+    if (g.getOption('drawAxesAtZero')) {
+      var r = g.toPercentXCoord(0);
+      if (r > 1 || r < 0 || isNaN(r)) r = 0;
+      axisX = halfUp(area.x + r * area.w);
+    } else {
+      axisX = halfUp(area.x);
+    }
+
+    context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
+    context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
+
+    context.beginPath();
+    context.moveTo(axisX, halfDown(area.y));
+    context.lineTo(axisX, halfDown(area.y + area.h));
+    context.closePath();
+    context.stroke();
+
+    // if there's a secondary y-axis, draw a vertical line for that, too.
+    if (g.numAxes() == 2) {
+      context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
+      context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
+      context.beginPath();
+      context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
+      context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
+      context.closePath();
+      context.stroke();
+    }
+  }
+
+  if (g.getOptionForAxis('drawAxis', 'x')) {
+    if (layout.xticks) {
+      var getAxisOption = makeOptionGetter('x');
+      for (i = 0; i < layout.xticks.length; i++) {
+        tick = layout.xticks[i];
+        x = area.x + tick[0] * area.w;
+        y = area.y + area.h;
+
+        /* Tick marks are currently clipped, so don't bother drawing them.
+        context.beginPath();
+        context.moveTo(halfUp(x), halfDown(y));
+        context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
+        context.closePath();
+        context.stroke();
+        */
+
+        label = makeDiv(tick[1], 'x');
+        label.style.textAlign = 'center';
+        label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
+
+        var left = (x - getAxisOption('axisLabelWidth')/2);
+        if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
+          left = canvasWidth - getAxisOption('axisLabelWidth');
+          label.style.textAlign = 'right';
+        }
+        if (left < 0) {
+          left = 0;
+          label.style.textAlign = 'left';
+        }
+
+        label.style.left = left + 'px';
+        label.style.width = getAxisOption('axisLabelWidth') + 'px';
+        containerDiv.appendChild(label);
+        this.xlabels_.push(label);
+      }
+    }
+
+    context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
+    context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
+    context.beginPath();
+    var axisY;
+    if (g.getOption('drawAxesAtZero')) {
+      var r = g.toPercentYCoord(0, 0);
+      if (r > 1 || r < 0) r = 1;
+      axisY = halfDown(area.y + r * area.h);
+    } else {
+      axisY = halfDown(area.y + area.h);
+    }
+    context.moveTo(halfUp(area.x), axisY);
+    context.lineTo(halfUp(area.x + area.w), axisY);
+    context.closePath();
+    context.stroke();
+  }
+
+  context.restore();
+};
+
+return axes;
+})();
diff --git a/src/plugins/chart-labels.js b/src/plugins/chart-labels.js
new file mode 100644 (file)
index 0000000..504ed3a
--- /dev/null
@@ -0,0 +1,190 @@
+/**
+ * @license
+ * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+/*global Dygraph:false */
+
+Dygraph.Plugins.ChartLabels = (function() {
+
+"use strict";
+
+// TODO(danvk): move chart label options out of dygraphs and into the plugin.
+// TODO(danvk): only tear down & rebuild the DIVs when it's necessary.
+
+var chart_labels = function() {
+  this.title_div_ = null;
+  this.xlabel_div_ = null;
+  this.ylabel_div_ = null;
+  this.y2label_div_ = null;
+};
+
+chart_labels.prototype.toString = function() {
+  return "ChartLabels Plugin";
+};
+
+chart_labels.prototype.activate = function(g) {
+  return {
+    layout: this.layout,
+    // clearChart: this.clearChart,
+    didDrawChart: this.didDrawChart
+  };
+};
+
+// QUESTION: should there be a plugin-utils.js?
+var createDivInRect = function(r) {
+  var div = document.createElement('div');
+  div.style.position = 'absolute';
+  div.style.left = r.x + 'px';
+  div.style.top = r.y + 'px';
+  div.style.width = r.w + 'px';
+  div.style.height = r.h + 'px';
+  return div;
+};
+
+// Detach and null out any existing nodes.
+chart_labels.prototype.detachLabels_ = function() {
+  var els = [ this.title_div_,
+              this.xlabel_div_,
+              this.ylabel_div_,
+              this.y2label_div_ ];
+  for (var i = 0; i < els.length; i++) {
+    var el = els[i];
+    if (!el) continue;
+    if (el.parentNode) el.parentNode.removeChild(el);
+  }
+
+  this.title_div_ = null;
+  this.xlabel_div_ = null;
+  this.ylabel_div_ = null;
+  this.y2label_div_ = null;
+};
+
+var createRotatedDiv = function(g, box, axis, classes, html) {
+  // TODO(danvk): is this outer div actually necessary?
+  var div = document.createElement("div");
+  div.style.position = 'absolute';
+  if (axis == 1) {
+    // NOTE: this is cheating. Should be positioned relative to the box.
+    div.style.left = '0px';
+  } else {
+    div.style.left = box.x + 'px';
+  }
+  div.style.top = box.y + 'px';
+  div.style.width = box.w + 'px';
+  div.style.height = box.h + 'px';
+  div.style.fontSize = (g.getOption('yLabelWidth') - 2) + 'px';
+
+  var inner_div = document.createElement("div");
+  inner_div.style.position = 'absolute';
+  inner_div.style.width = box.h + 'px';
+  inner_div.style.height = box.w + 'px';
+  inner_div.style.top = (box.h / 2 - box.w / 2) + 'px';
+  inner_div.style.left = (box.w / 2 - box.h / 2) + 'px';
+  inner_div.style.textAlign = 'center';
+
+  // CSS rotation is an HTML5 feature which is not standardized. Hence every
+  // browser has its own name for the CSS style.
+  var val = 'rotate(' + (axis == 1 ? '-' : '') + '90deg)';
+  inner_div.style.transform = val;        // HTML5
+  inner_div.style.WebkitTransform = val;  // Safari/Chrome
+  inner_div.style.MozTransform = val;     // Firefox
+  inner_div.style.OTransform = val;       // Opera
+  inner_div.style.msTransform = val;      // IE9
+
+  var class_div = document.createElement("div");
+  class_div.className = classes;
+  class_div.innerHTML = html;
+
+  inner_div.appendChild(class_div);
+  div.appendChild(inner_div);
+  return div;
+};
+
+chart_labels.prototype.layout = function(e) {
+  this.detachLabels_();
+
+  var g = e.dygraph;
+  var div = e.chart_div;
+  if (g.getOption('title')) {
+    // QUESTION: should this return an absolutely-positioned div instead?
+    var title_rect = e.reserveSpaceTop(g.getOption('titleHeight'));
+    this.title_div_ = createDivInRect(title_rect);
+    this.title_div_.style.textAlign = 'center';
+    this.title_div_.style.fontSize = (g.getOption('titleHeight') - 8) + 'px';
+    this.title_div_.style.fontWeight = 'bold';
+    this.title_div_.style.zIndex = 10;
+
+    var class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-title';
+    class_div.innerHTML = g.getOption('title');
+    this.title_div_.appendChild(class_div);
+    div.appendChild(this.title_div_);
+  }
+
+  if (g.getOption('xlabel')) {
+    var x_rect = e.reserveSpaceBottom(g.getOption('xLabelHeight'));
+    this.xlabel_div_ = createDivInRect(x_rect);
+    this.xlabel_div_.style.textAlign = 'center';
+    this.xlabel_div_.style.fontSize = (g.getOption('xLabelHeight') - 2) + 'px';
+
+    var class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-xlabel';
+    class_div.innerHTML = g.getOption('xlabel');
+    this.xlabel_div_.appendChild(class_div);
+    div.appendChild(this.xlabel_div_);
+  }
+
+  if (g.getOption('ylabel')) {
+    // It would make sense to shift the chart here to make room for the y-axis
+    // label, but the default yAxisLabelWidth is large enough that this results
+    // in overly-padded charts. The y-axis label should fit fine. If it
+    // doesn't, the yAxisLabelWidth option can be increased.
+    var y_rect = e.reserveSpaceLeft(0);
+
+    this.ylabel_div_ = createRotatedDiv(
+        g, y_rect,
+        1,  // primary (left) y-axis
+        'dygraph-label dygraph-ylabel',
+        g.getOption('ylabel'));
+    div.appendChild(this.ylabel_div_);
+  }
+
+  if (g.getOption('y2label') && g.numAxes() == 2) {
+    // same logic applies here as for ylabel.
+    var y2_rect = e.reserveSpaceRight(0);
+    this.y2label_div_ = createRotatedDiv(
+        g, y2_rect,
+        2,  // secondary (right) y-axis
+        'dygraph-label dygraph-y2label',
+        g.getOption('y2label'));
+    div.appendChild(this.y2label_div_);
+  }
+};
+
+chart_labels.prototype.didDrawChart = function(e) {
+  var g = e.dygraph;
+  if (this.title_div_) {
+    this.title_div_.children[0].innerHTML = g.getOption('title');
+  }
+  if (this.xlabel_div_) {
+    this.xlabel_div_.children[0].innerHTML = g.getOption('xlabel');
+  }
+  if (this.ylabel_div_) {
+    this.ylabel_div_.children[0].children[0].innerHTML = g.getOption('ylabel');
+  }
+  if (this.y2label_div_) {
+    this.y2label_div_.children[0].children[0].innerHTML = g.getOption('y2label');
+  }
+};
+
+chart_labels.prototype.clearChart = function() {
+};
+
+chart_labels.prototype.destroy = function() {
+  this.detachLabels_();
+};
+
+
+return chart_labels;
+})();
diff --git a/src/plugins/grid.js b/src/plugins/grid.js
new file mode 100644 (file)
index 0000000..db1b42d
--- /dev/null
@@ -0,0 +1,124 @@
+/**
+ * @license
+ * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+/*global Dygraph:false */
+
+Dygraph.Plugins.Grid = (function() {
+
+/*
+
+Current bits of jankiness:
+- Direct layout access
+- Direct area access
+
+*/
+
+"use strict";
+
+
+/**
+ * Draws the gridlines, i.e. the gray horizontal & vertical lines running the
+ * length of the chart.
+ *
+ * @constructor
+ */
+var grid = function() {
+};
+
+grid.prototype.toString = function() {
+  return "Gridline Plugin";
+};
+
+grid.prototype.activate = function(g) {
+  return {
+    willDrawChart: this.willDrawChart
+  };
+};
+
+grid.prototype.willDrawChart = function(e) {
+  // Draw the new X/Y grid. Lines appear crisper when pixels are rounded to
+  // half-integers. This prevents them from drawing in two rows/cols.
+  var g = e.dygraph;
+  var ctx = e.drawingContext;
+  var layout = g.layout_;
+  var area = e.dygraph.plotter_.area;
+
+  function halfUp(x)  { return Math.round(x) + 0.5; }
+  function halfDown(y){ return Math.round(y) - 0.5; }
+
+  var x, y, i, ticks;
+  if (g.getOptionForAxis('drawGrid', 'y')) {
+    var axes = ["y", "y2"];
+    var strokeStyles = [], lineWidths = [], drawGrid = [], stroking = [], strokePattern = [];
+    for (var i = 0; i < axes.length; i++) {
+      drawGrid[i] = g.getOptionForAxis('drawGrid', axes[i]);
+      if (drawGrid[i]) {
+        strokeStyles[i] = g.getOptionForAxis('gridLineColor', axes[i]);
+        lineWidths[i] = g.getOptionForAxis('gridLineWidth', axes[i]);
+        strokePattern[i] = g.getOptionForAxis('gridLinePattern', axes[i]);
+        stroking[i] = strokePattern[i] && (strokePattern[i].length >= 2);
+      }
+    }
+    ticks = layout.yticks;
+    ctx.save();
+    // draw grids for the different y axes
+    for (i = 0; i < ticks.length; i++) {
+      var axis = ticks[i][0];
+      if(drawGrid[axis]) {
+        if (stroking[axis]) {
+          ctx.installPattern(strokePattern[axis]);
+        }
+        ctx.strokeStyle = strokeStyles[axis];
+        ctx.lineWidth = lineWidths[axis];
+
+        x = halfUp(area.x);
+        y = halfDown(area.y + ticks[i][1] * area.h);
+        ctx.beginPath();
+        ctx.moveTo(x, y);
+        ctx.lineTo(x + area.w, y);
+        ctx.closePath();
+        ctx.stroke();
+
+        if (stroking[axis]) {
+          ctx.uninstallPattern();
+        }
+      }
+    }
+    ctx.restore();
+  }
+
+  // draw grid for x axis
+  if (g.getOptionForAxis('drawGrid', 'x')) {
+    ticks = layout.xticks;
+    ctx.save();
+    var strokePattern = g.getOptionForAxis('gridLinePattern', 'x');
+    var stroking = strokePattern && (strokePattern.length >= 2);
+    if (stroking) {
+      ctx.installPattern(strokePattern);
+    }
+    ctx.strokeStyle = g.getOptionForAxis('gridLineColor', 'x');
+    ctx.lineWidth = g.getOptionForAxis('gridLineWidth', 'x');
+    for (i = 0; i < ticks.length; i++) {
+      x = halfUp(area.x + ticks[i][0] * area.w);
+      y = halfDown(area.y + area.h);
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x, area.y);
+      ctx.closePath();
+      ctx.stroke();
+    }
+    if (stroking) {
+      ctx.uninstallPattern();
+    }
+    ctx.restore();
+  }
+};
+
+grid.prototype.destroy = function() {
+};
+
+return grid;
+
+})();
diff --git a/src/plugins/legend.js b/src/plugins/legend.js
new file mode 100644 (file)
index 0000000..3db4d07
--- /dev/null
@@ -0,0 +1,366 @@
+/**
+ * @license
+ * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+/*global Dygraph:false */
+
+Dygraph.Plugins.Legend = (function() {
+/*
+Current bits of jankiness:
+- Uses two private APIs:
+    1. Dygraph.optionsViewForAxis_
+    2. dygraph.plotter_.area
+- Registers for a "predraw" event, which should be renamed.
+- I call calculateEmWidthInDiv more often than needed.
+*/
+
+/*global Dygraph:false */
+"use strict";
+
+
+/**
+ * Creates the legend, which appears when the user hovers over the chart.
+ * The legend can be either a user-specified or generated div.
+ *
+ * @constructor
+ */
+var legend = function() {
+  this.legend_div_ = null;
+  this.is_generated_div_ = false;  // do we own this div, or was it user-specified?
+};
+
+legend.prototype.toString = function() {
+  return "Legend Plugin";
+};
+
+// (defined below)
+var generateLegendDashHTML;
+
+/**
+ * This is called during the dygraph constructor, after options have been set
+ * but before the data is available.
+ *
+ * Proper tasks to do here include:
+ * - Reading your own options
+ * - DOM manipulation
+ * - Registering event listeners
+ *
+ * @param {Dygraph} g Graph instance.
+ * @return {object.<string, function(ev)>} Mapping of event names to callbacks.
+ */
+legend.prototype.activate = function(g) {
+  var div;
+  var divWidth = g.getOption('labelsDivWidth');
+
+  var userLabelsDiv = g.getOption('labelsDiv');
+  if (userLabelsDiv && null !== userLabelsDiv) {
+    if (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String) {
+      div = document.getElementById(userLabelsDiv);
+    } else {
+      div = userLabelsDiv;
+    }
+  } else {
+    // Default legend styles. These can be overridden in CSS by adding
+    // "!important" after your rule, e.g. "left: 30px !important;"
+    var messagestyle = {
+      "position": "absolute",
+      "fontSize": "14px",
+      "zIndex": 10,
+      "width": divWidth + "px",
+      "top": "0px",
+      "left": (g.size().width - divWidth - 2) + "px",
+      "background": "white",
+      "lineHeight": "normal",
+      "textAlign": "left",
+      "overflow": "hidden"};
+
+    // TODO(danvk): get rid of labelsDivStyles? CSS is better.
+    Dygraph.update(messagestyle, g.getOption('labelsDivStyles'));
+    div = document.createElement("div");
+    div.className = "dygraph-legend";
+    for (var name in messagestyle) {
+      if (!messagestyle.hasOwnProperty(name)) continue;
+
+      try {
+        div.style[name] = messagestyle[name];
+      } catch (e) {
+        console.warn("You are using unsupported css properties for your " +
+            "browser in labelsDivStyles");
+      }
+    }
+
+    // TODO(danvk): come up with a cleaner way to expose this.
+    g.graphDiv.appendChild(div);
+    this.is_generated_div_ = true;
+  }
+
+  this.legend_div_ = div;
+  this.one_em_width_ = 10;  // just a guess, will be updated.
+
+  return {
+    select: this.select,
+    deselect: this.deselect,
+    // TODO(danvk): rethink the name "predraw" before we commit to it in any API.
+    predraw: this.predraw,
+    didDrawChart: this.didDrawChart
+  };
+};
+
+// Needed for dashed lines.
+var calculateEmWidthInDiv = function(div) {
+  var sizeSpan = document.createElement('span');
+  sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+  div.appendChild(sizeSpan);
+  var oneEmWidth=sizeSpan.offsetWidth;
+  div.removeChild(sizeSpan);
+  return oneEmWidth;
+};
+
+var escapeHTML = function(str) {
+  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+};
+
+legend.prototype.select = function(e) {
+  var xValue = e.selectedX;
+  var points = e.selectedPoints;
+  var row = e.selectedRow;
+
+  var legendMode = e.dygraph.getOption('legend');
+  if (legendMode === 'never') {
+    this.legend_div_.style.display = 'none';
+    return;
+  }
+
+  if (legendMode === 'follow') {
+    // create floating legend div
+    var area = e.dygraph.plotter_.area;
+    var labelsDivWidth = e.dygraph.getOption('labelsDivWidth');
+    var yAxisLabelWidth = e.dygraph.getOptionForAxis('axisLabelWidth', 'y');
+    // determine floating [left, top] coordinates of the legend div
+    // within the plotter_ area
+    // offset 50 px to the right and down from the first selection point
+    // 50 px is guess based on mouse cursor size
+    var leftLegend = points[0].x * area.w + 50;
+    var topLegend  = points[0].y * area.h - 50;
+
+    // if legend floats to end of the chart area, it flips to the other
+    // side of the selection point
+    if ((leftLegend + labelsDivWidth + 1) > area.w) {
+      leftLegend = leftLegend - 2 * 50 - labelsDivWidth - (yAxisLabelWidth - area.x);
+    }
+
+    e.dygraph.graphDiv.appendChild(this.legend_div_);
+    this.legend_div_.style.left = yAxisLabelWidth + leftLegend + "px";
+    this.legend_div_.style.top = topLegend + "px";
+  }
+
+  var html = legend.generateLegendHTML(e.dygraph, xValue, points, this.one_em_width_, row);
+  this.legend_div_.innerHTML = html;
+  this.legend_div_.style.display = '';
+};
+
+legend.prototype.deselect = function(e) {
+  var legendMode = e.dygraph.getOption('legend');
+  if (legendMode !== 'always') {
+    this.legend_div_.style.display = "none";
+  }
+
+  // Have to do this every time, since styles might have changed.
+  var oneEmWidth = calculateEmWidthInDiv(this.legend_div_);
+  this.one_em_width_ = oneEmWidth;
+
+  var html = legend.generateLegendHTML(e.dygraph, undefined, undefined, oneEmWidth, null);
+  this.legend_div_.innerHTML = html;
+};
+
+legend.prototype.didDrawChart = function(e) {
+  this.deselect(e);
+};
+
+// Right edge should be flush with the right edge of the charting area (which
+// may not be the same as the right edge of the div, if we have two y-axes.
+// TODO(danvk): is any of this really necessary? Could just set "right" in "activate".
+/**
+ * Position the labels div so that:
+ * - its right edge is flush with the right edge of the charting area
+ * - its top edge is flush with the top edge of the charting area
+ * @private
+ */
+legend.prototype.predraw = function(e) {
+  // Don't touch a user-specified labelsDiv.
+  if (!this.is_generated_div_) return;
+
+  // TODO(danvk): only use real APIs for this.
+  e.dygraph.graphDiv.appendChild(this.legend_div_);
+  var area = e.dygraph.plotter_.area;
+  var labelsDivWidth = e.dygraph.getOption("labelsDivWidth");
+  this.legend_div_.style.left = area.x + area.w - labelsDivWidth - 1 + "px";
+  this.legend_div_.style.top = area.y + "px";
+  this.legend_div_.style.width = labelsDivWidth + "px";
+};
+
+/**
+ * Called when dygraph.destroy() is called.
+ * You should null out any references and detach any DOM elements.
+ */
+legend.prototype.destroy = function() {
+  this.legend_div_ = null;
+};
+
+/**
+ * @private
+ * Generates HTML for the legend which is displayed when hovering over the
+ * chart. If no selected points are specified, a default legend is returned
+ * (this may just be the empty string).
+ * @param {number} x The x-value of the selected points.
+ * @param {Object} sel_points List of selected points for the given
+ *   x-value. Should have properties like 'name', 'yval' and 'canvasy'.
+ * @param {number} oneEmWidth The pixel width for 1em in the legend. Only
+ *   relevant when displaying a legend with no selection (i.e. {legend:
+ *   'always'}) and with dashed lines.
+ * @param {number} row The selected row index.
+ */
+legend.generateLegendHTML = function(g, x, sel_points, oneEmWidth, row) {
+  // TODO(danvk): deprecate this option in place of {legend: 'never'}
+  if (g.getOption('showLabelsOnHighlight') !== true) return '';
+
+  // If no points are selected, we display a default legend. Traditionally,
+  // this has been blank. But a better default would be a conventional legend,
+  // which provides essential information for a non-interactive chart.
+  var html, sepLines, i, dash, strokePattern;
+  var labels = g.getLabels();
+
+  if (typeof(x) === 'undefined') {
+    if (g.getOption('legend') != 'always') {
+      return '';
+    }
+
+    sepLines = g.getOption('labelsSeparateLines');
+    html = '';
+    for (i = 1; i < labels.length; i++) {
+      var series = g.getPropertiesForSeries(labels[i]);
+      if (!series.visible) continue;
+
+      if (html !== '') html += (sepLines ? '<br/>' : ' ');
+      strokePattern = g.getOption("strokePattern", labels[i]);
+      dash = generateLegendDashHTML(strokePattern, series.color, oneEmWidth);
+      html += "<span style='font-weight: bold; color: " + series.color + ";'>" +
+          dash + " " + escapeHTML(labels[i]) + "</span>";
+    }
+    return html;
+  }
+
+  // TODO(danvk): remove this use of a private API
+  var xOptView = g.optionsViewForAxis_('x');
+  var xvf = xOptView('valueFormatter');
+  html = xvf.call(g, x, xOptView, labels[0], g, row, 0);
+  if (html !== '') {
+    html += ':';
+  }
+
+  var yOptViews = [];
+  var num_axes = g.numAxes();
+  for (i = 0; i < num_axes; i++) {
+    // TODO(danvk): remove this use of a private API
+    yOptViews[i] = g.optionsViewForAxis_('y' + (i ? 1 + i : ''));
+  }
+  var showZeros = g.getOption("labelsShowZeroValues");
+  sepLines = g.getOption("labelsSeparateLines");
+  var highlightSeries = g.getHighlightSeries();
+  for (i = 0; i < sel_points.length; i++) {
+    var pt = sel_points[i];
+    if (pt.yval === 0 && !showZeros) continue;
+    if (!Dygraph.isOK(pt.canvasy)) continue;
+    if (sepLines) html += "<br/>";
+
+    var series = g.getPropertiesForSeries(pt.name);
+    var yOptView = yOptViews[series.axis - 1];
+    var fmtFunc = yOptView('valueFormatter');
+    var yval = fmtFunc.call(g, pt.yval, yOptView, pt.name, g, row, labels.indexOf(pt.name));
+
+    var cls = (pt.name == highlightSeries) ? " class='highlight'" : "";
+
+    // TODO(danvk): use a template string here and make it an attribute.
+    html += "<span" + cls + ">" + " <b><span style='color: " + series.color + ";'>" +
+        escapeHTML(pt.name) + "</span></b>:&#160;" + yval + "</span>";
+  }
+  return html;
+};
+
+
+/**
+ * Generates html for the "dash" displayed on the legend when using "legend: always".
+ * In particular, this works for dashed lines with any stroke pattern. It will
+ * try to scale the pattern to fit in 1em width. Or if small enough repeat the
+ * pattern for 1em width.
+ *
+ * @param strokePattern The pattern
+ * @param color The color of the series.
+ * @param oneEmWidth The width in pixels of 1em in the legend.
+ * @private
+ */
+generateLegendDashHTML = function(strokePattern, color, oneEmWidth) {
+  // Easy, common case: a solid line
+  if (!strokePattern || strokePattern.length <= 1) {
+    return "<div style=\"display: inline-block; position: relative; " +
+    "bottom: .5ex; padding-left: 1em; height: 1px; " +
+    "border-bottom: 2px solid " + color + ";\"></div>";
+  }
+
+  var i, j, paddingLeft, marginRight;
+  var strokePixelLength = 0, segmentLoop = 0;
+  var normalizedPattern = [];
+  var loop;
+
+  // Compute the length of the pixels including the first segment twice, 
+  // since we repeat it.
+  for (i = 0; i <= strokePattern.length; i++) {
+    strokePixelLength += strokePattern[i%strokePattern.length];
+  }
+
+  // See if we can loop the pattern by itself at least twice.
+  loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0]));
+  if (loop > 1) {
+    // This pattern fits at least two times, no scaling just convert to em;
+    for (i = 0; i < strokePattern.length; i++) {
+      normalizedPattern[i] = strokePattern[i]/oneEmWidth;
+    }
+    // Since we are repeating the pattern, we don't worry about repeating the
+    // first segment in one draw.
+    segmentLoop = normalizedPattern.length;
+  } else {
+    // If the pattern doesn't fit in the legend we scale it to fit.
+    loop = 1;
+    for (i = 0; i < strokePattern.length; i++) {
+      normalizedPattern[i] = strokePattern[i]/strokePixelLength;
+    }
+    // For the scaled patterns we do redraw the first segment.
+    segmentLoop = normalizedPattern.length+1;
+  }
+
+  // Now make the pattern.
+  var dash = "";
+  for (j = 0; j < loop; j++) {
+    for (i = 0; i < segmentLoop; i+=2) {
+      // The padding is the drawn segment.
+      paddingLeft = normalizedPattern[i%normalizedPattern.length];
+      if (i < strokePattern.length) {
+        // The margin is the space segment.
+        marginRight = normalizedPattern[(i+1)%normalizedPattern.length];
+      } else {
+        // The repeated first segment has no right margin.
+        marginRight = 0;
+      }
+      dash += "<div style=\"display: inline-block; position: relative; " +
+        "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
+        paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
+        ";\"></div>";
+    }
+  }
+  return dash;
+};
+
+
+return legend;
+})();
diff --git a/src/plugins/range-selector.js b/src/plugins/range-selector.js
new file mode 100644 (file)
index 0000000..4c1e938
--- /dev/null
@@ -0,0 +1,798 @@
+/**
+ * @license
+ * Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+/*global Dygraph:false,TouchEvent:false */
+
+/**
+ * @fileoverview This file contains the RangeSelector plugin used to provide
+ * a timeline range selector widget for dygraphs.
+ */
+
+Dygraph.Plugins.RangeSelector = (function() {
+
+/*global Dygraph:false */
+"use strict";
+
+var rangeSelector = function() {
+  this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
+  this.hasTouchInterface_ = typeof(TouchEvent) != 'undefined';
+  this.isMobileDevice_ = /mobile|android/gi.test(navigator.appVersion);
+  this.interfaceCreated_ = false;
+};
+
+rangeSelector.prototype.toString = function() {
+  return "RangeSelector Plugin";
+};
+
+rangeSelector.prototype.activate = function(dygraph) {
+  this.dygraph_ = dygraph;
+  if (this.getOption_('showRangeSelector')) {
+    this.createInterface_();
+  }
+  return {
+    layout: this.reserveSpace_,
+    predraw: this.renderStaticLayer_,
+    didDrawChart: this.renderInteractiveLayer_
+  };
+};
+
+rangeSelector.prototype.destroy = function() {
+  this.bgcanvas_ = null;
+  this.fgcanvas_ = null;
+  this.leftZoomHandle_ = null;
+  this.rightZoomHandle_ = null;
+};
+
+//------------------------------------------------------------------
+// Private methods
+//------------------------------------------------------------------
+
+rangeSelector.prototype.getOption_ = function(name, opt_series) {
+  return this.dygraph_.getOption(name, opt_series);
+};
+
+rangeSelector.prototype.setDefaultOption_ = function(name, value) {
+  this.dygraph_.attrs_[name] = value;
+};
+
+/**
+ * @private
+ * Creates the range selector elements and adds them to the graph.
+ */
+rangeSelector.prototype.createInterface_ = function() {
+  this.createCanvases_();
+  this.createZoomHandles_();
+  this.initInteraction_();
+
+  // Range selector and animatedZooms have a bad interaction. See issue 359.
+  if (this.getOption_('animatedZooms')) {
+    console.warn('Animated zooms and range selector are not compatible; disabling animatedZooms.');
+    this.dygraph_.updateOptions({animatedZooms: false}, true);
+  }
+
+  this.interfaceCreated_ = true;
+  this.addToGraph_();
+};
+
+/**
+ * @private
+ * Adds the range selector to the graph.
+ */
+rangeSelector.prototype.addToGraph_ = function() {
+  var graphDiv = this.graphDiv_ = this.dygraph_.graphDiv;
+  graphDiv.appendChild(this.bgcanvas_);
+  graphDiv.appendChild(this.fgcanvas_);
+  graphDiv.appendChild(this.leftZoomHandle_);
+  graphDiv.appendChild(this.rightZoomHandle_);
+};
+
+/**
+ * @private
+ * Removes the range selector from the graph.
+ */
+rangeSelector.prototype.removeFromGraph_ = function() {
+  var graphDiv = this.graphDiv_;
+  graphDiv.removeChild(this.bgcanvas_);
+  graphDiv.removeChild(this.fgcanvas_);
+  graphDiv.removeChild(this.leftZoomHandle_);
+  graphDiv.removeChild(this.rightZoomHandle_);
+  this.graphDiv_ = null;
+};
+
+/**
+ * @private
+ * Called by Layout to allow range selector to reserve its space.
+ */
+rangeSelector.prototype.reserveSpace_ = function(e) {
+  if (this.getOption_('showRangeSelector')) {
+    e.reserveSpaceBottom(this.getOption_('rangeSelectorHeight') + 4);
+  }
+};
+
+/**
+ * @private
+ * Renders the static portion of the range selector at the predraw stage.
+ */
+rangeSelector.prototype.renderStaticLayer_ = function() {
+  if (!this.updateVisibility_()) {
+    return;
+  }
+  this.resize_();
+  this.drawStaticLayer_();
+};
+
+/**
+ * @private
+ * Renders the interactive portion of the range selector after the chart has been drawn.
+ */
+rangeSelector.prototype.renderInteractiveLayer_ = function() {
+  if (!this.updateVisibility_() || this.isChangingRange_) {
+    return;
+  }
+  this.placeZoomHandles_();
+  this.drawInteractiveLayer_();
+};
+
+/**
+ * @private
+ * Check to see if the range selector is enabled/disabled and update visibility accordingly.
+ */
+rangeSelector.prototype.updateVisibility_ = function() {
+  var enabled = this.getOption_('showRangeSelector');
+  if (enabled) {
+    if (!this.interfaceCreated_) {
+      this.createInterface_();
+    } else if (!this.graphDiv_ || !this.graphDiv_.parentNode) {
+      this.addToGraph_();
+    }
+  } else if (this.graphDiv_) {
+    this.removeFromGraph_();
+    var dygraph = this.dygraph_;
+    setTimeout(function() { dygraph.width_ = 0; dygraph.resize(); }, 1);
+  }
+  return enabled;
+};
+
+/**
+ * @private
+ * Resizes the range selector.
+ */
+rangeSelector.prototype.resize_ = function() {
+  function setElementRect(canvas, context, rect) {
+    var canvasScale = Dygraph.getContextPixelRatio(context);
+
+    canvas.style.top = rect.y + 'px';
+    canvas.style.left = rect.x + 'px';
+    canvas.width = rect.w * canvasScale;
+    canvas.height = rect.h * canvasScale;
+    canvas.style.width = rect.w + 'px';
+    canvas.style.height = rect.h + 'px';
+
+    if(canvasScale != 1) {
+      context.scale(canvasScale, canvasScale);
+    }
+  }
+
+  var plotArea = this.dygraph_.layout_.getPlotArea();
+
+  var xAxisLabelHeight = 0;
+  if (this.dygraph_.getOptionForAxis('drawAxis', 'x')) {
+    xAxisLabelHeight = this.getOption_('xAxisHeight') || (this.getOption_('axisLabelFontSize') + 2 * this.getOption_('axisTickSize'));
+  }
+  this.canvasRect_ = {
+    x: plotArea.x,
+    y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
+    w: plotArea.w,
+    h: this.getOption_('rangeSelectorHeight')
+  };
+
+  setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_);
+  setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_);
+};
+
+/**
+ * @private
+ * Creates the background and foreground canvases.
+ */
+rangeSelector.prototype.createCanvases_ = function() {
+  this.bgcanvas_ = Dygraph.createCanvas();
+  this.bgcanvas_.className = 'dygraph-rangesel-bgcanvas';
+  this.bgcanvas_.style.position = 'absolute';
+  this.bgcanvas_.style.zIndex = 9;
+  this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_);
+
+  this.fgcanvas_ = Dygraph.createCanvas();
+  this.fgcanvas_.className = 'dygraph-rangesel-fgcanvas';
+  this.fgcanvas_.style.position = 'absolute';
+  this.fgcanvas_.style.zIndex = 9;
+  this.fgcanvas_.style.cursor = 'default';
+  this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_);
+};
+
+/**
+ * @private
+ * Creates the zoom handle elements.
+ */
+rangeSelector.prototype.createZoomHandles_ = function() {
+  var img = new Image();
+  img.className = 'dygraph-rangesel-zoomhandle';
+  img.style.position = 'absolute';
+  img.style.zIndex = 10;
+  img.style.visibility = 'hidden'; // Initially hidden so they don't show up in the wrong place.
+  img.style.cursor = 'col-resize';
+//TODO: change image to more options
+  if (/MSIE 7/.test(navigator.userAgent)) { // IE7 doesn't support embedded src data.
+    img.width = 7;
+    img.height = 14;
+    img.style.backgroundColor = 'white';
+    img.style.border = '1px solid #333333'; // Just show box in IE7.
+  } else {
+    img.width = 9;
+    img.height = 16;
+    img.src = 'data:image/png;base64,' +
+'iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA' +
+'zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv' +
+'bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl' +
+'6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s' +
+'qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
+  }
+
+  if (this.isMobileDevice_) {
+    img.width *= 2;
+    img.height *= 2;
+  }
+
+  this.leftZoomHandle_ = img;
+  this.rightZoomHandle_ = img.cloneNode(false);
+};
+
+/**
+ * @private
+ * Sets up the interaction for the range selector.
+ */
+rangeSelector.prototype.initInteraction_ = function() {
+  var self = this;
+  var topElem = document;
+  var clientXLast = 0;
+  var handle = null;
+  var isZooming = false;
+  var isPanning = false;
+  var dynamic = !this.isMobileDevice_;
+
+  // We cover iframes during mouse interactions. See comments in
+  // dygraph-utils.js for more info on why this is a good idea.
+  var tarp = new Dygraph.IFrameTarp();
+
+  // functions, defined below.  Defining them this way (rather than with
+  // "function foo() {...}" makes JSHint happy.
+  var toXDataWindow, onZoomStart, onZoom, onZoomEnd, doZoom, isMouseInPanZone,
+      onPanStart, onPan, onPanEnd, doPan, onCanvasHover;
+
+  // Touch event functions
+  var onZoomHandleTouchEvent, onCanvasTouchEvent, addTouchEvents;
+
+  toXDataWindow = function(zoomHandleStatus) {
+    var xDataLimits = self.dygraph_.xAxisExtremes();
+    var fact = (xDataLimits[1] - xDataLimits[0])/self.canvasRect_.w;
+    var xDataMin = xDataLimits[0] + (zoomHandleStatus.leftHandlePos - self.canvasRect_.x)*fact;
+    var xDataMax = xDataLimits[0] + (zoomHandleStatus.rightHandlePos - self.canvasRect_.x)*fact;
+    return [xDataMin, xDataMax];
+  };
+
+  onZoomStart = function(e) {
+    Dygraph.cancelEvent(e);
+    isZooming = true;
+    clientXLast = e.clientX;
+    handle = e.target ? e.target : e.srcElement;
+    if (e.type === 'mousedown' || e.type === 'dragstart') {
+      // These events are removed manually.
+      Dygraph.addEvent(topElem, 'mousemove', onZoom);
+      Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
+    }
+    self.fgcanvas_.style.cursor = 'col-resize';
+    tarp.cover();
+    return true;
+  };
+
+  onZoom = function(e) {
+    if (!isZooming) {
+      return false;
+    }
+    Dygraph.cancelEvent(e);
+
+    var delX = e.clientX - clientXLast;
+    if (Math.abs(delX) < 4) {
+      return true;
+    }
+    clientXLast = e.clientX;
+
+    // Move handle.
+    var zoomHandleStatus = self.getZoomHandleStatus_();
+    var newPos;
+    if (handle == self.leftZoomHandle_) {
+      newPos = zoomHandleStatus.leftHandlePos + delX;
+      newPos = Math.min(newPos, zoomHandleStatus.rightHandlePos - handle.width - 3);
+      newPos = Math.max(newPos, self.canvasRect_.x);
+    } else {
+      newPos = zoomHandleStatus.rightHandlePos + delX;
+      newPos = Math.min(newPos, self.canvasRect_.x + self.canvasRect_.w);
+      newPos = Math.max(newPos, zoomHandleStatus.leftHandlePos + handle.width + 3);
+    }
+    var halfHandleWidth = handle.width/2;
+    handle.style.left = (newPos - halfHandleWidth) + 'px';
+    self.drawInteractiveLayer_();
+
+    // Zoom on the fly.
+    if (dynamic) {
+      doZoom();
+    }
+    return true;
+  };
+
+  onZoomEnd = function(e) {
+    if (!isZooming) {
+      return false;
+    }
+    isZooming = false;
+    tarp.uncover();
+    Dygraph.removeEvent(topElem, 'mousemove', onZoom);
+    Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'default';
+
+    // If on a slower device, zoom now.
+    if (!dynamic) {
+      doZoom();
+    }
+    return true;
+  };
+
+  doZoom = function() {
+    try {
+      var zoomHandleStatus = self.getZoomHandleStatus_();
+      self.isChangingRange_ = true;
+      if (!zoomHandleStatus.isZoomed) {
+        self.dygraph_.resetZoom();
+      } else {
+        var xDataWindow = toXDataWindow(zoomHandleStatus);
+        self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
+      }
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  isMouseInPanZone = function(e) {
+    var rect = self.leftZoomHandle_.getBoundingClientRect();
+    var leftHandleClientX = rect.left + rect.width/2;
+    rect = self.rightZoomHandle_.getBoundingClientRect();
+    var rightHandleClientX = rect.left + rect.width/2;
+    return (e.clientX > leftHandleClientX && e.clientX < rightHandleClientX);
+  };
+
+  onPanStart = function(e) {
+    if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
+      Dygraph.cancelEvent(e);
+      isPanning = true;
+      clientXLast = e.clientX;
+      if (e.type === 'mousedown') {
+        // These events are removed manually.
+        Dygraph.addEvent(topElem, 'mousemove', onPan);
+        Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
+      }
+      return true;
+    }
+    return false;
+  };
+
+  onPan = function(e) {
+    if (!isPanning) {
+      return false;
+    }
+    Dygraph.cancelEvent(e);
+
+    var delX = e.clientX - clientXLast;
+    if (Math.abs(delX) < 4) {
+      return true;
+    }
+    clientXLast = e.clientX;
+
+    // Move range view
+    var zoomHandleStatus = self.getZoomHandleStatus_();
+    var leftHandlePos = zoomHandleStatus.leftHandlePos;
+    var rightHandlePos = zoomHandleStatus.rightHandlePos;
+    var rangeSize = rightHandlePos - leftHandlePos;
+    if (leftHandlePos + delX <= self.canvasRect_.x) {
+      leftHandlePos = self.canvasRect_.x;
+      rightHandlePos = leftHandlePos + rangeSize;
+    } else if (rightHandlePos + delX >= self.canvasRect_.x + self.canvasRect_.w) {
+      rightHandlePos = self.canvasRect_.x + self.canvasRect_.w;
+      leftHandlePos = rightHandlePos - rangeSize;
+    } else {
+      leftHandlePos += delX;
+      rightHandlePos += delX;
+    }
+    var halfHandleWidth = self.leftZoomHandle_.width/2;
+    self.leftZoomHandle_.style.left = (leftHandlePos - halfHandleWidth) + 'px';
+    self.rightZoomHandle_.style.left = (rightHandlePos - halfHandleWidth) + 'px';
+    self.drawInteractiveLayer_();
+
+    // Do pan on the fly.
+    if (dynamic) {
+      doPan();
+    }
+    return true;
+  };
+
+  onPanEnd = function(e) {
+    if (!isPanning) {
+      return false;
+    }
+    isPanning = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onPan);
+    Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
+    // If on a slower device, do pan now.
+    if (!dynamic) {
+      doPan();
+    }
+    return true;
+  };
+
+  doPan = function() {
+    try {
+      self.isChangingRange_ = true;
+      self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
+      self.dygraph_.drawGraph_(false);
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  onCanvasHover = function(e) {
+    if (isZooming || isPanning) {
+      return;
+    }
+    var cursor = isMouseInPanZone(e) ? 'move' : 'default';
+    if (cursor != self.fgcanvas_.style.cursor) {
+      self.fgcanvas_.style.cursor = cursor;
+    }
+  };
+
+  onZoomHandleTouchEvent = function(e) {
+    if (e.type == 'touchstart' && e.targetTouches.length == 1) {
+      if (onZoomStart(e.targetTouches[0])) {
+        Dygraph.cancelEvent(e);
+      }
+    } else if (e.type == 'touchmove' && e.targetTouches.length == 1) {
+      if (onZoom(e.targetTouches[0])) {
+        Dygraph.cancelEvent(e);
+      }
+    } else {
+      onZoomEnd(e);
+    }
+  };
+
+  onCanvasTouchEvent = function(e) {
+    if (e.type == 'touchstart' && e.targetTouches.length == 1) {
+      if (onPanStart(e.targetTouches[0])) {
+        Dygraph.cancelEvent(e);
+      }
+    } else if (e.type == 'touchmove' && e.targetTouches.length == 1) {
+      if (onPan(e.targetTouches[0])) {
+        Dygraph.cancelEvent(e);
+      }
+    } else {
+      onPanEnd(e);
+    }
+  };
+
+  addTouchEvents = function(elem, fn) {
+    var types = ['touchstart', 'touchend', 'touchmove', 'touchcancel'];
+    for (var i = 0; i < types.length; i++) {
+      self.dygraph_.addAndTrackEvent(elem, types[i], fn);
+    }
+  };
+
+  this.setDefaultOption_('interactionModel', Dygraph.Interaction.dragIsPanInteractionModel);
+  this.setDefaultOption_('panEdgeFraction', 0.0001);
+
+  var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
+  this.dygraph_.addAndTrackEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
+  this.dygraph_.addAndTrackEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
+
+  this.dygraph_.addAndTrackEvent(this.fgcanvas_, 'mousedown', onPanStart);
+  this.dygraph_.addAndTrackEvent(this.fgcanvas_, 'mousemove', onCanvasHover);
+
+  // Touch events
+  if (this.hasTouchInterface_) {
+    addTouchEvents(this.leftZoomHandle_, onZoomHandleTouchEvent);
+    addTouchEvents(this.rightZoomHandle_, onZoomHandleTouchEvent);
+    addTouchEvents(this.fgcanvas_, onCanvasTouchEvent);
+  }
+};
+
+/**
+ * @private
+ * Draws the static layer in the background canvas.
+ */
+rangeSelector.prototype.drawStaticLayer_ = function() {
+  var ctx = this.bgcanvas_ctx_;
+  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+  try {
+    this.drawMiniPlot_();
+  } catch(ex) {
+    console.warn(ex);
+  }
+
+  var margin = 0.5;
+  this.bgcanvas_ctx_.lineWidth = this.getOption_('rangeSelectorBackgroundLineWidth');
+  ctx.strokeStyle = this.getOption_('rangeSelectorBackgroundStrokeColor');
+  ctx.beginPath();
+  ctx.moveTo(margin, margin);
+  ctx.lineTo(margin, this.canvasRect_.h-margin);
+  ctx.lineTo(this.canvasRect_.w-margin, this.canvasRect_.h-margin);
+  ctx.lineTo(this.canvasRect_.w-margin, margin);
+  ctx.stroke();
+};
+
+
+/**
+ * @private
+ * Draws the mini plot in the background canvas.
+ */
+rangeSelector.prototype.drawMiniPlot_ = function() {
+  var fillStyle = this.getOption_('rangeSelectorPlotFillColor');
+  var fillGradientStyle = this.getOption_('rangeSelectorPlotFillGradientColor');
+  var strokeStyle = this.getOption_('rangeSelectorPlotStrokeColor');
+  if (!fillStyle && !strokeStyle) {
+    return;
+  }
+
+  var stepPlot = this.getOption_('stepPlot');
+
+  var combinedSeriesData = this.computeCombinedSeriesAndLimits_();
+  var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin;
+
+  // Draw the mini plot.
+  var ctx = this.bgcanvas_ctx_;
+  var margin = 0.5;
+
+  var xExtremes = this.dygraph_.xAxisExtremes();
+  var xRange = Math.max(xExtremes[1] - xExtremes[0], 1.e-30);
+  var xFact = (this.canvasRect_.w - margin)/xRange;
+  var yFact = (this.canvasRect_.h - margin)/yRange;
+  var canvasWidth = this.canvasRect_.w - margin;
+  var canvasHeight = this.canvasRect_.h - margin;
+
+  var prevX = null, prevY = null;
+
+  ctx.beginPath();
+  ctx.moveTo(margin, canvasHeight);
+  for (var i = 0; i < combinedSeriesData.data.length; i++) {
+    var dataPoint = combinedSeriesData.data[i];
+    var x = ((dataPoint[0] !== null) ? ((dataPoint[0] - xExtremes[0])*xFact) : NaN);
+    var y = ((dataPoint[1] !== null) ? (canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact) : NaN);
+
+    // Skip points that don't change the x-value. Overly fine-grained points
+    // can cause major slowdowns with the ctx.fill() call below.
+    if (!stepPlot && prevX !== null && Math.round(x) == Math.round(prevX)) {
+      continue;
+    }
+
+    if (isFinite(x) && isFinite(y)) {
+      if(prevX === null) {
+        ctx.lineTo(x, canvasHeight);
+      }
+      else if (stepPlot) {
+        ctx.lineTo(x, prevY);
+      }
+      ctx.lineTo(x, y);
+      prevX = x;
+      prevY = y;
+    }
+    else {
+      if(prevX !== null) {
+        if (stepPlot) {
+          ctx.lineTo(x, prevY);
+          ctx.lineTo(x, canvasHeight);
+        }
+        else {
+          ctx.lineTo(prevX, canvasHeight);
+        }
+      }
+      prevX = prevY = null;
+    }
+  }
+  ctx.lineTo(canvasWidth, canvasHeight);
+  ctx.closePath();
+
+  if (fillStyle) {
+    var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
+    if (fillGradientStyle) {
+      lingrad.addColorStop(0, fillGradientStyle);
+    }
+    lingrad.addColorStop(1, fillStyle);
+    this.bgcanvas_ctx_.fillStyle = lingrad;
+    ctx.fill();
+  }
+
+  if (strokeStyle) {
+    this.bgcanvas_ctx_.strokeStyle = strokeStyle;
+    this.bgcanvas_ctx_.lineWidth = this.getOption_('rangeSelectorPlotLineWidth');
+    ctx.stroke();
+  }
+};
+
+/**
+ * @private
+ * Computes and returns the combined series data along with min/max for the mini plot.
+ * The combined series consists of averaged values for all series.
+ * When series have error bars, the error bars are ignored.
+ * @return {Object} An object containing combined series array, ymin, ymax.
+ */
+rangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
+  var g = this.dygraph_;
+  var logscale = this.getOption_('logscale');
+  var i;
+
+  // Select series to combine. By default, all series are combined.
+  var numColumns = g.numColumns();
+  var labels = g.getLabels();
+  var includeSeries = new Array(numColumns);
+  var anySet = false;
+  for (i = 1; i < numColumns; i++) {
+    var include = this.getOption_('showInRangeSelector', labels[i]);
+    includeSeries[i] = include;
+    if (include !== null) anySet = true;  // it's set explicitly for this series
+  }
+  if (!anySet) {
+    for (i = 0; i < includeSeries.length; i++) includeSeries[i] = true;
+  }
+
+  // Create a combined series (average of selected series values).
+  // TODO(danvk): short-circuit if there's only one series.
+  var rolledSeries = [];
+  var dataHandler = g.dataHandler_;
+  var options = g.attributes_;
+  for (i = 1; i < g.numColumns(); i++) {
+    if (!includeSeries[i]) continue;
+    var series = dataHandler.extractSeries(g.rawData_, i, options);
+    if (g.rollPeriod() > 1) {
+      series = dataHandler.rollingAverage(series, g.rollPeriod(), options);
+    }
+
+    rolledSeries.push(series);
+  }
+
+  var combinedSeries = [];
+  for (i = 0; i < rolledSeries[0].length; i++) {
+    var sum = 0;
+    var count = 0;
+    for (var j = 0; j < rolledSeries.length; j++) {
+      var y = rolledSeries[j][i][1];
+      if (y === null || isNaN(y)) continue;
+      count++;
+      sum += y;
+    }
+    combinedSeries.push([rolledSeries[0][i][0], sum / count]);
+  }
+
+  // Compute the y range.
+  var yMin = Number.MAX_VALUE;
+  var yMax = -Number.MAX_VALUE;
+  for (i = 0; i < combinedSeries.length; i++) {
+    var yVal = combinedSeries[i][1];
+    if (yVal !== null && isFinite(yVal) && (!logscale || yVal > 0)) {
+      yMin = Math.min(yMin, yVal);
+      yMax = Math.max(yMax, yVal);
+    }
+  }
+
+  // Convert Y data to log scale if needed.
+  // Also, expand the Y range to compress the mini plot a little.
+  var extraPercent = 0.25;
+  if (logscale) {
+    yMax = Dygraph.log10(yMax);
+    yMax += yMax*extraPercent;
+    yMin = Dygraph.log10(yMin);
+    for (i = 0; i < combinedSeries.length; i++) {
+      combinedSeries[i][1] = Dygraph.log10(combinedSeries[i][1]);
+    }
+  } else {
+    var yExtra;
+    var yRange = yMax - yMin;
+    if (yRange <= Number.MIN_VALUE) {
+      yExtra = yMax*extraPercent;
+    } else {
+      yExtra = yRange*extraPercent;
+    }
+    yMax += yExtra;
+    yMin -= yExtra;
+  }
+
+  return {data: combinedSeries, yMin: yMin, yMax: yMax};
+};
+
+/**
+ * @private
+ * Places the zoom handles in the proper position based on the current X data window.
+ */
+rangeSelector.prototype.placeZoomHandles_ = function() {
+  var xExtremes = this.dygraph_.xAxisExtremes();
+  var xWindowLimits = this.dygraph_.xAxisRange();
+  var xRange = xExtremes[1] - xExtremes[0];
+  var leftPercent = Math.max(0, (xWindowLimits[0] - xExtremes[0])/xRange);
+  var rightPercent = Math.max(0, (xExtremes[1] - xWindowLimits[1])/xRange);
+  var leftCoord = this.canvasRect_.x + this.canvasRect_.w*leftPercent;
+  var rightCoord = this.canvasRect_.x + this.canvasRect_.w*(1 - rightPercent);
+  var handleTop = Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height)/2);
+  var halfHandleWidth = this.leftZoomHandle_.width/2;
+  this.leftZoomHandle_.style.left = (leftCoord - halfHandleWidth) + 'px';
+  this.leftZoomHandle_.style.top = handleTop + 'px';
+  this.rightZoomHandle_.style.left = (rightCoord - halfHandleWidth) + 'px';
+  this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top;
+
+  this.leftZoomHandle_.style.visibility = 'visible';
+  this.rightZoomHandle_.style.visibility = 'visible';
+};
+
+/**
+ * @private
+ * Draws the interactive layer in the foreground canvas.
+ */
+rangeSelector.prototype.drawInteractiveLayer_ = function() {
+  var ctx = this.fgcanvas_ctx_;
+  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+  var margin = 1;
+  var width = this.canvasRect_.w - margin;
+  var height = this.canvasRect_.h - margin;
+  var zoomHandleStatus = this.getZoomHandleStatus_();
+
+  ctx.strokeStyle = this.getOption_('rangeSelectorForegroundStrokeColor');
+  ctx.lineWidth = this.getOption_('rangeSelectorForegroundLineWidth');
+  if (!zoomHandleStatus.isZoomed) {
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(margin, height);
+    ctx.lineTo(width, height);
+    ctx.lineTo(width, margin);
+    ctx.stroke();
+  } else {
+    var leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x);
+    var rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x);
+
+    ctx.fillStyle = 'rgba(240, 240, 240, ' + this.getOption_('rangeSelectorAlpha').toString() + ')';
+    ctx.fillRect(0, 0, leftHandleCanvasPos, this.canvasRect_.h);
+    ctx.fillRect(rightHandleCanvasPos, 0, this.canvasRect_.w - rightHandleCanvasPos, this.canvasRect_.h);
+
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(leftHandleCanvasPos, margin);
+    ctx.lineTo(leftHandleCanvasPos, height);
+    ctx.lineTo(rightHandleCanvasPos, height);
+    ctx.lineTo(rightHandleCanvasPos, margin);
+    ctx.lineTo(width, margin);
+    ctx.stroke();
+  }
+};
+
+/**
+ * @private
+ * Returns the current zoom handle position information.
+ * @return {Object} The zoom handle status.
+ */
+rangeSelector.prototype.getZoomHandleStatus_ = function() {
+  var halfHandleWidth = this.leftZoomHandle_.width/2;
+  var leftHandlePos = parseFloat(this.leftZoomHandle_.style.left) + halfHandleWidth;
+  var rightHandlePos = parseFloat(this.rightZoomHandle_.style.left) + halfHandleWidth;
+  return {
+      leftHandlePos: leftHandlePos,
+      rightHandlePos: rightHandlePos,
+      isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
+  };
+};
+
+return rangeSelector;
+
+})();
diff --git a/src/polyfills/console.js b/src/polyfills/console.js
new file mode 100644 (file)
index 0000000..4a6f542
--- /dev/null
@@ -0,0 +1,15 @@
+// Console-polyfill. MIT license.
+// https://github.com/paulmillr/console-polyfill
+// Make it safe to do console.log() always.
+(function(con) {
+  'use strict';
+  var prop, method;
+  var empty = {};
+  var dummy = function() {};
+  var properties = 'memory'.split(',');
+  var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
+     'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
+     'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(',');
+  while (prop = properties.pop()) con[prop] = con[prop] || empty;
+  while (method = methods.pop()) con[method] = con[method] || dummy;
+})(this.console = this.console || {}); // Using `this` for web workers.
diff --git a/src/polyfills/dashed-canvas.js b/src/polyfills/dashed-canvas.js
new file mode 100644 (file)
index 0000000..0fc6874
--- /dev/null
@@ -0,0 +1,180 @@
+/**
+ * @license
+ * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
+
+(function() {
+'use strict';
+
+/**
+ * @fileoverview Adds support for dashed lines to the HTML5 canvas.
+ *
+ * Usage:
+ *   var ctx = canvas.getContext("2d");
+ *   ctx.installPattern([10, 5])  // draw 10 pixels, skip 5 pixels, repeat.
+ *   ctx.beginPath();
+ *   ctx.moveTo(100, 100);  // start the first line segment.
+ *   ctx.lineTo(150, 200);
+ *   ctx.lineTo(200, 100);
+ *   ctx.moveTo(300, 150);  // start a second, unconnected line
+ *   ctx.lineTo(400, 250);
+ *   ...
+ *   ctx.stroke();          // draw the dashed line.
+ *   ctx.uninstallPattern();
+ *
+ * This is designed to leave the canvas untouched when it's not used.
+ * If you never install a pattern, or call uninstallPattern(), then the canvas
+ * will be exactly as it would have if you'd never used this library. The only
+ * difference from the standard canvas will be the "installPattern" method of
+ * the drawing context.
+ */
+
+/**
+ * Change the stroking style of the canvas drawing context from a solid line to
+ * a pattern (e.g. dashes, dash-dot-dash, etc.)
+ *
+ * Once you've installed the pattern, you can draw with it by using the
+ * beginPath(), moveTo(), lineTo() and stroke() method calls. Note that some
+ * more advanced methods (e.g. quadraticCurveTo() and bezierCurveTo()) are not
+ * supported. See file overview for a working example.
+ *
+ * Side effects of calling this method include adding an "isPatternInstalled"
+ * property and "uninstallPattern" method to this particular canvas context.
+ * You must call uninstallPattern() before calling installPattern() again.
+ *
+ * @param {Array.<number>} pattern A description of the stroke pattern. Even
+ * indices indicate a draw and odd indices indicate a gap (in pixels). The
+ * array should have a even length as any odd lengthed array could be expressed
+ * as a smaller even length array.
+ */
+CanvasRenderingContext2D.prototype.installPattern = function(pattern) {
+  if (typeof(this.isPatternInstalled) !== 'undefined') {
+    throw "Must un-install old line pattern before installing a new one.";
+  }
+  this.isPatternInstalled = true;
+
+  var dashedLineToHistory = [0, 0];
+
+  // list of connected line segements:
+  // [ [x1, y1], ..., [xn, yn] ], [ [x1, y1], ..., [xn, yn] ]
+  var segments = [];
+
+  // Stash away copies of the unmodified line-drawing functions.
+  var realBeginPath = this.beginPath;
+  var realLineTo = this.lineTo;
+  var realMoveTo = this.moveTo;
+  var realStroke = this.stroke;
+
+  /** @type {function()|undefined} */
+  this.uninstallPattern = function() {
+    this.beginPath = realBeginPath;
+    this.lineTo = realLineTo;
+    this.moveTo = realMoveTo;
+    this.stroke = realStroke;
+    this.uninstallPattern = undefined;
+    this.isPatternInstalled = undefined;
+  };
+
+  // Keep our own copies of the line segments as they're drawn.
+  this.beginPath = function() {
+    segments = [];
+    realBeginPath.call(this);
+  };
+  this.moveTo = function(x, y) {
+    segments.push([[x, y]]);
+    realMoveTo.call(this, x, y);
+  };
+  this.lineTo = function(x, y) {
+    var last = segments[segments.length - 1];
+    last.push([x, y]);
+  };
+
+  this.stroke = function() {
+    if (segments.length === 0) {
+      // Maybe the user is drawing something other than a line.
+      // TODO(danvk): test this case.
+      realStroke.call(this);
+      return;
+    }
+
+    for (var i = 0; i < segments.length; i++) {
+      var seg = segments[i];
+      var x1 = seg[0][0], y1 = seg[0][1];
+      for (var j = 1; j < seg.length; j++) {
+        // Draw a dashed line from (x1, y1) - (x2, y2)
+        var x2 = seg[j][0], y2 = seg[j][1];
+        this.save();
+
+        // Calculate transformation parameters
+        var dx = (x2-x1);
+        var dy = (y2-y1);
+        var len = Math.sqrt(dx*dx + dy*dy);
+        var rot = Math.atan2(dy, dx);
+
+        // Set transformation
+        this.translate(x1, y1);
+        realMoveTo.call(this, 0, 0);
+        this.rotate(rot);
+
+        // Set last pattern index we used for this pattern.
+        var patternIndex = dashedLineToHistory[0];
+        var x = 0;
+        while (len > x) {
+          // Get the length of the pattern segment we are dealing with.
+          var segment = pattern[patternIndex];
+          // If our last draw didn't complete the pattern segment all the way
+          // we will try to finish it. Otherwise we will try to do the whole
+          // segment.
+          if (dashedLineToHistory[1]) {
+            x += dashedLineToHistory[1];
+          } else {
+            x += segment;
+          }
+
+          if (x > len) {
+            // We were unable to complete this pattern index all the way, keep
+            // where we are the history so our next draw continues where we
+            // left off in the pattern.
+            dashedLineToHistory = [patternIndex, x-len];
+            x = len;
+          } else {
+            // We completed this patternIndex, we put in the history that we
+            // are on the beginning of the next segment.
+            dashedLineToHistory = [(patternIndex+1)%pattern.length, 0];
+          }
+
+          // We do a line on a even pattern index and just move on a odd
+          // pattern index.  The move is the empty space in the dash.
+          if (patternIndex % 2 === 0) {
+            realLineTo.call(this, x, 0);
+          } else {
+            realMoveTo.call(this, x, 0);
+          }
+
+          // If we are not done, next loop process the next pattern segment, or
+          // the first segment again if we are at the end of the pattern.
+          patternIndex = (patternIndex+1) % pattern.length;
+        }
+
+        this.restore();
+        x1 = x2;
+        y1 = y2;
+      }
+    }
+    realStroke.call(this);
+    segments = [];
+  };
+};
+
+/**
+ * Removes the previously-installed pattern.
+ * You must call installPattern() before calling this. You can install at most
+ * one pattern at a time--there is no pattern stack.
+ */
+CanvasRenderingContext2D.prototype.uninstallPattern = function() {
+  // This will be replaced by a non-error version when a pattern is installed.
+  throw "Must install a line pattern before uninstalling it.";
+};
+
+})();
diff --git a/test.sh b/test.sh
deleted file mode 100755 (executable)
index 3194c72..0000000
--- a/test.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-which phantomjs > /dev/null
-if [ $? != 0 ]; then
-  echo You must install phantomjs to use command-line testing.
-  echo Visit http://www.phantomjs.org/ to get it.
-  echo
-  echo OR open auto_tests/misc/local.html in a browser.
-  echo OR follow the instructions in auto_tests/README
-  exit 1
-fi
-
-# Don't run tests if the documentation doesn't parse.
-./generate-documentation.py > /dev/null
-if [ $? != 0 ]; then
-  echo Failed to generate documentation. Fix this before running tests.
-  exit 1
-fi
-
-phantomjs phantom-driver.js $* | tee /tmp/test-results.txt
-trap "rm -f /tmp/test-results.txt" EXIT
-if grep -q 'FAIL' /tmp/test-results.txt; then
-  echo One or more tests failed.
-  exit 1
-fi