Adds HEAD of dygraphs
authordsc <dsc@less.ly>
Sun, 26 Feb 2012 11:22:37 +0000 (03:22 -0800)
committerdsc <dsc@less.ly>
Sun, 26 Feb 2012 11:22:37 +0000 (03:22 -0800)
static/vendor/dygraph-1.2.js [new file with mode: 0644]
static/vendor/dygraph-1.2.min.js [new file with mode: 0644]
static/vendor/dygraph-1.2dev.js [new file with mode: 0644]
static/vendor/dygraph-1.2dev.min.js [new file with mode: 0644]
static/vendor/dygraph.js [changed from file to symlink]
static/vendor/dygraph.min.js [changed from file to symlink]

diff --git a/static/vendor/dygraph-1.2.js b/static/vendor/dygraph-1.2.js
new file mode 100644 (file)
index 0000000..1638ad7
--- /dev/null
@@ -0,0 +1,7333 @@
+/**
+ * @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.
+ */
+
+/*jshint globalstrict: true */
+/*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;
+  this.datasets = [];
+  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;
+};
+
+DygraphLayout.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+DygraphLayout.prototype.addDataset = function(setname, set_xy) {
+  this.datasets[setname] = set_xy;
+};
+
+DygraphLayout.prototype.getPlotArea = function() {
+  return this.computePlotArea_();
+};
+
+// Compute the box which the chart should be drawn in. This is the canvas's
+// box, less space needed for axis and chart labels.
+DygraphLayout.prototype.computePlotArea_ = function() {
+  var area = {
+    // TODO(danvk): per-axis setting.
+    x: 0,
+    y: 0
+  };
+  if (this.attr_('drawYAxis')) {
+   area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
+  }
+
+  area.w = this.dygraph_.width_ - area.x - this.attr_('rightGap');
+  area.h = this.dygraph_.height_;
+  if (this.attr_('drawXAxis')) {
+    if (this.attr_('xAxisHeight')) {
+      area.h -= this.attr_('xAxisHeight');
+    } else {
+      area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+    }
+  }
+
+  // Shrink the drawing area to accomodate additional y-axes.
+  if (this.dygraph_.numAxes() == 2) {
+    // TODO(danvk): per-axis setting.
+    area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
+  } else if (this.dygraph_.numAxes() > 2) {
+    this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
+                        "to use " + this.dygraph_.numAxes() + ")");
+  }
+
+  // Add space for chart labels: title, xlabel and ylabel.
+  if (this.attr_('title')) {
+    area.h -= this.attr_('titleHeight');
+    area.y += this.attr_('titleHeight');
+  }
+  if (this.attr_('xlabel')) {
+    area.h -= this.attr_('xLabelHeight');
+  }
+  if (this.attr_('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.
+  }
+
+  if (this.attr_('y2label')) {
+    // same logic applies here as for ylabel.
+    // TODO(danvk): make yAxisLabelWidth a per-axis property
+  }
+
+  // Add space for range selector, if needed.
+  if (this.attr_('showRangeSelector')) {
+    area.h -= this.attr_('rangeSelectorHeight') + 4;
+  }
+
+  return 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.attr_('xValueParser') || function(x) { return x; };
+  for (var i = 0; i < ann.length; i++) {
+    var a = {};
+    if (!ann[i].xval && !ann[i].x) {
+      this.dygraph_.error("Annotations must have an 'x' property");
+      return;
+    }
+    if (ann[i].icon &&
+        !(ann[i].hasOwnProperty('width') &&
+          ann[i].hasOwnProperty('height'))) {
+      this.dygraph_.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.setDateWindow = function(dateWindow) {
+  this.dateWindow_ = dateWindow;
+};
+
+DygraphLayout.prototype.evaluate = function() {
+  this._evaluateLimits();
+  this._evaluateLineCharts();
+  this._evaluateLineTicks();
+  this._evaluateAnnotations();
+};
+
+DygraphLayout.prototype._evaluateLimits = function() {
+  this.minxval = this.maxxval = null;
+  if (this.dateWindow_) {
+    this.minxval = this.dateWindow_[0];
+    this.maxxval = this.dateWindow_[1];
+  } else {
+    for (var name in this.datasets) {
+      if (!this.datasets.hasOwnProperty(name)) continue;
+      var series = this.datasets[name];
+      if (series.length > 1) {
+        var x1 = series[0][0];
+        if (!this.minxval || x1 < this.minxval) this.minxval = x1;
+
+        var x2 = series[series.length - 1][0];
+        if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
+      }
+    }
+  }
+  this.xrange = this.maxxval - this.minxval;
+  this.xscale = (this.xrange !== 0 ? 1/this.xrange : 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 (axis.g.attr_("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)) {
+        axis.g.error('axis ' + i + ' of graph at ' + axis.g +
+            ' can\'t be displayed in log scale for range [' +
+            axis.minyval + ' - ' + axis.maxyval + ']');
+      }
+    }
+  }
+};
+
+DygraphLayout._calcYNormal = function(axis, value) {
+  if (axis.logscale) {
+    return 1.0 - ((Dygraph.log10(value) - Dygraph.log10(axis.minyval)) * axis.ylogscale);
+  } else {
+    return 1.0 - ((value - axis.minyval) * axis.yscale);
+  }
+};
+
+DygraphLayout.prototype._evaluateLineCharts = function() {
+  // add all the rects
+  this.points = [];
+  // An array to keep track of how many points will be drawn for each set.
+  // This will allow for the canvas renderer to not have to check every point
+  // for every data set since the points are added in order of the sets in
+  // datasets.
+  this.setPointsLengths = [];
+
+  for (var setName in this.datasets) {
+    if (!this.datasets.hasOwnProperty(setName)) continue;
+
+    var dataset = this.datasets[setName];
+    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+
+    var setPointsLength = 0;
+
+    for (var j = 0; j < dataset.length; j++) {
+      var item = dataset[j];
+      var xValue = parseFloat(item[0]);
+      var yValue = parseFloat(item[1]);
+
+      // Range from 0-1 where 0 represents left and 1 represents right.
+      var xNormal = (xValue - this.minxval) * this.xscale;
+      // Range from 0-1 where 0 represents top and 1 represents bottom
+      var yNormal = DygraphLayout._calcYNormal(axis, yValue);
+
+      var point = {
+        // TODO(danvk): here
+        x: xNormal,
+        y: yNormal,
+        xval: xValue,
+        yval: yValue,
+        name: setName
+      };
+      this.points.push(point);
+      setPointsLength += 1;
+    }
+    this.setPointsLengths.push(setPointsLength);
+  }
+};
+
+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.xscale * (tick.v - this.minxval);
+    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]);
+      }
+    }
+  }
+};
+
+
+/**
+ * Behaves the same way as PlotKit.Layout, but also copies the errors
+ * @private
+ */
+DygraphLayout.prototype.evaluateWithError = function() {
+  this.evaluate();
+  if (!(this.attr_('errorBars') || this.attr_('customBars'))) return;
+
+  // Copy over the error terms
+  var i = 0;  // index in this.points
+  for (var setName in this.datasets) {
+    if (!this.datasets.hasOwnProperty(setName)) continue;
+    var j = 0;
+    var dataset = this.datasets[setName];
+    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+    for (j = 0; j < dataset.length; j++, i++) {
+      var item = dataset[j];
+      var xv = parseFloat(item[0]);
+      var yv = parseFloat(item[1]);
+
+      if (xv == this.points[i].xval &&
+          yv == this.points[i].yval) {
+        var errorMinus = parseFloat(item[2]);
+        var errorPlus = parseFloat(item[3]);
+
+        var yv_minus = yv - errorMinus;
+        var yv_plus = yv + errorPlus;
+        this.points[i].y_top = DygraphLayout._calcYNormal(axis, yv_minus);
+        this.points[i].y_bottom = DygraphLayout._calcYNormal(axis, yv_plus);
+      }
+    }
+  }
+};
+
+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 (i = 0; i < this.points.length; i++) {
+    var p = this.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.datasets;
+  this.datasets = [];
+};
+
+/**
+ * Return a copy of the point at the indicated index, with its yval unstacked.
+ * @param int index of point in layout_.points
+ */
+DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
+  var point = this.points[idx];
+
+  // Clone the point since we modify it
+  var unstackedPoint = {};
+  for (var pt in point) {
+    unstackedPoint[pt] = point[pt];
+  }
+
+  if (!this.attr_("stackedGraph")) {
+    return unstackedPoint;
+  }
+
+  // The unstacked yval is equal to the current yval minus the yval of the
+  // next point at the same xval.
+  for (var i = idx+1; i < this.points.length; i++) {
+    if (this.points[i].xval == point.xval) {
+      unstackedPoint.yval -= this.points[i].yval;
+      break;
+    }
+  }
+
+  return unstackedPoint;
+};
+/**
+ * @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
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false,RGBColor:false */
+"use strict";
+
+
+var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
+  this.dygraph_ = dygraph;
+
+  this.layout = layout;
+  this.element = element;
+  this.elementContext = elementContext;
+  this.container = this.element.parentNode;
+
+  this.height = this.element.height;
+  this.width = this.element.width;
+
+  // --- check whether everything is ok before we return
+  if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element)))
+      throw "Canvas is not supported.";
+
+  // internal state
+  this.xlabels = [];
+  this.ylabels = [];
+  this.annotations = [];
+  this.chartLabels = {};
+
+  this.area = layout.getPlotArea();
+  this.container.style.position = "relative";
+  this.container.style.width = this.width + "px";
+
+  // Set up a clipping area for the canvas (and the interaction canvas).
+  // This ensures that we don't overdraw.
+  if (this.dygraph_.isUsingExcanvas_) {
+    this._createIEClipArea();
+  } else {
+    // 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();
+    }
+  }
+};
+
+DygraphCanvasRenderer.prototype.attr_ = function(x) {
+  return this.dygraph_.attr_(x);
+};
+
+DygraphCanvasRenderer.prototype.clear = function() {
+  var context;
+  if (this.isIE) {
+    // VML takes a while to start up, so we just poll every this.IEDelay
+    try {
+      if (this.clearDelay) {
+        this.clearDelay.cancel();
+        this.clearDelay = null;
+      }
+      context = this.elementContext;
+    }
+    catch (e) {
+      // TODO(danvk): this is broken, since MochiKit.Async is gone.
+      // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
+      // this.clearDelay.addCallback(bind(this.clear, this));
+      return;
+    }
+  }
+
+  context = this.elementContext;
+  context.clearRect(0, 0, this.width, this.height);
+
+  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);
+  removeArray(this.annotations);
+
+  for (var k in this.chartLabels) {
+    if (!this.chartLabels.hasOwnProperty(k)) continue;
+    var el = this.chartLabels[k];
+    if (el.parentNode) el.parentNode.removeChild(el);
+  }
+  this.xlabels = [];
+  this.ylabels = [];
+  this.annotations = [];
+  this.chartLabels = {};
+};
+
+
+DygraphCanvasRenderer.isSupported = function(canvasName) {
+  var canvas = null;
+  try {
+    if (typeof(canvasName) == 'undefined' || canvasName === null) {
+      canvas = document.createElement("canvas");
+    } else {
+      canvas = canvasName;
+    }
+    canvas.getContext("2d");
+  }
+  catch (e) {
+    var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
+    var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
+    if ((!ie) || (ie[1] < 6) || (opera))
+      return false;
+    return true;
+  }
+  return true;
+};
+
+/**
+ * @param { [String] } colors Array of color strings. Should have one entry for
+ * each series to be rendered.
+ */
+DygraphCanvasRenderer.prototype.setColors = function(colors) {
+  this.colorScheme_ = colors;
+};
+
+/**
+ * Draw an X/Y grid on top of the existing plot
+ */
+DygraphCanvasRenderer.prototype.render = function() {
+  // 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 ctx = this.elementContext;
+  function halfUp(x)  { return Math.round(x) + 0.5; }
+  function halfDown(y){ return Math.round(y) - 0.5; }
+
+  if (this.attr_('underlayCallback')) {
+    // NOTE: we pass the dygraph object to this callback twice to avoid breaking
+    // users who expect a deprecated form of this callback.
+    this.attr_('underlayCallback')(ctx, this.area, this.dygraph_, this.dygraph_);
+  }
+
+  var x, y, i, ticks;
+  if (this.attr_('drawYGrid')) {
+    ticks = this.layout.yticks;
+    // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
+    ctx.save();
+    ctx.strokeStyle = this.attr_('gridLineColor');
+    ctx.lineWidth = this.attr_('gridLineWidth');
+    for (i = 0; i < ticks.length; i++) {
+      // TODO(danvk): allow secondary axes to draw a grid, too.
+      if (ticks[i][0] !== 0) continue;
+      x = halfUp(this.area.x);
+      y = halfDown(this.area.y + ticks[i][1] * this.area.h);
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x + this.area.w, y);
+      ctx.closePath();
+      ctx.stroke();
+    }
+  }
+
+  if (this.attr_('drawXGrid')) {
+    ticks = this.layout.xticks;
+    ctx.save();
+    ctx.strokeStyle = this.attr_('gridLineColor');
+    ctx.lineWidth = this.attr_('gridLineWidth');
+    for (i=0; i<ticks.length; i++) {
+      x = halfUp(this.area.x + ticks[i][0] * this.area.w);
+      y = halfDown(this.area.y + this.area.h);
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x, this.area.y);
+      ctx.closePath();
+      ctx.stroke();
+    }
+  }
+
+  // Do the ordinary rendering, as before
+  this._renderLineChart();
+  this._renderAxis();
+  this._renderChartLabels();
+  this._renderAnnotations();
+};
+
+DygraphCanvasRenderer.prototype._createIEClipArea = function() {
+  var className = 'dygraph-clip-div';
+  var graphDiv = this.dygraph_.graphDiv;
+
+  // Remove old clip divs.
+  for (var i = graphDiv.childNodes.length-1; i >= 0; i--) {
+    if (graphDiv.childNodes[i].className == className) {
+      graphDiv.removeChild(graphDiv.childNodes[i]);
+    }
+  }
+
+  // Determine background color to give clip divs.
+  var backgroundColor = document.bgColor;
+  var element = this.dygraph_.graphDiv;
+  while (element != document) {
+    var bgcolor = element.currentStyle.backgroundColor;
+    if (bgcolor && bgcolor != 'transparent') {
+      backgroundColor = bgcolor;
+      break;
+    }
+    element = element.parentNode;
+  }
+
+  function createClipDiv(area) {
+    if (area.w === 0 || area.h === 0) {
+      return;
+    }
+    var elem = document.createElement('div');
+    elem.className = className;
+    elem.style.backgroundColor = backgroundColor;
+    elem.style.position = 'absolute';
+    elem.style.left = area.x + 'px';
+    elem.style.top = area.y + 'px';
+    elem.style.width = area.w + 'px';
+    elem.style.height = area.h + 'px';
+    graphDiv.appendChild(elem);
+  }
+
+  var plotArea = this.area;
+  // Left side
+  createClipDiv({
+    x:0, y:0,
+    w:plotArea.x,
+    h:this.height
+  });
+
+  // Top
+  createClipDiv({
+    x: plotArea.x, y: 0,
+    w: this.width - plotArea.x,
+    h: plotArea.y
+  });
+
+  // Right side
+  createClipDiv({
+    x: plotArea.x + plotArea.w, y: 0,
+    w: this.width-plotArea.x - plotArea.w,
+    h: this.height
+  });
+
+  // Bottom
+  createClipDiv({
+    x: plotArea.x,
+    y: plotArea.y + plotArea.h,
+    w: this.width - plotArea.x,
+    h: this.height - plotArea.h - plotArea.y
+  });
+};
+
+DygraphCanvasRenderer.prototype._renderAxis = function() {
+  if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) 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 = this.elementContext;
+
+  var label, x, y, tick, i;
+
+  var labelStyle = {
+    position: "absolute",
+    fontSize: this.attr_('axisLabelFontSize') + "px",
+    zIndex: 10,
+    color: this.attr_('axisLabelColor'),
+    width: this.attr_('axisLabelWidth') + "px",
+    // height: this.attr_('axisLabelFontSize') + 2 + "px",
+    lineHeight: "normal",  // Something other than "normal" line-height screws up label positioning.
+    overflow: "hidden"
+  };
+  var makeDiv = function(txt, axis, prec_axis) {
+    var div = document.createElement("div");
+    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();
+  context.strokeStyle = this.attr_('axisLineColor');
+  context.lineWidth = this.attr_('axisLineWidth');
+
+  if (this.attr_('drawYAxis')) {
+    if (this.layout.yticks && this.layout.yticks.length > 0) {
+      var num_axes = this.dygraph_.numAxes();
+      for (i = 0; i < this.layout.yticks.length; i++) {
+        tick = this.layout.yticks[i];
+        if (typeof(tick) == "function") return;
+        x = this.area.x;
+        var sgn = 1;
+        var prec_axis = 'y1';
+        if (tick[0] == 1) {  // right-side y-axis
+          x = this.area.x + this.area.w;
+          sgn = -1;
+          prec_axis = 'y2';
+        }
+        y = this.area.y + tick[1] * this.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 - this.attr_('axisLabelFontSize') / 2);
+        if (top < 0) top = 0;
+
+        if (top + this.attr_('axisLabelFontSize') + 3 > this.height) {
+          label.style.bottom = "0px";
+        } else {
+          label.style.top = top + "px";
+        }
+        if (tick[0] === 0) {
+          label.style.left = (this.area.x - this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
+          label.style.textAlign = "right";
+        } else if (tick[0] == 1) {
+          label.style.left = (this.area.x + this.area.w +
+                              this.attr_('axisTickSize')) + "px";
+          label.style.textAlign = "left";
+        }
+        label.style.width = this.attr_('yAxisLabelWidth') + "px";
+        this.container.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];
+      var fontSize = this.attr_('axisLabelFontSize');
+      var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
+      if (bottom > this.height - 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.
+    context.beginPath();
+    context.moveTo(halfUp(this.area.x), halfDown(this.area.y));
+    context.lineTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h));
+    context.closePath();
+    context.stroke();
+
+    // if there's a secondary y-axis, draw a vertical line for that, too.
+    if (this.dygraph_.numAxes() == 2) {
+      context.beginPath();
+      context.moveTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y));
+      context.lineTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
+      context.closePath();
+      context.stroke();
+    }
+  }
+
+  if (this.attr_('drawXAxis')) {
+    if (this.layout.xticks) {
+      for (i = 0; i < this.layout.xticks.length; i++) {
+        tick = this.layout.xticks[i];
+        x = this.area.x + tick[0] * this.area.w;
+        y = this.area.y + this.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 + this.attr_('axisTickSize')) + 'px';
+
+        var left = (x - this.attr_('axisLabelWidth')/2);
+        if (left + this.attr_('axisLabelWidth') > this.width) {
+          left = this.width - this.attr_('xAxisLabelWidth');
+          label.style.textAlign = "right";
+        }
+        if (left < 0) {
+          left = 0;
+          label.style.textAlign = "left";
+        }
+
+        label.style.left = left + "px";
+        label.style.width = this.attr_('xAxisLabelWidth') + "px";
+        this.container.appendChild(label);
+        this.xlabels.push(label);
+      }
+    }
+
+    context.beginPath();
+    context.moveTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h));
+    context.lineTo(halfUp(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
+    context.closePath();
+    context.stroke();
+  }
+
+  context.restore();
+};
+
+
+DygraphCanvasRenderer.prototype._renderChartLabels = function() {
+  var div, class_div;
+
+  // Generate divs for the chart title, xlabel and ylabel.
+  // Space for these divs has already been taken away from the charting area in
+  // the DygraphCanvasRenderer constructor.
+  if (this.attr_('title')) {
+    div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.top = '0px';
+    div.style.left = this.area.x + 'px';
+    div.style.width = this.area.w + 'px';
+    div.style.height = this.attr_('titleHeight') + 'px';
+    div.style.textAlign = 'center';
+    div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px';
+    div.style.fontWeight = 'bold';
+    class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-title';
+    class_div.innerHTML = this.attr_('title');
+    div.appendChild(class_div);
+    this.container.appendChild(div);
+    this.chartLabels.title = div;
+  }
+
+  if (this.attr_('xlabel')) {
+    div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.bottom = 0;  // TODO(danvk): this is lazy. Calculate style.top.
+    div.style.left = this.area.x + 'px';
+    div.style.width = this.area.w + 'px';
+    div.style.height = this.attr_('xLabelHeight') + 'px';
+    div.style.textAlign = 'center';
+    div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px';
+
+    class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-xlabel';
+    class_div.innerHTML = this.attr_('xlabel');
+    div.appendChild(class_div);
+    this.container.appendChild(div);
+    this.chartLabels.xlabel = div;
+  }
+
+  var that = this;
+  function createRotatedDiv(axis, classes, html) {
+    var box = {
+      left: 0,
+      top: that.area.y,
+      width: that.attr_('yLabelWidth'),
+      height: that.area.h
+    };
+    // TODO(danvk): is this outer div actually necessary?
+    div = document.createElement("div");
+    div.style.position = 'absolute';
+    if (axis == 1) {
+      div.style.left = box.left;
+    } else {
+      div.style.right = box.left;
+    }
+    div.style.top = box.top + 'px';
+    div.style.width = box.width + 'px';
+    div.style.height = box.height + 'px';
+    div.style.fontSize = (that.attr_('yLabelWidth') - 2) + 'px';
+
+    var inner_div = document.createElement("div");
+    inner_div.style.position = 'absolute';
+    inner_div.style.width = box.height + 'px';
+    inner_div.style.height = box.width + 'px';
+    inner_div.style.top = (box.height / 2 - box.width / 2) + 'px';
+    inner_div.style.left = (box.width / 2 - box.height / 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
+
+    if (typeof(document.documentMode) !== 'undefined' &&
+        document.documentMode < 9) {
+      // We're dealing w/ an old version of IE, so we have to rotate the text
+      // using a BasicImage transform. This uses a different origin of rotation
+      // than HTML5 rotation (top left of div vs. its center).
+      inner_div.style.filter =
+          'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
+          (axis == 1 ? '3' : '1') + ')';
+      inner_div.style.left = '0px';
+      inner_div.style.top = '0px';
+    }
+
+    class_div = document.createElement("div");
+    class_div.className = classes;
+    class_div.innerHTML = html;
+
+    inner_div.appendChild(class_div);
+    div.appendChild(inner_div);
+    return div;
+  }
+
+  var div;
+  if (this.attr_('ylabel')) {
+    div = createRotatedDiv(1, 'dygraph-label dygraph-ylabel',
+                           this.attr_('ylabel'));
+    this.container.appendChild(div);
+    this.chartLabels.ylabel = div;
+  }
+  if (this.attr_('y2label') && this.dygraph_.numAxes() == 2) {
+    div = createRotatedDiv(2, 'dygraph-label dygraph-y2label',
+                           this.attr_('y2label'));
+    this.container.appendChild(div);
+    this.chartLabels.y2label = div;
+  }
+};
+
+
+DygraphCanvasRenderer.prototype._renderAnnotations = function() {
+  var annotationStyle = {
+    "position": "absolute",
+    "fontSize": this.attr_('axisLabelFontSize') + "px",
+    "zIndex": 10,
+    "overflow": "hidden"
+  };
+
+  var bindEvt = function(eventName, classEventName, p, self) {
+    return function(e) {
+      var a = p.annotation;
+      if (a.hasOwnProperty(eventName)) {
+        a[eventName](a, p, self.dygraph_, e);
+      } else if (self.dygraph_.attr_(classEventName)) {
+        self.dygraph_.attr_(classEventName)(a, p, self.dygraph_,e );
+      }
+    };
+  };
+
+  // Get a list of point with annotations.
+  var points = this.layout.annotated_points;
+  for (var i = 0; i < points.length; i++) {
+    var p = points[i];
+    if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w) {
+      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));
+    }
+    div.style.left = (p.canvasx - width / 2) + "px";
+    if (a.attachAtBottom) {
+      div.style.top = (this.area.h - height - tick_height) + "px";
+    } else {
+      div.style.top = (p.canvasy - height - tick_height) + "px";
+    }
+    div.style.width = width + "px";
+    div.style.height = height + "px";
+    div.title = p.annotation.text;
+    div.style.color = this.colors[p.name];
+    div.style.borderColor = this.colors[p.name];
+    a.div = div;
+
+    Dygraph.addEvent(div, 'click',
+        bindEvt('clickHandler', 'annotationClickHandler', p, this));
+    Dygraph.addEvent(div, 'mouseover',
+        bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
+    Dygraph.addEvent(div, 'mouseout',
+        bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
+    Dygraph.addEvent(div, 'dblclick',
+        bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
+
+    this.container.appendChild(div);
+    this.annotations.push(div);
+
+    var ctx = this.elementContext;
+    ctx.strokeStyle = this.colors[p.name];
+    ctx.beginPath();
+    if (!a.attachAtBottom) {
+      ctx.moveTo(p.canvasx, p.canvasy);
+      ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height);
+    } else {
+      ctx.moveTo(p.canvasx, this.area.h);
+      ctx.lineTo(p.canvasx, this.area.h - 2 - tick_height);
+    }
+    ctx.closePath();
+    ctx.stroke();
+  }
+};
+
+
+/**
+ * Actually draw the lines chart, including error bars.
+ * TODO(danvk): split this into several smaller functions.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._renderLineChart = function() {
+  var isNullOrNaN = function(x) {
+    return (x === null || isNaN(x));
+  };
+
+  // TODO(danvk): use this.attr_ for many of these.
+  var context = this.elementContext;
+  var fillAlpha = this.attr_('fillAlpha');
+  var errorBars = this.attr_("errorBars") || this.attr_("customBars");
+  var fillGraph = this.attr_("fillGraph");
+  var stackedGraph = this.attr_("stackedGraph");
+  var stepPlot = this.attr_("stepPlot");
+  var points = this.layout.points;
+  var pointsLength = points.length;
+  var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
+
+  var setNames = [];
+  for (var name in this.layout.datasets) {
+    if (this.layout.datasets.hasOwnProperty(name)) {
+      setNames.push(name);
+    }
+  }
+  var setCount = setNames.length;
+
+  // TODO(danvk): Move this mapping into Dygraph and get it out of here.
+  this.colors = {};
+  for (i = 0; i < setCount; i++) {
+    this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length];
+  }
+
+  // Update Points
+  // TODO(danvk): here
+  for (i = pointsLength; i--;) {
+    point = points[i];
+    point.canvasx = this.area.w * point.x + this.area.x;
+    point.canvasy = this.area.h * point.y + this.area.y;
+  }
+
+  // create paths
+  var ctx = context;
+  if (errorBars) {
+    if (fillGraph) {
+      this.dygraph_.warn("Can't use fillGraph option with error bars");
+    }
+
+    for (i = 0; i < setCount; i++) {
+      setName = setNames[i];
+      axis = this.dygraph_.axisPropertiesForSeries(setName);
+      color = this.colors[setName];
+
+      // setup graphics context
+      ctx.save();
+      prevX = NaN;
+      prevY = NaN;
+      prevYs = [-1, -1];
+      yscale = axis.yscale;
+      // should be same color as the lines but only 15% opaque.
+      rgb = new RGBColor(color);
+      err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
+                            fillAlpha + ')';
+      ctx.fillStyle = err_color;
+      ctx.beginPath();
+      for (j = 0; j < pointsLength; j++) {
+        point = points[j];
+        if (point.name == setName) {
+          if (!Dygraph.isOK(point.y)) {
+            prevX = NaN;
+            continue;
+          }
+
+          // TODO(danvk): here
+          if (stepPlot) {
+            newYs = [ point.y_bottom, point.y_top ];
+            prevY = point.y;
+          } else {
+            newYs = [ point.y_bottom, point.y_top ];
+          }
+          newYs[0] = this.area.h * newYs[0] + this.area.y;
+          newYs[1] = this.area.h * newYs[1] + this.area.y;
+          if (!isNaN(prevX)) {
+            if (stepPlot) {
+              ctx.moveTo(prevX, newYs[0]);
+            } else {
+              ctx.moveTo(prevX, prevYs[0]);
+            }
+            ctx.lineTo(point.canvasx, newYs[0]);
+            ctx.lineTo(point.canvasx, newYs[1]);
+            if (stepPlot) {
+              ctx.lineTo(prevX, newYs[1]);
+            } else {
+              ctx.lineTo(prevX, prevYs[1]);
+            }
+            ctx.closePath();
+          }
+          prevYs = newYs;
+          prevX = point.canvasx;
+        }
+      }
+      ctx.fill();
+    }
+  } else if (fillGraph) {
+    var baseline = [];  // for stacked graphs: baseline for filling
+
+    // process sets in reverse order (needed for stacked graphs)
+    for (i = setCount - 1; i >= 0; i--) {
+      setName = setNames[i];
+      color = this.colors[setName];
+      axis = this.dygraph_.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 = this.area.h * axisY + this.area.y;
+
+      // setup graphics context
+      ctx.save();
+      prevX = NaN;
+      prevYs = [-1, -1];
+      yscale = axis.yscale;
+      // should be same color as the lines but only 15% opaque.
+      rgb = new RGBColor(color);
+      err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
+                            fillAlpha + ')';
+      ctx.fillStyle = err_color;
+      ctx.beginPath();
+      for (j = 0; j < pointsLength; j++) {
+        point = points[j];
+        if (point.name == setName) {
+          if (!Dygraph.isOK(point.y)) {
+            prevX = NaN;
+            continue;
+          }
+          if (stackedGraph) {
+            var lastY = baseline[point.canvasx];
+            if (lastY === undefined) lastY = axisY;
+            baseline[point.canvasx] = point.canvasy;
+            newYs = [ point.canvasy, lastY ];
+          } else {
+            newYs = [ point.canvasy, axisY ];
+          }
+          if (!isNaN(prevX)) {
+            ctx.moveTo(prevX, prevYs[0]);
+            if (stepPlot) {
+              ctx.lineTo(point.canvasx, prevYs[0]);
+            } else {
+              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();
+    }
+  }
+
+  // Drawing the lines.
+  var firstIndexInSet = 0;
+  var afterLastIndexInSet = 0;
+  var setLength = 0;
+  for (i = 0; i < setCount; i += 1) {
+    setLength = this.layout.setPointsLengths[i];
+    afterLastIndexInSet += setLength;
+    setName = setNames[i];
+    color = this.colors[setName];
+    var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
+
+    // setup graphics context
+    context.save();
+    var pointSize = this.dygraph_.attr_("pointSize", setName);
+    prevX = null;
+    prevY = null;
+    var drawPoints = this.dygraph_.attr_("drawPoints", setName);
+    var strokePattern = this.dygraph_.attr_("strokePattern", setName);
+    if (!Dygraph.isArrayLike(strokePattern)) {
+      strokePattern = null;
+    }
+    for (j = firstIndexInSet; j < afterLastIndexInSet; j++) {
+      point = points[j];
+      if (isNullOrNaN(point.canvasy)) {
+        if (stepPlot && prevX !== null) {
+          // Draw a horizontal line to the start of the missing data
+          ctx.beginPath();
+          ctx.strokeStyle = color;
+          ctx.lineWidth = this.attr_('strokeWidth');
+          this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+          ctx.stroke();
+        }
+        // this will make us move to the next point, not draw a line to it.
+        prevX = prevY = null;
+      } else {
+        // A point is "isolated" if it is non-null but both the previous
+        // and next points are null.
+        var isIsolated = (!prevX && (j == points.length - 1 ||
+                                     isNullOrNaN(points[j+1].canvasy)));
+        if (prevX === null) {
+          prevX = point.canvasx;
+          prevY = point.canvasy;
+        } else {
+          // Skip over points that will be drawn in the same pixel.
+          if (Math.round(prevX) == Math.round(point.canvasx) &&
+              Math.round(prevY) == Math.round(point.canvasy)) {
+            continue;
+          }
+          // TODO(antrob): skip over points that lie on a line that is already
+          // going to be drawn. There is no need to have more than 2
+          // consecutive points that are collinear.
+          if (strokeWidth) {
+            ctx.beginPath();
+            ctx.strokeStyle = color;
+            ctx.lineWidth = strokeWidth;
+            if (stepPlot) {
+              this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+            }
+            this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
+            prevX = point.canvasx;
+            prevY = point.canvasy;
+            ctx.stroke();
+          }
+        }
+
+        if (drawPoints || isIsolated) {
+          ctx.beginPath();
+          ctx.fillStyle = color;
+          ctx.arc(point.canvasx, point.canvasy, pointSize,
+                  0, 2 * Math.PI, false);
+          ctx.fill();
+        }
+      }
+    }
+    firstIndexInSet = afterLastIndexInSet;
+  }
+
+  context.restore();
+};
+
+/**
+ * This does dashed lines onto a canvas for a given pattern. You must call
+ * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
+ * the state of the line in regards to where we left off on drawing the pattern.
+ * You can draw a dashed line in several function calls and the pattern will be
+ * continous as long as you didn't call this function with a different pattern
+ * in between.
+ * @param ctx The canvas 2d context to draw on.
+ * @param x The start of the line's x coordinate.
+ * @param y The start of the line's y coordinate.
+ * @param x2 The end of the line's x coordinate.
+ * @param y2 The end of the line's y coordinate.
+ * @param pattern The dash pattern to draw, an array of integers where even 
+ * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
+ * is drawn, 2 is the space between.). A null pattern, array of length one, or
+ * empty array will do just a solid line.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) {
+  // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
+  // Modified by Russell Valentine to keep line history and continue the pattern
+  // where it left off.
+  var dx, dy, len, rot, patternIndex, segment;
+
+  // If we don't have a pattern or it is an empty array or of size one just
+  // do a solid line.
+  if (!pattern || pattern.length <= 1) {
+    ctx.moveTo(x, y);
+    ctx.lineTo(x2, y2);
+    return;
+  }
+
+  // If we have a different dash pattern than the last time this was called we
+  // reset our dash history and start the pattern from the begging 
+  // regardless of state of the last pattern.
+  if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) {
+    this._dashedLineToHistoryPattern = pattern;
+    this._dashedLineToHistory = [0, 0];
+  }
+  ctx.save();
+
+  // Calculate transformation parameters
+  dx = (x2-x);
+  dy = (y2-y);
+  len = Math.sqrt(dx*dx + dy*dy);
+  rot = Math.atan2(dy, dx);
+
+  // Set transformation
+  ctx.translate(x, y);
+  ctx.moveTo(0, 0);
+  ctx.rotate(rot);
+
+  // Set last pattern index we used for this pattern.
+  patternIndex = this._dashedLineToHistory[0];
+  x = 0;
+  while (len > x) {
+    // Get the length of the pattern segment we are dealing with.
+    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 (this._dashedLineToHistory[1]) {
+      x += this._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.
+      this._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.
+      this._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) {
+      ctx.lineTo(x, 0);
+    } else {
+      ctx.moveTo(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;
+  }
+  ctx.restore();
+};
+/**
+ * @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/
+
+ */
+
+/*jshint globalstrict: true */
+/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager: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) {
+  if (arguments.length > 0) {
+    if (arguments.length == 4) {
+      // 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.
+      this.warn("Using deprecated four-argument dygraph constructor");
+      this.__old_init__(div, data, arguments[2], arguments[3]);
+    } else {
+      this.__init__(div, data, opts);
+    }
+  }
+};
+
+Dygraph.NAME = "Dygraph";
+Dygraph.VERSION = "1.2";
+Dygraph.__repr__ = function() {
+  return "[" + this.NAME + " " + this.VERSION + "]";
+};
+
+/**
+ * Returns information about the Dygraph class.
+ */
+Dygraph.toString = function() {
+  return this.__repr__();
+};
+
+// Various default values
+Dygraph.DEFAULT_ROLL_PERIOD = 1;
+Dygraph.DEFAULT_WIDTH = 480;
+Dygraph.DEFAULT_HEIGHT = 320;
+
+Dygraph.ANIMATION_STEPS = 10;
+Dygraph.ANIMATION_DURATION = 200;
+
+// 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
+ * @param {String} name The name of the point's data series
+ * @param {Dygraph} g The dygraph object
+ */
+Dygraph.numberValueFormatter = function(x, opts, pt, g) {
+  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');
+
+  // 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))) {
+    return x.toExponential(digits);
+  } else {
+    return '' + Dygraph.round_(x, digits);
+  }
+};
+
+/**
+ * variant for use as an axisLabelFormatter.
+ * @private
+ */
+Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) {
+  return Dygraph.numberValueFormatter(x, opts, g);
+};
+
+/**
+ * Convert a JS date (millis since epoch) to YYYY/MM/DD
+ * @param {Number} date The JavaScript date (ms since epoch)
+ * @return {String} A date of the form "YYYY/MM/DD"
+ * @private
+ */
+Dygraph.dateString_ = function(date) {
+  var zeropad = Dygraph.zeropad;
+  var d = new Date(date);
+
+  // Get the year:
+  var year = "" + d.getFullYear();
+  // Get a 0 padded month string
+  var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
+  // Get a 0 padded day string
+  var day = zeropad(d.getDate());
+
+  var ret = "";
+  var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
+  if (frac) ret = " " + Dygraph.hmsString_(date);
+
+  return year + "/" + month + "/" + day + ret;
+};
+
+/**
+ * Convert a JS date to a string appropriate to display on an axis that
+ * is displaying values at the stated granularity.
+ * @param {Date} date The date to format
+ * @param {Number} granularity One of the Dygraph granularity constants
+ * @return {String} The formatted date
+ * @private
+ */
+Dygraph.dateAxisFormatter = function(date, granularity) {
+  if (granularity >= Dygraph.DECADAL) {
+    return date.strftime('%Y');
+  } else if (granularity >= Dygraph.MONTHLY) {
+    return date.strftime('%b %y');
+  } else {
+    var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
+    if (frac === 0 || granularity >= Dygraph.DAILY) {
+      return new Date(date.getTime() + 3600*1000).strftime('%d%b');
+    } else {
+      return Dygraph.hmsString_(date.getTime());
+    }
+  }
+};
+
+
+// Default attribute values.
+Dygraph.DEFAULT_ATTRS = {
+  highlightCircleSize: 3,
+
+  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,
+
+  axisTickSize: 3,
+  axisLabelFontSize: 14,
+  xAxisLabelWidth: 50,
+  yAxisLabelWidth: 50,
+  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,
+  hideOverlayOnMouseOut: true,
+
+  // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
+  legend: 'onmouseover',  // the only relevant value at the moment is 'always'.
+
+  stepPlot: false,
+  avoidMinZero: false,
+
+  // Sizes of the various chart labels.
+  titleHeight: 28,
+  xLabelHeight: 18,
+  yLabelWidth: 18,
+
+  drawXAxis: true,
+  drawYAxis: true,
+  axisLineColor: "black",
+  axisLineWidth: 0.3,
+  gridLineWidth: 0.3,
+  axisLabelColor: "black",
+  axisLabelFont: "Arial",  // TODO(danvk): is this implemented?
+  axisLabelWidth: 50,
+  drawYGrid: true,
+  drawXGrid: true,
+  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",
+  rangeSelectorPlotFillColor: "#A7B1C4",
+
+  // per-axis options
+  axes: {
+    x: {
+      pixelsPerLabel: 60,
+      axisLabelFormatter: Dygraph.dateAxisFormatter,
+      valueFormatter: Dygraph.dateString_,
+      ticker: null  // will be set in dygraph-tickers.js
+    },
+    y: {
+      pixelsPerLabel: 30,
+      valueFormatter: Dygraph.numberValueFormatter,
+      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+      ticker: null  // will be set in dygraph-tickers.js
+    },
+    y2: {
+      pixelsPerLabel: 30,
+      valueFormatter: Dygraph.numberValueFormatter,
+      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+      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;
+
+// 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) {
+  // Hack for IE: if we're using excanvas and the document hasn't finished
+  // loading yet (and hence may not have initialized whatever it needs to
+  // initialize), then keep calling this routine periodically until it has.
+  if (/MSIE/.test(navigator.userAgent) && !window.opera &&
+      typeof(G_vmlCanvasManager) != 'undefined' &&
+      document.readyState != 'complete') {
+    var self = this;
+    setTimeout(function() { self.__init__(div, file, attrs); }, 100);
+    return;
+  }
+
+  // Support two-argument constructor
+  if (attrs === null || attrs === undefined) { attrs = {}; }
+
+  attrs = Dygraph.mapLegacyOptions_(attrs);
+
+  if (!div) {
+    Dygraph.error("Constructing dygraph with a non-existent div!");
+    return;
+  }
+
+  this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
+
+  // 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.is_initial_draw_ = true;
+  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.
+  this.width_ = div.clientWidth;
+  this.height_ = div.clientHeight;
+
+  // 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.
+  }
+
+  // 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_ = [];
+
+  // Create the containing DIV and other interactive elements
+  this.createInterface_();
+
+  this.start_();
+};
+
+/**
+ * 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) 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 (seriesName &&
+      typeof(this.user_attrs_[seriesName]) != 'undefined' &&
+      this.user_attrs_[seriesName] !== null &&
+      typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
+    return this.user_attrs_[seriesName][name];
+  } else if (typeof(this.user_attrs_[name]) != 'undefined') {
+    return this.user_attrs_[name];
+  } else if (typeof(this.attrs_[name]) != 'undefined') {
+    return this.attrs_[name];
+  } else {
+    return null;
+  }
+};
+
+/**
+ * @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][opt]) {
+      return axis_opts[axis][opt];
+    }
+    // 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][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 left = this.rawData_[0][0];
+  var right = this.rawData_[this.rawData_.length - 1][0];
+  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();
+  return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+};
+
+/**
+ * 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.axes_[axis].logscale) {
+    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 = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
+    //
+    // Move denominator to both sides:
+    // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
+    //
+    // subtract logr1, and take the negative value.
+    // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
+    //
+    // Swap both sides of the equation, and we can compute the log of the
+    // return value. Which means we just need to use that as the exponent in
+    // e^exponent.
+    // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
+
+    var logr1 = Dygraph.log10(yRange[1]);
+    var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
+    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;
+  if (!this.axes_[axis].logscale) {
+    // 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]);
+  } else {
+    var logr1 = Dygraph.log10(yRange[1]);
+    pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(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();
+  return (x - xRange[0]) / (xRange[1] - xRange[0]);
+};
+
+/**
+ * Returns the number of columns (including the independent variable).
+ * @return { Integer } The number of columns.
+ */
+Dygraph.prototype.numColumns = function() {
+  return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ * @return { Integer } The number of rows, less any header.
+ */
+Dygraph.prototype.numRows = function() {
+  return this.rawData_.length;
+};
+
+/**
+ * Returns the full range of the x-axis, as determined by the most extreme
+ * values in the data set. Not affected by zooming, visibility, etc.
+ * TODO(danvk): merge w/ xAxisExtremes
+ * @return { Array<Number> } A [low, high] pair
+ * @private
+ */
+Dygraph.prototype.fullXRange_ = function() {
+  if (this.numRows() > 0) {
+    return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]];
+  } else {
+    return [0, 1];
+  }
+};
+
+/**
+ * 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");
+  this.graphDiv.style.width = this.width_ + "px";
+  this.graphDiv.style.height = this.height_ + "px";
+  enclosing.appendChild(this.graphDiv);
+
+  // Create the canvas for interactive parts of the chart.
+  this.canvas_ = Dygraph.createCanvas();
+  this.canvas_.style.position = "absolute";
+  this.canvas_.width = this.width_;
+  this.canvas_.height = this.height_;
+  this.canvas_.style.width = this.width_ + "px";    // for IE
+  this.canvas_.style.height = this.height_ + "px";  // for IE
+
+  this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
+
+  // ... and for static parts of the chart.
+  this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
+  this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
+
+  if (this.attr_('showRangeSelector')) {
+    // The range selector must be created here so that its canvases and contexts get created here.
+    // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
+    // The range selector also sets xAxisHeight in order to reserve space.
+    this.rangeSelector_ = new DygraphRangeSelector(this);
+  }
+
+  // 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);
+
+  if (this.rangeSelector_) {
+    // This needs to happen after the graph canvases are added to the div and the layout object is created.
+    this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
+  }
+
+  var dygraph = this;
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
+    dygraph.mouseMove_(e);
+  });
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
+    dygraph.mouseOut_(e);
+  });
+
+  this.createStatusMessage_();
+  this.createDragInterface_();
+
+  this.resizeHandler = function(e) {
+    dygraph.resize();
+  }
+
+  // Update when the window is resized.
+  // TODO(danvk): drop frames depending on complexity of the chart.
+  Dygraph.addEvent(window, 'resize', this.resizeHandler);
+};
+
+/**
+ * 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() {
+  var removeRecursive = function(node) {
+    while (node.hasChildNodes()) {
+      removeRecursive(node.firstChild);
+      node.removeChild(node.firstChild);
+    }
+  };
+  removeRecursive(this.maindiv_);
+
+  var nullOut = function(obj) {
+    for (var n in obj) {
+      if (typeof(obj[n]) === 'object') {
+        obj[n] = null;
+      }
+    }
+  };
+  // remove event handlers
+  Dygraph.removeEvent(window,'resize',this.resizeHandler);
+  this.resizeHandler = 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() {
+  if (this.isUsingExcanvas_) {
+    var elem = document.createElement("div");
+    elem.style.position = 'absolute';
+    elem.style.backgroundColor = 'white';
+    elem.style.filter = 'alpha(opacity=0)';
+    elem.style.width = this.width_ + "px";
+    elem.style.height = this.height_ + "px";
+    this.graphDiv.appendChild(elem);
+    return elem;
+  } else {
+    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 num = this.attr_("labels").length - 1;
+  this.colors_ = [];
+  var colors = this.attr_('colors');
+  var i;
+  if (!colors) {
+    var sat = this.attr_('colorSaturation') || 1.0;
+    var val = this.attr_('colorValue') || 0.5;
+    var half = Math.ceil(num / 2);
+    for (i = 1; i <= num; i++) {
+      if (!this.visibility()[i-1]) continue;
+      // alternate colors for high contrast.
+      var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
+      var hue = (1.0 * idx/ (1 + num));
+      this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
+    }
+  } else {
+    for (i = 0; i < num; i++) {
+      if (!this.visibility()[i]) continue;
+      var colorStr = colors[i % colors.length];
+      this.colors_.push(colorStr);
+    }
+  }
+
+  this.plotter_.setColors(this.colors_);
+};
+
+/**
+ * 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.
+ * @return {Array<string>} The list of colors.
+ */
+Dygraph.prototype.getColors = function() {
+  return this.colors_;
+};
+
+/**
+ * Create the div that contains information on the selected point(s)
+ * This goes in the top right of the canvas, unless an external div has already
+ * been specified.
+ * @private
+ */
+Dygraph.prototype.createStatusMessage_ = function() {
+  var userLabelsDiv = this.user_attrs_.labelsDiv;
+  if (userLabelsDiv && null !== userLabelsDiv &&
+      (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
+    this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv);
+  }
+  if (!this.attr_("labelsDiv")) {
+    var divWidth = this.attr_('labelsDivWidth');
+    var messagestyle = {
+      "position": "absolute",
+      "fontSize": "14px",
+      "zIndex": 10,
+      "width": divWidth + "px",
+      "top": "0px",
+      "left": (this.width_ - divWidth - 2) + "px",
+      "background": "white",
+      "textAlign": "left",
+      "overflow": "hidden"};
+    Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
+    var div = document.createElement("div");
+    div.className = "dygraph-legend";
+    for (var name in messagestyle) {
+      if (messagestyle.hasOwnProperty(name)) {
+        div.style[name] = messagestyle[name];
+      }
+    }
+    this.graphDiv.appendChild(div);
+    this.attrs_.labelsDiv = div;
+  }
+};
+
+/**
+ * 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
+ */
+Dygraph.prototype.positionLabelsDiv_ = function() {
+  // Don't touch a user-specified labelsDiv.
+  if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
+
+  var area = this.plotter_.area;
+  var div = this.attr_("labelsDiv");
+  div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
+  div.style.top = area.y + "px";
+};
+
+/**
+ * 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.attr_('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); };
+};
+
+/**
+ * @private
+ * Converts page the x-coordinate of the event to pixel x-coordinates on the
+ * canvas (i.e. DOM Coords).
+ */
+Dygraph.prototype.dragGetX_ = function(e, context) {
+  return Dygraph.pageX(e) - context.px;
+};
+
+/**
+ * @private
+ * Converts page the y-coordinate of the event to pixel y-coordinates on the
+ * canvas (i.e. DOM Coords).
+ */
+Dygraph.prototype.dragGetY_ = function(e, context) {
+  return Dygraph.pageY(e) - context.py;
+};
+
+/**
+ * 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,
+
+    // 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] ...]
+
+    initializeMouseDown: function(event, g, context) {
+      // prevents mouse drags from selecting page text.
+      if (event.preventDefault) {
+        event.preventDefault();  // Firefox, Chrome, etc.
+      } else {
+        event.returnValue = false;  // IE
+        event.cancelBubble = true;
+      }
+
+      context.px = Dygraph.findPosX(g.canvas_);
+      context.py = Dygraph.findPosY(g.canvas_);
+      context.dragStartX = g.dragGetX_(event, context);
+      context.dragStartY = g.dragGetY_(event, context);
+    }
+  };
+
+  var interactionModel = this.attr_("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;
+    Dygraph.addEvent(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.
+  Dygraph.addEvent(document, 'mouseup', function(event) {
+    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;
+      }
+    }
+  });
+};
+
+/**
+ * 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));
+    }
+  }
+
+  if (this.isUsingExcanvas_) {
+    this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0];
+  }
+};
+
+/**
+ * Clear the zoom rectangle (and perform no zoom).
+ * @private
+ */
+Dygraph.prototype.clearZoomRect_ = function() {
+  this.currentZoomRectArgs_ = null;
+  this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.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);
+};
+
+/**
+ * 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));
+};
+
+/**
+ * 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 yAxisRange is null (i.e. "fit to data", the animation
+  // can produce strange effects. Rather than the y-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.attr_("zoomCallback")) {
+      that.attr_("zoomCallback")(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.attr_("zoomCallback")) {
+      var xRange = that.xAxisRange();
+      that.attr_("zoomCallback")(xRange[0], xRange[1], that.yAxisRanges());
+    }
+  });
+};
+
+/**
+ * Reset the zoom to the original view coordinates. This is the same as
+ * double-clicking on the graph.
+ *
+ * @private
+ */
+Dygraph.prototype.doUnzoom_ = 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 (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.attr_("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.attr_("zoomCallback")) {
+        this.attr_("zoomCallback")(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[1];
+
+      // 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++) {
+        newValueRanges.push(this.axes_[i].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.attr_("zoomCallback")) {
+            that.attr_("zoomCallback")(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.attr_("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);
+};
+
+/**
+ * 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) return;
+
+  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+
+  var lastx = -1;
+  var i;
+
+  // Loop through all the points and find the date nearest to our current
+  // location.
+  var minDist = 1e+100;
+  var idx = -1;
+  for (i = 0; i < points.length; i++) {
+    var point = points[i];
+    if (point === null) continue;
+    var dist = Math.abs(point.canvasx - canvasx);
+    if (dist > minDist) continue;
+    minDist = dist;
+    idx = i;
+  }
+  if (idx >= 0) lastx = points[idx].xval;
+
+  // Extract the points we've selected
+  this.selPoints_ = [];
+  var l = points.length;
+  if (!this.attr_("stackedGraph")) {
+    for (i = 0; i < l; i++) {
+      if (points[i].xval == lastx) {
+        this.selPoints_.push(points[i]);
+      }
+    }
+  } else {
+    // Need to 'unstack' points starting from the bottom
+    var cumulative_sum = 0;
+    for (i = l - 1; i >= 0; i--) {
+      if (points[i].xval == lastx) {
+        var p = {};  // Clone the point since we modify it
+        for (var k in points[i]) {
+          p[k] = points[i][k];
+        }
+        p.yval -= cumulative_sum;
+        cumulative_sum += p.yval;
+        this.selPoints_.push(p);
+      }
+    }
+    this.selPoints_.reverse();
+  }
+
+  if (this.attr_("highlightCallback")) {
+    var px = this.lastx_;
+    if (px !== null && lastx != px) {
+      // only fire if the selected point has changed.
+      this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
+    }
+  }
+
+  // Save last x position for callbacks.
+  this.lastx_ = lastx;
+
+  this.updateSelection_();
+};
+
+/**
+ * Transforms layout_.points index into data row number.
+ * @param int layout_.points index
+ * @return int row number, or -1 if none could be found.
+ * @private
+ */
+Dygraph.prototype.idxToRow_ = function(idx) {
+  if (idx < 0) return -1;
+
+  // make sure that you get the boundaryIds record which is also defined (see bug #236)
+  var boundaryIdx = -1;
+  for (var i = 0; i < this.boundaryIds_.length; i++) {
+    if (this.boundaryIds_[i] !== undefined) {
+      boundaryIdx = i;
+      break;
+    }
+  }
+  if (boundaryIdx < 0) return -1;
+  for (var name in this.layout_.datasets) {
+    if (idx < this.layout_.datasets[name].length) {
+      return this.boundaryIds_[boundaryIdx][0] + idx;
+    }
+    idx -= this.layout_.datasets[name].length;
+  }
+  return -1;
+};
+
+/**
+ * @private
+ * Generates legend html dash for any stroke pattern. It will try to scale the
+ * pattern to fit in 1em width. Or if small enough repeat the partern 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.
+ */
+Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
+  var dash = "";
+  var i, j, paddingLeft, marginRight;
+  var strokePixelLength = 0, segmentLoop = 0;
+  var normalizedPattern = [];
+  var loop;
+  // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
+  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if(isIE) {
+    return "&mdash;";
+  }
+  if (!strokePattern || strokePattern.length <= 1) {
+    // Solid line
+    dash = "<div style=\"display: inline-block; position: relative; " +
+    "bottom: .5ex; padding-left: 1em; height: 1px; " +
+    "border-bottom: 2px solid " + color + ";\"></div>";
+  } else {
+    // 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.
+    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;
+};
+
+/**
+ * @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.
+ */
+Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
+  // 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, c, dash, strokePattern;
+  if (typeof(x) === 'undefined') {
+    if (this.attr_('legend') != 'always') return '';
+
+    sepLines = this.attr_('labelsSeparateLines');
+    var labels = this.attr_('labels');
+    html = '';
+    for (i = 1; i < labels.length; i++) {
+      if (!this.visibility()[i - 1]) continue;
+      c = this.plotter_.colors[labels[i]];
+      if (html !== '') html += (sepLines ? '<br/>' : ' ');
+      strokePattern = this.attr_("strokePattern", labels[i]);
+      dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
+      html += "<span style='font-weight: bold; color: " + c + ";'>" + dash + 
+        " " + labels[i] + "</span>";
+    }
+    return html;
+  }
+
+  var xOptView = this.optionsViewForAxis_('x');
+  var xvf = xOptView('valueFormatter');
+  html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":";
+
+  var yOptViews = [];
+  var num_axes = this.numAxes();
+  for (i = 0; i < num_axes; i++) {
+    yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : ''));
+  }
+  var showZeros = this.attr_("labelsShowZeroValues");
+  sepLines = this.attr_("labelsSeparateLines");
+  for (i = 0; i < this.selPoints_.length; i++) {
+    var pt = this.selPoints_[i];
+    if (pt.yval === 0 && !showZeros) continue;
+    if (!Dygraph.isOK(pt.canvasy)) continue;
+    if (sepLines) html += "<br/>";
+
+    var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]];
+    var fmtFunc = yOptView('valueFormatter');
+    c = this.plotter_.colors[pt.name];
+    var yval = fmtFunc(pt.yval, yOptView, pt.name, this);
+
+    // TODO(danvk): use a template string here and make it an attribute.
+    html += " <b><span style='color: " + c + ";'>" + pt.name +
+        "</span></b>:" + yval;
+  }
+  return html;
+};
+
+/**
+ * @private
+ * Displays information about the selected points in the legend. If there is no
+ * selection, the legend will be cleared.
+ * @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'.
+ */
+Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
+  var labelsDiv = this.attr_("labelsDiv");
+  var sizeSpan = document.createElement('span');
+  // Calculates the width of 1em in pixels for the legend.
+  sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+  labelsDiv.appendChild(sizeSpan);
+  var oneEmWidth=sizeSpan.offsetWidth;
+
+  var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
+  if (labelsDiv !== null) {
+    labelsDiv.innerHTML = html;
+  } else {
+    if (typeof(this.shown_legend_error_) == 'undefined') {
+      this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
+      this.shown_legend_error_ = true;
+    }
+  }
+};
+
+/**
+ * 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() {
+  // Clear the previously drawn vertical, if there is one
+  var i;
+  var ctx = this.canvas_ctx_;
+  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.attr_('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.isUsingExcanvas_ && this.currentZoomRectArgs_) {
+    Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_);
+  }
+
+  if (this.selPoints_.length > 0) {
+    // Set the status message to indicate the selected point(s)
+    if (this.attr_('showLabelsOnHighlight')) {
+      this.setLegendHTML_(this.lastx_, this.selPoints_);
+    }
+
+    // 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.attr_('highlightCircleSize', pt.name);
+      ctx.beginPath();
+      ctx.fillStyle = this.plotter_.colors[pt.name];
+      ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
+      ctx.fill();
+    }
+    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 { Integer } row number that should be highlighted (i.e. appear with
+ * hover dots on the chart). Set to false to clear any selection.
+ */
+Dygraph.prototype.setSelection = function(row) {
+  // Extract the points we've selected
+  this.selPoints_ = [];
+  var pos = 0;
+
+  if (row !== false) {
+    row = row - this.boundaryIds_[0][0];
+  }
+
+  if (row !== false && row >= 0) {
+    for (var i in this.layout_.datasets) {
+      if (row < this.layout_.datasets[i].length) {
+        var point = this.layout_.points[pos+row];
+
+        if (this.attr_("stackedGraph")) {
+          point = this.layout_.unstackPointAtIndex(pos+row);
+        }
+
+        this.selPoints_.push(point);
+      }
+      pos += this.layout_.datasets[i].length;
+    }
+  }
+
+  if (this.selPoints_.length) {
+    this.lastx_ = this.selPoints_[0].xval;
+    this.updateSelection_();
+  } else {
+    this.clearSelection();
+  }
+
+};
+
+/**
+ * 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.attr_("unhighlightCallback")) {
+    this.attr_("unhighlightCallback")(event);
+  }
+
+  if (this.attr_("hideOverlayOnMouseOut")) {
+    this.clearSelection();
+  }
+};
+
+/**
+ * Clears the current selection (i.e. points that were highlighted by moving
+ * the mouse over the chart).
+ */
+Dygraph.prototype.clearSelection = function() {
+  // Get rid of the overlay data
+  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
+  this.setLegendHTML_();
+  this.selPoints_ = [];
+  this.lastx_ = -1;
+};
+
+/**
+ * Returns the number of the currently selected row. To get data for this row,
+ * you can use the getValue method.
+ * @return { Integer } row number, or -1 if nothing is selected
+ */
+Dygraph.prototype.getSelection = function() {
+  if (!this.selPoints_ || this.selPoints_.length < 1) {
+    return -1;
+  }
+
+  for (var row=0; row<this.layout_.points.length; row++ ) {
+    if (this.layout_.points[row].x == this.selPoints_[0].x) {
+      return row + this.boundaryIds_[0][0];
+    }
+  }
+  return -1;
+};
+
+/**
+ * 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.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.fullXRange_();
+  }
+
+  var xAxisOptionsView = this.optionsViewForAxis_('x');
+  var xTicks = xAxisOptionsView('ticker')(
+      range[0],
+      range[1],
+      this.width_,  // 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);
+};
+
+/**
+ * @private
+ * Computes the range of the data series (including confidence intervals).
+ * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
+ * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
+ * @return [low, high]
+ */
+Dygraph.prototype.extremeValues_ = function(series) {
+  var minY = null, maxY = null, j, y;
+
+  var bars = this.attr_("errorBars") || this.attr_("customBars");
+  if (bars) {
+    // With custom bars, maxY is the max of the high values.
+    for (j = 0; j < series.length; j++) {
+      y = series[j][1][0];
+      if (!y) continue;
+      var low = y - series[j][1][1];
+      var high = y + series[j][1][2];
+      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;
+      }
+    }
+  } else {
+    for (j = 0; j < series.length; 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];
+};
+
+/**
+ * @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();
+
+  // TODO(danvk): move more computations out of drawGraph_ and into here.
+  this.computeYAxes_();
+
+  // Create a new plotter.
+  if (this.plotter_) this.plotter_.clear();
+  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_();
+
+  // Same thing applies for the labelsDiv. It's 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.
+  this.positionLabelsDiv_();
+
+  if (this.rangeSelector_) {
+    this.rangeSelector_.renderStaticLayer();
+  }
+
+  // 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 connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
+    var logScale = this.attr_('logscale', i);
+    var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints);
+    series = this.rollingAverage(series, this.rollPeriod_);
+    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);
+};
+
+/**
+ * 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.
+ *
+ * TODO(danvk): make this more of a true function
+ * @return [ datasets, seriesExtremes, boundaryIds ]
+ * @private
+ */
+Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
+  var boundaryIds = [];
+  var cumulative_y = [];  // For stacked series.
+  var datasets = [];
+  var extremes = {};  // series name -> [low, high]
+  var i, j, k;
+
+  // 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;
+  for (i = num_series; i >= 1; i--) {
+    if (!this.visibility()[i - 1]) continue;
+
+    // TODO(danvk): is this copy really necessary?
+    var series = [];
+    for (j = 0; j < rolledSeries[i].length; j++) {
+      series.push(rolledSeries[i][j]);
+    }
+
+    // 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.
+    var bars = this.attr_("errorBars") || this.attr_("customBars");
+    if (dateWindow) {
+      var low = dateWindow[0];
+      var high = dateWindow[1];
+      var pruned = [];
+      // TODO(danvk): do binary search instead of linear search.
+      // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+      var firstIdx = null, lastIdx = null;
+      for (k = 0; k < series.length; k++) {
+        if (series[k][0] >= low && firstIdx === null) {
+          firstIdx = k;
+        }
+        if (series[k][0] <= high) {
+          lastIdx = k;
+        }
+      }
+      if (firstIdx === null) firstIdx = 0;
+      if (firstIdx > 0) firstIdx--;
+      if (lastIdx === null) lastIdx = series.length - 1;
+      if (lastIdx < series.length - 1) lastIdx++;
+      boundaryIds[i-1] = [firstIdx, lastIdx];
+      for (k = firstIdx; k <= lastIdx; k++) {
+        pruned.push(series[k]);
+      }
+      series = pruned;
+    } else {
+      boundaryIds[i-1] = [0, series.length-1];
+    }
+
+    var seriesExtremes = this.extremeValues_(series);
+
+    if (bars) {
+      for (j=0; j<series.length; j++) {
+        series[j] = [series[j][0],
+                     series[j][1][0],
+                     series[j][1][1],
+                     series[j][1][2]];
+      }
+    } else if (this.attr_("stackedGraph")) {
+      var l = series.length;
+      var actual_y;
+      for (j = 0; j < l; j++) {
+        // If one data set has a NaN, let all subsequent stacked
+        // sets inherit the NaN -- only start at 0 for the first set.
+        var x = series[j][0];
+        if (cumulative_y[x] === undefined) {
+          cumulative_y[x] = 0;
+        }
+
+        actual_y = series[j][1];
+        cumulative_y[x] += actual_y;
+
+        series[j] = [x, cumulative_y[x]];
+
+        if (cumulative_y[x] > seriesExtremes[1]) {
+          seriesExtremes[1] = cumulative_y[x];
+        }
+        if (cumulative_y[x] < seriesExtremes[0]) {
+          seriesExtremes[0] = cumulative_y[x];
+        }
+      }
+    }
+
+    var seriesName = this.attr_("labels")[i];
+    extremes[seriesName] = seriesExtremes;
+    datasets[i] = series;
+  }
+
+  return [ datasets, extremes, 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.
+ *
+ * clearSelection, when undefined or true, causes this.clearSelection to be
+ * called at the end of the draw operation. This should rarely be defined,
+ * and never true (that is it should be undefined most of the time, and
+ * rarely false.)
+ *
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function(clearSelection) {
+  var start = new Date();
+
+  if (typeof(clearSelection) === 'undefined') {
+    clearSelection = true;
+  }
+
+  // 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.attr_('highlightCircleSize');
+
+  var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
+  var datasets = packed[0];
+  var extremes = packed[1];
+  this.boundaryIds_ = packed[2];
+
+  for (var i = 1; i < datasets.length; i++) {
+    if (!this.visibility()[i - 1]) continue;
+    this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
+  }
+
+  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.layout_.setDateWindow(this.dateWindow_);
+  this.zoomed_x_ = tmp_zoomed_x;
+  this.layout_.evaluateWithError();
+  this.renderGraph_(is_initial_draw, false);
+
+  if (this.attr_("timingName")) {
+    var end = new Date();
+    if (console) {
+      console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
+    }
+  }
+};
+
+Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
+  this.plotter_.clear();
+  this.plotter_.render();
+  this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+                                          this.canvas_.height);
+
+  // Generate a static legend before any particular point is selected.
+  this.setLegendHTML_();
+
+  if (!is_initial_draw) {
+    if (clearSelection) {
+      if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
+        // We should select the point nearest the page x/y here, but it's easier
+        // to just clear the selection. This prevents erroneous hover dots from
+        // being displayed.
+        this.clearSelection();
+      } else {
+        this.clearSelection();
+      }
+    }
+  }
+
+  if (this.rangeSelector_) {
+    this.rangeSelector_.renderInteractiveLayer();
+  }
+
+  if (this.attr_("drawCallback") !== null) {
+    this.attr_("drawCallback")(this, is_initial_draw);
+  }
+};
+
+/**
+ * @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_ and this.seriesToAxisMap_.
+ * axes_ = [ { options } ]
+ * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
+ *   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 i, valueWindows, seriesName, 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_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
+  this.seriesToAxisMap_ = {};
+
+  // Get a list of series names.
+  var labels = this.attr_("labels");
+  var series = {};
+  for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
+
+  // all options which could be applied per-axis:
+  var axisOptions = [
+    'includeZero',
+    'valueRange',
+    'labelsKMB',
+    'labelsKMG2',
+    'pixelsPerYLabel',
+    'yAxisLabelWidth',
+    'axisLabelFontSize',
+    'axisTickSize',
+    'logscale'
+  ];
+
+  // Copy global axis options over to the first axis.
+  for (i = 0; i < axisOptions.length; i++) {
+    var k = axisOptions[i];
+    v = this.attr_(k);
+    if (v) this.axes_[0][k] = v;
+  }
+
+  // Go through once and add all the axes.
+  for (seriesName in series) {
+    if (!series.hasOwnProperty(seriesName)) continue;
+    axis = this.attr_("axis", seriesName);
+    if (axis === null) {
+      this.seriesToAxisMap_[seriesName] = 0;
+      continue;
+    }
+    if (typeof(axis) == 'object') {
+      // Add a new axis, making a copy of its per-axis options.
+      opts = {};
+      Dygraph.update(opts, this.axes_[0]);
+      Dygraph.update(opts, { valueRange: null });  // shouldn't inherit this.
+      var yAxisId = this.axes_.length;
+      opts.yAxisId = yAxisId;
+      opts.g = this;
+      Dygraph.update(opts, axis);
+      this.axes_.push(opts);
+      this.seriesToAxisMap_[seriesName] = yAxisId;
+    }
+  }
+
+  // Go through one more time and assign series to an axis defined by another
+  // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
+  for (seriesName in series) {
+    if (!series.hasOwnProperty(seriesName)) continue;
+    axis = this.attr_("axis", seriesName);
+    if (typeof(axis) == 'string') {
+      if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
+        this.error("Series " + seriesName + " wants to share a y-axis with " +
+                   "series " + axis + ", which does not define its own axis.");
+        return null;
+      }
+      var idx = this.seriesToAxisMap_[axis];
+      this.seriesToAxisMap_[seriesName] = idx;
+    }
+  }
+
+  if (valueWindows !== undefined) {
+    // Restore valueWindow settings.
+    for (index = 0; index < valueWindows.length; index++) {
+      this.axes_[index].valueWindow = valueWindows[index];
+    }
+  }
+
+  // New axes options
+  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() {
+  var last_axis = 0;
+  for (var series in this.seriesToAxisMap_) {
+    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
+    var idx = this.seriesToAxisMap_[series];
+    if (idx > last_axis) last_axis = idx;
+  }
+  return 1 + last_axis;
+};
+
+/**
+ * @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.seriesToAxisMap_[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) {
+  // Build a map from axis number -> [list of series names]
+  var seriesForAxis = [], series;
+  for (series in this.seriesToAxisMap_) {
+    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
+    var idx = this.seriesToAxisMap_[series];
+    while (seriesForAxis.length <= idx) seriesForAxis.push([]);
+    seriesForAxis[idx].push(series);
+  }
+
+  // Compute extreme values, a span and tick marks for each axis.
+  for (var i = 0; i < this.axes_.length; i++) {
+    var axis = this.axes_[i];
+
+    if (!seriesForAxis[i]) {
+      // If no series are defined or visible then use a reasonable default
+      axis.extremeRange = [0, 1];
+    } else {
+      // Calculate the extremes of extremes.
+      series = seriesForAxis[i];
+      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);
+        }
+      }
+      if (axis.includeZero && minY > 0) minY = 0;
+
+      // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
+      if (minY == Infinity) minY = 0;
+      if (maxY == -Infinity) maxY = 1;
+
+      // Add some padding and round up to an integer to be human-friendly.
+      var span = maxY - minY;
+      // special case: if we have no sense of scale, use +/-10% of the sole value.
+      if (span === 0) { span = maxY; }
+
+      var maxAxisY, minAxisY;
+      if (axis.logscale) {
+        maxAxisY = maxY + 0.1 * span;
+        minAxisY = minY;
+      } else {
+        maxAxisY = maxY + 0.1 * span;
+        minAxisY = minY - 0.1 * span;
+
+        // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
+        if (!this.attr_("avoidMinZero")) {
+          if (minAxisY < 0 && minY >= 0) minAxisY = 0;
+          if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
+        }
+
+        if (this.attr_("includeZero")) {
+          if (maxY < 0) maxAxisY = 0;
+          if (minY > 0) minAxisY = 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.
+      axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
+    } else {
+      axis.computedValueRange = axis.extremeRange;
+    }
+
+    // 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.
+    var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
+    var ticker = opts('ticker');
+    if (i === 0 || axis.independentTicks) {
+      axis.ticks = ticker(axis.computedValueRange[0],
+                          axis.computedValueRange[1],
+                          this.height_,  // TODO(danvk): should be area.height
+                          opts,
+                          this);
+    } else {
+      var p_axis = this.axes_[0];
+      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.height_,  // TODO(danvk): should be area.height
+                          opts,
+                          this,
+                          tick_values);
+    }
+  }
+};
+
+/**
+ * Extracts one series from the raw data (a 2D array) into an array of (date,
+ * value) tuples.
+ *
+ * This is where undesirable points (i.e. negative values on log scales and
+ * missing values through which we wish to connect lines) are dropped.
+ * 
+ * @private
+ */
+Dygraph.prototype.extractSeries_ = function(rawData, i, logScale, connectSeparatedPoints) {
+  var series = [];
+  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. Note that this ignores
+      // connectSeparatedPoints.
+      if (point <= 0) {
+        point = null;
+      }
+      series.push([x, point]);
+    } else {
+      if (point !== null || !connectSeparatedPoints) {
+        series.push([x, point]);
+      }
+    }
+  }
+  return series;
+};
+
+/**
+ * @private
+ * Calculates the rolling average of a data set.
+ * If originalData is [label, val], rolls the average of those.
+ * If originalData is [label, [, it's interpreted as [value, stddev]
+ *   and the roll is returned in the same form, with appropriately reduced
+ *   stddev for each value.
+ * Note that this is where fractional input (i.e. '5/10') is converted into
+ *   decimal values.
+ * @param {Array} originalData The data in the appropriate format (see above)
+ * @param {Number} rollPeriod The number of points over which to average the
+ *                            data
+ */
+Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
+  if (originalData.length < 2)
+    return originalData;
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+  var sigma = this.attr_("sigma");
+
+  var low, high, i, j, y, sum, num_ok, stddev;
+  if (this.fractions_) {
+    var num = 0;
+    var den = 0;  // numerator/denominator
+    var mult = 100.0;
+    for (i = 0; i < originalData.length; i++) {
+      num += originalData[i][1][0];
+      den += originalData[i][1][1];
+      if (i - rollPeriod >= 0) {
+        num -= originalData[i - rollPeriod][1][0];
+        den -= originalData[i - rollPeriod][1][1];
+      }
+
+      var date = originalData[i][0];
+      var value = den ? num / den : 0.0;
+      if (this.attr_("errorBars")) {
+        if (this.attr_("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, (p - low) * mult, (high - p) * 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 * stddev, mult * stddev]];
+        }
+      } else {
+        rollingData[i] = [date, mult * value];
+      }
+    }
+  } else if (this.attr_("customBars")) {
+    low = 0;
+    var mid = 0;
+    high = 0;
+    var count = 0;
+    for (i = 0; i < originalData.length; i++) {
+      var data = originalData[i][1];
+      y = data[1];
+      rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
+
+      if (y !== null && !isNaN(y)) {
+        low += data[0];
+        mid += y;
+        high += data[2];
+        count += 1;
+      }
+      if (i - rollPeriod >= 0) {
+        var prev = originalData[i - rollPeriod];
+        if (prev[1][1] !== null && !isNaN(prev[1][1])) {
+          low -= prev[1][0];
+          mid -= prev[1][1];
+          high -= prev[1][2];
+          count -= 1;
+        }
+      }
+      if (count) {
+        rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
+                                                1.0 * (mid - low) / count,
+                                                1.0 * (high - mid) / count ]];
+      } else {
+        rollingData[i] = [originalData[i][0], [null, null, null]];
+      }
+    }
+  } else {
+    // 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 (!this.attr_("errorBars")){
+      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];
+        }
+      }
+
+    } else {
+      for (i = 0; i < originalData.length; i++) {
+        sum = 0;
+        var variance = 0;
+        num_ok = 0;
+        for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+          y = originalData[j][1][0];
+          if (y === null || isNaN(y)) continue;
+          num_ok++;
+          sum += originalData[j][1][0];
+          variance += Math.pow(originalData[j][1][1], 2);
+        }
+        if (num_ok) {
+          stddev = Math.sqrt(variance) / num_ok;
+          rollingData[i] = [originalData[i][0],
+                            [sum / num_ok, sigma * stddev, sigma * stddev]];
+        } else {
+          rollingData[i] = [originalData[i][0], [null, null, null]];
+        }
+      }
+    }
+  }
+
+  return rollingData;
+};
+
+/**
+ * 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;
+  }
+
+  if (isDate) {
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
+  } 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;
+  }
+};
+
+/**
+ * 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.
+ * @private
+ */
+
+// Parse the x as a float or return null if it's not a number.
+Dygraph.prototype.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 !== null && opt_line_no !== null) {
+    msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
+  }
+  this.error(msg);
+
+  return null;
+};
+
+/**
+ * @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 lines = data.split("\n");
+  var vals, j;
+
+  // Use the default delimiter or fall back to a tab if that makes sense.
+  var delim = this.attr_('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_.
+  }
+  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.attr_("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) {
+          this.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] = [this.parseFloat_(vals[0], i, line),
+                       this.parseFloat_(vals[1], i, line)];
+        }
+      }
+    } else if (this.attr_("errorBars")) {
+      // If there are error bars, values are (value, stddev) pairs
+      if (inFields.length % 2 != 1) {
+        this.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] = [this.parseFloat_(inFields[j], i, line),
+                               this.parseFloat_(inFields[j + 1], i, line)];
+      }
+    } else if (this.attr_("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] = [ this.parseFloat_(vals[0], i, line),
+                          this.parseFloat_(vals[1], i, line),
+                          this.parseFloat_(vals[2], i, line) ];
+          } else {
+            this.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] = this.parseFloat_(inFields[j], i, line);
+      }
+    }
+    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
+
+    if (fields.length != expectedCols) {
+      this.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) {
+        this.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) {
+    this.warn("CSV is out of order; order it correctly to speed loading.");
+    ret.sort(function(a,b) { return a[0] - b[0]; });
+  }
+
+  return ret;
+};
+
+/**
+ * @private
+ * 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 {[Object]} data
+ * @return {[Object]} data with numeric x values.
+ */
+Dygraph.prototype.parseArray_ = function(data) {
+  // Peek at the first x value to see if it's numeric.
+  if (data.length === 0) {
+    this.error("Can't plot empty data set");
+    return null;
+  }
+  if (data[0].length === 0) {
+    this.error("Data set cannot contain an empty row");
+    return null;
+  }
+
+  var i;
+  if (this.attr_("labels") === null) {
+    this.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);
+    }
+  }
+
+  if (Dygraph.isDateLike(data[0][0])) {
+    // Some intelligent defaults for a date x-axis.
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+
+    // Assume they're all dates.
+    var parsedData = Dygraph.clone(data);
+    for (i = 0; i < data.length; i++) {
+      if (parsedData[i].length === 0) {
+        this.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())) {
+        this.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.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+    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 {[Object]} 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.dateString_;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
+  } 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 {
+    this.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.attr_('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 {
+      this.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.attr_("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) {
+      this.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.attr_("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) {
+    this.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);
+  }
+};
+
+/**
+ * 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.predraw_();
+  } else if (typeof data == 'object' &&
+             typeof data.getColumnRange == 'function') {
+    // must be a DataTable from gviz.
+    this.parseDataTable_(data);
+    this.predraw_();
+  } else if (typeof data == 'string') {
+    // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
+    if (data.indexOf('\n') >= 0) {
+      this.loadedEvent_(data);
+    } else {
+      var req = new XMLHttpRequest();
+      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 {
+    this.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} 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;
+
+  // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
+  var file = input_attrs.file;
+  var attrs = Dygraph.mapLegacyOptions_(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);
+
+  if (file) {
+    this.file_ = file;
+    if (!block_redraw) this.start_();
+  } else {
+    if (!block_redraw) {
+      if (requiresNewPoints) {
+        this.predraw_();
+      } else {
+        this.renderGraph_(false, false);
+      }
+    }
+  }
+};
+
+/**
+ * Returns a copy of the options with deprecated names converted into current
+ * names. Also drops the (potentially-large) 'file' attribute. If the caller is
+ * interested in that, they should save a copy before calling this.
+ * @private
+ */
+Dygraph.mapLegacyOptions_ = function(attrs) {
+  var my_attrs = {};
+  for (var k in attrs) {
+    if (k == 'file') continue;
+    if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
+  }
+
+  var set = function(axis, opt, value) {
+    if (!my_attrs.axes) my_attrs.axes = {};
+    if (!my_attrs.axes[axis]) my_attrs.axes[axis] = {};
+    my_attrs.axes[axis][opt] = value;
+  };
+  var map = function(opt, axis, new_opt) {
+    if (typeof(attrs[opt]) != 'undefined') {
+      set(axis, new_opt, attrs[opt]);
+      delete my_attrs[opt];
+    }
+  };
+
+  // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
+  map('xValueFormatter', 'x', 'valueFormatter');
+  map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
+  map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
+  map('xTicker', 'x', 'ticker');
+  map('yValueFormatter', 'y', 'valueFormatter');
+  map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
+  map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
+  map('yTicker', 'y', 'ticker');
+  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)) {
+    this.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_) {
+    // TODO(danvk): there should be a clear() method.
+    this.maindiv_.innerHTML = "";
+    this.roller_ = null;
+    this.attrs_.labelsDiv = null;
+    this.createInterface_();
+    if (this.annotations_.length) {
+      // createInterface_ reset the layout, so we need to do this.
+      this.layout_.setAnnotations(this.annotations_);
+    }
+    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.attr_("visibility")) {
+    this.attrs_.visibility = [];
+  }
+  // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
+  while (this.attr_("visibility").length < this.numColumns() - 1) {
+    this.attrs_.visibility.push(true);
+  }
+  return this.attr_("visibility");
+};
+
+/**
+ * Changes the visiblity of a series.
+ */
+Dygraph.prototype.setVisibility = function(num, value) {
+  var x = this.visibility();
+  if (num < 0 || num >= x.length) {
+    this.warn("invalid series number in setVisibility: " + num);
+  } else {
+    x[num] = 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;
+  this.layout_.setAnnotations(this.annotations_);
+  if (!suppressDraw) {
+    this.predraw_();
+  }
+};
+
+/**
+ * Return the list of annotations.
+ */
+Dygraph.prototype.annotations = function() {
+  return this.annotations_;
+};
+
+/**
+ * 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) {
+  var labels = this.attr_("labels");
+  for (var i = 0; i < labels.length; i++) {
+    if (labels[i] == name) return i;
+  }
+  return null;
+};
+
+/**
+ * @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() {
+  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.
+    }
+  }
+
+  this.warn("Unable to add default annotation CSS rule; display may be off.");
+};
+
+// Older pages may still use this name.
+var DateGraph = Dygraph;
+/**
+ * @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.
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */
+"use strict";
+
+Dygraph.LOG_SCALE = 10;
+Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
+
+/** @private */
+Dygraph.log10 = function(x) {
+  return Math.log(x) / Dygraph.LN_TEN;
+};
+
+// Various logging levels.
+Dygraph.DEBUG = 1;
+Dygraph.INFO = 2;
+Dygraph.WARNING = 3;
+Dygraph.ERROR = 3;
+
+// Set this to log stack traces on warnings, etc.
+// This requires stacktrace.js, which is up to you to provide.
+// A copy can be found in the dygraphs repo, or at
+// https://github.com/eriwen/javascript-stacktrace
+Dygraph.LOG_STACK_TRACES = false;
+
+/** 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];
+
+/**
+ * @private
+ * Log an error on the JS console at the given severity.
+ * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
+ * @param { String } The message to log.
+ */
+Dygraph.log = function(severity, message) {
+  var st;
+  if (typeof(printStackTrace) != 'undefined') {
+    // Remove uninteresting bits: logging functions and paths.
+    st = printStackTrace({guess:false});
+    while (st[0].indexOf("stacktrace") != -1) {
+      st.splice(0, 1);
+    }
+
+    st.splice(0, 2);
+    for (var i = 0; i < st.length; i++) {
+      st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1')
+          .replace(/\@.*\/([^\/]*)/, '@$1')
+          .replace('[object Object].', '');
+    }
+    var top_msg = st.splice(0, 1)[0];
+    message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')';
+  }
+
+  if (typeof(console) != 'undefined') {
+    switch (severity) {
+      case Dygraph.DEBUG:
+        console.debug('dygraphs: ' + message);
+        break;
+      case Dygraph.INFO:
+        console.info('dygraphs: ' + message);
+        break;
+      case Dygraph.WARNING:
+        console.warn('dygraphs: ' + message);
+        break;
+      case Dygraph.ERROR:
+        console.error('dygraphs: ' + message);
+        break;
+    }
+  }
+
+  if (Dygraph.LOG_STACK_TRACES) {
+    console.log(st.join('\n'));
+  }
+};
+
+/** @private */
+Dygraph.info = function(message) {
+  Dygraph.log(Dygraph.INFO, message);
+};
+/** @private */
+Dygraph.prototype.info = Dygraph.info;
+
+/** @private */
+Dygraph.warn = function(message) {
+  Dygraph.log(Dygraph.WARNING, message);
+};
+/** @private */
+Dygraph.prototype.warn = Dygraph.warn;
+
+/** @private */
+Dygraph.error = function(message) {
+  Dygraph.log(Dygraph.ERROR, message);
+};
+/** @private */
+Dygraph.prototype.error = Dygraph.error;
+
+/**
+ * @private
+ * 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);
+ * };
+ */
+Dygraph.getContext = function(canvas) {
+  return canvas.getContext("2d");
+};
+
+/**
+ * @private
+ * Add an event handler. This smooths a difference between IE and the rest of
+ * the world.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param { Function } fn The function to call on the event. The function takes
+ * one parameter: the event object.
+ */
+Dygraph.addEvent = function addEvent(elem, type, fn) {
+  if (elem.addEventListener) {
+    elem.addEventListener(type, fn, false);
+  } else {
+    elem[type+fn] = function(){fn(window.event);};
+    elem.attachEvent('on'+type, elem[type+fn]);
+  }
+};
+
+/**
+ * @private
+ * Remove an event handler. This smooths a difference between IE and the rest of
+ * the world.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param { Function } fn The function to call on the event. The function takes
+ * one parameter: the event object.
+ */
+Dygraph.removeEvent = function addEvent(elem, type, fn) {
+  if (elem.removeEventListener) {
+    elem.removeEventListener(type, fn, false);
+  } else {
+    elem.detachEvent('on'+type, elem[type+fn]);
+    elem[type+fn] = null;
+  }
+};
+
+/**
+ * @private
+ * 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.
+ */
+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 x-coordinate of the supplied object relative to the left side
+ * of the page.
+ * @private
+ */
+Dygraph.findPosX = function(obj) {
+  var curleft = 0;
+  if(obj.offsetParent) {
+    var copyObj = obj;
+    while(1) {
+      curleft += copyObj.offsetLeft;
+      if(!copyObj.offsetParent) {
+        break;
+      }
+      copyObj = copyObj.offsetParent;
+    }
+  } else if(obj.x) {
+    curleft += obj.x;
+  }
+  // This handles the case where the object is inside a scrolled div.
+  while(obj && obj != document.body) {
+    curleft -= obj.scrollLeft;
+    obj = obj.parentNode;
+  }
+  return curleft;
+};
+
+/**
+ * Find the y-coordinate of the supplied object relative to the top of the
+ * page.
+ * @private
+ */
+Dygraph.findPosY = function(obj) {
+  var curtop = 0;
+  if(obj.offsetParent) {
+    var copyObj = obj;
+    while(1) {
+      curtop += copyObj.offsetTop;
+      if(!copyObj.offsetParent) {
+        break;
+      }
+      copyObj = copyObj.offsetParent;
+    }
+  } else if(obj.y) {
+    curtop += obj.y;
+  }
+  // This handles the case where the object is inside a scrolled div.
+  while(obj && obj != document.body) {
+    curtop -= obj.scrollTop;
+    obj = obj.parentNode;
+  }
+  return curtop;
+};
+
+/**
+ * @private
+ * 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
+ */
+Dygraph.pageX = function(e) {
+  if (e.pageX) {
+    return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
+  } else {
+    var de = document;
+    var b = document.body;
+    return e.clientX +
+        (de.scrollLeft || b.scrollLeft) -
+        (de.clientLeft || 0);
+  }
+};
+
+/**
+ * @private
+ * 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
+ */
+Dygraph.pageY = function(e) {
+  if (e.pageY) {
+    return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
+  } else {
+    var de = document;
+    var b = document.body;
+    return e.clientY +
+        (de.scrollTop || b.scrollTop) -
+        (de.clientTop || 0);
+  }
+};
+
+/**
+ * @private
+ * @param { Number } x The number to consider.
+ * @return { Boolean } Whether the number is zero or NaN.
+ */
+// TODO(danvk): rename this function to something like 'isNonZeroNan'.
+// TODO(danvk): determine when else this returns false (e.g. for undefined or null)
+Dygraph.isOK = function(x) {
+  return x && !isNaN(x);
+};
+
+/**
+ * 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);
+};
+
+/**
+ * @private
+ * Converts '9' to '09' (useful for dates)
+ */
+Dygraph.zeropad = function(x) {
+  if (x < 10) return "0" + x; else return "" + x;
+};
+
+/**
+ * Return a string version of the hours, minutes and seconds portion of a date.
+ * @param {Number} date The JavaScript date (ms since epoch)
+ * @return {String} A time of the form "HH:MM:SS"
+ * @private
+ */
+Dygraph.hmsString_ = function(date) {
+  var zeropad = Dygraph.zeropad;
+  var d = new Date(date);
+  if (d.getSeconds()) {
+    return zeropad(d.getHours()) + ":" +
+           zeropad(d.getMinutes()) + ":" +
+           zeropad(d.getSeconds());
+  } else {
+    return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
+  }
+};
+
+/**
+ * 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;
+};
+
+/**
+ * @private
+ * Implementation of binary search over an array.
+ * Currently does not work when val is outside the range of arry's values.
+ * @param { Integer } val the value to search for
+ * @param { Integer[] } arry is the value over which to search
+ * @param { Integer } 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 { Integer } [low] The first index in arry to consider (optional)
+ * @param { Integer } [high] The last index in arry to consider (optional)
+ */
+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];
+  if (element == val) {
+    return mid;
+  }
+
+  var idx;
+  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);
+  }
+  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);
+  }
+};
+
+/**
+ * @private
+ * 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} A date in YYYYMMDD format.
+ * @return {Number} Milliseconds since epoch.
+ */
+Dygraph.dateParser = function(dateStr) {
+  var dateStrSlashed;
+  var d;
+
+  // Let the system try the format first.
+  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)) {
+    Dygraph.error("Couldn't parse " + dateStr + " as a date");
+  }
+  return d;
+};
+
+/**
+ * @private
+ * 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 { Integer } millis since epoch
+ */
+Dygraph.dateStrToMillis = function(str) {
+  return new Date(str).getTime();
+};
+
+// These functions are all based on MochiKit.
+/**
+ * Copies all the properties from o to self.
+ *
+ * @private
+ */
+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.
+ *
+ * @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] = {};
+          }
+          Dygraph.updateDeep(self[k], o[k]);
+        } else {
+          self[k] = o[k];
+        }
+      }
+    }
+  }
+  return self;
+};
+
+/**
+ * @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;
+};
+
+/**
+ * @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.
+ * @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;
+};
+
+/**
+ * @private
+ * Create a new canvas element. This is more complex than a simple
+ * document.createElement("canvas") because of IE and excanvas.
+ */
+Dygraph.createCanvas = function() {
+  var canvas = document.createElement("canvas");
+
+  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
+    canvas = G_vmlCanvasManager.initElement(canvas);
+  }
+
+  return canvas;
+};
+
+/**
+ * @private
+ * Checks whether the user is on an Android browser.
+ * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
+ */
+Dygraph.isAndroid = function() {
+  return (/Android/).test(navigator.userAgent);
+};
+
+/**
+ * @private
+ * Call a function N times at a given interval, then call a cleanup function
+ * once. repeat_fn is called once immediately, then (times - 1) times
+ * asynchronously. If times=1, then cleanup_fn() is also called synchronously.
+ * @param repeat_fn {Function} Called repeatedly -- takes the number of calls
+ * (from 0 to times-1) as an argument.
+ * @param times {number} The number of times to call repeat_fn
+ * @param every_ms {number} Milliseconds between calls
+ * @param cleanup_fn {Function} A function to call after all repeat_fn calls.
+ * @private
+ */
+Dygraph.repeatAndCleanup = function(repeat_fn, times, every_ms, cleanup_fn) {
+  var count = 0;
+  var start_time = new Date().getTime();
+  repeat_fn(count);
+  if (times == 1) {
+    cleanup_fn();
+    return;
+  }
+
+  (function loop() {
+    if (count >= times) return;
+    var target_time = start_time + (1 + count) * every_ms;
+    setTimeout(function() {
+      count++;
+      repeat_fn(count);
+      if (count >= times - 1) {
+        cleanup_fn();
+      } else {
+        loop();
+      }
+    }, target_time - new Date().getTime());
+    // TODO(danvk): adjust every_ms to produce evenly-timed function calls.
+  })();
+};
+
+/**
+ * @private
+ * This function will scan the option list and determine if they
+ * require us to recalculate the pixel positions of each point.
+ * @param { List } a list of options to check.
+ * @return { Boolean } true if the graph needs new points else false.
+ */
+Dygraph.isPixelChangingOptionList = function(labels, attrs) {
+  // 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,
+    'digitsAfterDecimal': true,
+    'drawCallback': true,
+    'drawPoints': true,
+    'drawXGrid': true,
+    'drawYGrid': 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,
+    'maxNumberWidth': true,
+    'panEdgeFraction': true,
+    'pixelsPerYLabel': true,
+    'pointClickCallback': true,
+    'pointSize': true,
+    'rangeSelectorPlotFillColor': true,
+    'rangeSelectorPlotStrokeColor': true,
+    'showLabelsOnHighlight': true,
+    'showRoller': true,
+    'sigFigs': true,
+    'strokeWidth': true,
+    'underlayCallback': true,
+    'unhighlightCallback': true,
+    'xAxisLabelFormatter': true,
+    'xTicker': true,
+    'xValueFormatter': true,
+    'yAxisLabelFormatter': true,
+    'yValueFormatter': true,
+    'zoomCallback': true
+  };
+
+  // Assume that we do not require new points.
+  // This will change to true if we actually do need new points.
+  var requiresNewPoints = false;
+
+  // 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;
+    }
+  }
+
+  // Iterate through the list of updated options.
+  for (var property in attrs) {
+    // Break early if we already know we need new points from a previous option.
+    if (requiresNewPoints) {
+      break;
+    }
+    if (attrs.hasOwnProperty(property)) {
+      // Find out of this field is actually a series specific options list.
+      if (seriesNamesDictionary[property]) {
+        // This property value is a list of options for this series.
+        // If any of these sub properties are not pixel safe, set the flag.
+        for (var subProperty in attrs[property]) {
+          // Break early if we already know we need new points from a previous option.
+          if (requiresNewPoints) {
+            break;
+          }
+          if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
+            requiresNewPoints = true;
+          }
+        }
+      // If this was not a series specific option list, check if its a pixel changing property.
+      } else if (!pixelSafeOptions[property]) {
+        requiresNewPoints = true;
+      }
+    }
+  }
+
+  return requiresNewPoints;
+};
+
+/**
+ * Compares two arrays to see if they are equal. If either parameter is not an
+ * array it will return false. Does a shallow compare 
+ * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
+ * @param array1 first array
+ * @param array2 second array
+ * @return True if both parameters are arrays, and contents are equal.
+ */
+Dygraph.compareArrays = function(array1, array2) {
+  if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
+    return false;
+  }
+  if (array1.length !== array2.length) {
+    return false;
+  }
+  for (var i = 0; i < array1.length; i++) {
+    if (array1[i] !== array2[i]) {
+      return false;
+    }
+  }
+  return true;
+};
+/**
+ * @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
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * A wrapper around Dygraph that implements the gviz API.
+ * @param {Object} container The DOM object the visualization should live in.
+ */
+Dygraph.GVizChart = function(container) {
+  this.container = container;
+};
+
+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} 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} 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 col = 1;
+  var datasets = this.date_graph.layout_.datasets;
+  for (var k in datasets) {
+    if (!datasets.hasOwnProperty(k)) continue;
+    selection.push({row: row, column: col});
+    col++;
+  }
+
+  return selection;
+};
+
+/**
+ * @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)
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * A collection of functions to facilitate build custom interaction models.
+ * @class
+ */
+Dygraph.Interaction = {};
+
+/**
+ * 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();
+  context.dateRange = xRange[1] - xRange[0];
+  context.initialLeftmostDate = xRange[0];
+  context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
+
+  if (g.attr_("panEdgeFraction")) {
+    var maxXPixelsToDraw = g.width_ * g.attr_("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.attr_("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);
+      var boundedBottomValue = g.toDataYCoord(boundedBottomY);
+
+      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.
+  context.is2DPan = false;
+  for (i = 0; i < g.axes_.length; i++) {
+    axis = g.axes_[i];
+    var yRange = g.yAxisRange(i);
+    // TODO(konigsberg): These values should be in |context|.
+    // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
+    if (axis.logscale) {
+      axis.initialTopValue = Dygraph.log10(yRange[1]);
+      axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
+    } else {
+      axis.initialTopValue = yRange[1];
+      axis.dragValueRange = yRange[1] - yRange[0];
+    }
+    axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
+
+    // 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 = g.dragGetX_(event, context);
+  context.dragEndY = g.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;
+    }
+  }
+
+  g.dateWindow_ = [minDate, maxDate];
+
+  // y-axis scaling is automatic unless this is a full 2D pan.
+  if (context.is2DPan) {
+    // Adjust each axis appropriately.
+    for (var i = 0; i < g.axes_.length; i++) {
+      var axis = g.axes_[i];
+
+      var pixelsDragged = context.dragEndY - context.dragStartY;
+      var unitsDragged = pixelsDragged * axis.unitsPerPixel;
+
+      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
+
+      // In log scale, maxValue and minValue are the logs of those values.
+      var maxValue = axis.initialTopValue + unitsDragged;
+      if (boundedValue) {
+        maxValue = Math.min(maxValue, boundedValue[1]);
+      }
+      var minValue = maxValue - axis.dragValueRange;
+      if (boundedValue) {
+        if (minValue < boundedValue[0]) {
+          // Adjust maxValue, and recompute minValue.
+          maxValue = maxValue - (minValue - boundedValue[0]);
+          minValue = maxValue - axis.dragValueRange;
+        }
+      }
+      if (axis.logscale) {
+        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 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.endPan = function(event, g, context) {
+  context.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.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);
+  }
+
+  // TODO(konigsberg): Clear the context data from the axis.
+  // (replace with "context = {}" ?)
+  // TODO(konigsberg): mouseup should just delete the
+  // context object, and mousedown should create a new one.
+  context.isPanning = false;
+  context.is2DPan = false;
+  context.initialLeftmostDate = null;
+  context.dateRange = null;
+  context.valueRange = null;
+  context.boundedDates = null;
+  context.boundedValues = null;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.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;
+};
+
+Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
+  var clickCallback = g.attr_('clickCallback');
+  var pointClickCallback = g.attr_('pointClickCallback');
+
+  var selectedPoint = null;
+
+  // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
+  if (pointClickCallback) {
+    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.attr_('highlightCircleSize') + 2;
+    if (closestDistance <= radius * radius) {
+      selectedPoint = g.selPoints_[closestIdx];
+    }
+  }
+
+  if (selectedPoint) {
+    pointClickCallback(event, selectedPoint);
+  }
+
+  // TODO(danvk): pass along more info about the points, e.g. 'x'
+  if (clickCallback) {
+    clickCallback(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) {
+  context.isZooming = false;
+  context.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.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);
+  }
+
+  if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
+    g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
+               Math.max(context.dragStartX, context.dragEndX));
+  } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
+    g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
+               Math.max(context.dragStartY, context.dragEndY));
+  } else {
+    g.clearZoomRect_();
+  }
+  context.dragStartX = null;
+  context.dragStartY = null;
+};
+
+/**
+ * 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) {
+    context.initializeMouseDown(event, g, context);
+
+    if (event.altKey || event.shiftKey) {
+      Dygraph.startPan(event, g, context);
+    } else {
+      Dygraph.startZoom(event, g, context);
+    }
+  },
+
+  // Draw zoom rectangles when the mouse is down and the user moves around
+  mousemove: function(event, g, context) {
+    if (context.isZooming) {
+      Dygraph.moveZoom(event, g, context);
+    } else if (context.isPanning) {
+      Dygraph.movePan(event, g, context);
+    }
+  },
+
+  mouseup: function(event, g, context) {
+    if (context.isZooming) {
+      Dygraph.endZoom(event, g, context);
+    } else if (context.isPanning) {
+      Dygraph.endPan(event, g, context);
+    }
+  },
+
+  // Temporarily cancel the dragging event when the mouse leaves the graph
+  mouseout: function(event, g, context) {
+    if (context.isZooming) {
+      context.dragEndX = null;
+      context.dragEndY = null;
+    }
+  },
+
+  // Disable zooming out if panning.
+  dblclick: function(event, g, context) {
+    if (event.altKey || event.shiftKey) {
+      return;
+    }
+    // TODO(konigsberg): replace g.doUnzoom()_ with something that is
+    // friendlier to public use.
+    g.doUnzoom_();
+  }
+};
+
+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: function(event, g, context) {
+    // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
+    context.dragEndX = g.dragGetX_(event, context);
+    context.dragEndY = g.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);
+    }
+  }
+};
+
+// 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);
+    }
+  }
+};
+// Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
+// All Rights Reserved.
+
+/**
+ * @fileoverview This file contains the DygraphRangeSelector class used to provide
+ * a timeline range selector widget for dygraphs.
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * The DygraphRangeSelector class provides a timeline range selector widget.
+ * @param {Dygraph} dygraph The dygraph object
+ * @constructor
+ */
+var DygraphRangeSelector = function(dygraph) {
+  this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
+  this.isUsingExcanvas_ = dygraph.isUsingExcanvas_;
+  this.dygraph_ = dygraph;
+  this.createCanvases_();
+  if (this.isUsingExcanvas_) {
+    this.createIEPanOverlay_();
+  }
+  this.createZoomHandles_();
+  this.initInteraction_();
+};
+
+/**
+ * Adds the range selector to the dygraph.
+ * @param {Object} graphDiv The container div for the range selector.
+ * @param {DygraphLayout} layout The DygraphLayout object for this graph.
+ */
+DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) {
+  this.layout_ = layout;
+  this.resize_();
+  graphDiv.appendChild(this.bgcanvas_);
+  graphDiv.appendChild(this.fgcanvas_);
+  graphDiv.appendChild(this.leftZoomHandle_);
+  graphDiv.appendChild(this.rightZoomHandle_);
+};
+
+/**
+ * Renders the static background portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderStaticLayer = function() {
+  this.resize_();
+  this.drawStaticLayer_();
+};
+
+/**
+ * Renders the interactive foreground portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderInteractiveLayer = function() {
+  if (this.isChangingRange_) {
+    return;
+  }
+  this.placeZoomHandles_();
+  this.drawInteractiveLayer_();
+};
+
+/**
+ * @private
+ * Resizes the range selector.
+ */
+DygraphRangeSelector.prototype.resize_ = function() {
+  function setElementRect(canvas, rect) {
+    canvas.style.top = rect.y + 'px';
+    canvas.style.left = rect.x + 'px';
+    canvas.width = rect.w;
+    canvas.height = rect.h;
+    canvas.style.width = canvas.width + 'px';    // for IE
+    canvas.style.height = canvas.height + 'px';  // for IE
+  }
+
+  var plotArea = this.layout_.getPlotArea();
+  var xAxisLabelHeight = this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+  this.canvasRect_ = {
+    x: plotArea.x,
+    y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
+    w: plotArea.w,
+    h: this.attr_('rangeSelectorHeight')
+  };
+
+  setElementRect(this.bgcanvas_, this.canvasRect_);
+  setElementRect(this.fgcanvas_, this.canvasRect_);
+};
+
+DygraphRangeSelector.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+/**
+ * @private
+ * Creates the background and foreground canvases.
+ */
+DygraphRangeSelector.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 overlay divs for IE/Excanvas so that mouse events are handled properly.
+ */
+DygraphRangeSelector.prototype.createIEPanOverlay_ = function() {
+  this.iePanOverlay_ = document.createElement("div");
+  this.iePanOverlay_.style.position = 'absolute';
+  this.iePanOverlay_.style.backgroundColor = 'white';
+  this.iePanOverlay_.style.filter = 'alpha(opacity=0)';
+  this.iePanOverlay_.style.display = 'none';
+  this.iePanOverlay_.style.cursor = 'move';
+  this.fgcanvas_.appendChild(this.iePanOverlay_);
+};
+
+/**
+ * @private
+ * Creates the zoom handle elements.
+ */
+DygraphRangeSelector.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';
+  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=';
+  }
+
+  this.leftZoomHandle_ = img;
+  this.rightZoomHandle_ = img.cloneNode(false);
+};
+
+/**
+ * @private
+ * Sets up the interaction for the range selector.
+ */
+DygraphRangeSelector.prototype.initInteraction_ = function() {
+  var self = this;
+  var topElem = this.isIE_ ? document : window;
+  var xLast = 0;
+  var handle = null;
+  var isZooming = false;
+  var isPanning = false;
+
+  // 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, onCanvasMouseMove;
+
+  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;
+    xLast = e.screenX;
+    handle = e.target ? e.target : e.srcElement;
+    Dygraph.addEvent(topElem, 'mousemove', onZoom);
+    Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'col-resize';
+  };
+
+  onZoom = function(e) {
+    if (!isZooming) {
+      return;
+    }
+    var delX = e.screenX - xLast;
+    if (Math.abs(delX) < 4) {
+      return;
+    }
+    xLast = e.screenX;
+    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 not using excanvas).
+    if (!self.isUsingExcanvas_) {
+      doZoom();
+    }
+  };
+
+  onZoomEnd = function(e) {
+    if (!isZooming) {
+      return;
+    }
+    isZooming = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onZoom);
+    Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'default';
+
+    // If using excanvas, Zoom now.
+    if (self.isUsingExcanvas_) {
+      doZoom();
+    }
+  };
+
+  doZoom = function() {
+    try {
+      var zoomHandleStatus = self.getZoomHandleStatus_();
+      self.isChangingRange_ = true;
+      if (!zoomHandleStatus.isZoomed) {
+        self.dygraph_.doUnzoom_();
+      } else {
+        var xDataWindow = toXDataWindow(zoomHandleStatus);
+        self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
+      }
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  isMouseInPanZone = function(e) {
+    if (self.isUsingExcanvas_) {
+        return e.srcElement == self.iePanOverlay_;
+    } else {
+      // Getting clientX directly from the event is not accurate enough :(
+      var clientX;
+      if (e.offsetX != undefined) {
+        clientX = self.canvasRect_.x + e.offsetX;
+      } else {
+        clientX = e.clientX;
+      }
+      var zoomHandleStatus = self.getZoomHandleStatus_();
+      return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
+    }
+  };
+
+  onPanStart = function(e) {
+    if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
+      Dygraph.cancelEvent(e);
+      isPanning = true;
+      xLast = e.screenX;
+      Dygraph.addEvent(topElem, 'mousemove', onPan);
+      Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
+    }
+  };
+
+  onPan = function(e) {
+    if (!isPanning) {
+      return;
+    }
+    Dygraph.cancelEvent(e);
+
+    var delX = e.screenX - xLast;
+    if (Math.abs(delX) < 4) {
+      return;
+    }
+    xLast = e.screenX;
+
+    // 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 not using excanvas).
+    if (!self.isUsingExcanvas_) {
+      doPan();
+    }
+  };
+
+  onPanEnd = function(e) {
+    if (!isPanning) {
+      return;
+    }
+    isPanning = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onPan);
+    Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
+    // If using excanvas, do pan now.
+    if (self.isUsingExcanvas_) {
+      doPan();
+    }
+  };
+
+  doPan = function() {
+    try {
+      self.isChangingRange_ = true;
+      self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
+      self.dygraph_.drawGraph_(false);
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  onCanvasMouseMove = function(e) {
+    if (isZooming || isPanning) {
+      return;
+    }
+    var cursor = isMouseInPanZone(e) ? 'move' : 'default';
+    if (cursor != self.fgcanvas_.style.cursor) {
+      self.fgcanvas_.style.cursor = cursor;
+    }
+  };
+
+  this.dygraph_.attrs_.interactionModel =
+      Dygraph.Interaction.dragIsPanInteractionModel;
+  this.dygraph_.attrs_.panEdgeFraction = 0.0001;
+
+  var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
+  Dygraph.addEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
+  Dygraph.addEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
+
+  if (this.isUsingExcanvas_) {
+    Dygraph.addEvent(this.iePanOverlay_, 'mousedown', onPanStart);
+  } else {
+    Dygraph.addEvent(this.fgcanvas_, 'mousedown', onPanStart);
+    Dygraph.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove);
+  }
+};
+
+/**
+ * @private
+ * Draws the static layer in the background canvas.
+ */
+DygraphRangeSelector.prototype.drawStaticLayer_ = function() {
+  var ctx = this.bgcanvas_ctx_;
+  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+  try {
+    this.drawMiniPlot_();
+  } catch(ex) {
+    Dygraph.warn(ex);
+  }
+
+  var margin = 0.5;
+  this.bgcanvas_ctx_.lineWidth = 1;
+  ctx.strokeStyle = 'gray';
+  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.
+ */
+DygraphRangeSelector.prototype.drawMiniPlot_ = function() {
+  var fillStyle = this.attr_('rangeSelectorPlotFillColor');
+  var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor');
+  if (!fillStyle && !strokeStyle) {
+    return;
+  }
+
+  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;
+
+  ctx.beginPath();
+  ctx.moveTo(margin, canvasHeight);
+  for (var i = 0; i < combinedSeriesData.data.length; i++) {
+    var dataPoint = combinedSeriesData.data[i];
+    var x = (dataPoint[0] - xExtremes[0])*xFact;
+    var y = canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact;
+    if (isFinite(x) && isFinite(y)) {
+      ctx.lineTo(x, y);
+    }
+  }
+  ctx.lineTo(canvasWidth, canvasHeight);
+  ctx.closePath();
+
+  if (fillStyle) {
+    var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
+    lingrad.addColorStop(0, 'white');
+    lingrad.addColorStop(1, fillStyle);
+    this.bgcanvas_ctx_.fillStyle = lingrad;
+    ctx.fill();
+  }
+
+  if (strokeStyle) {
+    this.bgcanvas_ctx_.strokeStyle = strokeStyle;
+    this.bgcanvas_ctx_.lineWidth = 1.5;
+    ctx.stroke();
+  }
+};
+
+/**
+ * @private
+ * Computes and returns the combinded series data along with min/max for the mini plot.
+ * @return {Object} An object containing combinded series array, ymin, ymax.
+ */
+DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
+  var data = this.dygraph_.rawData_;
+  var logscale = this.attr_('logscale');
+
+  // Create a combined series (average of all series values).
+  var combinedSeries = [];
+  var sum;
+  var count;
+  var yVal, y;
+  var mutipleValues;
+  var i, j, k;
+
+  // Find out if data has multiple values per datapoint.
+  // Go to first data point that actually has values (see http://code.google.com/p/dygraphs/issues/detail?id=246)
+  for (i = 0; i < data.length; i++) {
+    if (data[i].length > 1 && data[i][1] != null) {
+      mutipleValues = typeof data[i][1] != 'number';
+      if (mutipleValues) {
+        sum = [];
+        count = [];
+        for (k = 0; k < data[i][1].length; k++) {
+          sum.push(0);
+          count.push(0);
+        }
+      }
+      break;
+    }
+  }
+
+  for (i = 0; i < data.length; i++) {
+    var dataPoint = data[i];
+    var xVal = dataPoint[0];
+
+    if (mutipleValues) {
+      for (k = 0; k < sum.length; k++) {
+        sum[k] = count[k] = 0;
+      }
+    } else {
+      sum = count = 0;
+    }
+
+    for (j = 1; j < dataPoint.length; j++) {
+      if (this.dygraph_.visibility()[j-1]) {
+        if (mutipleValues) {
+          for (k = 0; k < sum.length; k++) {
+            y = dataPoint[j][k];
+            if (y === null || isNaN(y)) continue;
+            sum[k] += y;
+            count[k]++;
+          }
+        } else {
+          y = dataPoint[j];
+          if (y === null || isNaN(y)) continue;
+          sum += y;
+          count++;
+        }
+      }
+    }
+
+    if (mutipleValues) {
+      for (k = 0; k < sum.length; k++) {
+        sum[k] /= count[k];
+      }
+      yVal = sum.slice(0);
+    } else {
+      yVal = sum/count;
+    }
+
+    combinedSeries.push([xVal, yVal]);
+  }
+
+  // Account for roll period, fractions.
+  combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
+
+  if (typeof combinedSeries[0][1] != 'number') {
+    for (i = 0; i < combinedSeries.length; i++) {
+      yVal = combinedSeries[i][1];
+      combinedSeries[i][1] = yVal[0];
+    }
+  }
+
+  // Compute the y range.
+  var yMin = Number.MAX_VALUE;
+  var yMax = -Number.MAX_VALUE;
+  for (i = 0; i < combinedSeries.length; i++) {
+    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.
+ */
+DygraphRangeSelector.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.
+ */
+DygraphRangeSelector.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 = 'black';
+  if (!zoomHandleStatus.isZoomed) {
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(margin, height);
+    ctx.lineTo(width, height);
+    ctx.lineTo(width, margin);
+    ctx.stroke();
+    if (this.iePanOverlay_) {
+      this.iePanOverlay_.style.display = 'none';
+    }
+  } 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, 0.6)';
+    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();
+
+    if (this.isUsingExcanvas_) {
+      this.iePanOverlay_.style.width = (rightHandleCanvasPos - leftHandleCanvasPos) + 'px';
+      this.iePanOverlay_.style.left = leftHandleCanvasPos + 'px';
+      this.iePanOverlay_.style.height = height + 'px';
+      this.iePanOverlay_.style.display = 'inline';
+    }
+  }
+};
+
+/**
+ * @private
+ * Returns the current zoom handle position information.
+ * @return {Object} The zoom handle status.
+ */
+DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() {
+  var halfHandleWidth = this.leftZoomHandle_.width/2;
+  var leftHandlePos = parseInt(this.leftZoomHandle_.style.left, 10) + halfHandleWidth;
+  var rightHandlePos = parseInt(this.rightZoomHandle_.style.left, 10) + halfHandleWidth;
+  return {
+      leftHandlePos: leftHandlePos,
+      rightHandlePos: rightHandlePos,
+      isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
+  };
+};
+/**
+ * @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 globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) {
+  var pixels_per_tick = 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;
+      if (kmg2) {
+        mults = [1, 2, 4, 8];
+      } else {
+        mults = [1, 2, 5];
+      }
+      var scale, low_val, high_val;
+      for (i = -10; i < 50; i++) {
+        var base_scale;
+        if (kmg2) {
+          base_scale = Math.pow(16, i);
+        } else {
+          base_scale = Math.pow(10, i);
+        }
+        var spacing = 0;
+        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;
+          // wish I could break out of both loops at once...
+          if (spacing > pixels_per_tick) break;
+        }
+        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} );
+      }
+    }
+  }
+
+  // Add formatted labels to the ticks.
+  var k;
+  var k_labels = [];
+  if (opts("labelsKMB")) {
+    k = 1000;
+    k_labels = [ "K", "M", "B", "T" ];
+  }
+  if (opts("labelsKMG2")) {
+    if (k) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+    k = 1024;
+    k_labels = [ "k", "M", "G", "T" ];
+  }
+
+  var formatter = opts('axisLabelFormatter');
+
+  // Add labels to the ticks.
+  for (i = 0; i < ticks.length; i++) {
+    if (ticks[i].label !== undefined) continue;  // Use current label.
+    tickV = ticks[i].v;
+    var absTickV = Math.abs(tickV);
+    // TODO(danvk): set granularity to something appropriate here.
+    var label = formatter(tickV, 0, opts, dygraph);
+    if (k_labels.length > 0) {
+      // TODO(danvk): should this be integrated into the axisLabelFormatter?
+      // Round up to an appropriate unit.
+      var n = k*k*k*k;
+      for (j = 3; j >= 0; j--, n /= k) {
+        if (absTickV >= n) {
+          label = Dygraph.round_(tickV / n, opts('digitsAfterDecimal')) +
+              k_labels[j];
+          break;
+        }
+      }
+    }
+    ticks[i].label = label;
+  }
+
+  return ticks;
+};
+
+
+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
+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.WEEKLY = 14;
+Dygraph.MONTHLY = 15;
+Dygraph.QUARTERLY = 16;
+Dygraph.BIANNUAL = 17;
+Dygraph.ANNUAL = 18;
+Dygraph.DECADAL = 19;
+Dygraph.CENTENNIAL = 20;
+Dygraph.NUM_GRANULARITIES = 21;
+
+Dygraph.SHORT_SPACINGS = [];
+Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]    = 1000 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]   = 1000 * 5;
+Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]    = 1000 * 10;
+Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
+Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]        = 1000 * 60;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]    = 1000 * 60 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]   = 1000 * 60 * 5;
+Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]    = 1000 * 60 * 10;
+Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
+Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]          = 1000 * 3600;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]      = 1000 * 3600 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]      = 1000 * 3600 * 6;
+Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
+Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
+
+/**
+ * @private
+ * 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.
+ */
+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} opts Function mapping from option name -> 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 = 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;
+};
+
+Dygraph.numDateTicks = function(start_time, end_time, granularity) {
+  if (granularity < Dygraph.MONTHLY) {
+    // Generate one tick mark for every fixed interval of time.
+    var spacing = Dygraph.SHORT_SPACINGS[granularity];
+    return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
+  } else {
+    var year_mod = 1;  // e.g. to only print one point every 10 years.
+    var num_months = 12;
+    if (granularity == Dygraph.QUARTERLY) num_months = 3;
+    if (granularity == Dygraph.BIANNUAL) num_months = 2;
+    if (granularity == Dygraph.ANNUAL) num_months = 1;
+    if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
+    if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
+
+    var msInYear = 365.2524 * 24 * 3600 * 1000;
+    var num_years = 1.0 * (end_time - start_time) / msInYear;
+    return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
+  }
+};
+
+Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
+  var formatter = opts("axisLabelFormatter");
+  var ticks = [];
+  var t;
+
+  if (granularity < Dygraph.MONTHLY) {
+    // Generate one tick mark for every fixed interval of time.
+    var spacing = Dygraph.SHORT_SPACINGS[granularity];
+
+    // Find a time less than start_time which occurs on a "nice" time boundary
+    // for this granularity.
+    var g = spacing / 1000;
+    var d = new Date(start_time);
+    var x;
+    if (g <= 60) {  // seconds
+      x = d.getSeconds(); d.setSeconds(x - x % g);
+    } else {
+      d.setSeconds(0);
+      g /= 60;
+      if (g <= 60) {  // minutes
+        x = d.getMinutes(); d.setMinutes(x - x % g);
+      } else {
+        d.setMinutes(0);
+        g /= 60;
+
+        if (g <= 24) {  // days
+          x = d.getHours(); d.setHours(x - x % g);
+        } else {
+          d.setHours(0);
+          g /= 24;
+
+          if (g == 7) {  // one week
+            d.setDate(d.getDate() - d.getDay());
+          }
+        }
+      }
+    }
+    start_time = d.getTime();
+
+    for (t = start_time; t <= end_time; t += spacing) {
+      ticks.push({ v:t,
+                   label: formatter(new Date(t), granularity, opts, dg)
+                 });
+    }
+  } else {
+    // Display a tick mark on the first of a set of months of each year.
+    // Years get a tick mark iff y % year_mod == 0. This is useful for
+    // displaying a tick mark once every 10 years, say, on long time scales.
+    var months;
+    var year_mod = 1;  // e.g. to only print one point every 10 years.
+
+    if (granularity == Dygraph.MONTHLY) {
+      months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
+    } else if (granularity == Dygraph.QUARTERLY) {
+      months = [ 0, 3, 6, 9 ];
+    } else if (granularity == Dygraph.BIANNUAL) {
+      months = [ 0, 6 ];
+    } else if (granularity == Dygraph.ANNUAL) {
+      months = [ 0 ];
+    } else if (granularity == Dygraph.DECADAL) {
+      months = [ 0 ];
+      year_mod = 10;
+    } else if (granularity == Dygraph.CENTENNIAL) {
+      months = [ 0 ];
+      year_mod = 100;
+    } else {
+      Dygraph.warn("Span of dates is too long");
+    }
+
+    var start_year = new Date(start_time).getFullYear();
+    var end_year   = new Date(end_time).getFullYear();
+    var zeropad = Dygraph.zeropad;
+    for (var i = start_year; i <= end_year; i++) {
+      if (i % year_mod !== 0) continue;
+      for (var j = 0; j < months.length; j++) {
+        var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
+        t = Dygraph.dateStrToMillis(date_str);
+        if (t < start_time || t > end_time) continue;
+        ticks.push({ v:t,
+                     label: formatter(new Date(t), granularity, opts, dg)
+                   });
+      }
+    }
+  }
+
+  return ticks;
+};
+
+// These are set here so that this file can be included after dygraph.js.
+Dygraph.DEFAULT_ATTRS.axes.x.ticker = Dygraph.dateTicker;
+Dygraph.DEFAULT_ATTRS.axes.y.ticker = Dygraph.numericTicks;
+Dygraph.DEFAULT_ATTRS.axes.y2.ticker = Dygraph.numericTicks;
+/**
+ * A class to parse color values
+ *
+ * NOTE: modified by danvk. I removed the "getHelpXML" function to reduce the
+ * file size, added "use strict" and a few "var" declarations where needed.
+ *
+ * @author Stoyan Stefanov <sstoo@gmail.com>
+ * @link   http://www.phpied.com/rgb-color-parser-in-javascript/
+ * @license Use it if you like it
+ */
+"use strict";
+
+function RGBColor(color_string)
+{
+    this.ok = false;
+
+    // strip any leading #
+    if (color_string.charAt(0) == '#') { // remove # if any
+        color_string = color_string.substr(1,6);
+    }
+
+    color_string = color_string.replace(/ /g,'');
+    color_string = color_string.toLowerCase();
+
+    // before getting into regexps, try simple matches
+    // and overwrite the input
+    var simple_colors = {
+        aliceblue: 'f0f8ff',
+        antiquewhite: 'faebd7',
+        aqua: '00ffff',
+        aquamarine: '7fffd4',
+        azure: 'f0ffff',
+        beige: 'f5f5dc',
+        bisque: 'ffe4c4',
+        black: '000000',
+        blanchedalmond: 'ffebcd',
+        blue: '0000ff',
+        blueviolet: '8a2be2',
+        brown: 'a52a2a',
+        burlywood: 'deb887',
+        cadetblue: '5f9ea0',
+        chartreuse: '7fff00',
+        chocolate: 'd2691e',
+        coral: 'ff7f50',
+        cornflowerblue: '6495ed',
+        cornsilk: 'fff8dc',
+        crimson: 'dc143c',
+        cyan: '00ffff',
+        darkblue: '00008b',
+        darkcyan: '008b8b',
+        darkgoldenrod: 'b8860b',
+        darkgray: 'a9a9a9',
+        darkgreen: '006400',
+        darkkhaki: 'bdb76b',
+        darkmagenta: '8b008b',
+        darkolivegreen: '556b2f',
+        darkorange: 'ff8c00',
+        darkorchid: '9932cc',
+        darkred: '8b0000',
+        darksalmon: 'e9967a',
+        darkseagreen: '8fbc8f',
+        darkslateblue: '483d8b',
+        darkslategray: '2f4f4f',
+        darkturquoise: '00ced1',
+        darkviolet: '9400d3',
+        deeppink: 'ff1493',
+        deepskyblue: '00bfff',
+        dimgray: '696969',
+        dodgerblue: '1e90ff',
+        feldspar: 'd19275',
+        firebrick: 'b22222',
+        floralwhite: 'fffaf0',
+        forestgreen: '228b22',
+        fuchsia: 'ff00ff',
+        gainsboro: 'dcdcdc',
+        ghostwhite: 'f8f8ff',
+        gold: 'ffd700',
+        goldenrod: 'daa520',
+        gray: '808080',
+        green: '008000',
+        greenyellow: 'adff2f',
+        honeydew: 'f0fff0',
+        hotpink: 'ff69b4',
+        indianred : 'cd5c5c',
+        indigo : '4b0082',
+        ivory: 'fffff0',
+        khaki: 'f0e68c',
+        lavender: 'e6e6fa',
+        lavenderblush: 'fff0f5',
+        lawngreen: '7cfc00',
+        lemonchiffon: 'fffacd',
+        lightblue: 'add8e6',
+        lightcoral: 'f08080',
+        lightcyan: 'e0ffff',
+        lightgoldenrodyellow: 'fafad2',
+        lightgrey: 'd3d3d3',
+        lightgreen: '90ee90',
+        lightpink: 'ffb6c1',
+        lightsalmon: 'ffa07a',
+        lightseagreen: '20b2aa',
+        lightskyblue: '87cefa',
+        lightslateblue: '8470ff',
+        lightslategray: '778899',
+        lightsteelblue: 'b0c4de',
+        lightyellow: 'ffffe0',
+        lime: '00ff00',
+        limegreen: '32cd32',
+        linen: 'faf0e6',
+        magenta: 'ff00ff',
+        maroon: '800000',
+        mediumaquamarine: '66cdaa',
+        mediumblue: '0000cd',
+        mediumorchid: 'ba55d3',
+        mediumpurple: '9370d8',
+        mediumseagreen: '3cb371',
+        mediumslateblue: '7b68ee',
+        mediumspringgreen: '00fa9a',
+        mediumturquoise: '48d1cc',
+        mediumvioletred: 'c71585',
+        midnightblue: '191970',
+        mintcream: 'f5fffa',
+        mistyrose: 'ffe4e1',
+        moccasin: 'ffe4b5',
+        navajowhite: 'ffdead',
+        navy: '000080',
+        oldlace: 'fdf5e6',
+        olive: '808000',
+        olivedrab: '6b8e23',
+        orange: 'ffa500',
+        orangered: 'ff4500',
+        orchid: 'da70d6',
+        palegoldenrod: 'eee8aa',
+        palegreen: '98fb98',
+        paleturquoise: 'afeeee',
+        palevioletred: 'd87093',
+        papayawhip: 'ffefd5',
+        peachpuff: 'ffdab9',
+        peru: 'cd853f',
+        pink: 'ffc0cb',
+        plum: 'dda0dd',
+        powderblue: 'b0e0e6',
+        purple: '800080',
+        red: 'ff0000',
+        rosybrown: 'bc8f8f',
+        royalblue: '4169e1',
+        saddlebrown: '8b4513',
+        salmon: 'fa8072',
+        sandybrown: 'f4a460',
+        seagreen: '2e8b57',
+        seashell: 'fff5ee',
+        sienna: 'a0522d',
+        silver: 'c0c0c0',
+        skyblue: '87ceeb',
+        slateblue: '6a5acd',
+        slategray: '708090',
+        snow: 'fffafa',
+        springgreen: '00ff7f',
+        steelblue: '4682b4',
+        tan: 'd2b48c',
+        teal: '008080',
+        thistle: 'd8bfd8',
+        tomato: 'ff6347',
+        turquoise: '40e0d0',
+        violet: 'ee82ee',
+        violetred: 'd02090',
+        wheat: 'f5deb3',
+        white: 'ffffff',
+        whitesmoke: 'f5f5f5',
+        yellow: 'ffff00',
+        yellowgreen: '9acd32'
+    };
+    for (var key in simple_colors) {
+        if (color_string == key) {
+            color_string = simple_colors[key];
+        }
+    }
+    // emd of simple type-in colors
+
+    // array of color definition objects
+    var color_defs = [
+        {
+            re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
+            example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
+            process: function (bits){
+                return [
+                    parseInt(bits[1]),
+                    parseInt(bits[2]),
+                    parseInt(bits[3])
+                ];
+            }
+        },
+        {
+            re: /^(\w{2})(\w{2})(\w{2})$/,
+            example: ['#00ff00', '336699'],
+            process: function (bits){
+                return [
+                    parseInt(bits[1], 16),
+                    parseInt(bits[2], 16),
+                    parseInt(bits[3], 16)
+                ];
+            }
+        },
+        {
+            re: /^(\w{1})(\w{1})(\w{1})$/,
+            example: ['#fb0', 'f0f'],
+            process: function (bits){
+                return [
+                    parseInt(bits[1] + bits[1], 16),
+                    parseInt(bits[2] + bits[2], 16),
+                    parseInt(bits[3] + bits[3], 16)
+                ];
+            }
+        }
+    ];
+
+    // search through the definitions to find a match
+    for (var i = 0; i < color_defs.length; i++) {
+        var re = color_defs[i].re;
+        var processor = color_defs[i].process;
+        var bits = re.exec(color_string);
+        if (bits) {
+            var channels = processor(bits);
+            this.r = channels[0];
+            this.g = channels[1];
+            this.b = channels[2];
+            this.ok = true;
+        }
+
+    }
+
+    // validate/cleanup values
+    this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
+    this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
+    this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
+
+    // some getters
+    this.toRGB = function () {
+        return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
+    }
+    this.toHex = function () {
+        var r = this.r.toString(16);
+        var g = this.g.toString(16);
+        var b = this.b.toString(16);
+        if (r.length == 1) r = '0' + r;
+        if (g.length == 1) g = '0' + g;
+        if (b.length == 1) b = '0' + b;
+        return '#' + r + g + b;
+    }
+
+
+}
+
+Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(x,pad,r){if(typeof (r)=="undefined"){r=10}for(;parseInt(x,10)<r&&r>1;r/=10){x=pad.toString()+x}return x.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(d){return Date.ext.locales[d.locale].a[d.getDay()]},A:function(d){return Date.ext.locales[d.locale].A[d.getDay()]},b:function(d){return Date.ext.locales[d.locale].b[d.getMonth()]},B:function(d){return Date.ext.locales[d.locale].B[d.getMonth()]},c:"toLocaleString",C:function(d){return Date.ext.util.xPad(parseInt(d.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(d){return Date.ext.util.xPad(parseInt(Date.ext.util.G(d)/100,10),0)},G:function(d){var y=d.getFullYear();var V=parseInt(Date.ext.formats.V(d),10);var W=parseInt(Date.ext.formats.W(d),10);if(W>V){y++}else{if(W===0&&V>=52){y--}}return y},H:["getHours","0"],I:function(d){var I=d.getHours()%12;return Date.ext.util.xPad(I===0?12:I,0)},j:function(d){var ms=d-new Date(""+d.getFullYear()+"/1/1 GMT");ms+=d.getTimezoneOffset()*60000;var doy=parseInt(ms/60000/60/24,10)+1;return Date.ext.util.xPad(doy,0,100)},m:function(d){return Date.ext.util.xPad(d.getMonth()+1,0)},M:["getMinutes","0"],p:function(d){return Date.ext.locales[d.locale].p[d.getHours()>=12?1:0]},P:function(d){return Date.ext.locales[d.locale].P[d.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(d){var dow=d.getDay();return dow===0?7:dow},U:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=6-d.getDay();var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0)},V:function(d){var woy=parseInt(Date.ext.formats.W(d),10);var dow1_1=(new Date(""+d.getFullYear()+"/1/1")).getDay();var idow=woy+(dow1_1>4||dow1_1<=1?0:1);if(idow==53&&(new Date(""+d.getFullYear()+"/12/31")).getDay()<4){idow=1}else{if(idow===0){idow=Date.ext.formats.V(new Date(""+(d.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(idow,0)},w:"getDay",W:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=7-Date.ext.formats.u(d);var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0,10)},y:function(d){return Date.ext.util.xPad(d.getFullYear()%100,0)},Y:"getFullYear",z:function(d){var o=d.getTimezoneOffset();var H=Date.ext.util.xPad(parseInt(Math.abs(o/60),10),0);var M=Date.ext.util.xPad(o%60,0);return(o>0?"-":"+")+H+M},Z:function(d){return d.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(d){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(fmt){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var d=this;while(fmt.match(/%[cDhnrRtTxXzZ]/)){fmt=fmt.replace(/%([cDhnrRtTxXzZ])/g,function(m0,m1){var f=Date.ext.aggregates[m1];return(f=="locale"?Date.ext.locales[d.locale][m1]:f)})}var str=fmt.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(m0,m1){var f=Date.ext.formats[m1];if(typeof (f)=="string"){return d[f]()}else{if(typeof (f)=="function"){return f.call(d,d)}else{if(typeof (f)=="object"&&typeof (f[0])=="string"){return Date.ext.util.xPad(d[f[0]](),f[1])}else{return m1}}}});d=null;return str};
diff --git a/static/vendor/dygraph-1.2.min.js b/static/vendor/dygraph-1.2.min.js
new file mode 100644 (file)
index 0000000..2455834
--- /dev/null
@@ -0,0 +1 @@
+"use strict";var DygraphLayout=function(a){this.dygraph_=a;this.datasets=[];this.annotations=[];this.yAxes_=null;this.xTicks_=null;this.yTicks_=null};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.getPlotArea=function(){return this.computePlotArea_()};DygraphLayout.prototype.computePlotArea_=function(){var a={x:0,y:0};if(this.attr_("drawYAxis")){a.x=this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize")}a.w=this.dygraph_.width_-a.x-this.attr_("rightGap");a.h=this.dygraph_.height_;if(this.attr_("drawXAxis")){if(this.attr_("xAxisHeight")){a.h-=this.attr_("xAxisHeight")}else{a.h-=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize")}}if(this.dygraph_.numAxes()==2){a.w-=(this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize"))}else{if(this.dygraph_.numAxes()>2){this.dygraph_.error("Only two y-axes are supported at this time. (Trying to use "+this.dygraph_.numAxes()+")")}}if(this.attr_("title")){a.h-=this.attr_("titleHeight");a.y+=this.attr_("titleHeight")}if(this.attr_("xlabel")){a.h-=this.attr_("xLabelHeight")}if(this.attr_("ylabel")){}if(this.attr_("y2label")){}if(this.attr_("showRangeSelector")){a.h-=this.attr_("rangeSelectorHeight")+4}return a};DygraphLayout.prototype.setAnnotations=function(d){this.annotations=[];var e=this.attr_("xValueParser")||function(a){return a};for(var c=0;c<d.length;c++){var b={};if(!d[c].xval&&!d[c].x){this.dygraph_.error("Annotations must have an 'x' property");return}if(d[c].icon&&!(d[c].hasOwnProperty("width")&&d[c].hasOwnProperty("height"))){this.dygraph_.error("Must set width and height when setting annotation.icon property");return}Dygraph.update(b,d[c]);if(!b.xval){b.xval=e(b.x)}this.annotations.push(b)}};DygraphLayout.prototype.setXTicks=function(a){this.xTicks_=a};DygraphLayout.prototype.setYAxes=function(a){this.yAxes_=a};DygraphLayout.prototype.setDateWindow=function(a){this.dateWindow_=a};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks();this._evaluateAnnotations()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.dateWindow_){this.minxval=this.dateWindow_[0];this.maxxval=this.dateWindow_[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var e=this.datasets[c];if(e.length>1){var b=e[0][0];if(!this.minxval||b<this.minxval){this.minxval=b}var a=e[e.length-1][0];if(!this.maxxval||a>this.maxxval){this.maxxval=a}}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!==0?1/this.xrange:1);for(var d=0;d<this.yAxes_.length;d++){var f=this.yAxes_[d];f.minyval=f.computedValueRange[0];f.maxyval=f.computedValueRange[1];f.yrange=f.maxyval-f.minyval;f.yscale=(f.yrange!==0?1/f.yrange:1);if(f.g.attr_("logscale")){f.ylogrange=Dygraph.log10(f.maxyval)-Dygraph.log10(f.minyval);f.ylogscale=(f.ylogrange!==0?1/f.ylogrange:1);if(!isFinite(f.ylogrange)||isNaN(f.ylogrange)){f.g.error("axis "+d+" of graph at "+f.g+" can't be displayed in log scale for range ["+f.minyval+" - "+f.maxyval+"]")}}}};DygraphLayout._calcYNormal=function(a,b){if(a.logscale){return 1-((Dygraph.log10(b)-Dygraph.log10(a.minyval))*a.ylogscale)}else{return 1-((b-a.minyval)*a.yscale)}};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=[];this.setPointsLengths=[];for(var f in this.datasets){if(!this.datasets.hasOwnProperty(f)){continue}var b=this.datasets[f];var a=this.dygraph_.axisPropertiesForSeries(f);var g=0;for(var d=0;d<b.length;d++){var l=b[d];var c=parseFloat(l[0]);var h=parseFloat(l[1]);var k=(c-this.minxval)*this.xscale;var e=DygraphLayout._calcYNormal(a,h);var i={x:k,y:e,xval:c,yval:h,name:f};this.points.push(i);g+=1}this.setPointsLengths.push(g)}};DygraphLayout.prototype._evaluateLineTicks=function(){var d,c,b,f;this.xticks=[];for(d=0;d<this.xTicks_.length;d++){c=this.xTicks_[d];b=c.label;f=this.xscale*(c.v-this.minxval);if((f>=0)&&(f<=1)){this.xticks.push([f,b])}}this.yticks=[];for(d=0;d<this.yAxes_.length;d++){var e=this.yAxes_[d];for(var a=0;a<e.ticks.length;a++){c=e.ticks[a];b=c.label;f=this.dygraph_.toPercentYCoord(c.v,d);if((f>=0)&&(f<=1)){this.yticks.push([d,f,b])}}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!(this.attr_("errorBars")||this.attr_("customBars"))){return}var g=0;for(var k in this.datasets){if(!this.datasets.hasOwnProperty(k)){continue}var f=0;var e=this.datasets[k];var d=this.dygraph_.axisPropertiesForSeries(k);for(f=0;f<e.length;f++,g++){var n=e[f];var b=parseFloat(n[0]);var l=parseFloat(n[1]);if(b==this.points[g].xval&&l==this.points[g].yval){var h=parseFloat(n[2]);var c=parseFloat(n[3]);var m=l-h;var a=l+c;this.points[g].y_top=DygraphLayout._calcYNormal(d,m);this.points[g].y_bottom=DygraphLayout._calcYNormal(d,a)}}}};DygraphLayout.prototype._evaluateAnnotations=function(){var d;var f={};for(d=0;d<this.annotations.length;d++){var b=this.annotations[d];f[b.xval+","+b.series]=b}this.annotated_points=[];if(!this.annotations||!this.annotations.length){return}for(d=0;d<this.points.length;d++){var e=this.points[d];var c=e.xval+","+e.name;if(c in f){e.annotation=f[c];this.annotated_points.push(e)}}};DygraphLayout.prototype.removeAllDatasets=function(){delete this.datasets;this.datasets=[]};DygraphLayout.prototype.unstackPointAtIndex=function(b){var a=this.points[b];var d={};for(var e in a){d[e]=a[e]}if(!this.attr_("stackedGraph")){return d}for(var c=b+1;c<this.points.length;c++){if(this.points[c].xval==a.xval){d.yval-=this.points[c].yval;break}}return d};"use strict";var DygraphCanvasRenderer=function(d,c,b,e){this.dygraph_=d;this.layout=e;this.element=c;this.elementContext=b;this.container=this.element.parentNode;this.height=this.element.height;this.width=this.element.width;if(!this.isIE&&!(DygraphCanvasRenderer.isSupported(this.element))){throw"Canvas is not supported."}this.xlabels=[];this.ylabels=[];this.annotations=[];this.chartLabels={};this.area=e.getPlotArea();this.container.style.position="relative";this.container.style.width=this.width+"px";if(this.dygraph_.isUsingExcanvas_){this._createIEClipArea()}else{if(!Dygraph.isAndroid()){var a=this.dygraph_.canvas_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip();a=this.dygraph_.hidden_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip()}}};DygraphCanvasRenderer.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphCanvasRenderer.prototype.clear=function(){var c;if(this.isIE){try{if(this.clearDelay){this.clearDelay.cancel();this.clearDelay=null}c=this.elementContext}catch(f){return}}c=this.elementContext;c.clearRect(0,0,this.width,this.height);function a(g){for(var e=0;e<g.length;e++){var h=g[e];if(h.parentNode){h.parentNode.removeChild(h)}}}a(this.xlabels);a(this.ylabels);a(this.annotations);for(var b in this.chartLabels){if(!this.chartLabels.hasOwnProperty(b)){continue}var d=this.chartLabels[b];if(d.parentNode){d.parentNode.removeChild(d)}}this.xlabels=[];this.ylabels=[];this.annotations=[];this.chartLabels={}};DygraphCanvasRenderer.isSupported=function(f){var b=null;try{if(typeof(f)=="undefined"||f===null){b=document.createElement("canvas")}else{b=f}b.getContext("2d")}catch(c){var d=navigator.appVersion.match(/MSIE (\d\.\d)/);var a=(navigator.userAgent.toLowerCase().indexOf("opera")!=-1);if((!d)||(d[1]<6)||(a)){return false}return true}return true};DygraphCanvasRenderer.prototype.setColors=function(a){this.colorScheme_=a};DygraphCanvasRenderer.prototype.render=function(){var b=this.elementContext;function c(h){return Math.round(h)+0.5}function g(h){return Math.round(h)-0.5}if(this.attr_("underlayCallback")){this.attr_("underlayCallback")(b,this.area,this.dygraph_,this.dygraph_)}var a,f,d,e;if(this.attr_("drawYGrid")){e=this.layout.yticks;b.save();b.strokeStyle=this.attr_("gridLineColor");b.lineWidth=this.attr_("gridLineWidth");for(d=0;d<e.length;d++){if(e[d][0]!==0){continue}a=c(this.area.x);f=g(this.area.y+e[d][1]*this.area.h);b.beginPath();b.moveTo(a,f);b.lineTo(a+this.area.w,f);b.closePath();b.stroke()}}if(this.attr_("drawXGrid")){e=this.layout.xticks;b.save();b.strokeStyle=this.attr_("gridLineColor");b.lineWidth=this.attr_("gridLineWidth");for(d=0;d<e.length;d++){a=c(this.area.x+e[d][0]*this.area.w);f=g(this.area.y+this.area.h);b.beginPath();b.moveTo(a,f);b.lineTo(a,this.area.y);b.closePath();b.stroke()}}this._renderLineChart();this._renderAxis();this._renderChartLabels();this._renderAnnotations()};DygraphCanvasRenderer.prototype._createIEClipArea=function(){var g="dygraph-clip-div";var f=this.dygraph_.graphDiv;for(var e=f.childNodes.length-1;e>=0;e--){if(f.childNodes[e].className==g){f.removeChild(f.childNodes[e])}}var c=document.bgColor;var d=this.dygraph_.graphDiv;while(d!=document){var a=d.currentStyle.backgroundColor;if(a&&a!="transparent"){c=a;break}d=d.parentNode}function b(j){if(j.w===0||j.h===0){return}var i=document.createElement("div");i.className=g;i.style.backgroundColor=c;i.style.position="absolute";i.style.left=j.x+"px";i.style.top=j.y+"px";i.style.width=j.w+"px";i.style.height=j.h+"px";f.appendChild(i)}var h=this.area;b({x:0,y:0,w:h.x,h:this.height});b({x:h.x,y:0,w:this.width-h.x,h:h.y});b({x:h.x+h.w,y:0,w:this.width-h.x-h.w,h:this.height});b({x:h.x,y:h.y+h.h,w:this.width-h.x,h:this.height-h.h-h.y})};DygraphCanvasRenderer.prototype._renderAxis=function(){if(!this.attr_("drawXAxis")&&!this.attr_("drawYAxis")){return}function q(i){return Math.round(i)+0.5}function p(i){return Math.round(i)-0.5}var d=this.elementContext;var l,n,m,s,r;var a={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,color:this.attr_("axisLabelColor"),width:this.attr_("axisLabelWidth")+"px",lineHeight:"normal",overflow:"hidden"};var g=function(i,v,w){var x=document.createElement("div");for(var u in a){if(a.hasOwnProperty(u)){x.style[u]=a[u]}}var t=document.createElement("div");t.className="dygraph-axis-label dygraph-axis-label-"+v+(w?" dygraph-axis-label-"+w:"");t.innerHTML=i;x.appendChild(t);return x};d.save();d.strokeStyle=this.attr_("axisLineColor");d.lineWidth=this.attr_("axisLineWidth");if(this.attr_("drawYAxis")){if(this.layout.yticks&&this.layout.yticks.length>0){var b=this.dygraph_.numAxes();for(r=0;r<this.layout.yticks.length;r++){s=this.layout.yticks[r];if(typeof(s)=="function"){return}n=this.area.x;var j=1;var c="y1";if(s[0]==1){n=this.area.x+this.area.w;j=-1;c="y2"}m=this.area.y+s[1]*this.area.h;l=g(s[2],"y",b==2?c:null);var o=(m-this.attr_("axisLabelFontSize")/2);if(o<0){o=0}if(o+this.attr_("axisLabelFontSize")+3>this.height){l.style.bottom="0px"}else{l.style.top=o+"px"}if(s[0]===0){l.style.left=(this.area.x-this.attr_("yAxisLabelWidth")-this.attr_("axisTickSize"))+"px";l.style.textAlign="right"}else{if(s[0]==1){l.style.left=(this.area.x+this.area.w+this.attr_("axisTickSize"))+"px";l.style.textAlign="left"}}l.style.width=this.attr_("yAxisLabelWidth")+"px";this.container.appendChild(l);this.ylabels.push(l)}var h=this.ylabels[0];var e=this.attr_("axisLabelFontSize");var k=parseInt(h.style.top,10)+e;if(k>this.height-e){h.style.top=(parseInt(h.style.top,10)-e/2)+"px"}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y));d.lineTo(q(this.area.x),p(this.area.y+this.area.h));d.closePath();d.stroke();if(this.dygraph_.numAxes()==2){d.beginPath();d.moveTo(p(this.area.x+this.area.w),p(this.area.y));d.lineTo(p(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}}if(this.attr_("drawXAxis")){if(this.layout.xticks){for(r=0;r<this.layout.xticks.length;r++){s=this.layout.xticks[r];n=this.area.x+s[0]*this.area.w;m=this.area.y+this.area.h;l=g(s[1],"x");l.style.textAlign="center";l.style.top=(m+this.attr_("axisTickSize"))+"px";var f=(n-this.attr_("axisLabelWidth")/2);if(f+this.attr_("axisLabelWidth")>this.width){f=this.width-this.attr_("xAxisLabelWidth");l.style.textAlign="right"}if(f<0){f=0;l.style.textAlign="left"}l.style.left=f+"px";l.style.width=this.attr_("xAxisLabelWidth")+"px";this.container.appendChild(l);this.xlabels.push(l)}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y+this.area.h));d.lineTo(q(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}d.restore()};DygraphCanvasRenderer.prototype._renderChartLabels=function(){var d,a;if(this.attr_("title")){d=document.createElement("div");d.style.position="absolute";d.style.top="0px";d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("titleHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("titleHeight")-8)+"px";d.style.fontWeight="bold";a=document.createElement("div");a.className="dygraph-label dygraph-title";a.innerHTML=this.attr_("title");d.appendChild(a);this.container.appendChild(d);this.chartLabels.title=d}if(this.attr_("xlabel")){d=document.createElement("div");d.style.position="absolute";d.style.bottom=0;d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("xLabelHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("xLabelHeight")-2)+"px";a=document.createElement("div");a.className="dygraph-label dygraph-xlabel";a.innerHTML=this.attr_("xlabel");d.appendChild(a);this.container.appendChild(d);this.chartLabels.xlabel=d}var c=this;function b(h,g,f){var i={left:0,top:c.area.y,width:c.attr_("yLabelWidth"),height:c.area.h};d=document.createElement("div");d.style.position="absolute";if(h==1){d.style.left=i.left}else{d.style.right=i.left}d.style.top=i.top+"px";d.style.width=i.width+"px";d.style.height=i.height+"px";d.style.fontSize=(c.attr_("yLabelWidth")-2)+"px";var e=document.createElement("div");e.style.position="absolute";e.style.width=i.height+"px";e.style.height=i.width+"px";e.style.top=(i.height/2-i.width/2)+"px";e.style.left=(i.width/2-i.height/2)+"px";e.style.textAlign="center";var j="rotate("+(h==1?"-":"")+"90deg)";e.style.transform=j;e.style.WebkitTransform=j;e.style.MozTransform=j;e.style.OTransform=j;e.style.msTransform=j;if(typeof(document.documentMode)!=="undefined"&&document.documentMode<9){e.style.filter="progid:DXImageTransform.Microsoft.BasicImage(rotation="+(h==1?"3":"1")+")";e.style.left="0px";e.style.top="0px"}a=document.createElement("div");a.className=g;a.innerHTML=f;e.appendChild(a);d.appendChild(e);return d}var d;if(this.attr_("ylabel")){d=b(1,"dygraph-label dygraph-ylabel",this.attr_("ylabel"));this.container.appendChild(d);this.chartLabels.ylabel=d}if(this.attr_("y2label")&&this.dygraph_.numAxes()==2){d=b(2,"dygraph-label dygraph-y2label",this.attr_("y2label"));this.container.appendChild(d);this.chartLabels.y2label=d}};DygraphCanvasRenderer.prototype._renderAnnotations=function(){var h={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,overflow:"hidden"};var j=function(i,q,r,a){return function(s){var p=r.annotation;if(p.hasOwnProperty(i)){p[i](p,r,a.dygraph_,s)}else{if(a.dygraph_.attr_(q)){a.dygraph_.attr_(q)(p,r,a.dygraph_,s)}}}};var m=this.layout.annotated_points;for(var g=0;g<m.length;g++){var e=m[g];if(e.canvasx<this.area.x||e.canvasx>this.area.x+this.area.w){continue}var k=e.annotation;var l=6;if(k.hasOwnProperty("tickHeight")){l=k.tickHeight}var c=document.createElement("div");for(var b in h){if(h.hasOwnProperty(b)){c.style[b]=h[b]}}if(!k.hasOwnProperty("icon")){c.className="dygraphDefaultAnnotation"}if(k.hasOwnProperty("cssClass")){c.className+=" "+k.cssClass}var d=k.hasOwnProperty("width")?k.width:16;var n=k.hasOwnProperty("height")?k.height:16;if(k.hasOwnProperty("icon")){var f=document.createElement("img");f.src=k.icon;f.width=d;f.height=n;c.appendChild(f)}else{if(e.annotation.hasOwnProperty("shortText")){c.appendChild(document.createTextNode(e.annotation.shortText))}}c.style.left=(e.canvasx-d/2)+"px";if(k.attachAtBottom){c.style.top=(this.area.h-n-l)+"px"}else{c.style.top=(e.canvasy-n-l)+"px"}c.style.width=d+"px";c.style.height=n+"px";c.title=e.annotation.text;c.style.color=this.colors[e.name];c.style.borderColor=this.colors[e.name];k.div=c;Dygraph.addEvent(c,"click",j("clickHandler","annotationClickHandler",e,this));Dygraph.addEvent(c,"mouseover",j("mouseOverHandler","annotationMouseOverHandler",e,this));Dygraph.addEvent(c,"mouseout",j("mouseOutHandler","annotationMouseOutHandler",e,this));Dygraph.addEvent(c,"dblclick",j("dblClickHandler","annotationDblClickHandler",e,this));this.container.appendChild(c);this.annotations.push(c);var o=this.elementContext;o.strokeStyle=this.colors[e.name];o.beginPath();if(!k.attachAtBottom){o.moveTo(e.canvasx,e.canvasy);o.lineTo(e.canvasx,e.canvasy-2-l)}else{o.moveTo(e.canvasx,this.area.h);o.lineTo(e.canvasx,this.area.h-2-l)}o.closePath();o.stroke()}};DygraphCanvasRenderer.prototype._renderLineChart=function(){var G=function(i){return(i===null||isNaN(i))};var M=this.elementContext;var m=this.attr_("fillAlpha");var C=this.attr_("errorBars")||this.attr_("customBars");var k=this.attr_("fillGraph");var w=this.attr_("stackedGraph");var v=this.attr_("stepPlot");var A=this.layout.points;var z=A.length;var u,K,I,b,a,f,t,F,n,D,h,d,r;var E=[];for(var l in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(l)){E.push(l)}}var x=E.length;this.colors={};for(K=0;K<x;K++){this.colors[E[K]]=this.colorScheme_[K%this.colorScheme_.length]}for(K=z;K--;){u=A[K];u.canvasx=this.area.w*u.x+this.area.x;u.canvasy=this.area.h*u.y+this.area.y}var q=M;if(C){if(k){this.dygraph_.warn("Can't use fillGraph option with error bars")}for(K=0;K<x;K++){F=E[K];r=this.dygraph_.axisPropertiesForSeries(F);t=this.colors[F];q.save();b=NaN;a=NaN;f=[-1,-1];d=r.yscale;h=new RGBColor(t);D="rgba("+h.r+","+h.g+","+h.b+","+m+")";q.fillStyle=D;q.beginPath();for(I=0;I<z;I++){u=A[I];if(u.name==F){if(!Dygraph.isOK(u.y)){b=NaN;continue}if(v){n=[u.y_bottom,u.y_top];a=u.y}else{n=[u.y_bottom,u.y_top]}n[0]=this.area.h*n[0]+this.area.y;n[1]=this.area.h*n[1]+this.area.y;if(!isNaN(b)){if(v){q.moveTo(b,n[0])}else{q.moveTo(b,f[0])}q.lineTo(u.canvasx,n[0]);q.lineTo(u.canvasx,n[1]);if(v){q.lineTo(b,n[1])}else{q.lineTo(b,f[1])}q.closePath()}f=n;b=u.canvasx}}q.fill()}}else{if(k){var H=[];for(K=x-1;K>=0;K--){F=E[K];t=this.colors[F];r=this.dygraph_.axisPropertiesForSeries(F);var e=1+r.minyval*r.yscale;if(e<0){e=0}else{if(e>1){e=1}}e=this.area.h*e+this.area.y;q.save();b=NaN;f=[-1,-1];d=r.yscale;h=new RGBColor(t);D="rgba("+h.r+","+h.g+","+h.b+","+m+")";q.fillStyle=D;q.beginPath();for(I=0;I<z;I++){u=A[I];if(u.name==F){if(!Dygraph.isOK(u.y)){b=NaN;continue}if(w){var g=H[u.canvasx];if(g===undefined){g=e}H[u.canvasx]=u.canvasy;n=[u.canvasy,g]}else{n=[u.canvasy,e]}if(!isNaN(b)){q.moveTo(b,f[0]);if(v){q.lineTo(u.canvasx,f[0])}else{q.lineTo(u.canvasx,n[0])}q.lineTo(u.canvasx,n[1]);q.lineTo(b,f[1]);q.closePath()}f=n;b=u.canvasx}}q.fill()}}}var L=0;var o=0;var c=0;for(K=0;K<x;K+=1){c=this.layout.setPointsLengths[K];o+=c;F=E[K];t=this.colors[F];var y=this.dygraph_.attr_("strokeWidth",F);M.save();var B=this.dygraph_.attr_("pointSize",F);b=null;a=null;var p=this.dygraph_.attr_("drawPoints",F);var s=this.dygraph_.attr_("strokePattern",F);if(!Dygraph.isArrayLike(s)){s=null}for(I=L;I<o;I++){u=A[I];if(G(u.canvasy)){if(v&&b!==null){q.beginPath();q.strokeStyle=t;q.lineWidth=this.attr_("strokeWidth");this._dashedLine(q,b,a,u.canvasx,a,s);q.stroke()}b=a=null}else{var J=(!b&&(I==A.length-1||G(A[I+1].canvasy)));if(b===null){b=u.canvasx;a=u.canvasy}else{if(Math.round(b)==Math.round(u.canvasx)&&Math.round(a)==Math.round(u.canvasy)){continue}if(y){q.beginPath();q.strokeStyle=t;q.lineWidth=y;if(v){this._dashedLine(q,b,a,u.canvasx,a,s)}this._dashedLine(q,b,a,u.canvasx,u.canvasy,s);b=u.canvasx;a=u.canvasy;q.stroke()}}if(p||J){q.beginPath();q.fillStyle=t;q.arc(u.canvasx,u.canvasy,B,0,2*Math.PI,false);q.fill()}}}L=o}M.restore()};DygraphCanvasRenderer.prototype._dashedLine=function(j,i,g,a,h,f){var l,k,e,b,c,d;if(!f||f.length<=1){j.moveTo(i,g);j.lineTo(a,h);return}if(!Dygraph.compareArrays(f,this._dashedLineToHistoryPattern)){this._dashedLineToHistoryPattern=f;this._dashedLineToHistory=[0,0]}j.save();l=(a-i);k=(h-g);e=Math.sqrt(l*l+k*k);b=Math.atan2(k,l);j.translate(i,g);j.moveTo(0,0);j.rotate(b);c=this._dashedLineToHistory[0];i=0;while(e>i){d=f[c];if(this._dashedLineToHistory[1]){i+=this._dashedLineToHistory[1]}else{i+=d}if(i>e){this._dashedLineToHistory=[c,i-e];i=e}else{this._dashedLineToHistory=[(c+1)%f.length,0]}if(c%2===0){j.lineTo(i,0)}else{j.moveTo(i,0)}c=(c+1)%f.length}j.restore()};"use strict";var Dygraph=function(c,b,a){if(arguments.length>0){if(arguments.length==4){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.2";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.ANIMATION_STEPS=10;Dygraph.ANIMATION_DURATION=200;Dygraph.numberValueFormatter=function(a,e,h,d){var b=e("sigFigs");if(b!==null){return Dygraph.floatFormat(a,b)}var f=e("digitsAfterDecimal");var c=e("maxNumberWidth");if(a!==0&&(Math.abs(a)>=Math.pow(10,c)||Math.abs(a)<Math.pow(10,-f))){return a.toExponential(f)}else{return""+Dygraph.round_(a,f)}};Dygraph.numberAxisLabelFormatter=function(a,d,c,b){return Dygraph.numberValueFormatter(a,c,b)};Dygraph.dateString_=function(e){var i=Dygraph.zeropad;var h=new Date(e);var f=""+h.getFullYear();var g=i(h.getMonth()+1);var a=i(h.getDate());var c="";var b=h.getHours()*3600+h.getMinutes()*60+h.getSeconds();if(b){c=" "+Dygraph.hmsString_(e)}return f+"/"+g+"/"+a+c};Dygraph.dateAxisFormatter=function(b,c){if(c>=Dygraph.DECADAL){return b.strftime("%Y")}else{if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a===0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}}};Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,digitsAfterDecimal:2,maxNumberWidth:6,sigFigs:null,strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueParser:Dygraph.dateParser,delimiter:",",sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true,legend:"onmouseover",stepPlot:false,avoidMinZero:false,titleHeight:28,xLabelHeight:18,yLabelWidth:18,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.3,gridLineWidth:0.3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",interactionModel:null,animatedZooms:false,showRangeSelector:false,rangeSelectorHeight:40,rangeSelectorPlotStrokeColor:"#808FAB",rangeSelectorPlotFillColor:"#A7B1C4",axes:{x:{pixelsPerLabel:60,axisLabelFormatter:Dygraph.dateAxisFormatter,valueFormatter:Dygraph.dateString_,ticker:null},y:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null},y2:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null}}};Dygraph.HORIZONTAL=1;Dygraph.VERTICAL=2;Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!==null){var a=["Date"];for(var c=0;c<e.length;c++){a.push(e[c])}Dygraph.update(b,{labels:a})}this.__init__(f,d,b)};Dygraph.prototype.__init__=function(d,c,b){if(/MSIE/.test(navigator.userAgent)&&!window.opera&&typeof(G_vmlCanvasManager)!="undefined"&&document.readyState!="complete"){var a=this;setTimeout(function(){a.__init__(d,c,b)},100);return}if(b===null||b===undefined){b={}}b=Dygraph.mapLegacyOptions_(b);if(!d){Dygraph.error("Constructing dygraph with a non-existent div!");return}this.isUsingExcanvas_=typeof(G_vmlCanvasManager)!="undefined";this.maindiv_=d;this.file_=c;this.rollPeriod_=b.rollPeriod||Dygraph.DEFAULT_ROLL_PERIOD;this.previousVerticalX_=-1;this.fractions_=b.fractions||false;this.dateWindow_=b.dateWindow||null;this.is_initial_draw_=true;this.annotations_=[];this.zoomed_x_=false;this.zoomed_y_=false;d.innerHTML="";if(d.style.width===""&&b.width){d.style.width=b.width+"px"}if(d.style.height===""&&b.height){d.style.height=b.height+"px"}if(d.style.height===""&&d.clientHeight===0){d.style.height=Dygraph.DEFAULT_HEIGHT+"px";if(d.style.width===""){d.style.width=Dygraph.DEFAULT_WIDTH+"px"}}this.width_=d.clientWidth;this.height_=d.clientHeight;if(b.stackedGraph){b.fillGraph=true}this.user_attrs_={};Dygraph.update(this.user_attrs_,b);this.attrs_={};Dygraph.updateDeep(this.attrs_,Dygraph.DEFAULT_ATTRS);this.boundaryIds_=[];this.createInterface_();this.start_()};Dygraph.prototype.isZoomed=function(a){if(a==null){return this.zoomed_x_||this.zoomed_y_}if(a==="x"){return this.zoomed_x_}if(a==="y"){return this.zoomed_y_}throw"axis parameter is ["+a+"] must be null, 'x' or 'y'."};Dygraph.prototype.toString=function(){var a=this.maindiv_;var b=(a&&a.id)?a.id:a;return"[Dygraph "+b+"]"};Dygraph.prototype.attr_=function(b,a){if(a&&typeof(this.user_attrs_[a])!="undefined"&&this.user_attrs_[a]!==null&&typeof(this.user_attrs_[a][b])!="undefined"){return this.user_attrs_[a][b]}else{if(typeof(this.user_attrs_[b])!="undefined"){return this.user_attrs_[b]}else{if(typeof(this.attrs_[b])!="undefined"){return this.attrs_[b]}else{return null}}}};Dygraph.prototype.optionsViewForAxis_=function(b){var a=this;return function(c){var d=a.user_attrs_.axes;if(d&&d[b]&&d[b][c]){return d[b][c]}if(typeof(a.user_attrs_[c])!="undefined"){return a.user_attrs_[c]}d=a.attrs_.axes;if(d&&d[b]&&d[b][c]){return d[b][c]}if(b=="y"&&a.axes_[0].hasOwnProperty(c)){return a.axes_[0][c]}else{if(b=="y2"&&a.axes_[1].hasOwnProperty(c)){return a.axes_[1][c]}}return a.attr_(c)}};Dygraph.prototype.rollPeriod=function(){return this.rollPeriod_};Dygraph.prototype.xAxisRange=function(){return this.dateWindow_?this.dateWindow_:this.xAxisExtremes()};Dygraph.prototype.xAxisExtremes=function(){var b=this.rawData_[0][0];var a=this.rawData_[this.rawData_.length-1][0];return[b,a]};Dygraph.prototype.yAxisRange=function(a){if(typeof(a)=="undefined"){a=0}if(a<0||a>=this.axes_.length){return null}var b=this.axes_[a];return[b.computedValueRange[0],b.computedValueRange[1]]};Dygraph.prototype.yAxisRanges=function(){var a=[];for(var b=0;b<this.axes_.length;b++){a.push(this.yAxisRange(b))}return a};Dygraph.prototype.toDomCoords=function(a,c,b){return[this.toDomXCoord(a),this.toDomYCoord(c,b)]};Dygraph.prototype.toDomXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return c.x+(b-a[0])/(a[1]-a[0])*c.w};Dygraph.prototype.toDomYCoord=function(d,a){var c=this.toPercentYCoord(d,a);if(c===null){return null}var b=this.plotter_.area;return b.y+c*b.h};Dygraph.prototype.toDataCoords=function(a,c,b){return[this.toDataXCoord(a),this.toDataYCoord(c,b)]};Dygraph.prototype.toDataXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return a[0]+(b-c.x)/c.w*(a[1]-a[0])};Dygraph.prototype.toDataYCoord=function(h,b){if(h===null){return null}var c=this.plotter_.area;var g=this.yAxisRange(b);if(typeof(b)=="undefined"){b=0}if(!this.axes_[b].logscale){return g[0]+(c.y+c.h-h)/c.h*(g[1]-g[0])}else{var f=(h-c.y)/c.h;var a=Dygraph.log10(g[1]);var e=a-(f*(a-Dygraph.log10(g[0])));var d=Math.pow(Dygraph.LOG_SCALE,e);return d}};Dygraph.prototype.toPercentYCoord=function(e,b){if(e===null){return null}if(typeof(b)=="undefined"){b=0}var d=this.yAxisRange(b);var c;if(!this.axes_[b].logscale){c=(d[1]-e)/(d[1]-d[0])}else{var a=Dygraph.log10(d[1]);c=(a-Dygraph.log10(e))/(a-Dygraph.log10(d[0]))}return c};Dygraph.prototype.toPercentXCoord=function(b){if(b===null){return null}var a=this.xAxisRange();return(b-a[0])/(a[1]-a[0])};Dygraph.prototype.numColumns=function(){return this.rawData_[0]?this.rawData_[0].length:this.attr_("labels").length};Dygraph.prototype.numRows=function(){return this.rawData_.length};Dygraph.prototype.fullXRange_=function(){if(this.numRows()>0){return[this.rawData_[0][0],this.rawData_[this.numRows()-1][0]]}else{return[0,1]}};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";a.appendChild(this.graphDiv);this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.canvas_ctx_=Dygraph.getContext(this.canvas_);this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.hidden_ctx_=Dygraph.getContext(this.hidden_);if(this.attr_("showRangeSelector")){this.rangeSelector_=new DygraphRangeSelector(this)}this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.createMouseEventElement_();this.layout_=new DygraphLayout(this);if(this.rangeSelector_){this.rangeSelector_.addToGraph(this.graphDiv,this.layout_)}var b=this;Dygraph.addEvent(this.mouseEventElement_,"mousemove",function(c){b.mouseMove_(c)});Dygraph.addEvent(this.mouseEventElement_,"mouseout",function(c){b.mouseOut_(c)});this.createStatusMessage_();this.createDragInterface_();this.resizeHandler=function(c){b.resize()};Dygraph.addEvent(window,"resize",this.resizeHandler)};Dygraph.prototype.destroy=function(){var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};Dygraph.removeEvent(window,"resize",this.resizeHandler);this.resizeHandler=null;b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.prototype.createMouseEventElement_=function(){if(this.isUsingExcanvas_){var a=document.createElement("div");a.style.position="absolute";a.style.backgroundColor="white";a.style.filter="alpha(opacity=0)";a.style.width=this.width_+"px";a.style.height=this.height_+"px";this.graphDiv.appendChild(a);return a}else{return this.canvas_}};Dygraph.prototype.setColors_=function(){var e=this.attr_("labels").length-1;this.colors_=[];var a=this.attr_("colors");var d;if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var j=Math.ceil(e/2);for(d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var g=d%2?Math.ceil(d/2):(j+d/2);var f=(1*g/(1+e));this.colors_.push(Dygraph.hsvToRGB(f,c,b))}}else{for(d=0;d<e;d++){if(!this.visibility()[d]){continue}var h=a[d%a.length];this.colors_.push(h)}}this.plotter_.setColors(this.colors_)};Dygraph.prototype.getColors=function(){return this.colors_};Dygraph.prototype.createStatusMessage_=function(){var d=this.user_attrs_.labelsDiv;if(d&&null!==d&&(typeof(d)=="string"||d instanceof String)){this.user_attrs_.labelsDiv=document.getElementById(d)}if(!this.attr_("labelsDiv")){var a=this.attr_("labelsDivWidth");var c={position:"absolute",fontSize:"14px",zIndex:10,width:a+"px",top:"0px",left:(this.width_-a-2)+"px",background:"white",textAlign:"left",overflow:"hidden"};Dygraph.update(c,this.attr_("labelsDivStyles"));var e=document.createElement("div");e.className="dygraph-legend";for(var b in c){if(c.hasOwnProperty(b)){e.style[b]=c[b]}}this.graphDiv.appendChild(e);this.attrs_.labelsDiv=e}};Dygraph.prototype.positionLabelsDiv_=function(){if(this.user_attrs_.hasOwnProperty("labelsDiv")){return}var a=this.plotter_.area;var b=this.attr_("labelsDiv");b.style.left=a.x+a.w-this.attr_("labelsDivWidth")-1+"px";b.style.top=a.y+"px"};Dygraph.prototype.createRollInterface_=function(){if(!this.roller_){this.roller_=document.createElement("input");this.roller_.type="text";this.roller_.style.display="none";this.graphDiv.appendChild(this.roller_)}var e=this.attr_("showRoller")?"block":"none";var d=this.plotter_.area;var b={position:"absolute",zIndex:10,top:(d.y+d.h-25)+"px",left:(d.x+1)+"px",display:e};this.roller_.size="2";this.roller_.value=this.rollPeriod_;for(var a in b){if(b.hasOwnProperty(a)){this.roller_.style[a]=b[a]}}var c=this;this.roller_.onchange=function(){c.adjustRoll(c.roller_.value)}};Dygraph.prototype.dragGetX_=function(b,a){return Dygraph.pageX(b)-a.px};Dygraph.prototype.dragGetY_=function(b,a){return Dygraph.pageY(b)-a.py};Dygraph.prototype.createDragInterface_=function(){var c={isZooming:false,isPanning:false,is2DPan:false,dragStartX:null,dragStartY:null,dragEndX:null,dragEndY:null,dragDirection:null,prevEndX:null,prevEndY:null,prevDragDirection:null,initialLeftmostDate:null,xUnitsPerPixel:null,dateRange:null,px:0,py:0,boundedDates:null,boundedValues:null,initializeMouseDown:function(i,h,f){if(i.preventDefault){i.preventDefault()}else{i.returnValue=false;i.cancelBubble=true}f.px=Dygraph.findPosX(h.canvas_);f.py=Dygraph.findPosY(h.canvas_);f.dragStartX=h.dragGetX_(i,f);f.dragStartY=h.dragGetY_(i,f)}};var e=this.attr_("interactionModel");var b=this;var d=function(f){return function(g){f(g,b,c)}};for(var a in e){if(!e.hasOwnProperty(a)){continue}Dygraph.addEvent(this.mouseEventElement_,a,d(e[a]))}Dygraph.addEvent(document,"mouseup",function(g){if(c.isZooming||c.isPanning){c.isZooming=false;c.dragStartX=null;c.dragStartY=null}if(c.isPanning){c.isPanning=false;c.draggingDate=null;c.dateRange=null;for(var f=0;f<b.axes_.length;f++){delete b.axes_[f].draggingValue;delete b.axes_[f].dragValueRange}}})};Dygraph.prototype.drawZoomRect_=function(e,c,i,b,g,a,f,d){var h=this.canvas_ctx_;if(a==Dygraph.HORIZONTAL){h.clearRect(Math.min(c,f),this.layout_.getPlotArea().y,Math.abs(c-f),this.layout_.getPlotArea().h)}else{if(a==Dygraph.VERTICAL){h.clearRect(this.layout_.getPlotArea().x,Math.min(b,d),this.layout_.getPlotArea().w,Math.abs(b-d))}}if(e==Dygraph.HORIZONTAL){if(i&&c){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(Math.min(c,i),this.layout_.getPlotArea().y,Math.abs(i-c),this.layout_.getPlotArea().h)}}else{if(e==Dygraph.VERTICAL){if(g&&b){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(this.layout_.getPlotArea().x,Math.min(b,g),this.layout_.getPlotArea().w,Math.abs(g-b))}}}if(this.isUsingExcanvas_){this.currentZoomRectArgs_=[e,c,i,b,g,0,0,0]}};Dygraph.prototype.clearZoomRect_=function(){this.currentZoomRectArgs_=null;this.canvas_ctx_.clearRect(0,0,this.canvas_.width,this.canvas_.height)};Dygraph.prototype.doZoomX_=function(c,a){this.currentZoomRectArgs_=null;var b=this.toDataXCoord(c);var d=this.toDataXCoord(a);this.doZoomXDates_(b,d)};Dygraph.zoomAnimationFunction=function(c,b){var a=1.5;return(1-Math.pow(a,-c))/(1-Math.pow(a,-b))};Dygraph.prototype.doZoomXDates_=function(c,e){var a=this.xAxisRange();var d=[c,e];this.zoomed_x_=true;var b=this;this.doAnimatedZoom(a,d,null,null,function(){if(b.attr_("zoomCallback")){b.attr_("zoomCallback")(c,e,b.yAxisRanges())}})};Dygraph.prototype.doZoomY_=function(h,f){this.currentZoomRectArgs_=null;var c=this.yAxisRanges();var b=[];for(var e=0;e<this.axes_.length;e++){var d=this.toDataYCoord(h,e);var a=this.toDataYCoord(f,e);b.push([a,d])}this.zoomed_y_=true;var g=this;this.doAnimatedZoom(null,null,c,b,function(){if(g.attr_("zoomCallback")){var i=g.xAxisRange();g.attr_("zoomCallback")(i[0],i[1],g.yAxisRanges())}})};Dygraph.prototype.doUnzoom_=function(){var c=false,d=false,a=false;if(this.dateWindow_!==null){c=true;d=true}for(var f=0;f<this.axes_.length;f++){if(this.axes_[f].valueWindow!==null){c=true;a=true}}this.clearSelection();if(c){this.zoomed_x_=false;this.zoomed_y_=false;var e=this.rawData_[0][0];var b=this.rawData_[this.rawData_.length-1][0];if(!this.attr_("animatedZooms")){this.dateWindow_=null;for(f=0;f<this.axes_.length;f++){if(this.axes_[f].valueWindow!==null){delete this.axes_[f].valueWindow}}this.drawGraph_();if(this.attr_("zoomCallback")){this.attr_("zoomCallback")(e,b,this.yAxisRanges())}return}var k=null,l=null,j=null,g=null;if(d){k=this.xAxisRange();l=[e,b]}if(a){j=this.yAxisRanges();var m=this.gatherDatasets_(this.rolledSeries_,null);var n=m[1];this.computeYAxisRanges_(n);g=[];for(f=0;f<this.axes_.length;f++){g.push(this.axes_[f].extremeRange)}}var h=this;this.doAnimatedZoom(k,l,j,g,function(){h.dateWindow_=null;for(var o=0;o<h.axes_.length;o++){if(h.axes_[o].valueWindow!==null){delete h.axes_[o].valueWindow}}if(h.attr_("zoomCallback")){h.attr_("zoomCallback")(e,b,h.yAxisRanges())}})}};Dygraph.prototype.doAnimatedZoom=function(a,e,b,c,m){var i=this.attr_("animatedZooms")?Dygraph.ANIMATION_STEPS:1;var l=[];var k=[];var f,d;if(a!==null&&e!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);l[f-1]=[a[0]*(1-d)+d*e[0],a[1]*(1-d)+d*e[1]]}}if(b!==null&&c!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);var n=[];for(var g=0;g<this.axes_.length;g++){n.push([b[g][0]*(1-d)+d*c[g][0],b[g][1]*(1-d)+d*c[g][1]])}k[f-1]=n}}var h=this;Dygraph.repeatAndCleanup(function(p){if(k.length){for(var o=0;o<h.axes_.length;o++){var j=k[p][o];h.axes_[o].valueWindow=[j[0],j[1]]}}if(l.length){h.dateWindow_=l[p]}h.drawGraph_()},i,Dygraph.ANIMATION_DURATION/i,m)};Dygraph.prototype.mouseMove_=function(b){var r=this.layout_.points;if(r===undefined){return}var a=Dygraph.pageX(b)-Dygraph.findPosX(this.mouseEventElement_);var j=-1;var f;var n=1e+100;var o=-1;for(f=0;f<r.length;f++){var q=r[f];if(q===null){continue}var h=Math.abs(q.canvasx-a);if(h>n){continue}n=h;o=f}if(o>=0){j=r[o].xval}this.selPoints_=[];var d=r.length;if(!this.attr_("stackedGraph")){for(f=0;f<d;f++){if(r[f].xval==j){this.selPoints_.push(r[f])}}}else{var g=0;for(f=d-1;f>=0;f--){if(r[f].xval==j){var c={};for(var e in r[f]){c[e]=r[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}this.selPoints_.reverse()}if(this.attr_("highlightCallback")){var m=this.lastx_;if(m!==null&&j!=m){this.attr_("highlightCallback")(b,j,this.selPoints_,this.idxToRow_(o))}}this.lastx_=j;this.updateSelection_()};Dygraph.prototype.idxToRow_=function(a){if(a<0){return -1}var d=-1;for(var c=0;c<this.boundaryIds_.length;c++){if(this.boundaryIds_[c]!==undefined){d=c;break}}if(d<0){return -1}for(var b in this.layout_.datasets){if(a<this.layout_.datasets[b].length){return this.boundaryIds_[d][0]+a}a-=this.layout_.datasets[b].length}return -1};Dygraph.prototype.generateLegendDashHTML_=function(o,d,n){var h="";var f,e,b,k;var c=0,m=0;var l=[];var g;var a=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(a){return"&mdash;"}if(!o||o.length<=1){h='<div style="display: inline-block; position: relative; bottom: .5ex; padding-left: 1em; height: 1px; border-bottom: 2px solid '+d+';"></div>'}else{for(f=0;f<=o.length;f++){c+=o[f%o.length]}g=Math.floor(n/(c-o[0]));if(g>1){for(f=0;f<o.length;f++){l[f]=o[f]/n}m=l.length}else{g=1;for(f=0;f<o.length;f++){l[f]=o[f]/c}m=l.length+1}for(e=0;e<g;e++){for(f=0;f<m;f+=2){b=l[f%l.length];if(f<o.length){k=l[(f+1)%l.length]}else{k=0}h+='<div style="display: inline-block; position: relative; bottom: .5ex; margin-right: '+k+"em; padding-left: "+b+"em; height: 1px; border-bottom: 2px solid "+d+';"></div>'}}}return h};Dygraph.prototype.generateLegendHTML_=function(k,f,b){var l,u,o,s,m,g;if(typeof(k)==="undefined"){if(this.attr_("legend")!="always"){return""}u=this.attr_("labelsSeparateLines");var r=this.attr_("labels");l="";for(o=1;o<r.length;o++){if(!this.visibility()[o-1]){continue}s=this.plotter_.colors[r[o]];if(l!==""){l+=(u?"<br/>":" ")}g=this.attr_("strokePattern",r[o]);m=this.generateLegendDashHTML_(g,s,b);l+="<span style='font-weight: bold; color: "+s+";'>"+m+" "+r[o]+"</span>"}return l}var t=this.optionsViewForAxis_("x");var h=t("valueFormatter");l=h(k,t,this.attr_("labels")[0],this)+":";var p=[];var d=this.numAxes();for(o=0;o<d;o++){p[o]=this.optionsViewForAxis_("y"+(o?1+o:""))}var e=this.attr_("labelsShowZeroValues");u=this.attr_("labelsSeparateLines");for(o=0;o<this.selPoints_.length;o++){var n=this.selPoints_[o];if(n.yval===0&&!e){continue}if(!Dygraph.isOK(n.canvasy)){continue}if(u){l+="<br/>"}var j=p[this.seriesToAxisMap_[n.name]];var q=j("valueFormatter");s=this.plotter_.colors[n.name];var a=q(n.yval,j,n.name,this);l+=" <b><span style='color: "+s+";'>"+n.name+"</span></b>:"+a}return l};Dygraph.prototype.setLegendHTML_=function(b,e){var c=this.attr_("labelsDiv");var f=document.createElement("span");f.setAttribute("style","margin: 0; padding: 0 0 0 1em; border: 0;");c.appendChild(f);var a=f.offsetWidth;var d=this.generateLegendHTML_(b,e,a);if(c!==null){c.innerHTML=d}else{if(typeof(this.shown_legend_error_)=="undefined"){this.error("labelsDiv is set to something nonexistent; legend will not be shown.");this.shown_legend_error_=true}}};Dygraph.prototype.updateSelection_=function(){var d;var h=this.canvas_ctx_;if(this.previousVerticalX_>=0){var e=0;var f=this.attr_("labels");for(d=1;d<f.length;d++){var b=this.attr_("highlightCircleSize",f[d]);if(b>e){e=b}}var g=this.previousVerticalX_;h.clearRect(g-e-1,0,2*e+2,this.height_)}if(this.isUsingExcanvas_&&this.currentZoomRectArgs_){Dygraph.prototype.drawZoomRect_.apply(this,this.currentZoomRectArgs_)}if(this.selPoints_.length>0){if(this.attr_("showLabelsOnHighlight")){this.setLegendHTML_(this.lastx_,this.selPoints_)}var c=this.selPoints_[0].canvasx;h.save();for(d=0;d<this.selPoints_.length;d++){var j=this.selPoints_[d];if(!Dygraph.isOK(j.canvasy)){continue}var a=this.attr_("highlightCircleSize",j.name);h.beginPath();h.fillStyle=this.plotter_.colors[j.name];h.arc(c,j.canvasy,a,0,2*Math.PI,false);h.fill()}h.restore();this.previousVerticalX_=c}};Dygraph.prototype.setSelection=function(c){this.selPoints_=[];var d=0;if(c!==false){c=c-this.boundaryIds_[0][0]}if(c!==false&&c>=0){for(var b in this.layout_.datasets){if(c<this.layout_.datasets[b].length){var a=this.layout_.points[d+c];if(this.attr_("stackedGraph")){a=this.layout_.unstackPointAtIndex(d+c)}this.selPoints_.push(a)}d+=this.layout_.datasets[b].length}}if(this.selPoints_.length){this.lastx_=this.selPoints_[0].xval;this.updateSelection_()}else{this.clearSelection()}};Dygraph.prototype.mouseOut_=function(a){if(this.attr_("unhighlightCallback")){this.attr_("unhighlightCallback")(a)}if(this.attr_("hideOverlayOnMouseOut")){this.clearSelection()}};Dygraph.prototype.clearSelection=function(){this.canvas_ctx_.clearRect(0,0,this.width_,this.height_);this.setLegendHTML_();this.selPoints_=[];this.lastx_=-1};Dygraph.prototype.getSelection=function(){if(!this.selPoints_||this.selPoints_.length<1){return -1}for(var a=0;a<this.layout_.points.length;a++){if(this.layout_.points[a].x==this.selPoints_[0].x){return a+this.boundaryIds_[0][0]}}return -1};Dygraph.prototype.loadedEvent_=function(a){this.rawData_=this.parseCSV_(a);this.predraw_()};Dygraph.prototype.addXTicks_=function(){var a;if(this.dateWindow_){a=[this.dateWindow_[0],this.dateWindow_[1]]}else{a=this.fullXRange_()}var c=this.optionsViewForAxis_("x");var b=c("ticker")(a[0],a[1],this.width_,c,this);this.layout_.setXTicks(b)};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null,c,g;var b=this.attr_("errorBars")||this.attr_("customBars");if(b){for(c=0;c<d.length;c++){g=d[c][1][0];if(!g){continue}var a=g-d[c][1][1];var e=g+d[c][1][2];if(a>g){a=g}if(e<g){e=g}if(f===null||e>f){f=e}if(h===null||a<h){h=a}}}else{for(c=0;c<d.length;c++){g=d[c][1];if(g===null||isNaN(g)){continue}if(f===null||g>f){f=g}if(h===null||g<h){h=g}}}return[h,f]};Dygraph.prototype.predraw_=function(){var f=new Date();this.computeYAxes_();if(this.plotter_){this.plotter_.clear()}this.plotter_=new DygraphCanvasRenderer(this,this.hidden_,this.hidden_ctx_,this.layout_);this.createRollInterface_();this.positionLabelsDiv_();if(this.rangeSelector_){this.rangeSelector_.renderStaticLayer()}this.rolledSeries_=[null];for(var c=1;c<this.numColumns();c++){var e=this.attr_("connectSeparatedPoints",c);var d=this.attr_("logscale",c);var b=this.extractSeries_(this.rawData_,c,d,e);b=this.rollingAverage(b,this.rollPeriod_);this.rolledSeries_.push(b)}this.drawGraph_();var a=new Date();this.drawingTimeMs_=(a-f)};Dygraph.prototype.gatherDatasets_=function(w,c){var s=[];var b=[];var e=[];var a={};var u,t,r;var m=w.length-1;for(u=m;u>=1;u--){if(!this.visibility()[u-1]){continue}var h=[];for(t=0;t<w[u].length;t++){h.push(w[u][t])}var o=this.attr_("errorBars")||this.attr_("customBars");if(c){var A=c[0];var f=c[1];var p=[];var d=null,z=null;for(r=0;r<h.length;r++){if(h[r][0]>=A&&d===null){d=r}if(h[r][0]<=f){z=r}}if(d===null){d=0}if(d>0){d--}if(z===null){z=h.length-1}if(z<h.length-1){z++}s[u-1]=[d,z];for(r=d;r<=z;r++){p.push(h[r])}h=p}else{s[u-1]=[0,h.length-1]}var n=this.extremeValues_(h);if(o){for(t=0;t<h.length;t++){h[t]=[h[t][0],h[t][1][0],h[t][1][1],h[t][1][2]]}}else{if(this.attr_("stackedGraph")){var q=h.length;var y;for(t=0;t<q;t++){var g=h[t][0];if(b[g]===undefined){b[g]=0}y=h[t][1];b[g]+=y;h[t]=[g,b[g]];if(b[g]>n[1]){n[1]=b[g]}if(b[g]<n[0]){n[0]=b[g]}}}}var v=this.attr_("labels")[u];a[v]=n;e[u]=h}return[e,a,s]};Dygraph.prototype.drawGraph_=function(j){var a=new Date();if(typeof(j)==="undefined"){j=true}var e=this.is_initial_draw_;this.is_initial_draw_=false;this.layout_.removeAllDatasets();this.setColors_();this.attrs_.pointSize=0.5*this.attr_("highlightCircleSize");var g=this.gatherDatasets_(this.rolledSeries_,this.dateWindow_);var d=g[0];var h=g[1];this.boundaryIds_=g[2];for(var f=1;f<d.length;f++){if(!this.visibility()[f-1]){continue}this.layout_.addDataset(this.attr_("labels")[f],d[f])}this.computeYAxisRanges_(h);this.layout_.setYAxes(this.axes_);this.addXTicks_();var b=this.zoomed_x_;this.layout_.setDateWindow(this.dateWindow_);this.zoomed_x_=b;this.layout_.evaluateWithError();this.renderGraph_(e,false);if(this.attr_("timingName")){var c=new Date();if(console){console.log(this.attr_("timingName")+" - drawGraph: "+(c-a)+"ms")}}};Dygraph.prototype.renderGraph_=function(a,b){this.plotter_.clear();this.plotter_.render();this.canvas_.getContext("2d").clearRect(0,0,this.canvas_.width,this.canvas_.height);this.setLegendHTML_();if(!a){if(b){if(typeof(this.selPoints_)!=="undefined"&&this.selPoints_.length){this.clearSelection()}else{this.clearSelection()}}}if(this.rangeSelector_){this.rangeSelector_.renderInteractiveLayer()}if(this.attr_("drawCallback")!==null){this.attr_("drawCallback")(this,a)}};Dygraph.prototype.computeYAxes_=function(){var g,c,m,b,j,a,p;if(this.axes_!==undefined&&this.user_attrs_.hasOwnProperty("valueRange")===false){c=[];for(j=0;j<this.axes_.length;j++){c.push(this.axes_[j].valueWindow)}}this.axes_=[{yAxisId:0,g:this}];this.seriesToAxisMap_={};var h=this.attr_("labels");var f={};for(g=1;g<h.length;g++){f[h[g]]=(g-1)}var e=["includeZero","valueRange","labelsKMB","labelsKMG2","pixelsPerYLabel","yAxisLabelWidth","axisLabelFontSize","axisTickSize","logscale"];for(g=0;g<e.length;g++){var d=e[g];p=this.attr_(d);if(p){this.axes_[0][d]=p}}for(m in f){if(!f.hasOwnProperty(m)){continue}b=this.attr_("axis",m);if(b===null){this.seriesToAxisMap_[m]=0;continue}if(typeof(b)=="object"){a={};Dygraph.update(a,this.axes_[0]);Dygraph.update(a,{valueRange:null});var o=this.axes_.length;a.yAxisId=o;a.g=this;Dygraph.update(a,b);this.axes_.push(a);this.seriesToAxisMap_[m]=o}}for(m in f){if(!f.hasOwnProperty(m)){continue}b=this.attr_("axis",m);if(typeof(b)=="string"){if(!this.seriesToAxisMap_.hasOwnProperty(b)){this.error("Series "+m+" wants to share a y-axis with series "+b+", which does not define its own axis.");return null}var n=this.seriesToAxisMap_[b];this.seriesToAxisMap_[m]=n}}if(c!==undefined){for(j=0;j<c.length;j++){this.axes_[j].valueWindow=c[j]}}for(b=0;b<this.axes_.length;b++){if(b===0){a=this.optionsViewForAxis_("y"+(b?"2":""));p=a("valueRange");if(p){this.axes_[b].valueRange=p}}else{var l=this.user_attrs_.axes;if(l&&l.y2){p=l.y2.valueRange;if(p){this.axes_[b].valueRange=p}}}}};Dygraph.prototype.numAxes=function(){var c=0;for(var b in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(b)){continue}var a=this.seriesToAxisMap_[b];if(a>c){c=a}}return 1+c};Dygraph.prototype.axisPropertiesForSeries=function(a){return this.axes_[this.seriesToAxisMap_[a]]};Dygraph.prototype.computeYAxisRanges_=function(a){var g=[],h;for(h in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(h)){continue}var p=this.seriesToAxisMap_[h];while(g.length<=p){g.push([])}g[p].push(h)}for(var u=0;u<this.axes_.length;u++){var b=this.axes_[u];if(!g[u]){b.extremeRange=[0,1]}else{h=g[u];var x=Infinity;var w=-Infinity;var o,m;for(var s=0;s<h.length;s++){if(!a.hasOwnProperty(h[s])){continue}o=a[h[s]][0];if(o!==null){x=Math.min(o,x)}m=a[h[s]][1];if(m!==null){w=Math.max(m,w)}}if(b.includeZero&&x>0){x=0}if(x==Infinity){x=0}if(w==-Infinity){w=1}var t=w-x;if(t===0){t=w}var d,z;if(b.logscale){d=w+0.1*t;z=x}else{d=w+0.1*t;z=x-0.1*t;if(!this.attr_("avoidMinZero")){if(z<0&&x>=0){z=0}if(d>0&&w<=0){d=0}}if(this.attr_("includeZero")){if(w<0){d=0}if(x>0){z=0}}}b.extremeRange=[z,d]}if(b.valueWindow){b.computedValueRange=[b.valueWindow[0],b.valueWindow[1]]}else{if(b.valueRange){b.computedValueRange=[b.valueRange[0],b.valueRange[1]]}else{b.computedValueRange=b.extremeRange}}var n=this.optionsViewForAxis_("y"+(u?"2":""));var y=n("ticker");if(u===0||b.independentTicks){b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this)}else{var l=this.axes_[0];var e=l.ticks;var f=l.computedValueRange[1]-l.computedValueRange[0];var A=b.computedValueRange[1]-b.computedValueRange[0];var c=[];for(var r=0;r<e.length;r++){var q=(e[r].v-l.computedValueRange[0])/f;var v=b.computedValueRange[0]+q*A;c.push(v)}b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this,c)}}};Dygraph.prototype.extractSeries_=function(h,e,g,f){var d=[];for(var c=0;c<h.length;c++){var b=h[c][0];var a=h[c][e];if(g){if(a<=0){a=null}d.push([b,a])}else{if(a!==null||!f){d.push([b,a])}}}return d};Dygraph.prototype.rollingAverage=function(l,d){if(l.length<2){return l}d=Math.min(d,l.length);var b=[];var s=this.attr_("sigma");var E,o,w,v,m,c,D,x;if(this.fractions_){var k=0;var h=0;var e=100;for(w=0;w<l.length;w++){k+=l[w][1][0];h+=l[w][1][1];if(w-d>=0){k-=l[w-d][1][0];h-=l[w-d][1][1]}var A=l[w][0];var u=h?k/h:0;if(this.attr_("errorBars")){if(this.attr_("wilsonInterval")){if(h){var r=u<0?0:u,t=h;var z=s*Math.sqrt(r*(1-r)/t+s*s/(4*t*t));var a=1+s*s/h;E=(r+s*s/(2*h)-z)/a;o=(r+s*s/(2*h)+z)/a;b[w]=[A,[r*e,(r-E)*e,(o-r)*e]]}else{b[w]=[A,[0,0,0]]}}else{x=h?s*Math.sqrt(u*(1-u)/h):1;b[w]=[A,[e*u,e*x,e*x]]}}else{b[w]=[A,e*u]}}}else{if(this.attr_("customBars")){E=0;var B=0;o=0;var g=0;for(w=0;w<l.length;w++){var C=l[w][1];m=C[1];b[w]=[l[w][0],[m,m-C[0],C[2]-m]];if(m!==null&&!isNaN(m)){E+=C[0];B+=m;o+=C[2];g+=1}if(w-d>=0){var q=l[w-d];if(q[1][1]!==null&&!isNaN(q[1][1])){E-=q[1][0];B-=q[1][1];o-=q[1][2];g-=1}}if(g){b[w]=[l[w][0],[1*B/g,1*(B-E)/g,1*(o-B)/g]]}else{b[w]=[l[w][0],[null,null,null]]}}}else{if(!this.attr_("errorBars")){if(d==1){return l}for(w=0;w<l.length;w++){c=0;D=0;for(v=Math.max(0,w-d+1);v<w+1;v++){m=l[v][1];if(m===null||isNaN(m)){continue}D++;c+=l[v][1]}if(D){b[w]=[l[w][0],c/D]}else{b[w]=[l[w][0],null]}}}else{for(w=0;w<l.length;w++){c=0;var f=0;D=0;for(v=Math.max(0,w-d+1);v<w+1;v++){m=l[v][1][0];if(m===null||isNaN(m)){continue}D++;c+=l[v][1][0];f+=Math.pow(l[v][1][1],2)}if(D){x=Math.sqrt(f)/D;b[w]=[l[w][0],[c/D,s*x,s*x]]}else{b[w]=[l[w][0],[null,null,null]]}}}}}return b};Dygraph.prototype.detectTypeFromString_=function(b){var a=false;var c=b.indexOf("-");if((c>0&&(b[c-1]!="e"&&b[c-1]!="E"))||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}if(a){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueParser=function(d){return parseFloat(d)};this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}};Dygraph.prototype.parseFloat_=function(a,c,b){var e=parseFloat(a);if(!isNaN(e)){return e}if(/^ *$/.test(a)){return null}if(/^ *nan *$/i.test(a)){return NaN}var d="Unable to parse '"+a+"' as a number";if(b!==null&&c!==null){d+=" on line "+(1+c)+" ('"+b+"') of CSV."}this.error(d);return null};Dygraph.prototype.parseCSV_=function(s){var r=[];var a=s.split("\n");var g,k;var p=this.attr_("delimiter");if(a[0].indexOf(p)==-1&&a[0].indexOf("\t")>=0){p="\t"}var b=0;if(!("labels" in this.user_attrs_)){b=1;this.attrs_.labels=a[0].split(p)}var o=0;var m;var q=false;var c=this.attr_("labels").length;var f=false;for(var l=b;l<a.length;l++){var e=a[l];o=l;if(e.length===0){continue}if(e[0]=="#"){continue}var d=e.split(p);if(d.length<2){continue}var h=[];if(!q){this.detectTypeFromString_(d[0]);m=this.attr_("xValueParser");q=true}h[0]=m(d[0],this);if(this.fractions_){for(k=1;k<d.length;k++){g=d[k].split("/");if(g.length!=2){this.error('Expected fractional "num/den" values in CSV data but found a value \''+d[k]+"' on line "+(1+l)+" ('"+e+"') which is not of this form.");h[k]=[0,0]}else{h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e)]}}}else{if(this.attr_("errorBars")){if(d.length%2!=1){this.error("Expected alternating (value, stdev.) pairs in CSV data but line "+(1+l)+" has an odd number of values ("+(d.length-1)+"): '"+e+"'")}for(k=1;k<d.length;k+=2){h[(k+1)/2]=[this.parseFloat_(d[k],l,e),this.parseFloat_(d[k+1],l,e)]}}else{if(this.attr_("customBars")){for(k=1;k<d.length;k++){var t=d[k];if(/^ *$/.test(t)){h[k]=[null,null,null]}else{g=t.split(";");if(g.length==3){h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e),this.parseFloat_(g[2],l,e)]}else{this.warn('When using customBars, values must be either blank or "low;center;high" tuples (got "'+t+'" on line '+(1+l))}}}}else{for(k=1;k<d.length;k++){h[k]=this.parseFloat_(d[k],l,e)}}}}if(r.length>0&&h[0]<r[r.length-1][0]){f=true}if(h.length!=c){this.error("Number of columns in line "+l+" ("+h.length+") does not agree with number of labels ("+c+") "+e)}if(l===0&&this.attr_("labels")){var n=true;for(k=0;n&&k<h.length;k++){if(h[k]){n=false}}if(n){this.warn("The dygraphs 'labels' option is set, but the first row of CSV data ('"+e+"') appears to also contain labels. Will drop the CSV labels and use the option labels.");continue}}r.push(h)}if(f){this.warn("CSV is out of order; order it correctly to speed loading.");r.sort(function(j,i){return j[0]-i[0]})}return r};Dygraph.prototype.parseArray_=function(b){if(b.length===0){this.error("Can't plot empty data set");return null}if(b[0].length===0){this.error("Data set cannot contain an empty row");return null}var a;if(this.attr_("labels")===null){this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter");this.attrs_.labels=["X"];for(a=1;a<b[0].length;a++){this.attrs_.labels.push("Y"+a)}}if(Dygraph.isDateLike(b[0][0])){this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter;this.attrs_.axes.x.ticker=Dygraph.dateTicker;var c=Dygraph.clone(b);for(a=0;a<b.length;a++){if(c[a].length===0){this.error("Row "+(1+a)+" of data is empty");return null}if(c[a][0]===null||typeof(c[a][0].getTime)!="function"||isNaN(c[a][0].getTime())){this.error("x value in row "+(1+a)+" is not a Date");return null}c[a][0]=c[a][0].getTime()}return c}else{this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.axisLabelFormatter=Dygraph.numberAxisLabelFormatter;this.attrs_.axes.x.ticker=Dygraph.numericTicks;return b}};Dygraph.prototype.parseDataTable_=function(w){var d=function(i){var j=String.fromCharCode(65+i%26);i=Math.floor(i/26);while(i>0){j=String.fromCharCode(65+(i-1)%26)+j.toLowerCase();i=Math.floor((i-1)/26)}return j};var h=w.getNumberOfColumns();var g=w.getNumberOfRows();var f=w.getColumnType(0);if(f=="date"||f=="datetime"){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{if(f=="number"){this.attrs_.xValueParser=function(i){return parseFloat(i)};this.attrs_.axes.x.valueFormatter=function(i){return i};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+f+"')");return null}}var m=[];var t={};var s=false;var q,o;for(q=1;q<h;q++){var b=w.getColumnType(q);if(b=="number"){m.push(q)}else{if(b=="string"&&this.attr_("displayAnnotations")){var r=m[m.length-1];if(!t.hasOwnProperty(r)){t[r]=[q]}else{t[r].push(q)}s=true}else{this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true")}}}var u=[w.getColumnLabel(0)];for(q=0;q<m.length;q++){u.push(w.getColumnLabel(m[q]));if(this.attr_("errorBars")){q+=1}}this.attrs_.labels=u;h=u.length;var v=[];var l=false;var a=[];for(q=0;q<g;q++){var e=[];if(typeof(w.getValue(q,0))==="undefined"||w.getValue(q,0)===null){this.warn("Ignoring row "+q+" of DataTable because of undefined or null first column.");continue}if(f=="date"||f=="datetime"){e.push(w.getValue(q,0).getTime())}else{e.push(w.getValue(q,0))}if(!this.attr_("errorBars")){for(o=0;o<m.length;o++){var c=m[o];e.push(w.getValue(q,c));if(s&&t.hasOwnProperty(c)&&w.getValue(q,t[c][0])!==null){var p={};p.series=w.getColumnLabel(c);p.xval=e[0];p.shortText=d(a.length);p.text="";for(var n=0;n<t[c].length;n++){if(n){p.text+="\n"}p.text+=w.getValue(q,t[c][n])}a.push(p)}}for(o=0;o<e.length;o++){if(!isFinite(e[o])){e[o]=null}}}else{for(o=0;o<h-1;o++){e.push([w.getValue(q,1+2*o),w.getValue(q,2+2*o)])}}if(v.length>0&&e[0]<v[v.length-1][0]){l=true}v.push(e)}if(l){this.warn("DataTable is out of order; order it correctly to speed loading.");v.sort(function(j,i){return j[0]-i[0]})}this.rawData_=v;if(a.length>0){this.setAnnotations(a,true)}};Dygraph.prototype.start_=function(){var c=this.file_;if(typeof c=="function"){c=c()}if(Dygraph.isArrayLike(c)){this.rawData_=this.parseArray_(c);this.predraw_()}else{if(typeof c=="object"&&typeof c.getColumnRange=="function"){this.parseDataTable_(c);this.predraw_()}else{if(typeof c=="string"){if(c.indexOf("\n")>=0){this.loadedEvent_(c)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status===200||b.status===0){a.loadedEvent_(b.responseText)}}};b.open("GET",c,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof c))}}}};Dygraph.prototype.updateOptions=function(e,b){if(typeof(b)=="undefined"){b=false}var d=e.file;var c=Dygraph.mapLegacyOptions_(e);if("rollPeriod" in c){this.rollPeriod_=c.rollPeriod}if("dateWindow" in c){this.dateWindow_=c.dateWindow;if(!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_x_=(c.dateWindow!==null)}}if("valueRange" in c&&!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_y_=(c.valueRange!==null)}var a=Dygraph.isPixelChangingOptionList(this.attr_("labels"),c);Dygraph.updateDeep(this.user_attrs_,c);if(d){this.file_=d;if(!b){this.start_()}}else{if(!b){if(a){this.predraw_()}else{this.renderGraph_(false,false)}}}};Dygraph.mapLegacyOptions_=function(c){var a={};for(var b in c){if(b=="file"){continue}if(c.hasOwnProperty(b)){a[b]=c[b]}}var e=function(g,f,h){if(!a.axes){a.axes={}}if(!a.axes[g]){a.axes[g]={}}a.axes[g][f]=h};var d=function(f,g,h){if(typeof(c[f])!="undefined"){e(g,h,c[f]);delete a[f]}};d("xValueFormatter","x","valueFormatter");d("pixelsPerXLabel","x","pixelsPerLabel");d("xAxisLabelFormatter","x","axisLabelFormatter");d("xTicker","x","ticker");d("yValueFormatter","y","valueFormatter");d("pixelsPerYLabel","y","pixelsPerLabel");d("yAxisLabelFormatter","y","axisLabelFormatter");d("yTicker","y","ticker");return a};Dygraph.prototype.resize=function(d,b){if(this.resize_lock){return}this.resize_lock=true;if((d===null)!=(b===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");d=b=null}var a=this.width_;var c=this.height_;if(d){this.maindiv_.style.width=d+"px";this.maindiv_.style.height=b+"px";this.width_=d;this.height_=b}else{this.width_=this.maindiv_.clientWidth;this.height_=this.maindiv_.clientHeight}if(a!=this.width_||c!=this.height_){this.maindiv_.innerHTML="";this.roller_=null;this.attrs_.labelsDiv=null;this.createInterface_();if(this.annotations_.length){this.layout_.setAnnotations(this.annotations_)}this.predraw_()}this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.predraw_()};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length<this.numColumns()-1){this.attrs_.visibility.push(true)}return this.attr_("visibility")};Dygraph.prototype.setVisibility=function(b,c){var a=this.visibility();if(b<0||b>=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.predraw_()}};Dygraph.prototype.size=function(){return{width:this.width_,height:this.height_}};Dygraph.prototype.setAnnotations=function(b,a){Dygraph.addAnnotationRule();this.annotations_=b;this.layout_.setAnnotations(this.annotations_);if(!a){this.predraw_()}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.indexFromSetName=function(a){var c=this.attr_("labels");for(var b=0;b<c.length;b++){if(c[b]==a){return b}}return null};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var f="border: 1px solid black; background-color: white; text-align: center;";var e=document.createElement("style");e.type="text/css";document.getElementsByTagName("head")[0].appendChild(e);for(var b=0;b<document.styleSheets.length;b++){if(document.styleSheets[b].disabled){continue}var d=document.styleSheets[b];try{if(d.insertRule){var a=d.cssRules?d.cssRules.length:0;d.insertRule(".dygraphDefaultAnnotation { "+f+" }",a)}else{if(d.addRule){d.addRule(".dygraphDefaultAnnotation",f)}}Dygraph.addedAnnotationCSS=true;return}catch(c){}}this.warn("Unable to add default annotation CSS rule; display may be off.")};var DateGraph=Dygraph;"use strict";Dygraph.LOG_SCALE=10;Dygraph.LN_TEN=Math.log(Dygraph.LOG_SCALE);Dygraph.log10=function(a){return Math.log(a)/Dygraph.LN_TEN};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.LOG_STACK_TRACES=false;Dygraph.DOTTED_LINE=[2,2];Dygraph.DASHED_LINE=[7,3];Dygraph.DOT_DASH_LINE=[7,2,2,2];Dygraph.log=function(b,d){var a;if(typeof(printStackTrace)!="undefined"){a=printStackTrace({guess:false});while(a[0].indexOf("stacktrace")!=-1){a.splice(0,1)}a.splice(0,2);for(var c=0;c<a.length;c++){a[c]=a[c].replace(/\([^)]*\/(.*)\)/,"@$1").replace(/\@.*\/([^\/]*)/,"@$1").replace("[object Object].","")}var e=a.splice(0,1)[0];d+=" ("+e.replace(/^.*@ ?/,"")+")"}if(typeof(console)!="undefined"){switch(b){case Dygraph.DEBUG:console.debug("dygraphs: "+d);break;case Dygraph.INFO:console.info("dygraphs: "+d);break;case Dygraph.WARNING:console.warn("dygraphs: "+d);break;case Dygraph.ERROR:console.error("dygraphs: "+d);break}}if(Dygraph.LOG_STACK_TRACES){console.log(a.join("\n"))}};Dygraph.info=function(a){Dygraph.log(Dygraph.INFO,a)};Dygraph.prototype.info=Dygraph.info;Dygraph.warn=function(a){Dygraph.log(Dygraph.WARNING,a)};Dygraph.prototype.warn=Dygraph.warn;Dygraph.error=function(a){Dygraph.log(Dygraph.ERROR,a)};Dygraph.prototype.error=Dygraph.error;Dygraph.getContext=function(a){return a.getContext("2d")};Dygraph.addEvent=function addEvent(c,b,a){if(c.addEventListener){c.addEventListener(b,a,false)}else{c[b+a]=function(){a(window.event)};c.attachEvent("on"+b,c[b+a])}};Dygraph.removeEvent=function addEvent(c,b,a){if(c.removeEventListener){c.removeEventListener(b,a,false)}else{c.detachEvent("on"+b,c[b+a]);c[b+a]=null}};Dygraph.cancelEvent=function(a){a=a?a:window.event;if(a.stopPropagation){a.stopPropagation()}if(a.preventDefault){a.preventDefault()}a.cancelBubble=true;a.cancel=true;a.returnValue=false;return false};Dygraph.hsvToRGB=function(h,g,k){var c;var d;var l;if(g===0){c=k;d=k;l=k}else{var e=Math.floor(h*6);var j=(h*6)-e;var b=k*(1-g);var a=k*(1-(g*j));var m=k*(1-(g*(1-j)));switch(e){case 1:c=a;d=k;l=b;break;case 2:c=b;d=k;l=m;break;case 3:c=b;d=a;l=k;break;case 4:c=m;d=b;l=k;break;case 5:c=k;d=b;l=a;break;case 6:case 0:c=k;d=m;l=b;break}}c=Math.floor(255*c+0.5);d=Math.floor(255*d+0.5);l=Math.floor(255*l+0.5);return"rgb("+c+","+d+","+l+")"};Dygraph.findPosX=function(b){var c=0;if(b.offsetParent){var a=b;while(1){c+=a.offsetLeft;if(!a.offsetParent){break}a=a.offsetParent}}else{if(b.x){c+=b.x}}while(b&&b!=document.body){c-=b.scrollLeft;b=b.parentNode}return c};Dygraph.findPosY=function(c){var b=0;if(c.offsetParent){var a=c;while(1){b+=a.offsetTop;if(!a.offsetParent){break}a=a.offsetParent}}else{if(c.y){b+=c.y}}while(c&&c!=document.body){b-=c.scrollTop;c=c.parentNode}return b};Dygraph.pageX=function(c){if(c.pageX){return(!c.pageX||c.pageX<0)?0:c.pageX}else{var d=document;var a=document.body;return c.clientX+(d.scrollLeft||a.scrollLeft)-(d.clientLeft||0)}};Dygraph.pageY=function(c){if(c.pageY){return(!c.pageY||c.pageY<0)?0:c.pageY}else{var d=document;var a=document.body;return c.clientY+(d.scrollTop||a.scrollTop)-(d.clientTop||0)}};Dygraph.isOK=function(a){return a&&!isNaN(a)};Dygraph.floatFormat=function(a,b){var c=Math.min(Math.max(1,b||2),21);return(Math.abs(a)<0.001&&a!==0)?a.toExponential(c-1):a.toPrecision(c)};Dygraph.zeropad=function(a){if(a<10){return"0"+a}else{return""+a}};Dygraph.hmsString_=function(a){var c=Dygraph.zeropad;var b=new Date(a);if(b.getSeconds()){return c(b.getHours())+":"+c(b.getMinutes())+":"+c(b.getSeconds())}else{return c(b.getHours())+":"+c(b.getMinutes())}};Dygraph.round_=function(c,b){var a=Math.pow(10,b);return Math.round(c*a)/a};Dygraph.binarySearch=function(a,d,i,e,b){if(e===null||e===undefined||b===null||b===undefined){e=0;b=d.length-1}if(e>b){return -1}if(i===null||i===undefined){i=0}var h=function(j){return j>=0&&j<d.length};var g=parseInt((e+b)/2,10);var c=d[g];if(c==a){return g}var f;if(c>a){if(i>0){f=g-1;if(h(f)&&d[f]<a){return g}}return Dygraph.binarySearch(a,d,i,e,g-1)}if(c<a){if(i<0){f=g+1;if(h(f)&&d[f]>a){return g}}return Dygraph.binarySearch(a,d,i,g+1,b)}};Dygraph.dateParser=function(a){var b;var c;c=Dygraph.dateStrToMillis(a);if(c&&!isNaN(c)){return c}if(a.search("-")!=-1){b=a.replace("-","/","g");while(b.search("-")!=-1){b=b.replace("-","/")}c=Dygraph.dateStrToMillis(b)}else{if(a.length==8){b=a.substr(0,4)+"/"+a.substr(4,2)+"/"+a.substr(6,2);c=Dygraph.dateStrToMillis(b)}else{c=Dygraph.dateStrToMillis(a)}}if(!c||isNaN(c)){Dygraph.error("Couldn't parse "+a+" as a date")}return c};Dygraph.dateStrToMillis=function(a){return new Date(a).getTime()};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.updateDeep=function(b,d){function c(e){return(typeof Node==="object"?e instanceof Node:typeof e==="object"&&typeof e.nodeType==="number"&&typeof e.nodeName==="string")}if(typeof(d)!="undefined"&&d!==null){for(var a in d){if(d.hasOwnProperty(a)){if(d[a]===null){b[a]=null}else{if(Dygraph.isArrayLike(d[a])){b[a]=d[a].slice()}else{if(c(d[a])){b[a]=d[a]}else{if(typeof(d[a])=="object"){if(typeof(b[a])!="object"){b[a]={}}Dygraph.updateDeep(b[a],d[a])}else{b[a]=d[a]}}}}}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a<c.length;a++){if(Dygraph.isArrayLike(c[a])){b.push(Dygraph.clone(c[a]))}else{b.push(c[a])}}return b};Dygraph.createCanvas=function(){var a=document.createElement("canvas");var b=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(b&&(typeof(G_vmlCanvasManager)!="undefined")){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.isAndroid=function(){return(/Android/).test(navigator.userAgent)};Dygraph.repeatAndCleanup=function(b,g,f,c){var e=0;var d=new Date().getTime();b(e);if(g==1){c();return}(function a(){if(e>=g){return}var h=d+(1+e)*f;setTimeout(function(){e++;b(e);if(e>=g-1){c()}else{a()}},h-new Date().getTime())})()};Dygraph.isPixelChangingOptionList=function(h,e){var d={annotationClickHandler:true,annotationDblClickHandler:true,annotationMouseOutHandler:true,annotationMouseOverHandler:true,axisLabelColor:true,axisLineColor:true,axisLineWidth:true,clickCallback:true,digitsAfterDecimal:true,drawCallback:true,drawPoints:true,drawXGrid:true,drawYGrid: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,maxNumberWidth:true,panEdgeFraction:true,pixelsPerYLabel:true,pointClickCallback:true,pointSize:true,rangeSelectorPlotFillColor:true,rangeSelectorPlotStrokeColor:true,showLabelsOnHighlight:true,showRoller:true,sigFigs:true,strokeWidth:true,underlayCallback:true,unhighlightCallback:true,xAxisLabelFormatter:true,xTicker:true,xValueFormatter:true,yAxisLabelFormatter:true,yValueFormatter:true,zoomCallback:true};var a=false;var b={};if(h){for(var f=1;f<h.length;f++){b[h[f]]=true}}for(var g in e){if(a){break}if(e.hasOwnProperty(g)){if(b[g]){for(var c in e[g]){if(a){break}if(e[g].hasOwnProperty(c)&&!d[c]){a=true}}}else{if(!d[g]){a=true}}}}return a};Dygraph.compareArrays=function(c,b){if(!Dygraph.isArrayLike(c)||!Dygraph.isArrayLike(b)){return false}if(c.length!==b.length){return false}for(var a=0;a<c.length;a++){if(c[a]!==b[a]){return false}}return true};"use strict";Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML="";if(typeof(this.date_graph)!="undefined"){this.date_graph.destroy()}this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var d=[];var e=this.date_graph.getSelection();if(e<0){return d}var b=1;var c=this.date_graph.layout_.datasets;for(var a in c){if(!c.hasOwnProperty(a)){continue}d.push({row:e,column:b});b++}return d};"use strict";Dygraph.Interaction={};Dygraph.Interaction.startPan=function(n,s,c){var q,b;c.isPanning=true;var j=s.xAxisRange();c.dateRange=j[1]-j[0];c.initialLeftmostDate=j[0];c.xUnitsPerPixel=c.dateRange/(s.plotter_.area.w-1);if(s.attr_("panEdgeFraction")){var v=s.width_*s.attr_("panEdgeFraction");var d=s.xAxisExtremes();var h=s.toDomXCoord(d[0])-v;var k=s.toDomXCoord(d[1])+v;var t=s.toDataXCoord(h);var u=s.toDataXCoord(k);c.boundedDates=[t,u];var f=[];var a=s.height_*s.attr_("panEdgeFraction");for(q=0;q<s.axes_.length;q++){b=s.axes_[q];var o=b.extremeRange;var p=s.toDomYCoord(o[0],q)+a;var r=s.toDomYCoord(o[1],q)-a;var m=s.toDataYCoord(p);var e=s.toDataYCoord(r);f[q]=[m,e]}c.boundedValues=f}c.is2DPan=false;for(q=0;q<s.axes_.length;q++){b=s.axes_[q];var l=s.yAxisRange(q);if(b.logscale){b.initialTopValue=Dygraph.log10(l[1]);b.dragValueRange=Dygraph.log10(l[1])-Dygraph.log10(l[0])}else{b.initialTopValue=l[1];b.dragValueRange=l[1]-l[0]}b.unitsPerPixel=b.dragValueRange/(s.plotter_.area.h-1);if(b.valueWindow||b.valueRange){c.is2DPan=true}}};Dygraph.Interaction.movePan=function(b,k,c){c.dragEndX=k.dragGetX_(b,c);c.dragEndY=k.dragGetY_(b,c);var h=c.initialLeftmostDate-(c.dragEndX-c.dragStartX)*c.xUnitsPerPixel;if(c.boundedDates){h=Math.max(h,c.boundedDates[0])}var a=h+c.dateRange;if(c.boundedDates){if(a>c.boundedDates[1]){h=h-(a-c.boundedDates[1]);a=h+c.dateRange}}k.dateWindow_=[h,a];if(c.is2DPan){for(var j=0;j<k.axes_.length;j++){var e=k.axes_[j];var d=c.dragEndY-c.dragStartY;var n=d*e.unitsPerPixel;var f=c.boundedValues?c.boundedValues[j]:null;var l=e.initialTopValue+n;if(f){l=Math.min(l,f[1])}var m=l-e.dragValueRange;if(f){if(m<f[0]){l=l-(m-f[0]);m=l-e.dragValueRange}}if(e.logscale){e.valueWindow=[Math.pow(Dygraph.LOG_SCALE,m),Math.pow(Dygraph.LOG_SCALE,l)]}else{e.valueWindow=[m,l]}}}k.drawGraph_(false)};Dygraph.Interaction.endPan=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}a.isPanning=false;a.is2DPan=false;a.initialLeftmostDate=null;a.dateRange=null;a.valueRange=null;a.boundedDates=null;a.boundedValues=null};Dygraph.Interaction.startZoom=function(c,b,a){a.isZooming=true};Dygraph.Interaction.moveZoom=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragStartX-a.dragEndX);var d=Math.abs(a.dragStartY-a.dragEndY);a.dragDirection=(e<d/2)?Dygraph.VERTICAL:Dygraph.HORIZONTAL;b.drawZoomRect_(a.dragDirection,a.dragStartX,a.dragEndX,a.dragStartY,a.dragEndY,a.prevDragDirection,a.prevEndX,a.prevEndY);a.prevEndX=a.dragEndX;a.prevEndY=a.dragEndY;a.prevDragDirection=a.dragDirection};Dygraph.Interaction.treatMouseOpAsClick=function(f,b,d){var k=f.attr_("clickCallback");var n=f.attr_("pointClickCallback");var j=null;if(n){var l=-1;var m=Number.MAX_VALUE;for(var e=0;e<f.selPoints_.length;e++){var c=f.selPoints_[e];var a=Math.pow(c.canvasx-d.dragEndX,2)+Math.pow(c.canvasy-d.dragEndY,2);if(!isNaN(a)&&(l==-1||a<m)){m=a;l=e}}var h=f.attr_("highlightCircleSize")+2;if(m<=h*h){j=f.selPoints_[l]}}if(j){n(b,j)}if(k){k(b,f.lastx_,f.selPoints_)}};Dygraph.Interaction.endZoom=function(c,b,a){a.isZooming=false;a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}if(e>=10&&a.dragDirection==Dygraph.HORIZONTAL){b.doZoomX_(Math.min(a.dragStartX,a.dragEndX),Math.max(a.dragStartX,a.dragEndX))}else{if(d>=10&&a.dragDirection==Dygraph.VERTICAL){b.doZoomY_(Math.min(a.dragStartY,a.dragEndY),Math.max(a.dragStartY,a.dragEndY))}else{b.clearZoomRect_()}}a.dragStartX=null;a.dragStartY=null};Dygraph.Interaction.defaultModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);if(c.altKey||c.shiftKey){Dygraph.startPan(c,b,a)}else{Dygraph.startZoom(c,b,a)}},mousemove:function(c,b,a){if(a.isZooming){Dygraph.moveZoom(c,b,a)}else{if(a.isPanning){Dygraph.movePan(c,b,a)}}},mouseup:function(c,b,a){if(a.isZooming){Dygraph.endZoom(c,b,a)}else{if(a.isPanning){Dygraph.endPan(c,b,a)}}},mouseout:function(c,b,a){if(a.isZooming){a.dragEndX=null;a.dragEndY=null}},dblclick:function(c,b,a){if(c.altKey||c.shiftKey){return}b.doUnzoom_()}};Dygraph.DEFAULT_ATTRS.interactionModel=Dygraph.Interaction.defaultModel;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(c,b,a){a.initializeMouseDown(c,b,a)},mouseup:function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}}};Dygraph.Interaction.dragIsPanInteractionModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);Dygraph.startPan(c,b,a)},mousemove:function(c,b,a){if(a.isPanning){Dygraph.movePan(c,b,a)}},mouseup:function(c,b,a){if(a.isPanning){Dygraph.endPan(c,b,a)}}};"use strict";var DygraphRangeSelector=function(a){this.isIE_=/MSIE/.test(navigator.userAgent)&&!window.opera;this.isUsingExcanvas_=a.isUsingExcanvas_;this.dygraph_=a;this.createCanvases_();if(this.isUsingExcanvas_){this.createIEPanOverlay_()}this.createZoomHandles_();this.initInteraction_()};DygraphRangeSelector.prototype.addToGraph=function(a,b){this.layout_=b;this.resize_();a.appendChild(this.bgcanvas_);a.appendChild(this.fgcanvas_);a.appendChild(this.leftZoomHandle_);a.appendChild(this.rightZoomHandle_)};DygraphRangeSelector.prototype.renderStaticLayer=function(){this.resize_();this.drawStaticLayer_()};DygraphRangeSelector.prototype.renderInteractiveLayer=function(){if(this.isChangingRange_){return}this.placeZoomHandles_();this.drawInteractiveLayer_()};DygraphRangeSelector.prototype.resize_=function(){function c(d,e){d.style.top=e.y+"px";d.style.left=e.x+"px";d.width=e.w;d.height=e.h;d.style.width=d.width+"px";d.style.height=d.height+"px"}var b=this.layout_.getPlotArea();var a=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize");this.canvasRect_={x:b.x,y:b.y+b.h+a+4,w:b.w,h:this.attr_("rangeSelectorHeight")};c(this.bgcanvas_,this.canvasRect_);c(this.fgcanvas_,this.canvasRect_)};DygraphRangeSelector.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphRangeSelector.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_)};DygraphRangeSelector.prototype.createIEPanOverlay_=function(){this.iePanOverlay_=document.createElement("div");this.iePanOverlay_.style.position="absolute";this.iePanOverlay_.style.backgroundColor="white";this.iePanOverlay_.style.filter="alpha(opacity=0)";this.iePanOverlay_.style.display="none";this.iePanOverlay_.style.cursor="move";this.fgcanvas_.appendChild(this.iePanOverlay_)};DygraphRangeSelector.prototype.createZoomHandles_=function(){var a=new Image();a.className="dygraph-rangesel-zoomhandle";a.style.position="absolute";a.style.zIndex=10;a.style.visibility="hidden";a.style.cursor="col-resize";if(/MSIE 7/.test(navigator.userAgent)){a.width=7;a.height=14;a.style.backgroundColor="white";a.style.border="1px solid #333333"}else{a.width=9;a.height=16;a.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAAzwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7sqSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII="}this.leftZoomHandle_=a;this.rightZoomHandle_=a.cloneNode(false)};DygraphRangeSelector.prototype.initInteraction_=function(){var i=this;var f=this.isIE_?document:window;var k=0;var p=null;var n=false;var c=false;var j,d,m,g,q,e,r,o,l,b,h;j=function(w){var v=i.dygraph_.xAxisExtremes();var t=(v[1]-v[0])/i.canvasRect_.w;var u=v[0]+(w.leftHandlePos-i.canvasRect_.x)*t;var s=v[0]+(w.rightHandlePos-i.canvasRect_.x)*t;return[u,s]};d=function(s){Dygraph.cancelEvent(s);n=true;k=s.screenX;p=s.target?s.target:s.srcElement;Dygraph.addEvent(f,"mousemove",m);Dygraph.addEvent(f,"mouseup",g);i.fgcanvas_.style.cursor="col-resize"};m=function(w){if(!n){return}var t=w.screenX-k;if(Math.abs(t)<4){return}k=w.screenX;var v=i.getZoomHandleStatus_();var s;if(p==i.leftZoomHandle_){s=v.leftHandlePos+t;s=Math.min(s,v.rightHandlePos-p.width-3);s=Math.max(s,i.canvasRect_.x)}else{s=v.rightHandlePos+t;s=Math.min(s,i.canvasRect_.x+i.canvasRect_.w);s=Math.max(s,v.leftHandlePos+p.width+3)}var u=p.width/2;p.style.left=(s-u)+"px";i.drawInteractiveLayer_();if(!i.isUsingExcanvas_){q()}};g=function(s){if(!n){return}n=false;Dygraph.removeEvent(f,"mousemove",m);Dygraph.removeEvent(f,"mouseup",g);i.fgcanvas_.style.cursor="default";if(i.isUsingExcanvas_){q()}};q=function(){try{var t=i.getZoomHandleStatus_();i.isChangingRange_=true;if(!t.isZoomed){i.dygraph_.doUnzoom_()}else{var s=j(t);i.dygraph_.doZoomXDates_(s[0],s[1])}}finally{i.isChangingRange_=false}};e=function(u){if(i.isUsingExcanvas_){return u.srcElement==i.iePanOverlay_}else{var s;if(u.offsetX!=undefined){s=i.canvasRect_.x+u.offsetX}else{s=u.clientX}var t=i.getZoomHandleStatus_();return(s>t.leftHandlePos&&s<t.rightHandlePos)}};r=function(s){if(!c&&e(s)&&i.getZoomHandleStatus_().isZoomed){Dygraph.cancelEvent(s);c=true;k=s.screenX;Dygraph.addEvent(f,"mousemove",o);Dygraph.addEvent(f,"mouseup",l)}};o=function(w){if(!c){return}Dygraph.cancelEvent(w);var t=w.screenX-k;if(Math.abs(t)<4){return}k=w.screenX;var v=i.getZoomHandleStatus_();var y=v.leftHandlePos;var s=v.rightHandlePos;var x=s-y;if(y+t<=i.canvasRect_.x){y=i.canvasRect_.x;s=y+x}else{if(s+t>=i.canvasRect_.x+i.canvasRect_.w){s=i.canvasRect_.x+i.canvasRect_.w;y=s-x}else{y+=t;s+=t}}var u=i.leftZoomHandle_.width/2;i.leftZoomHandle_.style.left=(y-u)+"px";i.rightZoomHandle_.style.left=(s-u)+"px";i.drawInteractiveLayer_();if(!i.isUsingExcanvas_){b()}};l=function(s){if(!c){return}c=false;Dygraph.removeEvent(f,"mousemove",o);Dygraph.removeEvent(f,"mouseup",l);if(i.isUsingExcanvas_){b()}};b=function(){try{i.isChangingRange_=true;i.dygraph_.dateWindow_=j(i.getZoomHandleStatus_());i.dygraph_.drawGraph_(false)}finally{i.isChangingRange_=false}};h=function(s){if(n||c){return}var t=e(s)?"move":"default";if(t!=i.fgcanvas_.style.cursor){i.fgcanvas_.style.cursor=t}};this.dygraph_.attrs_.interactionModel=Dygraph.Interaction.dragIsPanInteractionModel;this.dygraph_.attrs_.panEdgeFraction=0.0001;var a=window.opera?"mousedown":"dragstart";Dygraph.addEvent(this.leftZoomHandle_,a,d);Dygraph.addEvent(this.rightZoomHandle_,a,d);if(this.isUsingExcanvas_){Dygraph.addEvent(this.iePanOverlay_,"mousedown",r)}else{Dygraph.addEvent(this.fgcanvas_,"mousedown",r);Dygraph.addEvent(this.fgcanvas_,"mousemove",h)}};DygraphRangeSelector.prototype.drawStaticLayer_=function(){var a=this.bgcanvas_ctx_;a.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);try{this.drawMiniPlot_()}catch(b){Dygraph.warn(b)}var c=0.5;this.bgcanvas_ctx_.lineWidth=1;a.strokeStyle="gray";a.beginPath();a.moveTo(c,c);a.lineTo(c,this.canvasRect_.h-c);a.lineTo(this.canvasRect_.w-c,this.canvasRect_.h-c);a.lineTo(this.canvasRect_.w-c,c);a.stroke()};DygraphRangeSelector.prototype.drawMiniPlot_=function(){var p=this.attr_("rangeSelectorPlotFillColor");var l=this.attr_("rangeSelectorPlotStrokeColor");if(!p&&!l){return}var m=this.computeCombinedSeriesAndLimits_();var e=m.yMax-m.yMin;var r=this.bgcanvas_ctx_;var f=0.5;var j=this.dygraph_.xAxisExtremes();var b=Math.max(j[1]-j[0],1e-30);var q=(this.canvasRect_.w-f)/b;var o=(this.canvasRect_.h-f)/e;var d=this.canvasRect_.w-f;var h=this.canvasRect_.h-f;r.beginPath();r.moveTo(f,h);for(var g=0;g<m.data.length;g++){var a=m.data[g];var n=(a[0]-j[0])*q;var k=h-(a[1]-m.yMin)*o;if(isFinite(n)&&isFinite(k)){r.lineTo(n,k)}}r.lineTo(d,h);r.closePath();if(p){var c=this.bgcanvas_ctx_.createLinearGradient(0,0,0,h);c.addColorStop(0,"white");c.addColorStop(1,p);this.bgcanvas_ctx_.fillStyle=c;r.fill()}if(l){this.bgcanvas_ctx_.strokeStyle=l;this.bgcanvas_ctx_.lineWidth=1.5;r.stroke()}};DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_=function(){var u=this.dygraph_.rawData_;var t=this.attr_("logscale");var p=[];var c;var g;var f,m;var l;var s,r,q;for(s=0;s<u.length;s++){if(u[s].length>1&&u[s][1]!=null){l=typeof u[s][1]!="number";if(l){c=[];g=[];for(q=0;q<u[s][1].length;q++){c.push(0);g.push(0)}}break}}for(s=0;s<u.length;s++){var h=u[s];var d=h[0];if(l){for(q=0;q<c.length;q++){c[q]=g[q]=0}}else{c=g=0}for(r=1;r<h.length;r++){if(this.dygraph_.visibility()[r-1]){if(l){for(q=0;q<c.length;q++){m=h[r][q];if(m===null||isNaN(m)){continue}c[q]+=m;g[q]++}}else{m=h[r];if(m===null||isNaN(m)){continue}c+=m;g++}}}if(l){for(q=0;q<c.length;q++){c[q]/=g[q]}f=c.slice(0)}else{f=c/g}p.push([d,f])}p=this.dygraph_.rollingAverage(p,this.dygraph_.rollPeriod_);if(typeof p[0][1]!="number"){for(s=0;s<p.length;s++){f=p[s][1];p[s][1]=f[0]}}var a=Number.MAX_VALUE;var b=-Number.MAX_VALUE;for(s=0;s<p.length;s++){f=p[s][1];if(f!==null&&isFinite(f)&&(!t||f>0)){a=Math.min(a,f);b=Math.max(b,f)}}var n=0.25;if(t){b=Dygraph.log10(b);b+=b*n;a=Dygraph.log10(a);for(s=0;s<p.length;s++){p[s][1]=Dygraph.log10(p[s][1])}}else{var e;var o=b-a;if(o<=Number.MIN_VALUE){e=b*n}else{e=o*n}b+=e;a-=e}return{data:p,yMin:a,yMax:b}};DygraphRangeSelector.prototype.placeZoomHandles_=function(){var g=this.dygraph_.xAxisExtremes();var a=this.dygraph_.xAxisRange();var b=g[1]-g[0];var i=Math.max(0,(a[0]-g[0])/b);var e=Math.max(0,(g[1]-a[1])/b);var h=this.canvasRect_.x+this.canvasRect_.w*i;var d=this.canvasRect_.x+this.canvasRect_.w*(1-e);var c=Math.max(this.canvasRect_.y,this.canvasRect_.y+(this.canvasRect_.h-this.leftZoomHandle_.height)/2);var f=this.leftZoomHandle_.width/2;this.leftZoomHandle_.style.left=(h-f)+"px";this.leftZoomHandle_.style.top=c+"px";this.rightZoomHandle_.style.left=(d-f)+"px";this.rightZoomHandle_.style.top=this.leftZoomHandle_.style.top;this.leftZoomHandle_.style.visibility="visible";this.rightZoomHandle_.style.visibility="visible"};DygraphRangeSelector.prototype.drawInteractiveLayer_=function(){var b=this.fgcanvas_ctx_;b.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);var e=1;var d=this.canvasRect_.w-e;var a=this.canvasRect_.h-e;var g=this.getZoomHandleStatus_();b.strokeStyle="black";if(!g.isZoomed){b.beginPath();b.moveTo(e,e);b.lineTo(e,a);b.lineTo(d,a);b.lineTo(d,e);b.stroke();if(this.iePanOverlay_){this.iePanOverlay_.style.display="none"}}else{var f=Math.max(e,g.leftHandlePos-this.canvasRect_.x);var c=Math.min(d,g.rightHandlePos-this.canvasRect_.x);b.fillStyle="rgba(240, 240, 240, 0.6)";b.fillRect(0,0,f,this.canvasRect_.h);b.fillRect(c,0,this.canvasRect_.w-c,this.canvasRect_.h);b.beginPath();b.moveTo(e,e);b.lineTo(f,e);b.lineTo(f,a);b.lineTo(c,a);b.lineTo(c,e);b.lineTo(d,e);b.stroke();if(this.isUsingExcanvas_){this.iePanOverlay_.style.width=(c-f)+"px";this.iePanOverlay_.style.left=f+"px";this.iePanOverlay_.style.height=a+"px";this.iePanOverlay_.style.display="inline"}}};DygraphRangeSelector.prototype.getZoomHandleStatus_=function(){var b=this.leftZoomHandle_.width/2;var c=parseInt(this.leftZoomHandle_.style.left,10)+b;var a=parseInt(this.rightZoomHandle_.style.left,10)+b;return{leftHandlePos:c,rightHandlePos:a,isZoomed:(c-1>this.canvasRect_.x||a+1<this.canvasRect_.x+this.canvasRect_.w)}};"use strict";Dygraph.numericTicks=function(I,H,w,r,d,s){var C=r("pixelsPerLabel");var J=[];var F,D,v,A;if(s){for(F=0;F<s.length;F++){J.push({v:s[F]})}}else{if(r("logscale")){A=Math.floor(w/C);var o=Dygraph.binarySearch(I,Dygraph.PREFERRED_LOG_TICK_VALUES,1);var K=Dygraph.binarySearch(H,Dygraph.PREFERRED_LOG_TICK_VALUES,-1);if(o==-1){o=0}if(K==-1){K=Dygraph.PREFERRED_LOG_TICK_VALUES.length-1}var u=null;if(K-o>=A/4){for(var t=K;t>=o;t--){var p=Dygraph.PREFERRED_LOG_TICK_VALUES[t];var m=Math.log(p/I)/Math.log(H/I)*w;var G={v:p};if(u===null){u={tickValue:p,pixel_coord:m}}else{if(Math.abs(m-u.pixel_coord)>=C){u={tickValue:p,pixel_coord:m}}else{G.label=""}}J.push(G)}J.reverse()}}if(J.length===0){var h=r("labelsKMG2");var q;if(h){q=[1,2,4,8]}else{q=[1,2,5]}var L,z,c;for(F=-10;F<50;F++){var g;if(h){g=Math.pow(16,F)}else{g=Math.pow(10,F)}var f=0;for(D=0;D<q.length;D++){L=g*q[D];z=Math.floor(I/L)*L;c=Math.ceil(H/L)*L;A=Math.abs(c-z)/L;f=w/A;if(f>C){break}}if(f>C){break}}if(z>c){L*=-1}for(F=0;F<A;F++){v=z+F*L;J.push({v:v})}}}var B;var y=[];if(r("labelsKMB")){B=1000;y=["K","M","B","T"]}if(r("labelsKMG2")){if(B){Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!")}B=1024;y=["k","M","G","T"]}var E=r("axisLabelFormatter");for(F=0;F<J.length;F++){if(J[F].label!==undefined){continue}v=J[F].v;var e=Math.abs(v);var l=E(v,0,r,d);if(y.length>0){var x=B*B*B*B;for(D=3;D>=0;D--,x/=B){if(e>=x){l=Dygraph.round_(v/x,r("digitsAfterDecimal"))+y[D];break}}}J[F].label=l}return J};Dygraph.dateTicker=function(e,c,i,g,f,h){var d=Dygraph.pickDateTickGranularity(e,c,i,g);if(d>=0){return Dygraph.getDateAxis(e,c,d,g,f)}else{return[]}};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.WEEKLY=14;Dygraph.MONTHLY=15;Dygraph.QUARTERLY=16;Dygraph.BIANNUAL=17;Dygraph.ANNUAL=18;Dygraph.DECADAL=19;Dygraph.CENTENNIAL=20;Dygraph.NUM_GRANULARITIES=21;Dygraph.SHORT_SPACINGS=[];Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]=1000*1;Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]=1000*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]=1000*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]=1000*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY]=1000*30;Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]=1000*60;Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]=1000*60*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]=1000*60*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]=1000*60*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY]=1000*60*30;Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]=1000*3600;Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]=1000*3600*2;Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]=1000*3600*6;Dygraph.SHORT_SPACINGS[Dygraph.DAILY]=1000*86400;Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]=1000*604800;Dygraph.PREFERRED_LOG_TICK_VALUES=function(){var c=[];for(var b=-39;b<=39;b++){var a=Math.pow(10,b);for(var d=1;d<=9;d++){var e=a*d;c.push(e)}}return c}();Dygraph.pickDateTickGranularity=function(d,c,j,h){var g=h("pixelsPerLabel");for(var f=0;f<Dygraph.NUM_GRANULARITIES;f++){var e=Dygraph.numDateTicks(d,c,f);if(j/e>=g){return f}}return -1};Dygraph.numDateTicks=function(e,b,g){if(g<Dygraph.MONTHLY){var h=Dygraph.SHORT_SPACINGS[g];return Math.floor(0.5+1*(b-e)/h)}else{var f=1;var d=12;if(g==Dygraph.QUARTERLY){d=3}if(g==Dygraph.BIANNUAL){d=2}if(g==Dygraph.ANNUAL){d=1}if(g==Dygraph.DECADAL){d=1;f=10}if(g==Dygraph.CENTENNIAL){d=1;f=100}var c=365.2524*24*3600*1000;var a=1*(b-e)/c;return Math.floor(0.5+1*a*d/f)}};Dygraph.getDateAxis=function(n,h,a,l,w){var u=l("axisLabelFormatter");var z=[];var k;if(a<Dygraph.MONTHLY){var c=Dygraph.SHORT_SPACINGS[a];var v=c/1000;var y=new Date(n);var f;if(v<=60){f=y.getSeconds();y.setSeconds(f-f%v)}else{y.setSeconds(0);v/=60;if(v<=60){f=y.getMinutes();y.setMinutes(f-f%v)}else{y.setMinutes(0);v/=60;if(v<=24){f=y.getHours();y.setHours(f-f%v)}else{y.setHours(0);v/=24;if(v==7){y.setDate(y.getDate()-y.getDay())}}}}n=y.getTime();for(k=n;k<=h;k+=c){z.push({v:k,label:u(new Date(k),a,l,w)})}}else{var e;var o=1;if(a==Dygraph.MONTHLY){e=[0,1,2,3,4,5,6,7,8,9,10,11]}else{if(a==Dygraph.QUARTERLY){e=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){e=[0,6]}else{if(a==Dygraph.ANNUAL){e=[0]}else{if(a==Dygraph.DECADAL){e=[0];o=10}else{if(a==Dygraph.CENTENNIAL){e=[0];o=100}else{Dygraph.warn("Span of dates is too long")}}}}}}var s=new Date(n).getFullYear();var p=new Date(h).getFullYear();var b=Dygraph.zeropad;for(var r=s;r<=p;r++){if(r%o!==0){continue}for(var q=0;q<e.length;q++){var m=r+"/"+b(1+e[q])+"/01";k=Dygraph.dateStrToMillis(m);if(k<n||k>h){continue}z.push({v:k,label:u(new Date(k),a,l,w)})}}}return z};Dygraph.DEFAULT_ATTRS.axes.x.ticker=Dygraph.dateTicker;Dygraph.DEFAULT_ATTRS.axes.y.ticker=Dygraph.numericTicks;Dygraph.DEFAULT_ATTRS.axes.y2.ticker=Dygraph.numericTicks;"use strict";function RGBColor(f){this.ok=false;if(f.charAt(0)=="#"){f=f.substr(1,6)}f=f.replace(/ /g,"");f=f.toLowerCase();var b={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"};for(var g in b){if(f==g){f=b[g]}}var e=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var c=0;c<e.length;c++){var j=e[c].re;var a=e[c].process;var h=j.exec(f);if(h){var d=a(h);this.r=d[0];this.g=d[1];this.b=d[2];this.ok=true}}this.r=(this.r<0||isNaN(this.r))?0:((this.r>255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var l=this.r.toString(16);var k=this.g.toString(16);var i=this.b.toString(16);if(l.length==1){l="0"+l}if(k.length==1){k="0"+k}if(i.length==1){i="0"+i}return"#"+l+k+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)=="undefined"){b=10}for(;parseInt(a,10)<b&&b>1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:"toLocaleString",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:["getHours","0"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(""+c.getFullYear()+"/1/1 GMT");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:["getMinutes","0"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(""+e.getFullYear()+"/1/1")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(b,0)},w:"getDay",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:"getFullYear",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?"-":"+")+a+e},Z:function(a){return a.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(a){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g=="locale"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)=="string"){return c[g]()}else{if(typeof(g)=="function"){return g.call(c,c)}else{if(typeof(g)=="object"&&typeof(g[0])=="string"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};
\ No newline at end of file
diff --git a/static/vendor/dygraph-1.2dev.js b/static/vendor/dygraph-1.2dev.js
new file mode 100644 (file)
index 0000000..547e519
--- /dev/null
@@ -0,0 +1,7355 @@
+/**
+ * @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.
+ */
+
+/*jshint globalstrict: true */
+/*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;
+  this.datasets = [];
+  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;
+};
+
+DygraphLayout.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+DygraphLayout.prototype.addDataset = function(setname, set_xy) {
+  this.datasets.push(set_xy);
+  this.setNames.push(setname);
+};
+
+DygraphLayout.prototype.getPlotArea = function() {
+  return this.computePlotArea_();
+};
+
+// Compute the box which the chart should be drawn in. This is the canvas's
+// box, less space needed for axis and chart labels.
+DygraphLayout.prototype.computePlotArea_ = function() {
+  var area = {
+    // TODO(danvk): per-axis setting.
+    x: 0,
+    y: 0
+  };
+  if (this.attr_('drawYAxis')) {
+   area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
+  }
+
+  area.w = this.dygraph_.width_ - area.x - this.attr_('rightGap');
+  area.h = this.dygraph_.height_;
+  if (this.attr_('drawXAxis')) {
+    if (this.attr_('xAxisHeight')) {
+      area.h -= this.attr_('xAxisHeight');
+    } else {
+      area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+    }
+  }
+
+  // Shrink the drawing area to accomodate additional y-axes.
+  if (this.dygraph_.numAxes() == 2) {
+    // TODO(danvk): per-axis setting.
+    area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
+  } else if (this.dygraph_.numAxes() > 2) {
+    this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
+                        "to use " + this.dygraph_.numAxes() + ")");
+  }
+
+  // Add space for chart labels: title, xlabel and ylabel.
+  if (this.attr_('title')) {
+    area.h -= this.attr_('titleHeight');
+    area.y += this.attr_('titleHeight');
+  }
+  if (this.attr_('xlabel')) {
+    area.h -= this.attr_('xLabelHeight');
+  }
+  if (this.attr_('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.
+  }
+
+  if (this.attr_('y2label')) {
+    // same logic applies here as for ylabel.
+    // TODO(danvk): make yAxisLabelWidth a per-axis property
+  }
+
+  // Add space for range selector, if needed.
+  if (this.attr_('showRangeSelector')) {
+    area.h -= this.attr_('rangeSelectorHeight') + 4;
+  }
+
+  return 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.attr_('xValueParser') || function(x) { return x; };
+  for (var i = 0; i < ann.length; i++) {
+    var a = {};
+    if (!ann[i].xval && !ann[i].x) {
+      this.dygraph_.error("Annotations must have an 'x' property");
+      return;
+    }
+    if (ann[i].icon &&
+        !(ann[i].hasOwnProperty('width') &&
+          ann[i].hasOwnProperty('height'))) {
+      this.dygraph_.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.setDateWindow = function(dateWindow) {
+  this.dateWindow_ = dateWindow;
+};
+
+DygraphLayout.prototype.evaluate = function() {
+  this._evaluateLimits();
+  this._evaluateLineCharts();
+  this._evaluateLineTicks();
+  this._evaluateAnnotations();
+};
+
+DygraphLayout.prototype._evaluateLimits = function() {
+  this.minxval = this.maxxval = null;
+  if (this.dateWindow_) {
+    this.minxval = this.dateWindow_[0];
+    this.maxxval = this.dateWindow_[1];
+  } else {
+    for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+      var series = this.datasets[setIdx];
+      if (series.length > 1) {
+        var x1 = series[0][0];
+        if (!this.minxval || x1 < this.minxval) this.minxval = x1;
+
+        var x2 = series[series.length - 1][0];
+        if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
+      }
+    }
+  }
+  this.xrange = this.maxxval - this.minxval;
+  this.xscale = (this.xrange !== 0 ? 1/this.xrange : 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 (axis.g.attr_("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)) {
+        axis.g.error('axis ' + i + ' of graph at ' + axis.g +
+            ' can\'t be displayed in log scale for range [' +
+            axis.minyval + ' - ' + axis.maxyval + ']');
+      }
+    }
+  }
+};
+
+DygraphLayout._calcYNormal = function(axis, value) {
+  if (axis.logscale) {
+    return 1.0 - ((Dygraph.log10(value) - Dygraph.log10(axis.minyval)) * axis.ylogscale);
+  } else {
+    return 1.0 - ((value - axis.minyval) * axis.yscale);
+  }
+};
+
+DygraphLayout.prototype._evaluateLineCharts = function() {
+  // add all the rects
+  this.points = [];
+  // An array to keep track of how many points will be drawn for each set.
+  // This will allow for the canvas renderer to not have to check every point
+  // for every data set since the points are added in order of the sets in
+  // datasets.
+  this.setPointsLengths = [];
+  this.setPointsOffsets = [];
+
+  for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+    var dataset = this.datasets[setIdx];
+    var setName = this.setNames[setIdx];
+    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+
+    this.setPointsOffsets.push(this.points.length);
+    var setPointsLength = 0;
+
+    for (var j = 0; j < dataset.length; j++) {
+      var item = dataset[j];
+      var xValue = parseFloat(item[0]);
+      var yValue = parseFloat(item[1]);
+
+      // Range from 0-1 where 0 represents left and 1 represents right.
+      var xNormal = (xValue - this.minxval) * this.xscale;
+      // Range from 0-1 where 0 represents top and 1 represents bottom
+      var yNormal = DygraphLayout._calcYNormal(axis, yValue);
+
+      var point = {
+        // TODO(danvk): here
+        x: xNormal,
+        y: yNormal,
+        xval: xValue,
+        yval: yValue,
+        name: setName
+      };
+      this.points.push(point);
+      setPointsLength += 1;
+    }
+    this.setPointsLengths.push(setPointsLength);
+  }
+};
+
+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.xscale * (tick.v - this.minxval);
+    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]);
+      }
+    }
+  }
+};
+
+
+/**
+ * Behaves the same way as PlotKit.Layout, but also copies the errors
+ * @private
+ */
+DygraphLayout.prototype.evaluateWithError = function() {
+  this.evaluate();
+  if (!(this.attr_('errorBars') || this.attr_('customBars'))) return;
+
+  // Copy over the error terms
+  var i = 0;  // index in this.points
+  for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+    var j = 0;
+    var dataset = this.datasets[setIdx];
+    var setName = this.setNames[setIdx];
+    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+    for (j = 0; j < dataset.length; j++, i++) {
+      var item = dataset[j];
+      var xv = parseFloat(item[0]);
+      var yv = parseFloat(item[1]);
+
+      if (xv == this.points[i].xval &&
+          yv == this.points[i].yval) {
+        var errorMinus = parseFloat(item[2]);
+        var errorPlus = parseFloat(item[3]);
+
+        var yv_minus = yv - errorMinus;
+        var yv_plus = yv + errorPlus;
+        this.points[i].y_top = DygraphLayout._calcYNormal(axis, yv_minus);
+        this.points[i].y_bottom = DygraphLayout._calcYNormal(axis, yv_plus);
+      }
+    }
+  }
+};
+
+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 (i = 0; i < this.points.length; i++) {
+    var p = this.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.datasets;
+  delete this.setNames;
+  delete this.setPointsLengths;
+  delete this.setPointsOffsets;
+  this.datasets = [];
+  this.setNames = [];
+  this.setPointsLengths = [];
+  this.setPointsOffsets = [];
+};
+
+/**
+ * Return a copy of the point at the indicated index, with its yval unstacked.
+ * @param int index of point in layout_.points
+ */
+DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
+  var point = this.points[idx];
+
+  // Clone the point since we modify it
+  var unstackedPoint = {};
+  for (var pt in point) {
+    unstackedPoint[pt] = point[pt];
+  }
+
+  if (!this.attr_("stackedGraph")) {
+    return unstackedPoint;
+  }
+
+  // The unstacked yval is equal to the current yval minus the yval of the
+  // next point at the same xval.
+  for (var i = idx+1; i < this.points.length; i++) {
+    if (this.points[i].xval == point.xval) {
+      unstackedPoint.yval -= this.points[i].yval;
+      break;
+    }
+  }
+
+  return unstackedPoint;
+};
+/**
+ * @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
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false,RGBColor:false */
+"use strict";
+
+
+var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
+  this.dygraph_ = dygraph;
+
+  this.layout = layout;
+  this.element = element;
+  this.elementContext = elementContext;
+  this.container = this.element.parentNode;
+
+  this.height = this.element.height;
+  this.width = this.element.width;
+
+  // --- check whether everything is ok before we return
+  if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element)))
+      throw "Canvas is not supported.";
+
+  // internal state
+  this.xlabels = [];
+  this.ylabels = [];
+  this.annotations = [];
+  this.chartLabels = {};
+
+  this.area = layout.getPlotArea();
+  this.container.style.position = "relative";
+  this.container.style.width = this.width + "px";
+
+  // Set up a clipping area for the canvas (and the interaction canvas).
+  // This ensures that we don't overdraw.
+  if (this.dygraph_.isUsingExcanvas_) {
+    this._createIEClipArea();
+  } else {
+    // 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();
+    }
+  }
+};
+
+DygraphCanvasRenderer.prototype.attr_ = function(x) {
+  return this.dygraph_.attr_(x);
+};
+
+DygraphCanvasRenderer.prototype.clear = function() {
+  var context;
+  if (this.isIE) {
+    // VML takes a while to start up, so we just poll every this.IEDelay
+    try {
+      if (this.clearDelay) {
+        this.clearDelay.cancel();
+        this.clearDelay = null;
+      }
+      context = this.elementContext;
+    }
+    catch (e) {
+      // TODO(danvk): this is broken, since MochiKit.Async is gone.
+      // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
+      // this.clearDelay.addCallback(bind(this.clear, this));
+      return;
+    }
+  }
+
+  context = this.elementContext;
+  context.clearRect(0, 0, this.width, this.height);
+
+  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);
+  removeArray(this.annotations);
+
+  for (var k in this.chartLabels) {
+    if (!this.chartLabels.hasOwnProperty(k)) continue;
+    var el = this.chartLabels[k];
+    if (el.parentNode) el.parentNode.removeChild(el);
+  }
+  this.xlabels = [];
+  this.ylabels = [];
+  this.annotations = [];
+  this.chartLabels = {};
+};
+
+
+DygraphCanvasRenderer.isSupported = function(canvasName) {
+  var canvas = null;
+  try {
+    if (typeof(canvasName) == 'undefined' || canvasName === null) {
+      canvas = document.createElement("canvas");
+    } else {
+      canvas = canvasName;
+    }
+    canvas.getContext("2d");
+  }
+  catch (e) {
+    var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
+    var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
+    if ((!ie) || (ie[1] < 6) || (opera))
+      return false;
+    return true;
+  }
+  return true;
+};
+
+/**
+ * @param { [String] } colors Array of color strings. Should have one entry for
+ * each series to be rendered.
+ */
+DygraphCanvasRenderer.prototype.setColors = function(colors) {
+  this.colorScheme_ = colors;
+};
+
+/**
+ * Draw an X/Y grid on top of the existing plot
+ */
+DygraphCanvasRenderer.prototype.render = function() {
+  // 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 ctx = this.elementContext;
+  function halfUp(x)  { return Math.round(x) + 0.5; }
+  function halfDown(y){ return Math.round(y) - 0.5; }
+
+  if (this.attr_('underlayCallback')) {
+    // NOTE: we pass the dygraph object to this callback twice to avoid breaking
+    // users who expect a deprecated form of this callback.
+    this.attr_('underlayCallback')(ctx, this.area, this.dygraph_, this.dygraph_);
+  }
+
+  var x, y, i, ticks;
+  if (this.attr_('drawYGrid')) {
+    ticks = this.layout.yticks;
+    // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
+    ctx.save();
+    ctx.strokeStyle = this.attr_('gridLineColor');
+    ctx.lineWidth = this.attr_('gridLineWidth');
+    for (i = 0; i < ticks.length; i++) {
+      // TODO(danvk): allow secondary axes to draw a grid, too.
+      if (ticks[i][0] !== 0) continue;
+      x = halfUp(this.area.x);
+      y = halfDown(this.area.y + ticks[i][1] * this.area.h);
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x + this.area.w, y);
+      ctx.closePath();
+      ctx.stroke();
+    }
+  }
+
+  if (this.attr_('drawXGrid')) {
+    ticks = this.layout.xticks;
+    ctx.save();
+    ctx.strokeStyle = this.attr_('gridLineColor');
+    ctx.lineWidth = this.attr_('gridLineWidth');
+    for (i=0; i<ticks.length; i++) {
+      x = halfUp(this.area.x + ticks[i][0] * this.area.w);
+      y = halfDown(this.area.y + this.area.h);
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x, this.area.y);
+      ctx.closePath();
+      ctx.stroke();
+    }
+  }
+
+  // Do the ordinary rendering, as before
+  this._renderLineChart();
+  this._renderAxis();
+  this._renderChartLabels();
+  this._renderAnnotations();
+};
+
+DygraphCanvasRenderer.prototype._createIEClipArea = function() {
+  var className = 'dygraph-clip-div';
+  var graphDiv = this.dygraph_.graphDiv;
+
+  // Remove old clip divs.
+  for (var i = graphDiv.childNodes.length-1; i >= 0; i--) {
+    if (graphDiv.childNodes[i].className == className) {
+      graphDiv.removeChild(graphDiv.childNodes[i]);
+    }
+  }
+
+  // Determine background color to give clip divs.
+  var backgroundColor = document.bgColor;
+  var element = this.dygraph_.graphDiv;
+  while (element != document) {
+    var bgcolor = element.currentStyle.backgroundColor;
+    if (bgcolor && bgcolor != 'transparent') {
+      backgroundColor = bgcolor;
+      break;
+    }
+    element = element.parentNode;
+  }
+
+  function createClipDiv(area) {
+    if (area.w === 0 || area.h === 0) {
+      return;
+    }
+    var elem = document.createElement('div');
+    elem.className = className;
+    elem.style.backgroundColor = backgroundColor;
+    elem.style.position = 'absolute';
+    elem.style.left = area.x + 'px';
+    elem.style.top = area.y + 'px';
+    elem.style.width = area.w + 'px';
+    elem.style.height = area.h + 'px';
+    graphDiv.appendChild(elem);
+  }
+
+  var plotArea = this.area;
+  // Left side
+  createClipDiv({
+    x:0, y:0,
+    w:plotArea.x,
+    h:this.height
+  });
+
+  // Top
+  createClipDiv({
+    x: plotArea.x, y: 0,
+    w: this.width - plotArea.x,
+    h: plotArea.y
+  });
+
+  // Right side
+  createClipDiv({
+    x: plotArea.x + plotArea.w, y: 0,
+    w: this.width-plotArea.x - plotArea.w,
+    h: this.height
+  });
+
+  // Bottom
+  createClipDiv({
+    x: plotArea.x,
+    y: plotArea.y + plotArea.h,
+    w: this.width - plotArea.x,
+    h: this.height - plotArea.h - plotArea.y
+  });
+};
+
+DygraphCanvasRenderer.prototype._renderAxis = function() {
+  if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) 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 = this.elementContext;
+
+  var label, x, y, tick, i;
+
+  var labelStyle = {
+    position: "absolute",
+    fontSize: this.attr_('axisLabelFontSize') + "px",
+    zIndex: 10,
+    color: this.attr_('axisLabelColor'),
+    width: this.attr_('axisLabelWidth') + "px",
+    // height: this.attr_('axisLabelFontSize') + 2 + "px",
+    lineHeight: "normal",  // Something other than "normal" line-height screws up label positioning.
+    overflow: "hidden"
+  };
+  var makeDiv = function(txt, axis, prec_axis) {
+    var div = document.createElement("div");
+    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();
+  context.strokeStyle = this.attr_('axisLineColor');
+  context.lineWidth = this.attr_('axisLineWidth');
+
+  if (this.attr_('drawYAxis')) {
+    if (this.layout.yticks && this.layout.yticks.length > 0) {
+      var num_axes = this.dygraph_.numAxes();
+      for (i = 0; i < this.layout.yticks.length; i++) {
+        tick = this.layout.yticks[i];
+        if (typeof(tick) == "function") return;
+        x = this.area.x;
+        var sgn = 1;
+        var prec_axis = 'y1';
+        if (tick[0] == 1) {  // right-side y-axis
+          x = this.area.x + this.area.w;
+          sgn = -1;
+          prec_axis = 'y2';
+        }
+        y = this.area.y + tick[1] * this.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 - this.attr_('axisLabelFontSize') / 2);
+        if (top < 0) top = 0;
+
+        if (top + this.attr_('axisLabelFontSize') + 3 > this.height) {
+          label.style.bottom = "0px";
+        } else {
+          label.style.top = top + "px";
+        }
+        if (tick[0] === 0) {
+          label.style.left = (this.area.x - this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
+          label.style.textAlign = "right";
+        } else if (tick[0] == 1) {
+          label.style.left = (this.area.x + this.area.w +
+                              this.attr_('axisTickSize')) + "px";
+          label.style.textAlign = "left";
+        }
+        label.style.width = this.attr_('yAxisLabelWidth') + "px";
+        this.container.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];
+      var fontSize = this.attr_('axisLabelFontSize');
+      var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
+      if (bottom > this.height - 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.
+    context.beginPath();
+    context.moveTo(halfUp(this.area.x), halfDown(this.area.y));
+    context.lineTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h));
+    context.closePath();
+    context.stroke();
+
+    // if there's a secondary y-axis, draw a vertical line for that, too.
+    if (this.dygraph_.numAxes() == 2) {
+      context.beginPath();
+      context.moveTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y));
+      context.lineTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
+      context.closePath();
+      context.stroke();
+    }
+  }
+
+  if (this.attr_('drawXAxis')) {
+    if (this.layout.xticks) {
+      for (i = 0; i < this.layout.xticks.length; i++) {
+        tick = this.layout.xticks[i];
+        x = this.area.x + tick[0] * this.area.w;
+        y = this.area.y + this.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 + this.attr_('axisTickSize')) + 'px';
+
+        var left = (x - this.attr_('axisLabelWidth')/2);
+        if (left + this.attr_('axisLabelWidth') > this.width) {
+          left = this.width - this.attr_('xAxisLabelWidth');
+          label.style.textAlign = "right";
+        }
+        if (left < 0) {
+          left = 0;
+          label.style.textAlign = "left";
+        }
+
+        label.style.left = left + "px";
+        label.style.width = this.attr_('xAxisLabelWidth') + "px";
+        this.container.appendChild(label);
+        this.xlabels.push(label);
+      }
+    }
+
+    context.beginPath();
+    context.moveTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h));
+    context.lineTo(halfUp(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
+    context.closePath();
+    context.stroke();
+  }
+
+  context.restore();
+};
+
+
+DygraphCanvasRenderer.prototype._renderChartLabels = function() {
+  var div, class_div;
+
+  // Generate divs for the chart title, xlabel and ylabel.
+  // Space for these divs has already been taken away from the charting area in
+  // the DygraphCanvasRenderer constructor.
+  if (this.attr_('title')) {
+    div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.top = '0px';
+    div.style.left = this.area.x + 'px';
+    div.style.width = this.area.w + 'px';
+    div.style.height = this.attr_('titleHeight') + 'px';
+    div.style.textAlign = 'center';
+    div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px';
+    div.style.fontWeight = 'bold';
+    class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-title';
+    class_div.innerHTML = this.attr_('title');
+    div.appendChild(class_div);
+    this.container.appendChild(div);
+    this.chartLabels.title = div;
+  }
+
+  if (this.attr_('xlabel')) {
+    div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.bottom = 0;  // TODO(danvk): this is lazy. Calculate style.top.
+    div.style.left = this.area.x + 'px';
+    div.style.width = this.area.w + 'px';
+    div.style.height = this.attr_('xLabelHeight') + 'px';
+    div.style.textAlign = 'center';
+    div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px';
+
+    class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-xlabel';
+    class_div.innerHTML = this.attr_('xlabel');
+    div.appendChild(class_div);
+    this.container.appendChild(div);
+    this.chartLabels.xlabel = div;
+  }
+
+  var that = this;
+  function createRotatedDiv(axis, classes, html) {
+    var box = {
+      left: 0,
+      top: that.area.y,
+      width: that.attr_('yLabelWidth'),
+      height: that.area.h
+    };
+    // TODO(danvk): is this outer div actually necessary?
+    div = document.createElement("div");
+    div.style.position = 'absolute';
+    if (axis == 1) {
+      div.style.left = box.left;
+    } else {
+      div.style.right = box.left;
+    }
+    div.style.top = box.top + 'px';
+    div.style.width = box.width + 'px';
+    div.style.height = box.height + 'px';
+    div.style.fontSize = (that.attr_('yLabelWidth') - 2) + 'px';
+
+    var inner_div = document.createElement("div");
+    inner_div.style.position = 'absolute';
+    inner_div.style.width = box.height + 'px';
+    inner_div.style.height = box.width + 'px';
+    inner_div.style.top = (box.height / 2 - box.width / 2) + 'px';
+    inner_div.style.left = (box.width / 2 - box.height / 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
+
+    if (typeof(document.documentMode) !== 'undefined' &&
+        document.documentMode < 9) {
+      // We're dealing w/ an old version of IE, so we have to rotate the text
+      // using a BasicImage transform. This uses a different origin of rotation
+      // than HTML5 rotation (top left of div vs. its center).
+      inner_div.style.filter =
+          'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
+          (axis == 1 ? '3' : '1') + ')';
+      inner_div.style.left = '0px';
+      inner_div.style.top = '0px';
+    }
+
+    class_div = document.createElement("div");
+    class_div.className = classes;
+    class_div.innerHTML = html;
+
+    inner_div.appendChild(class_div);
+    div.appendChild(inner_div);
+    return div;
+  }
+
+  var div;
+  if (this.attr_('ylabel')) {
+    div = createRotatedDiv(1, 'dygraph-label dygraph-ylabel',
+                           this.attr_('ylabel'));
+    this.container.appendChild(div);
+    this.chartLabels.ylabel = div;
+  }
+  if (this.attr_('y2label') && this.dygraph_.numAxes() == 2) {
+    div = createRotatedDiv(2, 'dygraph-label dygraph-y2label',
+                           this.attr_('y2label'));
+    this.container.appendChild(div);
+    this.chartLabels.y2label = div;
+  }
+};
+
+
+DygraphCanvasRenderer.prototype._renderAnnotations = function() {
+  var annotationStyle = {
+    "position": "absolute",
+    "fontSize": this.attr_('axisLabelFontSize') + "px",
+    "zIndex": 10,
+    "overflow": "hidden"
+  };
+
+  var bindEvt = function(eventName, classEventName, p, self) {
+    return function(e) {
+      var a = p.annotation;
+      if (a.hasOwnProperty(eventName)) {
+        a[eventName](a, p, self.dygraph_, e);
+      } else if (self.dygraph_.attr_(classEventName)) {
+        self.dygraph_.attr_(classEventName)(a, p, self.dygraph_,e );
+      }
+    };
+  };
+
+  // Get a list of point with annotations.
+  var points = this.layout.annotated_points;
+  for (var i = 0; i < points.length; i++) {
+    var p = points[i];
+    if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w) {
+      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));
+    }
+    div.style.left = (p.canvasx - width / 2) + "px";
+    if (a.attachAtBottom) {
+      div.style.top = (this.area.h - height - tick_height) + "px";
+    } else {
+      div.style.top = (p.canvasy - height - tick_height) + "px";
+    }
+    div.style.width = width + "px";
+    div.style.height = height + "px";
+    div.title = p.annotation.text;
+    div.style.color = this.colors[p.name];
+    div.style.borderColor = this.colors[p.name];
+    a.div = div;
+
+    Dygraph.addEvent(div, 'click',
+        bindEvt('clickHandler', 'annotationClickHandler', p, this));
+    Dygraph.addEvent(div, 'mouseover',
+        bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
+    Dygraph.addEvent(div, 'mouseout',
+        bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
+    Dygraph.addEvent(div, 'dblclick',
+        bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
+
+    this.container.appendChild(div);
+    this.annotations.push(div);
+
+    var ctx = this.elementContext;
+    ctx.strokeStyle = this.colors[p.name];
+    ctx.beginPath();
+    if (!a.attachAtBottom) {
+      ctx.moveTo(p.canvasx, p.canvasy);
+      ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height);
+    } else {
+      ctx.moveTo(p.canvasx, this.area.h);
+      ctx.lineTo(p.canvasx, this.area.h - 2 - tick_height);
+    }
+    ctx.closePath();
+    ctx.stroke();
+  }
+};
+
+
+/**
+ * Actually draw the lines chart, including error bars.
+ * TODO(danvk): split this into several smaller functions.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._renderLineChart = function() {
+  var isNullOrNaN = function(x) {
+    return (x === null || isNaN(x));
+  };
+
+  // TODO(danvk): use this.attr_ for many of these.
+  var context = this.elementContext;
+  var fillAlpha = this.attr_('fillAlpha');
+  var errorBars = this.attr_("errorBars") || this.attr_("customBars");
+  var fillGraph = this.attr_("fillGraph");
+  var stackedGraph = this.attr_("stackedGraph");
+  var stepPlot = this.attr_("stepPlot");
+  var points = this.layout.points;
+  var pointsLength = points.length;
+  var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
+
+  var setNames = this.layout.setNames;
+  var setCount = setNames.length;
+
+  // TODO(danvk): Move this mapping into Dygraph and get it out of here.
+  this.colors = {};
+  for (i = 0; i < setCount; i++) {
+    this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length];
+  }
+
+  // Update Points
+  // TODO(danvk): here
+  for (i = pointsLength; i--;) {
+    point = points[i];
+    point.canvasx = this.area.w * point.x + this.area.x;
+    point.canvasy = this.area.h * point.y + this.area.y;
+  }
+
+  // create paths
+  var ctx = context;
+  if (errorBars) {
+    if (fillGraph) {
+      this.dygraph_.warn("Can't use fillGraph option with error bars");
+    }
+
+    for (i = 0; i < setCount; i++) {
+      setName = setNames[i];
+      axis = this.dygraph_.axisPropertiesForSeries(setName);
+      color = this.colors[setName];
+
+      // setup graphics context
+      ctx.save();
+      prevX = NaN;
+      prevY = NaN;
+      prevYs = [-1, -1];
+      yscale = axis.yscale;
+      // should be same color as the lines but only 15% opaque.
+      rgb = new RGBColor(color);
+      err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
+                            fillAlpha + ')';
+      ctx.fillStyle = err_color;
+      ctx.beginPath();
+      for (j = 0; j < pointsLength; j++) {
+        point = points[j];
+        if (point.name == setName) {
+          if (!Dygraph.isOK(point.y)) {
+            prevX = NaN;
+            continue;
+          }
+
+          // TODO(danvk): here
+          if (stepPlot) {
+            newYs = [ point.y_bottom, point.y_top ];
+            prevY = point.y;
+          } else {
+            newYs = [ point.y_bottom, point.y_top ];
+          }
+          newYs[0] = this.area.h * newYs[0] + this.area.y;
+          newYs[1] = this.area.h * newYs[1] + this.area.y;
+          if (!isNaN(prevX)) {
+            if (stepPlot) {
+              ctx.moveTo(prevX, newYs[0]);
+            } else {
+              ctx.moveTo(prevX, prevYs[0]);
+            }
+            ctx.lineTo(point.canvasx, newYs[0]);
+            ctx.lineTo(point.canvasx, newYs[1]);
+            if (stepPlot) {
+              ctx.lineTo(prevX, newYs[1]);
+            } else {
+              ctx.lineTo(prevX, prevYs[1]);
+            }
+            ctx.closePath();
+          }
+          prevYs = newYs;
+          prevX = point.canvasx;
+        }
+      }
+      ctx.fill();
+    }
+  } else if (fillGraph) {
+    var baseline = [];  // for stacked graphs: baseline for filling
+
+    // process sets in reverse order (needed for stacked graphs)
+    for (i = setCount - 1; i >= 0; i--) {
+      setName = setNames[i];
+      color = this.colors[setName];
+      axis = this.dygraph_.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 = this.area.h * axisY + this.area.y;
+
+      // setup graphics context
+      ctx.save();
+      prevX = NaN;
+      prevYs = [-1, -1];
+      yscale = axis.yscale;
+      // should be same color as the lines but only 15% opaque.
+      rgb = new RGBColor(color);
+      err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
+                            fillAlpha + ')';
+      ctx.fillStyle = err_color;
+      ctx.beginPath();
+      for (j = 0; j < pointsLength; j++) {
+        point = points[j];
+        if (point.name == setName) {
+          if (!Dygraph.isOK(point.y)) {
+            prevX = NaN;
+            continue;
+          }
+          if (stackedGraph) {
+            var lastY = baseline[point.canvasx];
+            if (lastY === undefined) lastY = axisY;
+            baseline[point.canvasx] = point.canvasy;
+            newYs = [ point.canvasy, lastY ];
+          } else {
+            newYs = [ point.canvasy, axisY ];
+          }
+          if (!isNaN(prevX)) {
+            ctx.moveTo(prevX, prevYs[0]);
+            if (stepPlot) {
+              ctx.lineTo(point.canvasx, prevYs[0]);
+            } else {
+              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();
+    }
+  }
+
+  // Drawing the lines.
+  var firstIndexInSet = 0;
+  var afterLastIndexInSet = 0;
+  var setLength = 0;
+  for (i = 0; i < setCount; i += 1) {
+    firstIndexInSet = this.layout.setPointsOffsets[i];
+    setLength = this.layout.setPointsLengths[i];
+    afterLastIndexInSet = firstIndexInSet + setLength;
+    setName = setNames[i];
+    color = this.colors[setName];
+    var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
+
+    // setup graphics context
+    context.save();
+    var pointSize = this.dygraph_.attr_("pointSize", setName);
+    prevX = null;
+    prevY = null;
+    var drawPoints = this.dygraph_.attr_("drawPoints", setName);
+    var strokePattern = this.dygraph_.attr_("strokePattern", setName);
+    if (!Dygraph.isArrayLike(strokePattern)) {
+      strokePattern = null;
+    }
+    for (j = firstIndexInSet; j < afterLastIndexInSet; j++) {
+      point = points[j];
+      if (isNullOrNaN(point.canvasy)) {
+        if (stepPlot && prevX !== null) {
+          // Draw a horizontal line to the start of the missing data
+          ctx.beginPath();
+          ctx.strokeStyle = color;
+          ctx.lineWidth = this.attr_('strokeWidth');
+          this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+          ctx.stroke();
+        }
+        // this will make us move to the next point, not draw a line to it.
+        prevX = prevY = null;
+      } else {
+        // A point is "isolated" if it is non-null but both the previous
+        // and next points are null.
+        var isIsolated = (!prevX && (j == points.length - 1 ||
+                                     isNullOrNaN(points[j+1].canvasy)));
+        if (prevX === null) {
+          prevX = point.canvasx;
+          prevY = point.canvasy;
+        } else {
+          // Skip over points that will be drawn in the same pixel.
+          if (Math.round(prevX) == Math.round(point.canvasx) &&
+              Math.round(prevY) == Math.round(point.canvasy)) {
+            continue;
+          }
+          // TODO(antrob): skip over points that lie on a line that is already
+          // going to be drawn. There is no need to have more than 2
+          // consecutive points that are collinear.
+          if (strokeWidth) {
+            ctx.beginPath();
+            ctx.strokeStyle = color;
+            ctx.lineWidth = strokeWidth;
+            if (stepPlot) {
+              this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+              prevX = point.canvasx;
+            }
+            this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
+            prevX = point.canvasx;
+            prevY = point.canvasy;
+            ctx.stroke();
+          }
+        }
+
+        if (drawPoints || isIsolated) {
+          ctx.beginPath();
+          ctx.fillStyle = color;
+          ctx.arc(point.canvasx, point.canvasy, pointSize,
+                  0, 2 * Math.PI, false);
+          ctx.fill();
+        }
+      }
+    }
+  }
+
+  context.restore();
+};
+
+/**
+ * This does dashed lines onto a canvas for a given pattern. You must call
+ * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
+ * the state of the line in regards to where we left off on drawing the pattern.
+ * You can draw a dashed line in several function calls and the pattern will be
+ * continous as long as you didn't call this function with a different pattern
+ * in between.
+ * @param ctx The canvas 2d context to draw on.
+ * @param x The start of the line's x coordinate.
+ * @param y The start of the line's y coordinate.
+ * @param x2 The end of the line's x coordinate.
+ * @param y2 The end of the line's y coordinate.
+ * @param pattern The dash pattern to draw, an array of integers where even 
+ * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
+ * is drawn, 2 is the space between.). A null pattern, array of length one, or
+ * empty array will do just a solid line.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) {
+  // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
+  // Modified by Russell Valentine to keep line history and continue the pattern
+  // where it left off.
+  var dx, dy, len, rot, patternIndex, segment;
+
+  // If we don't have a pattern or it is an empty array or of size one just
+  // do a solid line.
+  if (!pattern || pattern.length <= 1) {
+    ctx.moveTo(x, y);
+    ctx.lineTo(x2, y2);
+    return;
+  }
+
+  // If we have a different dash pattern than the last time this was called we
+  // reset our dash history and start the pattern from the begging 
+  // regardless of state of the last pattern.
+  if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) {
+    this._dashedLineToHistoryPattern = pattern;
+    this._dashedLineToHistory = [0, 0];
+  }
+  ctx.save();
+
+  // Calculate transformation parameters
+  dx = (x2-x);
+  dy = (y2-y);
+  len = Math.sqrt(dx*dx + dy*dy);
+  rot = Math.atan2(dy, dx);
+
+  // Set transformation
+  ctx.translate(x, y);
+  ctx.moveTo(0, 0);
+  ctx.rotate(rot);
+
+  // Set last pattern index we used for this pattern.
+  patternIndex = this._dashedLineToHistory[0];
+  x = 0;
+  while (len > x) {
+    // Get the length of the pattern segment we are dealing with.
+    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 (this._dashedLineToHistory[1]) {
+      x += this._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.
+      this._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.
+      this._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) {
+      ctx.lineTo(x, 0);
+    } else {
+      ctx.moveTo(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;
+  }
+  ctx.restore();
+};
+/**
+ * @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/
+
+ */
+
+/*jshint globalstrict: true */
+/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager: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) {
+  if (arguments.length > 0) {
+    if (arguments.length == 4) {
+      // 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.
+      this.warn("Using deprecated four-argument dygraph constructor");
+      this.__old_init__(div, data, arguments[2], arguments[3]);
+    } else {
+      this.__init__(div, data, opts);
+    }
+  }
+};
+
+Dygraph.NAME = "Dygraph";
+Dygraph.VERSION = "1.2dev";
+Dygraph.__repr__ = function() {
+  return "[" + this.NAME + " " + this.VERSION + "]";
+};
+
+/**
+ * Returns information about the Dygraph class.
+ */
+Dygraph.toString = function() {
+  return this.__repr__();
+};
+
+// Various default values
+Dygraph.DEFAULT_ROLL_PERIOD = 1;
+Dygraph.DEFAULT_WIDTH = 480;
+Dygraph.DEFAULT_HEIGHT = 320;
+
+Dygraph.ANIMATION_STEPS = 10;
+Dygraph.ANIMATION_DURATION = 200;
+
+// 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
+ * @param {String} name The name of the point's data series
+ * @param {Dygraph} g The dygraph object
+ */
+Dygraph.numberValueFormatter = function(x, opts, pt, g) {
+  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');
+
+  // 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))) {
+    return x.toExponential(digits);
+  } else {
+    return '' + Dygraph.round_(x, digits);
+  }
+};
+
+/**
+ * variant for use as an axisLabelFormatter.
+ * @private
+ */
+Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) {
+  return Dygraph.numberValueFormatter(x, opts, g);
+};
+
+/**
+ * Convert a JS date (millis since epoch) to YYYY/MM/DD
+ * @param {Number} date The JavaScript date (ms since epoch)
+ * @return {String} A date of the form "YYYY/MM/DD"
+ * @private
+ */
+Dygraph.dateString_ = function(date) {
+  var zeropad = Dygraph.zeropad;
+  var d = new Date(date);
+
+  // Get the year:
+  var year = "" + d.getFullYear();
+  // Get a 0 padded month string
+  var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
+  // Get a 0 padded day string
+  var day = zeropad(d.getDate());
+
+  var ret = "";
+  var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
+  if (frac) ret = " " + Dygraph.hmsString_(date);
+
+  return year + "/" + month + "/" + day + ret;
+};
+
+/**
+ * Convert a JS date to a string appropriate to display on an axis that
+ * is displaying values at the stated granularity.
+ * @param {Date} date The date to format
+ * @param {Number} granularity One of the Dygraph granularity constants
+ * @return {String} The formatted date
+ * @private
+ */
+Dygraph.dateAxisFormatter = function(date, granularity) {
+  if (granularity >= Dygraph.DECADAL) {
+    return date.strftime('%Y');
+  } else if (granularity >= Dygraph.MONTHLY) {
+    return date.strftime('%b %y');
+  } else {
+    var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
+    if (frac === 0 || granularity >= Dygraph.DAILY) {
+      return new Date(date.getTime() + 3600*1000).strftime('%d%b');
+    } else {
+      return Dygraph.hmsString_(date.getTime());
+    }
+  }
+};
+
+
+// Default attribute values.
+Dygraph.DEFAULT_ATTRS = {
+  highlightCircleSize: 3,
+
+  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,
+
+  axisTickSize: 3,
+  axisLabelFontSize: 14,
+  xAxisLabelWidth: 50,
+  yAxisLabelWidth: 50,
+  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,
+  hideOverlayOnMouseOut: true,
+
+  // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
+  legend: 'onmouseover',  // the only relevant value at the moment is 'always'.
+
+  stepPlot: false,
+  avoidMinZero: false,
+
+  // Sizes of the various chart labels.
+  titleHeight: 28,
+  xLabelHeight: 18,
+  yLabelWidth: 18,
+
+  drawXAxis: true,
+  drawYAxis: true,
+  axisLineColor: "black",
+  axisLineWidth: 0.3,
+  gridLineWidth: 0.3,
+  axisLabelColor: "black",
+  axisLabelFont: "Arial",  // TODO(danvk): is this implemented?
+  axisLabelWidth: 50,
+  drawYGrid: true,
+  drawXGrid: true,
+  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",
+  rangeSelectorPlotFillColor: "#A7B1C4",
+
+  // per-axis options
+  axes: {
+    x: {
+      pixelsPerLabel: 60,
+      axisLabelFormatter: Dygraph.dateAxisFormatter,
+      valueFormatter: Dygraph.dateString_,
+      ticker: null  // will be set in dygraph-tickers.js
+    },
+    y: {
+      pixelsPerLabel: 30,
+      valueFormatter: Dygraph.numberValueFormatter,
+      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+      ticker: null  // will be set in dygraph-tickers.js
+    },
+    y2: {
+      pixelsPerLabel: 30,
+      valueFormatter: Dygraph.numberValueFormatter,
+      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
+      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;
+
+// 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) {
+  // Hack for IE: if we're using excanvas and the document hasn't finished
+  // loading yet (and hence may not have initialized whatever it needs to
+  // initialize), then keep calling this routine periodically until it has.
+  if (/MSIE/.test(navigator.userAgent) && !window.opera &&
+      typeof(G_vmlCanvasManager) != 'undefined' &&
+      document.readyState != 'complete') {
+    var self = this;
+    setTimeout(function() { self.__init__(div, file, attrs); }, 100);
+    return;
+  }
+
+  // Support two-argument constructor
+  if (attrs === null || attrs === undefined) { attrs = {}; }
+
+  attrs = Dygraph.mapLegacyOptions_(attrs);
+
+  if (!div) {
+    Dygraph.error("Constructing dygraph with a non-existent div!");
+    return;
+  }
+
+  this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
+
+  // 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.is_initial_draw_ = true;
+  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.
+  this.width_ = div.clientWidth;
+  this.height_ = div.clientHeight;
+
+  // 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.
+  }
+
+  // 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_ = {};
+
+  // Create the containing DIV and other interactive elements
+  this.createInterface_();
+
+  this.start_();
+};
+
+/**
+ * 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) 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 (this.user_attrs_ !== null && seriesName &&
+      typeof(this.user_attrs_[seriesName]) != 'undefined' &&
+      this.user_attrs_[seriesName] !== null &&
+      typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
+    return this.user_attrs_[seriesName][name];
+  } else if (this.user_attrs_ !== null && typeof(this.user_attrs_[name]) != 'undefined') {
+    return this.user_attrs_[name];
+  } else if (this.attrs_ !== null && typeof(this.attrs_[name]) != 'undefined') {
+    return this.attrs_[name];
+  } else {
+    return null;
+  }
+};
+
+/**
+ * @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][opt]) {
+      return axis_opts[axis][opt];
+    }
+    // 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][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 left = this.rawData_[0][0];
+  var right = this.rawData_[this.rawData_.length - 1][0];
+  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();
+  return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+};
+
+/**
+ * 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.axes_[axis].logscale) {
+    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 = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
+    //
+    // Move denominator to both sides:
+    // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
+    //
+    // subtract logr1, and take the negative value.
+    // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
+    //
+    // Swap both sides of the equation, and we can compute the log of the
+    // return value. Which means we just need to use that as the exponent in
+    // e^exponent.
+    // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
+
+    var logr1 = Dygraph.log10(yRange[1]);
+    var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
+    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;
+  if (!this.axes_[axis].logscale) {
+    // 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]);
+  } else {
+    var logr1 = Dygraph.log10(yRange[1]);
+    pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(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();
+  return (x - xRange[0]) / (xRange[1] - xRange[0]);
+};
+
+/**
+ * Returns the number of columns (including the independent variable).
+ * @return { Integer } The number of columns.
+ */
+Dygraph.prototype.numColumns = function() {
+  return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ * @return { Integer } The number of rows, less any header.
+ */
+Dygraph.prototype.numRows = function() {
+  return this.rawData_.length;
+};
+
+/**
+ * Returns the full range of the x-axis, as determined by the most extreme
+ * values in the data set. Not affected by zooming, visibility, etc.
+ * TODO(danvk): merge w/ xAxisExtremes
+ * @return { Array<Number> } A [low, high] pair
+ * @private
+ */
+Dygraph.prototype.fullXRange_ = function() {
+  if (this.numRows() > 0) {
+    return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]];
+  } else {
+    return [0, 1];
+  }
+};
+
+/**
+ * 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");
+  this.graphDiv.style.width = this.width_ + "px";
+  this.graphDiv.style.height = this.height_ + "px";
+  enclosing.appendChild(this.graphDiv);
+
+  // Create the canvas for interactive parts of the chart.
+  this.canvas_ = Dygraph.createCanvas();
+  this.canvas_.style.position = "absolute";
+  this.canvas_.width = this.width_;
+  this.canvas_.height = this.height_;
+  this.canvas_.style.width = this.width_ + "px";    // for IE
+  this.canvas_.style.height = this.height_ + "px";  // for IE
+
+  this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
+
+  // ... and for static parts of the chart.
+  this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
+  this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
+
+  if (this.attr_('showRangeSelector')) {
+    // The range selector must be created here so that its canvases and contexts get created here.
+    // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
+    // The range selector also sets xAxisHeight in order to reserve space.
+    this.rangeSelector_ = new DygraphRangeSelector(this);
+  }
+
+  // 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);
+
+  if (this.rangeSelector_) {
+    // This needs to happen after the graph canvases are added to the div and the layout object is created.
+    this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
+  }
+
+  var dygraph = this;
+  
+  this.mouseMoveHandler = function(e) {
+         dygraph.mouseMove_(e);
+  };
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
+  
+  this.mouseOutHandler = function(e) {
+         dygraph.mouseOut_(e);
+  };
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
+
+  this.createStatusMessage_();
+  this.createDragInterface_();
+
+  this.resizeHandler = function(e) {
+    dygraph.resize();
+  };
+
+  // Update when the window is resized.
+  // TODO(danvk): drop frames depending on complexity of the chart.
+  Dygraph.addEvent(window, 'resize', this.resizeHandler);
+};
+
+/**
+ * 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() {
+  var removeRecursive = function(node) {
+    while (node.hasChildNodes()) {
+      removeRecursive(node.firstChild);
+      node.removeChild(node.firstChild);
+    }
+  };
+  
+  // remove mouse event handlers
+  Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
+  Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
+  removeRecursive(this.maindiv_);
+
+  var nullOut = function(obj) {
+    for (var n in obj) {
+      if (typeof(obj[n]) === 'object') {
+        obj[n] = null;
+      }
+    }
+  };
+  // remove event handlers
+  Dygraph.removeEvent(window,'resize',this.resizeHandler);
+  this.resizeHandler = 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() {
+  if (this.isUsingExcanvas_) {
+    var elem = document.createElement("div");
+    elem.style.position = 'absolute';
+    elem.style.backgroundColor = 'white';
+    elem.style.filter = 'alpha(opacity=0)';
+    elem.style.width = this.width_ + "px";
+    elem.style.height = this.height_ + "px";
+    this.graphDiv.appendChild(elem);
+    return elem;
+  } else {
+    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 num = this.attr_("labels").length - 1;
+  this.colors_ = [];
+  var colors = this.attr_('colors');
+  var i;
+  if (!colors) {
+    var sat = this.attr_('colorSaturation') || 1.0;
+    var val = this.attr_('colorValue') || 0.5;
+    var half = Math.ceil(num / 2);
+    for (i = 1; i <= num; i++) {
+      if (!this.visibility()[i-1]) continue;
+      // alternate colors for high contrast.
+      var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
+      var hue = (1.0 * idx/ (1 + num));
+      this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
+    }
+  } else {
+    for (i = 0; i < num; i++) {
+      if (!this.visibility()[i]) continue;
+      var colorStr = colors[i % colors.length];
+      this.colors_.push(colorStr);
+    }
+  }
+
+  this.plotter_.setColors(this.colors_);
+};
+
+/**
+ * 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.
+ * @return {Array<string>} The list of colors.
+ */
+Dygraph.prototype.getColors = function() {
+  return this.colors_;
+};
+
+/**
+ * Create the div that contains information on the selected point(s)
+ * This goes in the top right of the canvas, unless an external div has already
+ * been specified.
+ * @private
+ */
+Dygraph.prototype.createStatusMessage_ = function() {
+  var userLabelsDiv = this.user_attrs_.labelsDiv;
+  if (userLabelsDiv && null !== userLabelsDiv &&
+      (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
+    this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv);
+  }
+  if (!this.attr_("labelsDiv")) {
+    var divWidth = this.attr_('labelsDivWidth');
+    var messagestyle = {
+      "position": "absolute",
+      "fontSize": "14px",
+      "zIndex": 10,
+      "width": divWidth + "px",
+      "top": "0px",
+      "left": (this.width_ - divWidth - 2) + "px",
+      "background": "white",
+      "textAlign": "left",
+      "overflow": "hidden"};
+    Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
+    var div = document.createElement("div");
+    div.className = "dygraph-legend";
+    for (var name in messagestyle) {
+      if (messagestyle.hasOwnProperty(name)) {
+        div.style[name] = messagestyle[name];
+      }
+    }
+    this.graphDiv.appendChild(div);
+    this.attrs_.labelsDiv = div;
+  }
+};
+
+/**
+ * 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
+ */
+Dygraph.prototype.positionLabelsDiv_ = function() {
+  // Don't touch a user-specified labelsDiv.
+  if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
+
+  var area = this.plotter_.area;
+  var div = this.attr_("labelsDiv");
+  div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
+  div.style.top = area.y + "px";
+};
+
+/**
+ * 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.attr_('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); };
+};
+
+/**
+ * @private
+ * Converts page the x-coordinate of the event to pixel x-coordinates on the
+ * canvas (i.e. DOM Coords).
+ */
+Dygraph.prototype.dragGetX_ = function(e, context) {
+  return Dygraph.pageX(e) - context.px;
+};
+
+/**
+ * @private
+ * Converts page the y-coordinate of the event to pixel y-coordinates on the
+ * canvas (i.e. DOM Coords).
+ */
+Dygraph.prototype.dragGetY_ = function(e, context) {
+  return Dygraph.pageY(e) - context.py;
+};
+
+/**
+ * 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,
+
+    // 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] ...]
+
+    initializeMouseDown: function(event, g, context) {
+      // prevents mouse drags from selecting page text.
+      if (event.preventDefault) {
+        event.preventDefault();  // Firefox, Chrome, etc.
+      } else {
+        event.returnValue = false;  // IE
+        event.cancelBubble = true;
+      }
+
+      context.px = Dygraph.findPosX(g.canvas_);
+      context.py = Dygraph.findPosY(g.canvas_);
+      context.dragStartX = g.dragGetX_(event, context);
+      context.dragStartY = g.dragGetY_(event, context);
+    }
+  };
+
+  var interactionModel = this.attr_("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;
+    Dygraph.addEvent(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.
+  Dygraph.addEvent(document, 'mouseup', function(event) {
+    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;
+      }
+    }
+  });
+};
+
+/**
+ * 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));
+    }
+  }
+
+  if (this.isUsingExcanvas_) {
+    this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0];
+  }
+};
+
+/**
+ * Clear the zoom rectangle (and perform no zoom).
+ * @private
+ */
+Dygraph.prototype.clearZoomRect_ = function() {
+  this.currentZoomRectArgs_ = null;
+  this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.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);
+};
+
+/**
+ * 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));
+};
+
+/**
+ * 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 yAxisRange is null (i.e. "fit to data", the animation
+  // can produce strange effects. Rather than the y-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.attr_("zoomCallback")) {
+      that.attr_("zoomCallback")(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.attr_("zoomCallback")) {
+      var xRange = that.xAxisRange();
+      that.attr_("zoomCallback")(xRange[0], xRange[1], that.yAxisRanges());
+    }
+  });
+};
+
+/**
+ * Reset the zoom to the original view coordinates. This is the same as
+ * double-clicking on the graph.
+ *
+ * @private
+ */
+Dygraph.prototype.doUnzoom_ = 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.attr_("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.attr_("zoomCallback")) {
+        this.attr_("zoomCallback")(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[1];
+
+      // 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 : 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.attr_("zoomCallback")) {
+            that.attr_("zoomCallback")(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.attr_("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);
+};
+
+/**
+ * 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) return;
+
+  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+
+  var lastx = -1;
+  var i;
+
+  // Loop through all the points and find the date nearest to our current
+  // location.
+  var minDist = 1e+100;
+  var idx = -1;
+  for (i = 0; i < points.length; i++) {
+    var point = points[i];
+    if (point === null) continue;
+    var dist = Math.abs(point.canvasx - canvasx);
+    if (dist > minDist) continue;
+    minDist = dist;
+    idx = i;
+  }
+  if (idx >= 0) lastx = points[idx].xval;
+
+  // Extract the points we've selected
+  this.selPoints_ = [];
+  var l = points.length;
+  if (!this.attr_("stackedGraph")) {
+    for (i = 0; i < l; i++) {
+      if (points[i].xval == lastx) {
+        this.selPoints_.push(points[i]);
+      }
+    }
+  } else {
+    // Need to 'unstack' points starting from the bottom
+    var cumulative_sum = 0;
+    for (i = l - 1; i >= 0; i--) {
+      if (points[i].xval == lastx) {
+        var p = {};  // Clone the point since we modify it
+        for (var k in points[i]) {
+          p[k] = points[i][k];
+        }
+        p.yval -= cumulative_sum;
+        cumulative_sum += p.yval;
+        this.selPoints_.push(p);
+      }
+    }
+    this.selPoints_.reverse();
+  }
+
+  if (this.attr_("highlightCallback")) {
+    var px = this.lastx_;
+    if (px !== null && lastx != px) {
+      // only fire if the selected point has changed.
+      this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
+    }
+  }
+
+  // Save last x position for callbacks.
+  this.lastx_ = lastx;
+
+  this.updateSelection_();
+};
+
+/**
+ * Transforms layout_.points index into data row number.
+ * @param int layout_.points index
+ * @return int row number, or -1 if none could be found.
+ * @private
+ */
+Dygraph.prototype.idxToRow_ = function(idx) {
+  if (idx < 0) return -1;
+
+  // make sure that you get the boundaryIds record which is also defined (see bug #236)
+  var boundaryIdx = -1;
+  for (var i = 0; i < this.boundaryIds_.length; i++) {
+    if (this.boundaryIds_[i] !== undefined) {
+      boundaryIdx = i;
+      break;
+    }
+  }
+  if (boundaryIdx < 0) return -1;
+  for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+    var set = this.layout_.datasets[setIdx];
+    if (idx < set.length) {
+      return this.boundaryIds_[boundaryIdx][0] + idx;
+    }
+    idx -= set.length;
+  }
+  return -1;
+};
+
+/**
+ * @private
+ * Generates legend html dash for any stroke pattern. It will try to scale the
+ * pattern to fit in 1em width. Or if small enough repeat the partern 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.
+ */
+Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
+  var dash = "";
+  var i, j, paddingLeft, marginRight;
+  var strokePixelLength = 0, segmentLoop = 0;
+  var normalizedPattern = [];
+  var loop;
+  // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
+  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if(isIE) {
+    return "&mdash;";
+  }
+  if (!strokePattern || strokePattern.length <= 1) {
+    // Solid line
+    dash = "<div style=\"display: inline-block; position: relative; " +
+    "bottom: .5ex; padding-left: 1em; height: 1px; " +
+    "border-bottom: 2px solid " + color + ";\"></div>";
+  } else {
+    // 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.
+    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;
+};
+
+/**
+ * @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.
+ */
+Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
+  // 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, c, dash, strokePattern;
+  if (typeof(x) === 'undefined') {
+    if (this.attr_('legend') != 'always') return '';
+
+    sepLines = this.attr_('labelsSeparateLines');
+    var labels = this.attr_('labels');
+    html = '';
+    for (i = 1; i < labels.length; i++) {
+      if (!this.visibility()[i - 1]) continue;
+      c = this.plotter_.colors[labels[i]];
+      if (html !== '') html += (sepLines ? '<br/>' : ' ');
+      strokePattern = this.attr_("strokePattern", labels[i]);
+      dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
+      html += "<span style='font-weight: bold; color: " + c + ";'>" + dash + 
+        " " + labels[i] + "</span>";
+    }
+    return html;
+  }
+
+  var xOptView = this.optionsViewForAxis_('x');
+  var xvf = xOptView('valueFormatter');
+  html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":";
+
+  var yOptViews = [];
+  var num_axes = this.numAxes();
+  for (i = 0; i < num_axes; i++) {
+    yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : ''));
+  }
+  var showZeros = this.attr_("labelsShowZeroValues");
+  sepLines = this.attr_("labelsSeparateLines");
+  for (i = 0; i < this.selPoints_.length; i++) {
+    var pt = this.selPoints_[i];
+    if (pt.yval === 0 && !showZeros) continue;
+    if (!Dygraph.isOK(pt.canvasy)) continue;
+    if (sepLines) html += "<br/>";
+
+    var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]];
+    var fmtFunc = yOptView('valueFormatter');
+    c = this.plotter_.colors[pt.name];
+    var yval = fmtFunc(pt.yval, yOptView, pt.name, this);
+
+    // TODO(danvk): use a template string here and make it an attribute.
+    html += " <b><span style='color: " + c + ";'>" + pt.name +
+        "</span></b>:" + yval;
+  }
+  return html;
+};
+
+/**
+ * @private
+ * Displays information about the selected points in the legend. If there is no
+ * selection, the legend will be cleared.
+ * @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'.
+ */
+Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
+  var labelsDiv = this.attr_("labelsDiv");
+  var sizeSpan = document.createElement('span');
+  // Calculates the width of 1em in pixels for the legend.
+  sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+  labelsDiv.appendChild(sizeSpan);
+  var oneEmWidth=sizeSpan.offsetWidth;
+
+  var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
+  if (labelsDiv !== null) {
+    labelsDiv.innerHTML = html;
+  } else {
+    if (typeof(this.shown_legend_error_) == 'undefined') {
+      this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
+      this.shown_legend_error_ = true;
+    }
+  }
+};
+
+/**
+ * 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() {
+  // Clear the previously drawn vertical, if there is one
+  var i;
+  var ctx = this.canvas_ctx_;
+  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.attr_('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.isUsingExcanvas_ && this.currentZoomRectArgs_) {
+    Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_);
+  }
+
+  if (this.selPoints_.length > 0) {
+    // Set the status message to indicate the selected point(s)
+    if (this.attr_('showLabelsOnHighlight')) {
+      this.setLegendHTML_(this.lastx_, this.selPoints_);
+    }
+
+    // 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.attr_('highlightCircleSize', pt.name);
+      ctx.beginPath();
+      ctx.fillStyle = this.plotter_.colors[pt.name];
+      ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
+      ctx.fill();
+    }
+    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 { Integer } row number that should be highlighted (i.e. appear with
+ * hover dots on the chart). Set to false to clear any selection.
+ */
+Dygraph.prototype.setSelection = function(row) {
+  // Extract the points we've selected
+  this.selPoints_ = [];
+  var pos = 0;
+
+  if (row !== false) {
+    row = row - this.boundaryIds_[0][0];
+  }
+
+  if (row !== false && row >= 0) {
+    for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+      var set = this.layout_.datasets[setIdx];
+      if (row < set.length) {
+        var point = this.layout_.points[pos+row];
+
+        if (this.attr_("stackedGraph")) {
+          point = this.layout_.unstackPointAtIndex(pos+row);
+        }
+
+        this.selPoints_.push(point);
+      }
+      pos += set.length;
+    }
+  }
+
+  if (this.selPoints_.length) {
+    this.lastx_ = this.selPoints_[0].xval;
+    this.updateSelection_();
+  } else {
+    this.clearSelection();
+  }
+
+};
+
+/**
+ * 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.attr_("unhighlightCallback")) {
+    this.attr_("unhighlightCallback")(event);
+  }
+
+  if (this.attr_("hideOverlayOnMouseOut")) {
+    this.clearSelection();
+  }
+};
+
+/**
+ * Clears the current selection (i.e. points that were highlighted by moving
+ * the mouse over the chart).
+ */
+Dygraph.prototype.clearSelection = function() {
+  // Get rid of the overlay data
+  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
+  this.setLegendHTML_();
+  this.selPoints_ = [];
+  this.lastx_ = -1;
+};
+
+/**
+ * Returns the number of the currently selected row. To get data for this row,
+ * you can use the getValue method.
+ * @return { Integer } row number, or -1 if nothing is selected
+ */
+Dygraph.prototype.getSelection = function() {
+  if (!this.selPoints_ || this.selPoints_.length < 1) {
+    return -1;
+  }
+
+  for (var row=0; row<this.layout_.points.length; row++ ) {
+    if (this.layout_.points[row].x == this.selPoints_[0].x) {
+      return row + this.boundaryIds_[0][0];
+    }
+  }
+  return -1;
+};
+
+/**
+ * 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.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.fullXRange_();
+  }
+
+  var xAxisOptionsView = this.optionsViewForAxis_('x');
+  var xTicks = xAxisOptionsView('ticker')(
+      range[0],
+      range[1],
+      this.width_,  // 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);
+};
+
+/**
+ * @private
+ * Computes the range of the data series (including confidence intervals).
+ * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
+ * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
+ * @return [low, high]
+ */
+Dygraph.prototype.extremeValues_ = function(series) {
+  var minY = null, maxY = null, j, y;
+
+  var bars = this.attr_("errorBars") || this.attr_("customBars");
+  if (bars) {
+    // With custom bars, maxY is the max of the high values.
+    for (j = 0; j < series.length; j++) {
+      y = series[j][1][0];
+      if (!y) continue;
+      var low = y - series[j][1][1];
+      var high = y + series[j][1][2];
+      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;
+      }
+    }
+  } else {
+    for (j = 0; j < series.length; 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];
+};
+
+/**
+ * @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();
+
+  // TODO(danvk): move more computations out of drawGraph_ and into here.
+  this.computeYAxes_();
+
+  // Create a new plotter.
+  if (this.plotter_) this.plotter_.clear();
+  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_();
+
+  // Same thing applies for the labelsDiv. It's 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.
+  this.positionLabelsDiv_();
+
+  if (this.rangeSelector_) {
+    this.rangeSelector_.renderStaticLayer();
+  }
+
+  // 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 connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
+    var logScale = this.attr_('logscale', i);
+    var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints);
+    series = this.rollingAverage(series, this.rollPeriod_);
+    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);
+};
+
+/**
+ * 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.
+ *
+ * TODO(danvk): make this more of a true function
+ * @return [ datasets, seriesExtremes, boundaryIds ]
+ * @private
+ */
+Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
+  var boundaryIds = [];
+  var cumulative_y = [];  // For stacked series.
+  var datasets = [];
+  var extremes = {};  // series name -> [low, high]
+  var i, j, k;
+
+  // 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;
+  for (i = num_series; i >= 1; i--) {
+    if (!this.visibility()[i - 1]) continue;
+
+    // TODO(danvk): is this copy really necessary?
+    var series = [];
+    for (j = 0; j < rolledSeries[i].length; j++) {
+      series.push(rolledSeries[i][j]);
+    }
+
+    // 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.
+    var bars = this.attr_("errorBars") || this.attr_("customBars");
+    if (dateWindow) {
+      var low = dateWindow[0];
+      var high = dateWindow[1];
+      var pruned = [];
+      // TODO(danvk): do binary search instead of linear search.
+      // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+      var firstIdx = null, lastIdx = null;
+      for (k = 0; k < series.length; k++) {
+        if (series[k][0] >= low && firstIdx === null) {
+          firstIdx = k;
+        }
+        if (series[k][0] <= high) {
+          lastIdx = k;
+        }
+      }
+      if (firstIdx === null) firstIdx = 0;
+      if (firstIdx > 0) firstIdx--;
+      if (lastIdx === null) lastIdx = series.length - 1;
+      if (lastIdx < series.length - 1) lastIdx++;
+      boundaryIds[i-1] = [firstIdx, lastIdx];
+      for (k = firstIdx; k <= lastIdx; k++) {
+        pruned.push(series[k]);
+      }
+      series = pruned;
+    } else {
+      boundaryIds[i-1] = [0, series.length-1];
+    }
+
+    var seriesExtremes = this.extremeValues_(series);
+
+    if (bars) {
+      for (j=0; j<series.length; j++) {
+        series[j] = [series[j][0],
+                     series[j][1][0],
+                     series[j][1][1],
+                     series[j][1][2]];
+      }
+    } else if (this.attr_("stackedGraph")) {
+      var l = series.length;
+      var actual_y;
+      for (j = 0; j < l; j++) {
+        // If one data set has a NaN, let all subsequent stacked
+        // sets inherit the NaN -- only start at 0 for the first set.
+        var x = series[j][0];
+        if (cumulative_y[x] === undefined) {
+          cumulative_y[x] = 0;
+        }
+
+        actual_y = series[j][1];
+        cumulative_y[x] += actual_y;
+
+        series[j] = [x, cumulative_y[x]];
+
+        if (cumulative_y[x] > seriesExtremes[1]) {
+          seriesExtremes[1] = cumulative_y[x];
+        }
+        if (cumulative_y[x] < seriesExtremes[0]) {
+          seriesExtremes[0] = cumulative_y[x];
+        }
+      }
+    }
+
+    var seriesName = this.attr_("labels")[i];
+    extremes[seriesName] = seriesExtremes;
+    datasets[i] = series;
+  }
+
+  return [ datasets, extremes, 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.
+ *
+ * clearSelection, when undefined or true, causes this.clearSelection to be
+ * called at the end of the draw operation. This should rarely be defined,
+ * and never true (that is it should be undefined most of the time, and
+ * rarely false.)
+ *
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function(clearSelection) {
+  var start = new Date();
+
+  if (typeof(clearSelection) === 'undefined') {
+    clearSelection = true;
+  }
+
+  // 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.attr_('highlightCircleSize');
+
+  var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
+  var datasets = packed[0];
+  var extremes = packed[1];
+  this.boundaryIds_ = packed[2];
+
+  this.setIndexByName_ = {};
+  var labels = this.attr_("labels");
+  if (labels.length > 0) {
+    this.setIndexByName_[labels[0]] = 0;
+  }
+  for (var i = 1; i < datasets.length; i++) {
+    this.setIndexByName_[labels[i]] = i;
+    if (!this.visibility()[i - 1]) continue;
+    this.layout_.addDataset(labels[i], datasets[i]);
+  }
+
+  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.layout_.setDateWindow(this.dateWindow_);
+  this.zoomed_x_ = tmp_zoomed_x;
+  this.layout_.evaluateWithError();
+  this.renderGraph_(is_initial_draw, false);
+
+  if (this.attr_("timingName")) {
+    var end = new Date();
+    if (console) {
+      console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
+    }
+  }
+};
+
+Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
+  this.plotter_.clear();
+  this.plotter_.render();
+  this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+                                          this.canvas_.height);
+
+  // Generate a static legend before any particular point is selected.
+  this.setLegendHTML_();
+
+  if (!is_initial_draw) {
+    if (clearSelection) {
+      if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
+        // We should select the point nearest the page x/y here, but it's easier
+        // to just clear the selection. This prevents erroneous hover dots from
+        // being displayed.
+        this.clearSelection();
+      } else {
+        this.clearSelection();
+      }
+    }
+  }
+
+  if (this.rangeSelector_) {
+    this.rangeSelector_.renderInteractiveLayer();
+  }
+
+  if (this.attr_("drawCallback") !== null) {
+    this.attr_("drawCallback")(this, is_initial_draw);
+  }
+};
+
+/**
+ * @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_ and this.seriesToAxisMap_.
+ * axes_ = [ { options } ]
+ * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
+ *   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 i, valueWindows, seriesName, 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_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
+  this.seriesToAxisMap_ = {};
+
+  // Get a list of series names.
+  var labels = this.attr_("labels");
+  var series = {};
+  for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
+
+  // all options which could be applied per-axis:
+  var axisOptions = [
+    'includeZero',
+    'valueRange',
+    'labelsKMB',
+    'labelsKMG2',
+    'pixelsPerYLabel',
+    'yAxisLabelWidth',
+    'axisLabelFontSize',
+    'axisTickSize',
+    'logscale'
+  ];
+
+  // Copy global axis options over to the first axis.
+  for (i = 0; i < axisOptions.length; i++) {
+    var k = axisOptions[i];
+    v = this.attr_(k);
+    if (v) this.axes_[0][k] = v;
+  }
+
+  // Go through once and add all the axes.
+  for (seriesName in series) {
+    if (!series.hasOwnProperty(seriesName)) continue;
+    axis = this.attr_("axis", seriesName);
+    if (axis === null) {
+      this.seriesToAxisMap_[seriesName] = 0;
+      continue;
+    }
+    if (typeof(axis) == 'object') {
+      // Add a new axis, making a copy of its per-axis options.
+      opts = {};
+      Dygraph.update(opts, this.axes_[0]);
+      Dygraph.update(opts, { valueRange: null });  // shouldn't inherit this.
+      var yAxisId = this.axes_.length;
+      opts.yAxisId = yAxisId;
+      opts.g = this;
+      Dygraph.update(opts, axis);
+      this.axes_.push(opts);
+      this.seriesToAxisMap_[seriesName] = yAxisId;
+    }
+  }
+
+  // Go through one more time and assign series to an axis defined by another
+  // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
+  for (seriesName in series) {
+    if (!series.hasOwnProperty(seriesName)) continue;
+    axis = this.attr_("axis", seriesName);
+    if (typeof(axis) == 'string') {
+      if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
+        this.error("Series " + seriesName + " wants to share a y-axis with " +
+                   "series " + axis + ", which does not define its own axis.");
+        return null;
+      }
+      var idx = this.seriesToAxisMap_[axis];
+      this.seriesToAxisMap_[seriesName] = idx;
+    }
+  }
+
+  if (valueWindows !== undefined) {
+    // Restore valueWindow settings.
+    for (index = 0; index < valueWindows.length; index++) {
+      this.axes_[index].valueWindow = valueWindows[index];
+    }
+  }
+
+  // New axes options
+  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() {
+  var last_axis = 0;
+  for (var series in this.seriesToAxisMap_) {
+    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
+    var idx = this.seriesToAxisMap_[series];
+    if (idx > last_axis) last_axis = idx;
+  }
+  return 1 + last_axis;
+};
+
+/**
+ * @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.seriesToAxisMap_[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) {
+  // Build a map from axis number -> [list of series names]
+  var seriesForAxis = [], series;
+  for (series in this.seriesToAxisMap_) {
+    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
+    var idx = this.seriesToAxisMap_[series];
+    while (seriesForAxis.length <= idx) seriesForAxis.push([]);
+    seriesForAxis[idx].push(series);
+  }
+
+  // Compute extreme values, a span and tick marks for each axis.
+  for (var i = 0; i < this.axes_.length; i++) {
+    var axis = this.axes_[i];
+
+    if (!seriesForAxis[i]) {
+      // If no series are defined or visible then use a reasonable default
+      axis.extremeRange = [0, 1];
+    } else {
+      // Calculate the extremes of extremes.
+      series = seriesForAxis[i];
+      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);
+        }
+      }
+      if (axis.includeZero && minY > 0) minY = 0;
+
+      // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
+      if (minY == Infinity) minY = 0;
+      if (maxY == -Infinity) maxY = 1;
+
+      // Add some padding and round up to an integer to be human-friendly.
+      var span = maxY - minY;
+      // special case: if we have no sense of scale, use +/-10% of the sole value.
+      if (span === 0) { span = maxY; }
+
+      var maxAxisY, minAxisY;
+      if (axis.logscale) {
+        maxAxisY = maxY + 0.1 * span;
+        minAxisY = minY;
+      } else {
+        maxAxisY = maxY + 0.1 * span;
+        minAxisY = minY - 0.1 * span;
+
+        // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
+        if (!this.attr_("avoidMinZero")) {
+          if (minAxisY < 0 && minY >= 0) minAxisY = 0;
+          if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
+        }
+
+        if (this.attr_("includeZero")) {
+          if (maxY < 0) maxAxisY = 0;
+          if (minY > 0) minAxisY = 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.
+      axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
+    } else {
+      axis.computedValueRange = axis.extremeRange;
+    }
+
+    // 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.
+    var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
+    var ticker = opts('ticker');
+    if (i === 0 || axis.independentTicks) {
+      axis.ticks = ticker(axis.computedValueRange[0],
+                          axis.computedValueRange[1],
+                          this.height_,  // TODO(danvk): should be area.height
+                          opts,
+                          this);
+    } else {
+      var p_axis = this.axes_[0];
+      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.height_,  // TODO(danvk): should be area.height
+                          opts,
+                          this,
+                          tick_values);
+    }
+  }
+};
+
+/**
+ * Extracts one series from the raw data (a 2D array) into an array of (date,
+ * value) tuples.
+ *
+ * This is where undesirable points (i.e. negative values on log scales and
+ * missing values through which we wish to connect lines) are dropped.
+ * 
+ * @private
+ */
+Dygraph.prototype.extractSeries_ = function(rawData, i, logScale, connectSeparatedPoints) {
+  var series = [];
+  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. Note that this ignores
+      // connectSeparatedPoints.
+      if (point <= 0) {
+        point = null;
+      }
+      series.push([x, point]);
+    } else {
+      if (point !== null || !connectSeparatedPoints) {
+        series.push([x, point]);
+      }
+    }
+  }
+  return series;
+};
+
+/**
+ * @private
+ * Calculates the rolling average of a data set.
+ * If originalData is [label, val], rolls the average of those.
+ * If originalData is [label, [, it's interpreted as [value, stddev]
+ *   and the roll is returned in the same form, with appropriately reduced
+ *   stddev for each value.
+ * Note that this is where fractional input (i.e. '5/10') is converted into
+ *   decimal values.
+ * @param {Array} originalData The data in the appropriate format (see above)
+ * @param {Number} rollPeriod The number of points over which to average the
+ *                            data
+ */
+Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
+  if (originalData.length < 2)
+    return originalData;
+  rollPeriod = Math.min(rollPeriod, originalData.length);
+  var rollingData = [];
+  var sigma = this.attr_("sigma");
+
+  var low, high, i, j, y, sum, num_ok, stddev;
+  if (this.fractions_) {
+    var num = 0;
+    var den = 0;  // numerator/denominator
+    var mult = 100.0;
+    for (i = 0; i < originalData.length; i++) {
+      num += originalData[i][1][0];
+      den += originalData[i][1][1];
+      if (i - rollPeriod >= 0) {
+        num -= originalData[i - rollPeriod][1][0];
+        den -= originalData[i - rollPeriod][1][1];
+      }
+
+      var date = originalData[i][0];
+      var value = den ? num / den : 0.0;
+      if (this.attr_("errorBars")) {
+        if (this.attr_("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, (p - low) * mult, (high - p) * 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 * stddev, mult * stddev]];
+        }
+      } else {
+        rollingData[i] = [date, mult * value];
+      }
+    }
+  } else if (this.attr_("customBars")) {
+    low = 0;
+    var mid = 0;
+    high = 0;
+    var count = 0;
+    for (i = 0; i < originalData.length; i++) {
+      var data = originalData[i][1];
+      y = data[1];
+      rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
+
+      if (y !== null && !isNaN(y)) {
+        low += data[0];
+        mid += y;
+        high += data[2];
+        count += 1;
+      }
+      if (i - rollPeriod >= 0) {
+        var prev = originalData[i - rollPeriod];
+        if (prev[1][1] !== null && !isNaN(prev[1][1])) {
+          low -= prev[1][0];
+          mid -= prev[1][1];
+          high -= prev[1][2];
+          count -= 1;
+        }
+      }
+      if (count) {
+        rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
+                                                1.0 * (mid - low) / count,
+                                                1.0 * (high - mid) / count ]];
+      } else {
+        rollingData[i] = [originalData[i][0], [null, null, null]];
+      }
+    }
+  } else {
+    // 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 (!this.attr_("errorBars")){
+      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];
+        }
+      }
+
+    } else {
+      for (i = 0; i < originalData.length; i++) {
+        sum = 0;
+        var variance = 0;
+        num_ok = 0;
+        for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+          y = originalData[j][1][0];
+          if (y === null || isNaN(y)) continue;
+          num_ok++;
+          sum += originalData[j][1][0];
+          variance += Math.pow(originalData[j][1][1], 2);
+        }
+        if (num_ok) {
+          stddev = Math.sqrt(variance) / num_ok;
+          rollingData[i] = [originalData[i][0],
+                            [sum / num_ok, sigma * stddev, sigma * stddev]];
+        } else {
+          rollingData[i] = [originalData[i][0], [null, null, null]];
+        }
+      }
+    }
+  }
+
+  return rollingData;
+};
+
+/**
+ * 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;
+  }
+
+  if (isDate) {
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
+  } 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;
+  }
+};
+
+/**
+ * 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.
+ * @private
+ */
+
+// Parse the x as a float or return null if it's not a number.
+Dygraph.prototype.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 !== null && opt_line_no !== null) {
+    msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
+  }
+  this.error(msg);
+
+  return null;
+};
+
+/**
+ * @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 lines = data.split("\n");
+  var vals, j;
+
+  // Use the default delimiter or fall back to a tab if that makes sense.
+  var delim = this.attr_('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_.
+  }
+  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.attr_("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) {
+          this.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] = [this.parseFloat_(vals[0], i, line),
+                       this.parseFloat_(vals[1], i, line)];
+        }
+      }
+    } else if (this.attr_("errorBars")) {
+      // If there are error bars, values are (value, stddev) pairs
+      if (inFields.length % 2 != 1) {
+        this.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] = [this.parseFloat_(inFields[j], i, line),
+                               this.parseFloat_(inFields[j + 1], i, line)];
+      }
+    } else if (this.attr_("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] = [ this.parseFloat_(vals[0], i, line),
+                          this.parseFloat_(vals[1], i, line),
+                          this.parseFloat_(vals[2], i, line) ];
+          } else {
+            this.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] = this.parseFloat_(inFields[j], i, line);
+      }
+    }
+    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
+
+    if (fields.length != expectedCols) {
+      this.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) {
+        this.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) {
+    this.warn("CSV is out of order; order it correctly to speed loading.");
+    ret.sort(function(a,b) { return a[0] - b[0]; });
+  }
+
+  return ret;
+};
+
+/**
+ * @private
+ * 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 {[Object]} data
+ * @return {[Object]} data with numeric x values.
+ */
+Dygraph.prototype.parseArray_ = function(data) {
+  // Peek at the first x value to see if it's numeric.
+  if (data.length === 0) {
+    this.error("Can't plot empty data set");
+    return null;
+  }
+  if (data[0].length === 0) {
+    this.error("Data set cannot contain an empty row");
+    return null;
+  }
+
+  var i;
+  if (this.attr_("labels") === null) {
+    this.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);
+    }
+  }
+
+  if (Dygraph.isDateLike(data[0][0])) {
+    // Some intelligent defaults for a date x-axis.
+    this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+
+    // Assume they're all dates.
+    var parsedData = Dygraph.clone(data);
+    for (i = 0; i < data.length; i++) {
+      if (parsedData[i].length === 0) {
+        this.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())) {
+        this.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.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
+    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
+    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 {[Object]} 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.dateString_;
+    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
+    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
+  } 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 {
+    this.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.attr_('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 {
+      this.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.attr_("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) {
+      this.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.attr_("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) {
+    this.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);
+  }
+};
+
+/**
+ * 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.predraw_();
+  } else if (typeof data == 'object' &&
+             typeof data.getColumnRange == 'function') {
+    // must be a DataTable from gviz.
+    this.parseDataTable_(data);
+    this.predraw_();
+  } else if (typeof data == 'string') {
+    // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
+    if (data.indexOf('\n') >= 0) {
+      this.loadedEvent_(data);
+    } else {
+      var req = new XMLHttpRequest();
+      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 {
+    this.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} 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;
+
+  // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
+  var file = input_attrs.file;
+  var attrs = Dygraph.mapLegacyOptions_(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);
+
+  if (file) {
+    this.file_ = file;
+    if (!block_redraw) this.start_();
+  } else {
+    if (!block_redraw) {
+      if (requiresNewPoints) {
+        this.predraw_();
+      } else {
+        this.renderGraph_(false, false);
+      }
+    }
+  }
+};
+
+/**
+ * Returns a copy of the options with deprecated names converted into current
+ * names. Also drops the (potentially-large) 'file' attribute. If the caller is
+ * interested in that, they should save a copy before calling this.
+ * @private
+ */
+Dygraph.mapLegacyOptions_ = function(attrs) {
+  var my_attrs = {};
+  for (var k in attrs) {
+    if (k == 'file') continue;
+    if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
+  }
+
+  var set = function(axis, opt, value) {
+    if (!my_attrs.axes) my_attrs.axes = {};
+    if (!my_attrs.axes[axis]) my_attrs.axes[axis] = {};
+    my_attrs.axes[axis][opt] = value;
+  };
+  var map = function(opt, axis, new_opt) {
+    if (typeof(attrs[opt]) != 'undefined') {
+      set(axis, new_opt, attrs[opt]);
+      delete my_attrs[opt];
+    }
+  };
+
+  // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
+  map('xValueFormatter', 'x', 'valueFormatter');
+  map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
+  map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
+  map('xTicker', 'x', 'ticker');
+  map('yValueFormatter', 'y', 'valueFormatter');
+  map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
+  map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
+  map('yTicker', 'y', 'ticker');
+  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)) {
+    this.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_) {
+    // TODO(danvk): there should be a clear() method.
+    this.maindiv_.innerHTML = "";
+    this.roller_ = null;
+    this.attrs_.labelsDiv = null;
+    this.createInterface_();
+    if (this.annotations_.length) {
+      // createInterface_ reset the layout, so we need to do this.
+      this.layout_.setAnnotations(this.annotations_);
+    }
+    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.attr_("visibility")) {
+    this.attrs_.visibility = [];
+  }
+  // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
+  while (this.attr_("visibility").length < this.numColumns() - 1) {
+    this.attrs_.visibility.push(true);
+  }
+  return this.attr_("visibility");
+};
+
+/**
+ * Changes the visiblity of a series.
+ */
+Dygraph.prototype.setVisibility = function(num, value) {
+  var x = this.visibility();
+  if (num < 0 || num >= x.length) {
+    this.warn("invalid series number in setVisibility: " + num);
+  } else {
+    x[num] = 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;
+  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.
+ */
+Dygraph.prototype.getLabels = function(name) {
+  return this.attr_("labels").slice();
+};
+
+/**
+ * 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];
+};
+
+/**
+ * @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() {
+  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.
+    }
+  }
+
+  this.warn("Unable to add default annotation CSS rule; display may be off.");
+};
+
+// Older pages may still use this name.
+var DateGraph = Dygraph;
+/**
+ * @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.
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */
+"use strict";
+
+Dygraph.LOG_SCALE = 10;
+Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
+
+/** @private */
+Dygraph.log10 = function(x) {
+  return Math.log(x) / Dygraph.LN_TEN;
+};
+
+// Various logging levels.
+Dygraph.DEBUG = 1;
+Dygraph.INFO = 2;
+Dygraph.WARNING = 3;
+Dygraph.ERROR = 3;
+
+// Set this to log stack traces on warnings, etc.
+// This requires stacktrace.js, which is up to you to provide.
+// A copy can be found in the dygraphs repo, or at
+// https://github.com/eriwen/javascript-stacktrace
+Dygraph.LOG_STACK_TRACES = false;
+
+/** 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];
+
+/**
+ * @private
+ * Log an error on the JS console at the given severity.
+ * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
+ * @param { String } The message to log.
+ */
+Dygraph.log = function(severity, message) {
+  var st;
+  if (typeof(printStackTrace) != 'undefined') {
+    // Remove uninteresting bits: logging functions and paths.
+    st = printStackTrace({guess:false});
+    while (st[0].indexOf("stacktrace") != -1) {
+      st.splice(0, 1);
+    }
+
+    st.splice(0, 2);
+    for (var i = 0; i < st.length; i++) {
+      st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1')
+          .replace(/\@.*\/([^\/]*)/, '@$1')
+          .replace('[object Object].', '');
+    }
+    var top_msg = st.splice(0, 1)[0];
+    message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')';
+  }
+
+  if (typeof(console) != 'undefined') {
+    switch (severity) {
+      case Dygraph.DEBUG:
+        console.debug('dygraphs: ' + message);
+        break;
+      case Dygraph.INFO:
+        console.info('dygraphs: ' + message);
+        break;
+      case Dygraph.WARNING:
+        console.warn('dygraphs: ' + message);
+        break;
+      case Dygraph.ERROR:
+        console.error('dygraphs: ' + message);
+        break;
+    }
+  }
+
+  if (Dygraph.LOG_STACK_TRACES) {
+    console.log(st.join('\n'));
+  }
+};
+
+/** @private */
+Dygraph.info = function(message) {
+  Dygraph.log(Dygraph.INFO, message);
+};
+/** @private */
+Dygraph.prototype.info = Dygraph.info;
+
+/** @private */
+Dygraph.warn = function(message) {
+  Dygraph.log(Dygraph.WARNING, message);
+};
+/** @private */
+Dygraph.prototype.warn = Dygraph.warn;
+
+/** @private */
+Dygraph.error = function(message) {
+  Dygraph.log(Dygraph.ERROR, message);
+};
+/** @private */
+Dygraph.prototype.error = Dygraph.error;
+
+/**
+ * @private
+ * 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);
+ * };
+ */
+Dygraph.getContext = function(canvas) {
+  return canvas.getContext("2d");
+};
+
+/**
+ * @private
+ * Add an event handler. This smooths a difference between IE and the rest of
+ * the world.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param { Function } fn The function to call on the event. The function takes
+ * one parameter: the event object.
+ */
+Dygraph.addEvent = function addEvent(elem, type, fn) {
+  if (elem.addEventListener) {
+    elem.addEventListener(type, fn, false);
+  } else {
+    elem[type+fn] = function(){fn(window.event);};
+    elem.attachEvent('on'+type, elem[type+fn]);
+  }
+};
+
+/**
+ * @private
+ * Remove an event handler. This smooths a difference between IE and the rest of
+ * the world.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param { Function } fn The function to call on the event. The function takes
+ * one parameter: the event object.
+ */
+Dygraph.removeEvent = function addEvent(elem, type, fn) {
+  if (elem.removeEventListener) {
+    elem.removeEventListener(type, fn, false);
+  } else {
+    elem.detachEvent('on'+type, elem[type+fn]);
+    elem[type+fn] = null;
+  }
+};
+
+/**
+ * @private
+ * 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.
+ */
+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 x-coordinate of the supplied object relative to the left side
+ * of the page.
+ * @private
+ */
+Dygraph.findPosX = function(obj) {
+  var curleft = 0;
+  if(obj.offsetParent) {
+    var copyObj = obj;
+    while(1) {
+      curleft += copyObj.offsetLeft;
+      if(!copyObj.offsetParent) {
+        break;
+      }
+      copyObj = copyObj.offsetParent;
+    }
+  } else if(obj.x) {
+    curleft += obj.x;
+  }
+  // This handles the case where the object is inside a scrolled div.
+  while(obj && obj != document.body) {
+    curleft -= obj.scrollLeft;
+    obj = obj.parentNode;
+  }
+  return curleft;
+};
+
+/**
+ * Find the y-coordinate of the supplied object relative to the top of the
+ * page.
+ * @private
+ */
+Dygraph.findPosY = function(obj) {
+  var curtop = 0;
+  if(obj.offsetParent) {
+    var copyObj = obj;
+    while(1) {
+      curtop += copyObj.offsetTop;
+      if(!copyObj.offsetParent) {
+        break;
+      }
+      copyObj = copyObj.offsetParent;
+    }
+  } else if(obj.y) {
+    curtop += obj.y;
+  }
+  // This handles the case where the object is inside a scrolled div.
+  while(obj && obj != document.body) {
+    curtop -= obj.scrollTop;
+    obj = obj.parentNode;
+  }
+  return curtop;
+};
+
+/**
+ * @private
+ * 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
+ */
+Dygraph.pageX = function(e) {
+  if (e.pageX) {
+    return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
+  } else {
+    var de = document;
+    var b = document.body;
+    return e.clientX +
+        (de.scrollLeft || b.scrollLeft) -
+        (de.clientLeft || 0);
+  }
+};
+
+/**
+ * @private
+ * 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
+ */
+Dygraph.pageY = function(e) {
+  if (e.pageY) {
+    return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
+  } else {
+    var de = document;
+    var b = document.body;
+    return e.clientY +
+        (de.scrollTop || b.scrollTop) -
+        (de.clientTop || 0);
+  }
+};
+
+/**
+ * @private
+ * @param { Number } x The number to consider.
+ * @return { Boolean } Whether the number is zero or NaN.
+ */
+// TODO(danvk): rename this function to something like 'isNonZeroNan'.
+// TODO(danvk): determine when else this returns false (e.g. for undefined or null)
+Dygraph.isOK = function(x) {
+  return x && !isNaN(x);
+};
+
+/**
+ * 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);
+};
+
+/**
+ * @private
+ * Converts '9' to '09' (useful for dates)
+ */
+Dygraph.zeropad = function(x) {
+  if (x < 10) return "0" + x; else return "" + x;
+};
+
+/**
+ * Return a string version of the hours, minutes and seconds portion of a date.
+ * @param {Number} date The JavaScript date (ms since epoch)
+ * @return {String} A time of the form "HH:MM:SS"
+ * @private
+ */
+Dygraph.hmsString_ = function(date) {
+  var zeropad = Dygraph.zeropad;
+  var d = new Date(date);
+  if (d.getSeconds()) {
+    return zeropad(d.getHours()) + ":" +
+           zeropad(d.getMinutes()) + ":" +
+           zeropad(d.getSeconds());
+  } else {
+    return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
+  }
+};
+
+/**
+ * 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;
+};
+
+/**
+ * @private
+ * Implementation of binary search over an array.
+ * Currently does not work when val is outside the range of arry's values.
+ * @param { Integer } val the value to search for
+ * @param { Integer[] } arry is the value over which to search
+ * @param { Integer } 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 { Integer } [low] The first index in arry to consider (optional)
+ * @param { Integer } [high] The last index in arry to consider (optional)
+ */
+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];
+  if (element == val) {
+    return mid;
+  }
+
+  var idx;
+  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);
+  }
+  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);
+  }
+};
+
+/**
+ * @private
+ * 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} A date in YYYYMMDD format.
+ * @return {Number} Milliseconds since epoch.
+ */
+Dygraph.dateParser = function(dateStr) {
+  var dateStrSlashed;
+  var d;
+
+  // Let the system try the format first.
+  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)) {
+    Dygraph.error("Couldn't parse " + dateStr + " as a date");
+  }
+  return d;
+};
+
+/**
+ * @private
+ * 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 { Integer } millis since epoch
+ */
+Dygraph.dateStrToMillis = function(str) {
+  return new Date(str).getTime();
+};
+
+// These functions are all based on MochiKit.
+/**
+ * Copies all the properties from o to self.
+ *
+ * @private
+ */
+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.
+ *
+ * @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] = {};
+          }
+          Dygraph.updateDeep(self[k], o[k]);
+        } else {
+          self[k] = o[k];
+        }
+      }
+    }
+  }
+  return self;
+};
+
+/**
+ * @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;
+};
+
+/**
+ * @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.
+ * @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;
+};
+
+/**
+ * @private
+ * Create a new canvas element. This is more complex than a simple
+ * document.createElement("canvas") because of IE and excanvas.
+ */
+Dygraph.createCanvas = function() {
+  var canvas = document.createElement("canvas");
+
+  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
+    canvas = G_vmlCanvasManager.initElement(canvas);
+  }
+
+  return canvas;
+};
+
+/**
+ * @private
+ * Checks whether the user is on an Android browser.
+ * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
+ */
+Dygraph.isAndroid = function() {
+  return (/Android/).test(navigator.userAgent);
+};
+
+/**
+ * @private
+ * Call a function N times at a given interval, then call a cleanup function
+ * once. repeat_fn is called once immediately, then (times - 1) times
+ * asynchronously. If times=1, then cleanup_fn() is also called synchronously.
+ * @param repeat_fn {Function} Called repeatedly -- takes the number of calls
+ * (from 0 to times-1) as an argument.
+ * @param times {number} The number of times to call repeat_fn
+ * @param every_ms {number} Milliseconds between calls
+ * @param cleanup_fn {Function} A function to call after all repeat_fn calls.
+ * @private
+ */
+Dygraph.repeatAndCleanup = function(repeat_fn, times, every_ms, cleanup_fn) {
+  var count = 0;
+  var start_time = new Date().getTime();
+  repeat_fn(count);
+  if (times == 1) {
+    cleanup_fn();
+    return;
+  }
+
+  (function loop() {
+    if (count >= times) return;
+    var target_time = start_time + (1 + count) * every_ms;
+    setTimeout(function() {
+      count++;
+      repeat_fn(count);
+      if (count >= times - 1) {
+        cleanup_fn();
+      } else {
+        loop();
+      }
+    }, target_time - new Date().getTime());
+    // TODO(danvk): adjust every_ms to produce evenly-timed function calls.
+  })();
+};
+
+/**
+ * @private
+ * This function will scan the option list and determine if they
+ * require us to recalculate the pixel positions of each point.
+ * @param { List } a list of options to check.
+ * @return { Boolean } true if the graph needs new points else false.
+ */
+Dygraph.isPixelChangingOptionList = function(labels, attrs) {
+  // 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,
+    'digitsAfterDecimal': true,
+    'drawCallback': true,
+    'drawPoints': true,
+    'drawXGrid': true,
+    'drawYGrid': 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,
+    'maxNumberWidth': true,
+    'panEdgeFraction': true,
+    'pixelsPerYLabel': true,
+    'pointClickCallback': true,
+    'pointSize': true,
+    'rangeSelectorPlotFillColor': true,
+    'rangeSelectorPlotStrokeColor': true,
+    'showLabelsOnHighlight': true,
+    'showRoller': true,
+    'sigFigs': true,
+    'strokeWidth': true,
+    'underlayCallback': true,
+    'unhighlightCallback': true,
+    'xAxisLabelFormatter': true,
+    'xTicker': true,
+    'xValueFormatter': true,
+    'yAxisLabelFormatter': true,
+    'yValueFormatter': true,
+    'zoomCallback': true
+  };
+
+  // Assume that we do not require new points.
+  // This will change to true if we actually do need new points.
+  var requiresNewPoints = false;
+
+  // 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;
+    }
+  }
+
+  // Iterate through the list of updated options.
+  for (var property in attrs) {
+    // Break early if we already know we need new points from a previous option.
+    if (requiresNewPoints) {
+      break;
+    }
+    if (attrs.hasOwnProperty(property)) {
+      // Find out of this field is actually a series specific options list.
+      if (seriesNamesDictionary[property]) {
+        // This property value is a list of options for this series.
+        // If any of these sub properties are not pixel safe, set the flag.
+        for (var subProperty in attrs[property]) {
+          // Break early if we already know we need new points from a previous option.
+          if (requiresNewPoints) {
+            break;
+          }
+          if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
+            requiresNewPoints = true;
+          }
+        }
+      // If this was not a series specific option list, check if its a pixel changing property.
+      } else if (!pixelSafeOptions[property]) {
+        requiresNewPoints = true;
+      }
+    }
+  }
+
+  return requiresNewPoints;
+};
+
+/**
+ * Compares two arrays to see if they are equal. If either parameter is not an
+ * array it will return false. Does a shallow compare 
+ * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
+ * @param array1 first array
+ * @param array2 second array
+ * @return True if both parameters are arrays, and contents are equal.
+ */
+Dygraph.compareArrays = function(array1, array2) {
+  if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
+    return false;
+  }
+  if (array1.length !== array2.length) {
+    return false;
+  }
+  for (var i = 0; i < array1.length; i++) {
+    if (array1[i] !== array2[i]) {
+      return false;
+    }
+  }
+  return true;
+};
+/**
+ * @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
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * A wrapper around Dygraph that implements the gviz API.
+ * @param {Object} container The DOM object the visualization should live in.
+ */
+Dygraph.GVizChart = function(container) {
+  this.container = container;
+};
+
+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} 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} 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 datasets = this.date_graph.layout_.datasets;
+  for (var setIdx = 0; setIdx < datasets.length; ++setIdx) {
+    selection.push({row: row, column: setIdx + 1});
+  }
+
+  return selection;
+};
+/**
+ * @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)
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * A collection of functions to facilitate build custom interaction models.
+ * @class
+ */
+Dygraph.Interaction = {};
+
+/**
+ * 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();
+  context.dateRange = xRange[1] - xRange[0];
+  context.initialLeftmostDate = xRange[0];
+  context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
+
+  if (g.attr_("panEdgeFraction")) {
+    var maxXPixelsToDraw = g.width_ * g.attr_("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.attr_("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);
+      var boundedBottomValue = g.toDataYCoord(boundedBottomY);
+
+      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.
+  context.is2DPan = false;
+  for (i = 0; i < g.axes_.length; i++) {
+    axis = g.axes_[i];
+    var yRange = g.yAxisRange(i);
+    // TODO(konigsberg): These values should be in |context|.
+    // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
+    if (axis.logscale) {
+      axis.initialTopValue = Dygraph.log10(yRange[1]);
+      axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
+    } else {
+      axis.initialTopValue = yRange[1];
+      axis.dragValueRange = yRange[1] - yRange[0];
+    }
+    axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
+
+    // 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 = g.dragGetX_(event, context);
+  context.dragEndY = g.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;
+    }
+  }
+
+  g.dateWindow_ = [minDate, maxDate];
+
+  // y-axis scaling is automatic unless this is a full 2D pan.
+  if (context.is2DPan) {
+    // Adjust each axis appropriately.
+    for (var i = 0; i < g.axes_.length; i++) {
+      var axis = g.axes_[i];
+
+      var pixelsDragged = context.dragEndY - context.dragStartY;
+      var unitsDragged = pixelsDragged * axis.unitsPerPixel;
+
+      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
+
+      // In log scale, maxValue and minValue are the logs of those values.
+      var maxValue = axis.initialTopValue + unitsDragged;
+      if (boundedValue) {
+        maxValue = Math.min(maxValue, boundedValue[1]);
+      }
+      var minValue = maxValue - axis.dragValueRange;
+      if (boundedValue) {
+        if (minValue < boundedValue[0]) {
+          // Adjust maxValue, and recompute minValue.
+          maxValue = maxValue - (minValue - boundedValue[0]);
+          minValue = maxValue - axis.dragValueRange;
+        }
+      }
+      if (axis.logscale) {
+        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 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.endPan = function(event, g, context) {
+  context.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.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);
+  }
+
+  // TODO(konigsberg): Clear the context data from the axis.
+  // (replace with "context = {}" ?)
+  // TODO(konigsberg): mouseup should just delete the
+  // context object, and mousedown should create a new one.
+  context.isPanning = false;
+  context.is2DPan = false;
+  context.initialLeftmostDate = null;
+  context.dateRange = null;
+  context.valueRange = null;
+  context.boundedDates = null;
+  context.boundedValues = null;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.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;
+};
+
+Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
+  var clickCallback = g.attr_('clickCallback');
+  var pointClickCallback = g.attr_('pointClickCallback');
+
+  var selectedPoint = null;
+
+  // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
+  if (pointClickCallback) {
+    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.attr_('highlightCircleSize') + 2;
+    if (closestDistance <= radius * radius) {
+      selectedPoint = g.selPoints_[closestIdx];
+    }
+  }
+
+  if (selectedPoint) {
+    pointClickCallback(event, selectedPoint);
+  }
+
+  // TODO(danvk): pass along more info about the points, e.g. 'x'
+  if (clickCallback) {
+    clickCallback(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) {
+  context.isZooming = false;
+  context.dragEndX = g.dragGetX_(event, context);
+  context.dragEndY = g.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);
+  }
+
+  if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
+    g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
+               Math.max(context.dragStartX, context.dragEndX));
+  } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
+    g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
+               Math.max(context.dragStartY, context.dragEndY));
+  } else {
+    g.clearZoomRect_();
+  }
+  context.dragStartX = null;
+  context.dragStartY = null;
+};
+
+/**
+ * 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) {
+    context.initializeMouseDown(event, g, context);
+
+    if (event.altKey || event.shiftKey) {
+      Dygraph.startPan(event, g, context);
+    } else {
+      Dygraph.startZoom(event, g, context);
+    }
+  },
+
+  // Draw zoom rectangles when the mouse is down and the user moves around
+  mousemove: function(event, g, context) {
+    if (context.isZooming) {
+      Dygraph.moveZoom(event, g, context);
+    } else if (context.isPanning) {
+      Dygraph.movePan(event, g, context);
+    }
+  },
+
+  mouseup: function(event, g, context) {
+    if (context.isZooming) {
+      Dygraph.endZoom(event, g, context);
+    } else if (context.isPanning) {
+      Dygraph.endPan(event, g, context);
+    }
+  },
+
+  // Temporarily cancel the dragging event when the mouse leaves the graph
+  mouseout: function(event, g, context) {
+    if (context.isZooming) {
+      context.dragEndX = null;
+      context.dragEndY = null;
+    }
+  },
+
+  // Disable zooming out if panning.
+  dblclick: function(event, g, context) {
+    if (event.altKey || event.shiftKey) {
+      return;
+    }
+    // TODO(konigsberg): replace g.doUnzoom()_ with something that is
+    // friendlier to public use.
+    g.doUnzoom_();
+  }
+};
+
+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: function(event, g, context) {
+    // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
+    context.dragEndX = g.dragGetX_(event, context);
+    context.dragEndY = g.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);
+    }
+  }
+};
+
+// 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);
+    }
+  }
+};
+// Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
+// All Rights Reserved.
+
+/**
+ * @fileoverview This file contains the DygraphRangeSelector class used to provide
+ * a timeline range selector widget for dygraphs.
+ */
+
+/*jshint globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+/**
+ * The DygraphRangeSelector class provides a timeline range selector widget.
+ * @param {Dygraph} dygraph The dygraph object
+ * @constructor
+ */
+var DygraphRangeSelector = function(dygraph) {
+  this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
+  this.isUsingExcanvas_ = dygraph.isUsingExcanvas_;
+  this.dygraph_ = dygraph;
+  this.createCanvases_();
+  if (this.isUsingExcanvas_) {
+    this.createIEPanOverlay_();
+  }
+  this.createZoomHandles_();
+  this.initInteraction_();
+};
+
+/**
+ * Adds the range selector to the dygraph.
+ * @param {Object} graphDiv The container div for the range selector.
+ * @param {DygraphLayout} layout The DygraphLayout object for this graph.
+ */
+DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) {
+  this.layout_ = layout;
+  this.resize_();
+  graphDiv.appendChild(this.bgcanvas_);
+  graphDiv.appendChild(this.fgcanvas_);
+  graphDiv.appendChild(this.leftZoomHandle_);
+  graphDiv.appendChild(this.rightZoomHandle_);
+};
+
+/**
+ * Renders the static background portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderStaticLayer = function() {
+  this.resize_();
+  this.drawStaticLayer_();
+};
+
+/**
+ * Renders the interactive foreground portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderInteractiveLayer = function() {
+  if (this.isChangingRange_) {
+    return;
+  }
+  this.placeZoomHandles_();
+  this.drawInteractiveLayer_();
+};
+
+/**
+ * @private
+ * Resizes the range selector.
+ */
+DygraphRangeSelector.prototype.resize_ = function() {
+  function setElementRect(canvas, rect) {
+    canvas.style.top = rect.y + 'px';
+    canvas.style.left = rect.x + 'px';
+    canvas.width = rect.w;
+    canvas.height = rect.h;
+    canvas.style.width = canvas.width + 'px';    // for IE
+    canvas.style.height = canvas.height + 'px';  // for IE
+  }
+
+  var plotArea = this.layout_.getPlotArea();
+  var xAxisLabelHeight = this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+  this.canvasRect_ = {
+    x: plotArea.x,
+    y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
+    w: plotArea.w,
+    h: this.attr_('rangeSelectorHeight')
+  };
+
+  setElementRect(this.bgcanvas_, this.canvasRect_);
+  setElementRect(this.fgcanvas_, this.canvasRect_);
+};
+
+DygraphRangeSelector.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+/**
+ * @private
+ * Creates the background and foreground canvases.
+ */
+DygraphRangeSelector.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 overlay divs for IE/Excanvas so that mouse events are handled properly.
+ */
+DygraphRangeSelector.prototype.createIEPanOverlay_ = function() {
+  this.iePanOverlay_ = document.createElement("div");
+  this.iePanOverlay_.style.position = 'absolute';
+  this.iePanOverlay_.style.backgroundColor = 'white';
+  this.iePanOverlay_.style.filter = 'alpha(opacity=0)';
+  this.iePanOverlay_.style.display = 'none';
+  this.iePanOverlay_.style.cursor = 'move';
+  this.fgcanvas_.appendChild(this.iePanOverlay_);
+};
+
+/**
+ * @private
+ * Creates the zoom handle elements.
+ */
+DygraphRangeSelector.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';
+  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=';
+  }
+
+  this.leftZoomHandle_ = img;
+  this.rightZoomHandle_ = img.cloneNode(false);
+};
+
+/**
+ * @private
+ * Sets up the interaction for the range selector.
+ */
+DygraphRangeSelector.prototype.initInteraction_ = function() {
+  var self = this;
+  var topElem = this.isIE_ ? document : window;
+  var xLast = 0;
+  var handle = null;
+  var isZooming = false;
+  var isPanning = false;
+
+  // 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, onCanvasMouseMove;
+
+  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;
+    xLast = e.screenX;
+    handle = e.target ? e.target : e.srcElement;
+    Dygraph.addEvent(topElem, 'mousemove', onZoom);
+    Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'col-resize';
+  };
+
+  onZoom = function(e) {
+    if (!isZooming) {
+      return;
+    }
+    var delX = e.screenX - xLast;
+    if (Math.abs(delX) < 4) {
+      return;
+    }
+    xLast = e.screenX;
+    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 not using excanvas).
+    if (!self.isUsingExcanvas_) {
+      doZoom();
+    }
+  };
+
+  onZoomEnd = function(e) {
+    if (!isZooming) {
+      return;
+    }
+    isZooming = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onZoom);
+    Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
+    self.fgcanvas_.style.cursor = 'default';
+
+    // If using excanvas, Zoom now.
+    if (self.isUsingExcanvas_) {
+      doZoom();
+    }
+  };
+
+  doZoom = function() {
+    try {
+      var zoomHandleStatus = self.getZoomHandleStatus_();
+      self.isChangingRange_ = true;
+      if (!zoomHandleStatus.isZoomed) {
+        self.dygraph_.doUnzoom_();
+      } else {
+        var xDataWindow = toXDataWindow(zoomHandleStatus);
+        self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
+      }
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  isMouseInPanZone = function(e) {
+    if (self.isUsingExcanvas_) {
+        return e.srcElement == self.iePanOverlay_;
+    } else {
+      // Getting clientX directly from the event is not accurate enough :(
+      var clientX;
+      if (e.offsetX != undefined) {
+        clientX = self.canvasRect_.x + e.offsetX;
+      } else {
+        clientX = e.clientX;
+      }
+      var zoomHandleStatus = self.getZoomHandleStatus_();
+      return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
+    }
+  };
+
+  onPanStart = function(e) {
+    if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
+      Dygraph.cancelEvent(e);
+      isPanning = true;
+      xLast = e.screenX;
+      Dygraph.addEvent(topElem, 'mousemove', onPan);
+      Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
+    }
+  };
+
+  onPan = function(e) {
+    if (!isPanning) {
+      return;
+    }
+    Dygraph.cancelEvent(e);
+
+    var delX = e.screenX - xLast;
+    if (Math.abs(delX) < 4) {
+      return;
+    }
+    xLast = e.screenX;
+
+    // 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 not using excanvas).
+    if (!self.isUsingExcanvas_) {
+      doPan();
+    }
+  };
+
+  onPanEnd = function(e) {
+    if (!isPanning) {
+      return;
+    }
+    isPanning = false;
+    Dygraph.removeEvent(topElem, 'mousemove', onPan);
+    Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
+    // If using excanvas, do pan now.
+    if (self.isUsingExcanvas_) {
+      doPan();
+    }
+  };
+
+  doPan = function() {
+    try {
+      self.isChangingRange_ = true;
+      self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
+      self.dygraph_.drawGraph_(false);
+    } finally {
+      self.isChangingRange_ = false;
+    }
+  };
+
+  onCanvasMouseMove = function(e) {
+    if (isZooming || isPanning) {
+      return;
+    }
+    var cursor = isMouseInPanZone(e) ? 'move' : 'default';
+    if (cursor != self.fgcanvas_.style.cursor) {
+      self.fgcanvas_.style.cursor = cursor;
+    }
+  };
+
+  this.dygraph_.attrs_.interactionModel =
+      Dygraph.Interaction.dragIsPanInteractionModel;
+  this.dygraph_.attrs_.panEdgeFraction = 0.0001;
+
+  var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
+  Dygraph.addEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
+  Dygraph.addEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
+
+  if (this.isUsingExcanvas_) {
+    Dygraph.addEvent(this.iePanOverlay_, 'mousedown', onPanStart);
+  } else {
+    Dygraph.addEvent(this.fgcanvas_, 'mousedown', onPanStart);
+    Dygraph.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove);
+  }
+};
+
+/**
+ * @private
+ * Draws the static layer in the background canvas.
+ */
+DygraphRangeSelector.prototype.drawStaticLayer_ = function() {
+  var ctx = this.bgcanvas_ctx_;
+  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+  try {
+    this.drawMiniPlot_();
+  } catch(ex) {
+    Dygraph.warn(ex);
+  }
+
+  var margin = 0.5;
+  this.bgcanvas_ctx_.lineWidth = 1;
+  ctx.strokeStyle = 'gray';
+  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.
+ */
+DygraphRangeSelector.prototype.drawMiniPlot_ = function() {
+  var fillStyle = this.attr_('rangeSelectorPlotFillColor');
+  var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor');
+  if (!fillStyle && !strokeStyle) {
+    return;
+  }
+
+  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;
+
+  ctx.beginPath();
+  ctx.moveTo(margin, canvasHeight);
+  for (var i = 0; i < combinedSeriesData.data.length; i++) {
+    var dataPoint = combinedSeriesData.data[i];
+    var x = (dataPoint[0] - xExtremes[0])*xFact;
+    var y = canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact;
+    if (isFinite(x) && isFinite(y)) {
+      ctx.lineTo(x, y);
+    }
+  }
+  ctx.lineTo(canvasWidth, canvasHeight);
+  ctx.closePath();
+
+  if (fillStyle) {
+    var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
+    lingrad.addColorStop(0, 'white');
+    lingrad.addColorStop(1, fillStyle);
+    this.bgcanvas_ctx_.fillStyle = lingrad;
+    ctx.fill();
+  }
+
+  if (strokeStyle) {
+    this.bgcanvas_ctx_.strokeStyle = strokeStyle;
+    this.bgcanvas_ctx_.lineWidth = 1.5;
+    ctx.stroke();
+  }
+};
+
+/**
+ * @private
+ * Computes and returns the combinded series data along with min/max for the mini plot.
+ * @return {Object} An object containing combinded series array, ymin, ymax.
+ */
+DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
+  var data = this.dygraph_.rawData_;
+  var logscale = this.attr_('logscale');
+
+  // Create a combined series (average of all series values).
+  var combinedSeries = [];
+  var sum;
+  var count;
+  var yVal, y;
+  var mutipleValues;
+  var i, j, k;
+
+  // Find out if data has multiple values per datapoint.
+  // Go to first data point that actually has values (see http://code.google.com/p/dygraphs/issues/detail?id=246)
+  for (i = 0; i < data.length; i++) {
+    if (data[i].length > 1 && data[i][1] != null) {
+      mutipleValues = typeof data[i][1] != 'number';
+      if (mutipleValues) {
+        sum = [];
+        count = [];
+        for (k = 0; k < data[i][1].length; k++) {
+          sum.push(0);
+          count.push(0);
+        }
+      }
+      break;
+    }
+  }
+
+  for (i = 0; i < data.length; i++) {
+    var dataPoint = data[i];
+    var xVal = dataPoint[0];
+
+    if (mutipleValues) {
+      for (k = 0; k < sum.length; k++) {
+        sum[k] = count[k] = 0;
+      }
+    } else {
+      sum = count = 0;
+    }
+
+    for (j = 1; j < dataPoint.length; j++) {
+      if (this.dygraph_.visibility()[j-1]) {
+        if (mutipleValues) {
+          for (k = 0; k < sum.length; k++) {
+            y = dataPoint[j][k];
+            if (y === null || isNaN(y)) continue;
+            sum[k] += y;
+            count[k]++;
+          }
+        } else {
+          y = dataPoint[j];
+          if (y === null || isNaN(y)) continue;
+          sum += y;
+          count++;
+        }
+      }
+    }
+
+    if (mutipleValues) {
+      for (k = 0; k < sum.length; k++) {
+        sum[k] /= count[k];
+      }
+      yVal = sum.slice(0);
+    } else {
+      yVal = sum/count;
+    }
+
+    combinedSeries.push([xVal, yVal]);
+  }
+
+  // Account for roll period, fractions.
+  combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
+
+  if (typeof combinedSeries[0][1] != 'number') {
+    for (i = 0; i < combinedSeries.length; i++) {
+      yVal = combinedSeries[i][1];
+      combinedSeries[i][1] = yVal[0];
+    }
+  }
+
+  // Compute the y range.
+  var yMin = Number.MAX_VALUE;
+  var yMax = -Number.MAX_VALUE;
+  for (i = 0; i < combinedSeries.length; i++) {
+    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.
+ */
+DygraphRangeSelector.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.
+ */
+DygraphRangeSelector.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 = 'black';
+  if (!zoomHandleStatus.isZoomed) {
+    ctx.beginPath();
+    ctx.moveTo(margin, margin);
+    ctx.lineTo(margin, height);
+    ctx.lineTo(width, height);
+    ctx.lineTo(width, margin);
+    ctx.stroke();
+    if (this.iePanOverlay_) {
+      this.iePanOverlay_.style.display = 'none';
+    }
+  } 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, 0.6)';
+    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();
+
+    if (this.isUsingExcanvas_) {
+      this.iePanOverlay_.style.width = (rightHandleCanvasPos - leftHandleCanvasPos) + 'px';
+      this.iePanOverlay_.style.left = leftHandleCanvasPos + 'px';
+      this.iePanOverlay_.style.height = height + 'px';
+      this.iePanOverlay_.style.display = 'inline';
+    }
+  }
+};
+
+/**
+ * @private
+ * Returns the current zoom handle position information.
+ * @return {Object} The zoom handle status.
+ */
+DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() {
+  var halfHandleWidth = this.leftZoomHandle_.width/2;
+  var leftHandlePos = parseInt(this.leftZoomHandle_.style.left, 10) + halfHandleWidth;
+  var rightHandlePos = parseInt(this.rightZoomHandle_.style.left, 10) + halfHandleWidth;
+  return {
+      leftHandlePos: leftHandlePos,
+      rightHandlePos: rightHandlePos,
+      isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
+  };
+};
+/**
+ * @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 globalstrict: true */
+/*global Dygraph:false */
+"use strict";
+
+Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) {
+  var pixels_per_tick = 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;
+      if (kmg2) {
+        mults = [1, 2, 4, 8];
+      } else {
+        mults = [1, 2, 5];
+      }
+      var scale, low_val, high_val;
+      for (i = -10; i < 50; i++) {
+        var base_scale;
+        if (kmg2) {
+          base_scale = Math.pow(16, i);
+        } else {
+          base_scale = Math.pow(10, i);
+        }
+        var spacing = 0;
+        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;
+          // wish I could break out of both loops at once...
+          if (spacing > pixels_per_tick) break;
+        }
+        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} );
+      }
+    }
+  }
+
+  // Add formatted labels to the ticks.
+  var k;
+  var k_labels = [];
+  if (opts("labelsKMB")) {
+    k = 1000;
+    k_labels = [ "K", "M", "B", "T" ];
+  }
+  if (opts("labelsKMG2")) {
+    if (k) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+    k = 1024;
+    k_labels = [ "k", "M", "G", "T" ];
+  }
+
+  var formatter = opts('axisLabelFormatter');
+
+  // Add labels to the ticks.
+  for (i = 0; i < ticks.length; i++) {
+    if (ticks[i].label !== undefined) continue;  // Use current label.
+    tickV = ticks[i].v;
+    var absTickV = Math.abs(tickV);
+    // TODO(danvk): set granularity to something appropriate here.
+    var label = formatter(tickV, 0, opts, dygraph);
+    if (k_labels.length > 0) {
+      // TODO(danvk): should this be integrated into the axisLabelFormatter?
+      // Round up to an appropriate unit.
+      var n = k*k*k*k;
+      for (j = 3; j >= 0; j--, n /= k) {
+        if (absTickV >= n) {
+          label = Dygraph.round_(tickV / n, opts('digitsAfterDecimal')) +
+              k_labels[j];
+          break;
+        }
+      }
+    }
+    ticks[i].label = label;
+  }
+
+  return ticks;
+};
+
+
+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
+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.WEEKLY = 14;
+Dygraph.MONTHLY = 15;
+Dygraph.QUARTERLY = 16;
+Dygraph.BIANNUAL = 17;
+Dygraph.ANNUAL = 18;
+Dygraph.DECADAL = 19;
+Dygraph.CENTENNIAL = 20;
+Dygraph.NUM_GRANULARITIES = 21;
+
+Dygraph.SHORT_SPACINGS = [];
+Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]    = 1000 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]   = 1000 * 5;
+Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]    = 1000 * 10;
+Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
+Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]        = 1000 * 60;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]    = 1000 * 60 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]   = 1000 * 60 * 5;
+Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]    = 1000 * 60 * 10;
+Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
+Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]          = 1000 * 3600;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]      = 1000 * 3600 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]      = 1000 * 3600 * 6;
+Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
+Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
+
+/**
+ * @private
+ * 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.
+ */
+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} opts Function mapping from option name -> 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 = 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;
+};
+
+Dygraph.numDateTicks = function(start_time, end_time, granularity) {
+  if (granularity < Dygraph.MONTHLY) {
+    // Generate one tick mark for every fixed interval of time.
+    var spacing = Dygraph.SHORT_SPACINGS[granularity];
+    return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
+  } else {
+    var year_mod = 1;  // e.g. to only print one point every 10 years.
+    var num_months = 12;
+    if (granularity == Dygraph.QUARTERLY) num_months = 3;
+    if (granularity == Dygraph.BIANNUAL) num_months = 2;
+    if (granularity == Dygraph.ANNUAL) num_months = 1;
+    if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
+    if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
+
+    var msInYear = 365.2524 * 24 * 3600 * 1000;
+    var num_years = 1.0 * (end_time - start_time) / msInYear;
+    return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
+  }
+};
+
+Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
+  var formatter = opts("axisLabelFormatter");
+  var ticks = [];
+  var t;
+
+  if (granularity < Dygraph.MONTHLY) {
+    // Generate one tick mark for every fixed interval of time.
+    var spacing = Dygraph.SHORT_SPACINGS[granularity];
+
+    // Find a time less than start_time which occurs on a "nice" time boundary
+    // for this granularity.
+    var g = spacing / 1000;
+    var d = new Date(start_time);
+    var x;
+    if (g <= 60) {  // seconds
+      x = d.getSeconds(); d.setSeconds(x - x % g);
+    } else {
+      d.setSeconds(0);
+      g /= 60;
+      if (g <= 60) {  // minutes
+        x = d.getMinutes(); d.setMinutes(x - x % g);
+      } else {
+        d.setMinutes(0);
+        g /= 60;
+
+        if (g <= 24) {  // days
+          x = d.getHours(); d.setHours(x - x % g);
+        } else {
+          d.setHours(0);
+          g /= 24;
+
+          if (g == 7) {  // one week
+            d.setDate(d.getDate() - d.getDay());
+          }
+        }
+      }
+    }
+    start_time = d.getTime();
+
+    for (t = start_time; t <= end_time; t += spacing) {
+      ticks.push({ v:t,
+                   label: formatter(new Date(t), granularity, opts, dg)
+                 });
+    }
+  } else {
+    // Display a tick mark on the first of a set of months of each year.
+    // Years get a tick mark iff y % year_mod == 0. This is useful for
+    // displaying a tick mark once every 10 years, say, on long time scales.
+    var months;
+    var year_mod = 1;  // e.g. to only print one point every 10 years.
+
+    if (granularity == Dygraph.MONTHLY) {
+      months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
+    } else if (granularity == Dygraph.QUARTERLY) {
+      months = [ 0, 3, 6, 9 ];
+    } else if (granularity == Dygraph.BIANNUAL) {
+      months = [ 0, 6 ];
+    } else if (granularity == Dygraph.ANNUAL) {
+      months = [ 0 ];
+    } else if (granularity == Dygraph.DECADAL) {
+      months = [ 0 ];
+      year_mod = 10;
+    } else if (granularity == Dygraph.CENTENNIAL) {
+      months = [ 0 ];
+      year_mod = 100;
+    } else {
+      Dygraph.warn("Span of dates is too long");
+    }
+
+    var start_year = new Date(start_time).getFullYear();
+    var end_year   = new Date(end_time).getFullYear();
+    var zeropad = Dygraph.zeropad;
+    for (var i = start_year; i <= end_year; i++) {
+      if (i % year_mod !== 0) continue;
+      for (var j = 0; j < months.length; j++) {
+        var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
+        t = Dygraph.dateStrToMillis(date_str);
+        if (t < start_time || t > end_time) continue;
+        ticks.push({ v:t,
+                     label: formatter(new Date(t), granularity, opts, dg)
+                   });
+      }
+    }
+  }
+
+  return ticks;
+};
+
+// These are set here so that this file can be included after dygraph.js.
+Dygraph.DEFAULT_ATTRS.axes.x.ticker = Dygraph.dateTicker;
+Dygraph.DEFAULT_ATTRS.axes.y.ticker = Dygraph.numericTicks;
+Dygraph.DEFAULT_ATTRS.axes.y2.ticker = Dygraph.numericTicks;
+/**
+ * A class to parse color values
+ *
+ * NOTE: modified by danvk. I removed the "getHelpXML" function to reduce the
+ * file size, added "use strict" and a few "var" declarations where needed.
+ *
+ * @author Stoyan Stefanov <sstoo@gmail.com>
+ * @link   http://www.phpied.com/rgb-color-parser-in-javascript/
+ * @license Use it if you like it
+ */
+"use strict";
+
+function RGBColor(color_string)
+{
+    this.ok = false;
+
+    // strip any leading #
+    if (color_string.charAt(0) == '#') { // remove # if any
+        color_string = color_string.substr(1,6);
+    }
+
+    color_string = color_string.replace(/ /g,'');
+    color_string = color_string.toLowerCase();
+
+    // before getting into regexps, try simple matches
+    // and overwrite the input
+    var simple_colors = {
+        aliceblue: 'f0f8ff',
+        antiquewhite: 'faebd7',
+        aqua: '00ffff',
+        aquamarine: '7fffd4',
+        azure: 'f0ffff',
+        beige: 'f5f5dc',
+        bisque: 'ffe4c4',
+        black: '000000',
+        blanchedalmond: 'ffebcd',
+        blue: '0000ff',
+        blueviolet: '8a2be2',
+        brown: 'a52a2a',
+        burlywood: 'deb887',
+        cadetblue: '5f9ea0',
+        chartreuse: '7fff00',
+        chocolate: 'd2691e',
+        coral: 'ff7f50',
+        cornflowerblue: '6495ed',
+        cornsilk: 'fff8dc',
+        crimson: 'dc143c',
+        cyan: '00ffff',
+        darkblue: '00008b',
+        darkcyan: '008b8b',
+        darkgoldenrod: 'b8860b',
+        darkgray: 'a9a9a9',
+        darkgreen: '006400',
+        darkkhaki: 'bdb76b',
+        darkmagenta: '8b008b',
+        darkolivegreen: '556b2f',
+        darkorange: 'ff8c00',
+        darkorchid: '9932cc',
+        darkred: '8b0000',
+        darksalmon: 'e9967a',
+        darkseagreen: '8fbc8f',
+        darkslateblue: '483d8b',
+        darkslategray: '2f4f4f',
+        darkturquoise: '00ced1',
+        darkviolet: '9400d3',
+        deeppink: 'ff1493',
+        deepskyblue: '00bfff',
+        dimgray: '696969',
+        dodgerblue: '1e90ff',
+        feldspar: 'd19275',
+        firebrick: 'b22222',
+        floralwhite: 'fffaf0',
+        forestgreen: '228b22',
+        fuchsia: 'ff00ff',
+        gainsboro: 'dcdcdc',
+        ghostwhite: 'f8f8ff',
+        gold: 'ffd700',
+        goldenrod: 'daa520',
+        gray: '808080',
+        green: '008000',
+        greenyellow: 'adff2f',
+        honeydew: 'f0fff0',
+        hotpink: 'ff69b4',
+        indianred : 'cd5c5c',
+        indigo : '4b0082',
+        ivory: 'fffff0',
+        khaki: 'f0e68c',
+        lavender: 'e6e6fa',
+        lavenderblush: 'fff0f5',
+        lawngreen: '7cfc00',
+        lemonchiffon: 'fffacd',
+        lightblue: 'add8e6',
+        lightcoral: 'f08080',
+        lightcyan: 'e0ffff',
+        lightgoldenrodyellow: 'fafad2',
+        lightgrey: 'd3d3d3',
+        lightgreen: '90ee90',
+        lightpink: 'ffb6c1',
+        lightsalmon: 'ffa07a',
+        lightseagreen: '20b2aa',
+        lightskyblue: '87cefa',
+        lightslateblue: '8470ff',
+        lightslategray: '778899',
+        lightsteelblue: 'b0c4de',
+        lightyellow: 'ffffe0',
+        lime: '00ff00',
+        limegreen: '32cd32',
+        linen: 'faf0e6',
+        magenta: 'ff00ff',
+        maroon: '800000',
+        mediumaquamarine: '66cdaa',
+        mediumblue: '0000cd',
+        mediumorchid: 'ba55d3',
+        mediumpurple: '9370d8',
+        mediumseagreen: '3cb371',
+        mediumslateblue: '7b68ee',
+        mediumspringgreen: '00fa9a',
+        mediumturquoise: '48d1cc',
+        mediumvioletred: 'c71585',
+        midnightblue: '191970',
+        mintcream: 'f5fffa',
+        mistyrose: 'ffe4e1',
+        moccasin: 'ffe4b5',
+        navajowhite: 'ffdead',
+        navy: '000080',
+        oldlace: 'fdf5e6',
+        olive: '808000',
+        olivedrab: '6b8e23',
+        orange: 'ffa500',
+        orangered: 'ff4500',
+        orchid: 'da70d6',
+        palegoldenrod: 'eee8aa',
+        palegreen: '98fb98',
+        paleturquoise: 'afeeee',
+        palevioletred: 'd87093',
+        papayawhip: 'ffefd5',
+        peachpuff: 'ffdab9',
+        peru: 'cd853f',
+        pink: 'ffc0cb',
+        plum: 'dda0dd',
+        powderblue: 'b0e0e6',
+        purple: '800080',
+        red: 'ff0000',
+        rosybrown: 'bc8f8f',
+        royalblue: '4169e1',
+        saddlebrown: '8b4513',
+        salmon: 'fa8072',
+        sandybrown: 'f4a460',
+        seagreen: '2e8b57',
+        seashell: 'fff5ee',
+        sienna: 'a0522d',
+        silver: 'c0c0c0',
+        skyblue: '87ceeb',
+        slateblue: '6a5acd',
+        slategray: '708090',
+        snow: 'fffafa',
+        springgreen: '00ff7f',
+        steelblue: '4682b4',
+        tan: 'd2b48c',
+        teal: '008080',
+        thistle: 'd8bfd8',
+        tomato: 'ff6347',
+        turquoise: '40e0d0',
+        violet: 'ee82ee',
+        violetred: 'd02090',
+        wheat: 'f5deb3',
+        white: 'ffffff',
+        whitesmoke: 'f5f5f5',
+        yellow: 'ffff00',
+        yellowgreen: '9acd32'
+    };
+    for (var key in simple_colors) {
+        if (color_string == key) {
+            color_string = simple_colors[key];
+        }
+    }
+    // emd of simple type-in colors
+
+    // array of color definition objects
+    var color_defs = [
+        {
+            re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
+            example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
+            process: function (bits){
+                return [
+                    parseInt(bits[1]),
+                    parseInt(bits[2]),
+                    parseInt(bits[3])
+                ];
+            }
+        },
+        {
+            re: /^(\w{2})(\w{2})(\w{2})$/,
+            example: ['#00ff00', '336699'],
+            process: function (bits){
+                return [
+                    parseInt(bits[1], 16),
+                    parseInt(bits[2], 16),
+                    parseInt(bits[3], 16)
+                ];
+            }
+        },
+        {
+            re: /^(\w{1})(\w{1})(\w{1})$/,
+            example: ['#fb0', 'f0f'],
+            process: function (bits){
+                return [
+                    parseInt(bits[1] + bits[1], 16),
+                    parseInt(bits[2] + bits[2], 16),
+                    parseInt(bits[3] + bits[3], 16)
+                ];
+            }
+        }
+    ];
+
+    // search through the definitions to find a match
+    for (var i = 0; i < color_defs.length; i++) {
+        var re = color_defs[i].re;
+        var processor = color_defs[i].process;
+        var bits = re.exec(color_string);
+        if (bits) {
+            var channels = processor(bits);
+            this.r = channels[0];
+            this.g = channels[1];
+            this.b = channels[2];
+            this.ok = true;
+        }
+
+    }
+
+    // validate/cleanup values
+    this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
+    this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
+    this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
+
+    // some getters
+    this.toRGB = function () {
+        return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
+    }
+    this.toHex = function () {
+        var r = this.r.toString(16);
+        var g = this.g.toString(16);
+        var b = this.b.toString(16);
+        if (r.length == 1) r = '0' + r;
+        if (g.length == 1) g = '0' + g;
+        if (b.length == 1) b = '0' + b;
+        return '#' + r + g + b;
+    }
+
+
+}
+
+Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(x,pad,r){if(typeof (r)=="undefined"){r=10}for(;parseInt(x,10)<r&&r>1;r/=10){x=pad.toString()+x}return x.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(d){return Date.ext.locales[d.locale].a[d.getDay()]},A:function(d){return Date.ext.locales[d.locale].A[d.getDay()]},b:function(d){return Date.ext.locales[d.locale].b[d.getMonth()]},B:function(d){return Date.ext.locales[d.locale].B[d.getMonth()]},c:"toLocaleString",C:function(d){return Date.ext.util.xPad(parseInt(d.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(d){return Date.ext.util.xPad(parseInt(Date.ext.util.G(d)/100,10),0)},G:function(d){var y=d.getFullYear();var V=parseInt(Date.ext.formats.V(d),10);var W=parseInt(Date.ext.formats.W(d),10);if(W>V){y++}else{if(W===0&&V>=52){y--}}return y},H:["getHours","0"],I:function(d){var I=d.getHours()%12;return Date.ext.util.xPad(I===0?12:I,0)},j:function(d){var ms=d-new Date(""+d.getFullYear()+"/1/1 GMT");ms+=d.getTimezoneOffset()*60000;var doy=parseInt(ms/60000/60/24,10)+1;return Date.ext.util.xPad(doy,0,100)},m:function(d){return Date.ext.util.xPad(d.getMonth()+1,0)},M:["getMinutes","0"],p:function(d){return Date.ext.locales[d.locale].p[d.getHours()>=12?1:0]},P:function(d){return Date.ext.locales[d.locale].P[d.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(d){var dow=d.getDay();return dow===0?7:dow},U:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=6-d.getDay();var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0)},V:function(d){var woy=parseInt(Date.ext.formats.W(d),10);var dow1_1=(new Date(""+d.getFullYear()+"/1/1")).getDay();var idow=woy+(dow1_1>4||dow1_1<=1?0:1);if(idow==53&&(new Date(""+d.getFullYear()+"/12/31")).getDay()<4){idow=1}else{if(idow===0){idow=Date.ext.formats.V(new Date(""+(d.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(idow,0)},w:"getDay",W:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=7-Date.ext.formats.u(d);var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0,10)},y:function(d){return Date.ext.util.xPad(d.getFullYear()%100,0)},Y:"getFullYear",z:function(d){var o=d.getTimezoneOffset();var H=Date.ext.util.xPad(parseInt(Math.abs(o/60),10),0);var M=Date.ext.util.xPad(o%60,0);return(o>0?"-":"+")+H+M},Z:function(d){return d.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(d){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(fmt){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var d=this;while(fmt.match(/%[cDhnrRtTxXzZ]/)){fmt=fmt.replace(/%([cDhnrRtTxXzZ])/g,function(m0,m1){var f=Date.ext.aggregates[m1];return(f=="locale"?Date.ext.locales[d.locale][m1]:f)})}var str=fmt.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(m0,m1){var f=Date.ext.formats[m1];if(typeof (f)=="string"){return d[f]()}else{if(typeof (f)=="function"){return f.call(d,d)}else{if(typeof (f)=="object"&&typeof (f[0])=="string"){return Date.ext.util.xPad(d[f[0]](),f[1])}else{return m1}}}});d=null;return str};
diff --git a/static/vendor/dygraph-1.2dev.min.js b/static/vendor/dygraph-1.2dev.min.js
new file mode 100644 (file)
index 0000000..b9e5ecc
--- /dev/null
@@ -0,0 +1 @@
+"use strict";var DygraphLayout=function(a){this.dygraph_=a;this.datasets=[];this.setNames=[];this.annotations=[];this.yAxes_=null;this.xTicks_=null;this.yTicks_=null};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets.push(b);this.setNames.push(a)};DygraphLayout.prototype.getPlotArea=function(){return this.computePlotArea_()};DygraphLayout.prototype.computePlotArea_=function(){var a={x:0,y:0};if(this.attr_("drawYAxis")){a.x=this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize")}a.w=this.dygraph_.width_-a.x-this.attr_("rightGap");a.h=this.dygraph_.height_;if(this.attr_("drawXAxis")){if(this.attr_("xAxisHeight")){a.h-=this.attr_("xAxisHeight")}else{a.h-=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize")}}if(this.dygraph_.numAxes()==2){a.w-=(this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize"))}else{if(this.dygraph_.numAxes()>2){this.dygraph_.error("Only two y-axes are supported at this time. (Trying to use "+this.dygraph_.numAxes()+")")}}if(this.attr_("title")){a.h-=this.attr_("titleHeight");a.y+=this.attr_("titleHeight")}if(this.attr_("xlabel")){a.h-=this.attr_("xLabelHeight")}if(this.attr_("ylabel")){}if(this.attr_("y2label")){}if(this.attr_("showRangeSelector")){a.h-=this.attr_("rangeSelectorHeight")+4}return a};DygraphLayout.prototype.setAnnotations=function(d){this.annotations=[];var e=this.attr_("xValueParser")||function(a){return a};for(var c=0;c<d.length;c++){var b={};if(!d[c].xval&&!d[c].x){this.dygraph_.error("Annotations must have an 'x' property");return}if(d[c].icon&&!(d[c].hasOwnProperty("width")&&d[c].hasOwnProperty("height"))){this.dygraph_.error("Must set width and height when setting annotation.icon property");return}Dygraph.update(b,d[c]);if(!b.xval){b.xval=e(b.x)}this.annotations.push(b)}};DygraphLayout.prototype.setXTicks=function(a){this.xTicks_=a};DygraphLayout.prototype.setYAxes=function(a){this.yAxes_=a};DygraphLayout.prototype.setDateWindow=function(a){this.dateWindow_=a};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks();this._evaluateAnnotations()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.dateWindow_){this.minxval=this.dateWindow_[0];this.maxxval=this.dateWindow_[1]}else{for(var f=0;f<this.datasets.length;++f){var d=this.datasets[f];if(d.length>1){var b=d[0][0];if(!this.minxval||b<this.minxval){this.minxval=b}var a=d[d.length-1][0];if(!this.maxxval||a>this.maxxval){this.maxxval=a}}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!==0?1/this.xrange:1);for(var c=0;c<this.yAxes_.length;c++){var e=this.yAxes_[c];e.minyval=e.computedValueRange[0];e.maxyval=e.computedValueRange[1];e.yrange=e.maxyval-e.minyval;e.yscale=(e.yrange!==0?1/e.yrange:1);if(e.g.attr_("logscale")){e.ylogrange=Dygraph.log10(e.maxyval)-Dygraph.log10(e.minyval);e.ylogscale=(e.ylogrange!==0?1/e.ylogrange:1);if(!isFinite(e.ylogrange)||isNaN(e.ylogrange)){e.g.error("axis "+c+" of graph at "+e.g+" can't be displayed in log scale for range ["+e.minyval+" - "+e.maxyval+"]")}}}};DygraphLayout._calcYNormal=function(a,b){if(a.logscale){return 1-((Dygraph.log10(b)-Dygraph.log10(a.minyval))*a.ylogscale)}else{return 1-((b-a.minyval)*a.yscale)}};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=[];this.setPointsLengths=[];this.setPointsOffsets=[];for(var a=0;a<this.datasets.length;++a){var c=this.datasets[a];var g=this.setNames[a];var b=this.dygraph_.axisPropertiesForSeries(g);this.setPointsOffsets.push(this.points.length);var h=0;for(var e=0;e<c.length;e++){var m=c[e];var d=parseFloat(m[0]);var i=parseFloat(m[1]);var l=(d-this.minxval)*this.xscale;var f=DygraphLayout._calcYNormal(b,i);var k={x:l,y:f,xval:d,yval:i,name:g};this.points.push(k);h+=1}this.setPointsLengths.push(h)}};DygraphLayout.prototype._evaluateLineTicks=function(){var d,c,b,f;this.xticks=[];for(d=0;d<this.xTicks_.length;d++){c=this.xTicks_[d];b=c.label;f=this.xscale*(c.v-this.minxval);if((f>=0)&&(f<=1)){this.xticks.push([f,b])}}this.yticks=[];for(d=0;d<this.yAxes_.length;d++){var e=this.yAxes_[d];for(var a=0;a<e.ticks.length;a++){c=e.ticks[a];b=c.label;f=this.dygraph_.toPercentYCoord(c.v,d);if((f>=0)&&(f<=1)){this.yticks.push([d,f,b])}}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!(this.attr_("errorBars")||this.attr_("customBars"))){return}var h=0;for(var a=0;a<this.datasets.length;++a){var g=0;var f=this.datasets[a];var l=this.setNames[a];var e=this.dygraph_.axisPropertiesForSeries(l);for(g=0;g<f.length;g++,h++){var o=f[g];var c=parseFloat(o[0]);var m=parseFloat(o[1]);if(c==this.points[h].xval&&m==this.points[h].yval){var k=parseFloat(o[2]);var d=parseFloat(o[3]);var n=m-k;var b=m+d;this.points[h].y_top=DygraphLayout._calcYNormal(e,n);this.points[h].y_bottom=DygraphLayout._calcYNormal(e,b)}}}};DygraphLayout.prototype._evaluateAnnotations=function(){var d;var f={};for(d=0;d<this.annotations.length;d++){var b=this.annotations[d];f[b.xval+","+b.series]=b}this.annotated_points=[];if(!this.annotations||!this.annotations.length){return}for(d=0;d<this.points.length;d++){var e=this.points[d];var c=e.xval+","+e.name;if(c in f){e.annotation=f[c];this.annotated_points.push(e)}}};DygraphLayout.prototype.removeAllDatasets=function(){delete this.datasets;delete this.setNames;delete this.setPointsLengths;delete this.setPointsOffsets;this.datasets=[];this.setNames=[];this.setPointsLengths=[];this.setPointsOffsets=[]};DygraphLayout.prototype.unstackPointAtIndex=function(b){var a=this.points[b];var d={};for(var e in a){d[e]=a[e]}if(!this.attr_("stackedGraph")){return d}for(var c=b+1;c<this.points.length;c++){if(this.points[c].xval==a.xval){d.yval-=this.points[c].yval;break}}return d};"use strict";var DygraphCanvasRenderer=function(d,c,b,e){this.dygraph_=d;this.layout=e;this.element=c;this.elementContext=b;this.container=this.element.parentNode;this.height=this.element.height;this.width=this.element.width;if(!this.isIE&&!(DygraphCanvasRenderer.isSupported(this.element))){throw"Canvas is not supported."}this.xlabels=[];this.ylabels=[];this.annotations=[];this.chartLabels={};this.area=e.getPlotArea();this.container.style.position="relative";this.container.style.width=this.width+"px";if(this.dygraph_.isUsingExcanvas_){this._createIEClipArea()}else{if(!Dygraph.isAndroid()){var a=this.dygraph_.canvas_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip();a=this.dygraph_.hidden_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip()}}};DygraphCanvasRenderer.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphCanvasRenderer.prototype.clear=function(){var c;if(this.isIE){try{if(this.clearDelay){this.clearDelay.cancel();this.clearDelay=null}c=this.elementContext}catch(f){return}}c=this.elementContext;c.clearRect(0,0,this.width,this.height);function a(g){for(var e=0;e<g.length;e++){var h=g[e];if(h.parentNode){h.parentNode.removeChild(h)}}}a(this.xlabels);a(this.ylabels);a(this.annotations);for(var b in this.chartLabels){if(!this.chartLabels.hasOwnProperty(b)){continue}var d=this.chartLabels[b];if(d.parentNode){d.parentNode.removeChild(d)}}this.xlabels=[];this.ylabels=[];this.annotations=[];this.chartLabels={}};DygraphCanvasRenderer.isSupported=function(f){var b=null;try{if(typeof(f)=="undefined"||f===null){b=document.createElement("canvas")}else{b=f}b.getContext("2d")}catch(c){var d=navigator.appVersion.match(/MSIE (\d\.\d)/);var a=(navigator.userAgent.toLowerCase().indexOf("opera")!=-1);if((!d)||(d[1]<6)||(a)){return false}return true}return true};DygraphCanvasRenderer.prototype.setColors=function(a){this.colorScheme_=a};DygraphCanvasRenderer.prototype.render=function(){var b=this.elementContext;function c(h){return Math.round(h)+0.5}function g(h){return Math.round(h)-0.5}if(this.attr_("underlayCallback")){this.attr_("underlayCallback")(b,this.area,this.dygraph_,this.dygraph_)}var a,f,d,e;if(this.attr_("drawYGrid")){e=this.layout.yticks;b.save();b.strokeStyle=this.attr_("gridLineColor");b.lineWidth=this.attr_("gridLineWidth");for(d=0;d<e.length;d++){if(e[d][0]!==0){continue}a=c(this.area.x);f=g(this.area.y+e[d][1]*this.area.h);b.beginPath();b.moveTo(a,f);b.lineTo(a+this.area.w,f);b.closePath();b.stroke()}}if(this.attr_("drawXGrid")){e=this.layout.xticks;b.save();b.strokeStyle=this.attr_("gridLineColor");b.lineWidth=this.attr_("gridLineWidth");for(d=0;d<e.length;d++){a=c(this.area.x+e[d][0]*this.area.w);f=g(this.area.y+this.area.h);b.beginPath();b.moveTo(a,f);b.lineTo(a,this.area.y);b.closePath();b.stroke()}}this._renderLineChart();this._renderAxis();this._renderChartLabels();this._renderAnnotations()};DygraphCanvasRenderer.prototype._createIEClipArea=function(){var g="dygraph-clip-div";var f=this.dygraph_.graphDiv;for(var e=f.childNodes.length-1;e>=0;e--){if(f.childNodes[e].className==g){f.removeChild(f.childNodes[e])}}var c=document.bgColor;var d=this.dygraph_.graphDiv;while(d!=document){var a=d.currentStyle.backgroundColor;if(a&&a!="transparent"){c=a;break}d=d.parentNode}function b(j){if(j.w===0||j.h===0){return}var i=document.createElement("div");i.className=g;i.style.backgroundColor=c;i.style.position="absolute";i.style.left=j.x+"px";i.style.top=j.y+"px";i.style.width=j.w+"px";i.style.height=j.h+"px";f.appendChild(i)}var h=this.area;b({x:0,y:0,w:h.x,h:this.height});b({x:h.x,y:0,w:this.width-h.x,h:h.y});b({x:h.x+h.w,y:0,w:this.width-h.x-h.w,h:this.height});b({x:h.x,y:h.y+h.h,w:this.width-h.x,h:this.height-h.h-h.y})};DygraphCanvasRenderer.prototype._renderAxis=function(){if(!this.attr_("drawXAxis")&&!this.attr_("drawYAxis")){return}function q(i){return Math.round(i)+0.5}function p(i){return Math.round(i)-0.5}var d=this.elementContext;var l,n,m,s,r;var a={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,color:this.attr_("axisLabelColor"),width:this.attr_("axisLabelWidth")+"px",lineHeight:"normal",overflow:"hidden"};var g=function(i,v,w){var x=document.createElement("div");for(var u in a){if(a.hasOwnProperty(u)){x.style[u]=a[u]}}var t=document.createElement("div");t.className="dygraph-axis-label dygraph-axis-label-"+v+(w?" dygraph-axis-label-"+w:"");t.innerHTML=i;x.appendChild(t);return x};d.save();d.strokeStyle=this.attr_("axisLineColor");d.lineWidth=this.attr_("axisLineWidth");if(this.attr_("drawYAxis")){if(this.layout.yticks&&this.layout.yticks.length>0){var b=this.dygraph_.numAxes();for(r=0;r<this.layout.yticks.length;r++){s=this.layout.yticks[r];if(typeof(s)=="function"){return}n=this.area.x;var j=1;var c="y1";if(s[0]==1){n=this.area.x+this.area.w;j=-1;c="y2"}m=this.area.y+s[1]*this.area.h;l=g(s[2],"y",b==2?c:null);var o=(m-this.attr_("axisLabelFontSize")/2);if(o<0){o=0}if(o+this.attr_("axisLabelFontSize")+3>this.height){l.style.bottom="0px"}else{l.style.top=o+"px"}if(s[0]===0){l.style.left=(this.area.x-this.attr_("yAxisLabelWidth")-this.attr_("axisTickSize"))+"px";l.style.textAlign="right"}else{if(s[0]==1){l.style.left=(this.area.x+this.area.w+this.attr_("axisTickSize"))+"px";l.style.textAlign="left"}}l.style.width=this.attr_("yAxisLabelWidth")+"px";this.container.appendChild(l);this.ylabels.push(l)}var h=this.ylabels[0];var e=this.attr_("axisLabelFontSize");var k=parseInt(h.style.top,10)+e;if(k>this.height-e){h.style.top=(parseInt(h.style.top,10)-e/2)+"px"}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y));d.lineTo(q(this.area.x),p(this.area.y+this.area.h));d.closePath();d.stroke();if(this.dygraph_.numAxes()==2){d.beginPath();d.moveTo(p(this.area.x+this.area.w),p(this.area.y));d.lineTo(p(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}}if(this.attr_("drawXAxis")){if(this.layout.xticks){for(r=0;r<this.layout.xticks.length;r++){s=this.layout.xticks[r];n=this.area.x+s[0]*this.area.w;m=this.area.y+this.area.h;l=g(s[1],"x");l.style.textAlign="center";l.style.top=(m+this.attr_("axisTickSize"))+"px";var f=(n-this.attr_("axisLabelWidth")/2);if(f+this.attr_("axisLabelWidth")>this.width){f=this.width-this.attr_("xAxisLabelWidth");l.style.textAlign="right"}if(f<0){f=0;l.style.textAlign="left"}l.style.left=f+"px";l.style.width=this.attr_("xAxisLabelWidth")+"px";this.container.appendChild(l);this.xlabels.push(l)}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y+this.area.h));d.lineTo(q(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}d.restore()};DygraphCanvasRenderer.prototype._renderChartLabels=function(){var d,a;if(this.attr_("title")){d=document.createElement("div");d.style.position="absolute";d.style.top="0px";d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("titleHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("titleHeight")-8)+"px";d.style.fontWeight="bold";a=document.createElement("div");a.className="dygraph-label dygraph-title";a.innerHTML=this.attr_("title");d.appendChild(a);this.container.appendChild(d);this.chartLabels.title=d}if(this.attr_("xlabel")){d=document.createElement("div");d.style.position="absolute";d.style.bottom=0;d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("xLabelHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("xLabelHeight")-2)+"px";a=document.createElement("div");a.className="dygraph-label dygraph-xlabel";a.innerHTML=this.attr_("xlabel");d.appendChild(a);this.container.appendChild(d);this.chartLabels.xlabel=d}var c=this;function b(h,g,f){var i={left:0,top:c.area.y,width:c.attr_("yLabelWidth"),height:c.area.h};d=document.createElement("div");d.style.position="absolute";if(h==1){d.style.left=i.left}else{d.style.right=i.left}d.style.top=i.top+"px";d.style.width=i.width+"px";d.style.height=i.height+"px";d.style.fontSize=(c.attr_("yLabelWidth")-2)+"px";var e=document.createElement("div");e.style.position="absolute";e.style.width=i.height+"px";e.style.height=i.width+"px";e.style.top=(i.height/2-i.width/2)+"px";e.style.left=(i.width/2-i.height/2)+"px";e.style.textAlign="center";var j="rotate("+(h==1?"-":"")+"90deg)";e.style.transform=j;e.style.WebkitTransform=j;e.style.MozTransform=j;e.style.OTransform=j;e.style.msTransform=j;if(typeof(document.documentMode)!=="undefined"&&document.documentMode<9){e.style.filter="progid:DXImageTransform.Microsoft.BasicImage(rotation="+(h==1?"3":"1")+")";e.style.left="0px";e.style.top="0px"}a=document.createElement("div");a.className=g;a.innerHTML=f;e.appendChild(a);d.appendChild(e);return d}var d;if(this.attr_("ylabel")){d=b(1,"dygraph-label dygraph-ylabel",this.attr_("ylabel"));this.container.appendChild(d);this.chartLabels.ylabel=d}if(this.attr_("y2label")&&this.dygraph_.numAxes()==2){d=b(2,"dygraph-label dygraph-y2label",this.attr_("y2label"));this.container.appendChild(d);this.chartLabels.y2label=d}};DygraphCanvasRenderer.prototype._renderAnnotations=function(){var h={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,overflow:"hidden"};var j=function(i,q,r,a){return function(s){var p=r.annotation;if(p.hasOwnProperty(i)){p[i](p,r,a.dygraph_,s)}else{if(a.dygraph_.attr_(q)){a.dygraph_.attr_(q)(p,r,a.dygraph_,s)}}}};var m=this.layout.annotated_points;for(var g=0;g<m.length;g++){var e=m[g];if(e.canvasx<this.area.x||e.canvasx>this.area.x+this.area.w){continue}var k=e.annotation;var l=6;if(k.hasOwnProperty("tickHeight")){l=k.tickHeight}var c=document.createElement("div");for(var b in h){if(h.hasOwnProperty(b)){c.style[b]=h[b]}}if(!k.hasOwnProperty("icon")){c.className="dygraphDefaultAnnotation"}if(k.hasOwnProperty("cssClass")){c.className+=" "+k.cssClass}var d=k.hasOwnProperty("width")?k.width:16;var n=k.hasOwnProperty("height")?k.height:16;if(k.hasOwnProperty("icon")){var f=document.createElement("img");f.src=k.icon;f.width=d;f.height=n;c.appendChild(f)}else{if(e.annotation.hasOwnProperty("shortText")){c.appendChild(document.createTextNode(e.annotation.shortText))}}c.style.left=(e.canvasx-d/2)+"px";if(k.attachAtBottom){c.style.top=(this.area.h-n-l)+"px"}else{c.style.top=(e.canvasy-n-l)+"px"}c.style.width=d+"px";c.style.height=n+"px";c.title=e.annotation.text;c.style.color=this.colors[e.name];c.style.borderColor=this.colors[e.name];k.div=c;Dygraph.addEvent(c,"click",j("clickHandler","annotationClickHandler",e,this));Dygraph.addEvent(c,"mouseover",j("mouseOverHandler","annotationMouseOverHandler",e,this));Dygraph.addEvent(c,"mouseout",j("mouseOutHandler","annotationMouseOutHandler",e,this));Dygraph.addEvent(c,"dblclick",j("dblClickHandler","annotationDblClickHandler",e,this));this.container.appendChild(c);this.annotations.push(c);var o=this.elementContext;o.strokeStyle=this.colors[e.name];o.beginPath();if(!k.attachAtBottom){o.moveTo(e.canvasx,e.canvasy);o.lineTo(e.canvasx,e.canvasy-2-l)}else{o.moveTo(e.canvasx,this.area.h);o.lineTo(e.canvasx,this.area.h-2-l)}o.closePath();o.stroke()}};DygraphCanvasRenderer.prototype._renderLineChart=function(){var F=function(i){return(i===null||isNaN(i))};var L=this.elementContext;var l=this.attr_("fillAlpha");var B=this.attr_("errorBars")||this.attr_("customBars");var k=this.attr_("fillGraph");var v=this.attr_("stackedGraph");var u=this.attr_("stepPlot");var z=this.layout.points;var y=z.length;var t,J,H,b,a,f,s,E,m,C,h,d,q;var D=this.layout.setNames;var w=D.length;this.colors={};for(J=0;J<w;J++){this.colors[D[J]]=this.colorScheme_[J%this.colorScheme_.length]}for(J=y;J--;){t=z[J];t.canvasx=this.area.w*t.x+this.area.x;t.canvasy=this.area.h*t.y+this.area.y}var p=L;if(B){if(k){this.dygraph_.warn("Can't use fillGraph option with error bars")}for(J=0;J<w;J++){E=D[J];q=this.dygraph_.axisPropertiesForSeries(E);s=this.colors[E];p.save();b=NaN;a=NaN;f=[-1,-1];d=q.yscale;h=new RGBColor(s);C="rgba("+h.r+","+h.g+","+h.b+","+l+")";p.fillStyle=C;p.beginPath();for(H=0;H<y;H++){t=z[H];if(t.name==E){if(!Dygraph.isOK(t.y)){b=NaN;continue}if(u){m=[t.y_bottom,t.y_top];a=t.y}else{m=[t.y_bottom,t.y_top]}m[0]=this.area.h*m[0]+this.area.y;m[1]=this.area.h*m[1]+this.area.y;if(!isNaN(b)){if(u){p.moveTo(b,m[0])}else{p.moveTo(b,f[0])}p.lineTo(t.canvasx,m[0]);p.lineTo(t.canvasx,m[1]);if(u){p.lineTo(b,m[1])}else{p.lineTo(b,f[1])}p.closePath()}f=m;b=t.canvasx}}p.fill()}}else{if(k){var G=[];for(J=w-1;J>=0;J--){E=D[J];s=this.colors[E];q=this.dygraph_.axisPropertiesForSeries(E);var e=1+q.minyval*q.yscale;if(e<0){e=0}else{if(e>1){e=1}}e=this.area.h*e+this.area.y;p.save();b=NaN;f=[-1,-1];d=q.yscale;h=new RGBColor(s);C="rgba("+h.r+","+h.g+","+h.b+","+l+")";p.fillStyle=C;p.beginPath();for(H=0;H<y;H++){t=z[H];if(t.name==E){if(!Dygraph.isOK(t.y)){b=NaN;continue}if(v){var g=G[t.canvasx];if(g===undefined){g=e}G[t.canvasx]=t.canvasy;m=[t.canvasy,g]}else{m=[t.canvasy,e]}if(!isNaN(b)){p.moveTo(b,f[0]);if(u){p.lineTo(t.canvasx,f[0])}else{p.lineTo(t.canvasx,m[0])}p.lineTo(t.canvasx,m[1]);p.lineTo(b,f[1]);p.closePath()}f=m;b=t.canvasx}}p.fill()}}}var K=0;var n=0;var c=0;for(J=0;J<w;J+=1){K=this.layout.setPointsOffsets[J];c=this.layout.setPointsLengths[J];n=K+c;E=D[J];s=this.colors[E];var x=this.dygraph_.attr_("strokeWidth",E);L.save();var A=this.dygraph_.attr_("pointSize",E);b=null;a=null;var o=this.dygraph_.attr_("drawPoints",E);var r=this.dygraph_.attr_("strokePattern",E);if(!Dygraph.isArrayLike(r)){r=null}for(H=K;H<n;H++){t=z[H];if(F(t.canvasy)){if(u&&b!==null){p.beginPath();p.strokeStyle=s;p.lineWidth=this.attr_("strokeWidth");this._dashedLine(p,b,a,t.canvasx,a,r);p.stroke()}b=a=null}else{var I=(!b&&(H==z.length-1||F(z[H+1].canvasy)));if(b===null){b=t.canvasx;a=t.canvasy}else{if(Math.round(b)==Math.round(t.canvasx)&&Math.round(a)==Math.round(t.canvasy)){continue}if(x){p.beginPath();p.strokeStyle=s;p.lineWidth=x;if(u){this._dashedLine(p,b,a,t.canvasx,a,r);b=t.canvasx}this._dashedLine(p,b,a,t.canvasx,t.canvasy,r);b=t.canvasx;a=t.canvasy;p.stroke()}}if(o||I){p.beginPath();p.fillStyle=s;p.arc(t.canvasx,t.canvasy,A,0,2*Math.PI,false);p.fill()}}}}L.restore()};DygraphCanvasRenderer.prototype._dashedLine=function(j,i,g,a,h,f){var l,k,e,b,c,d;if(!f||f.length<=1){j.moveTo(i,g);j.lineTo(a,h);return}if(!Dygraph.compareArrays(f,this._dashedLineToHistoryPattern)){this._dashedLineToHistoryPattern=f;this._dashedLineToHistory=[0,0]}j.save();l=(a-i);k=(h-g);e=Math.sqrt(l*l+k*k);b=Math.atan2(k,l);j.translate(i,g);j.moveTo(0,0);j.rotate(b);c=this._dashedLineToHistory[0];i=0;while(e>i){d=f[c];if(this._dashedLineToHistory[1]){i+=this._dashedLineToHistory[1]}else{i+=d}if(i>e){this._dashedLineToHistory=[c,i-e];i=e}else{this._dashedLineToHistory=[(c+1)%f.length,0]}if(c%2===0){j.lineTo(i,0)}else{j.moveTo(i,0)}c=(c+1)%f.length}j.restore()};"use strict";var Dygraph=function(c,b,a){if(arguments.length>0){if(arguments.length==4){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.2dev";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.ANIMATION_STEPS=10;Dygraph.ANIMATION_DURATION=200;Dygraph.numberValueFormatter=function(a,e,h,d){var b=e("sigFigs");if(b!==null){return Dygraph.floatFormat(a,b)}var f=e("digitsAfterDecimal");var c=e("maxNumberWidth");if(a!==0&&(Math.abs(a)>=Math.pow(10,c)||Math.abs(a)<Math.pow(10,-f))){return a.toExponential(f)}else{return""+Dygraph.round_(a,f)}};Dygraph.numberAxisLabelFormatter=function(a,d,c,b){return Dygraph.numberValueFormatter(a,c,b)};Dygraph.dateString_=function(e){var i=Dygraph.zeropad;var h=new Date(e);var f=""+h.getFullYear();var g=i(h.getMonth()+1);var a=i(h.getDate());var c="";var b=h.getHours()*3600+h.getMinutes()*60+h.getSeconds();if(b){c=" "+Dygraph.hmsString_(e)}return f+"/"+g+"/"+a+c};Dygraph.dateAxisFormatter=function(b,c){if(c>=Dygraph.DECADAL){return b.strftime("%Y")}else{if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a===0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}}};Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,digitsAfterDecimal:2,maxNumberWidth:6,sigFigs:null,strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueParser:Dygraph.dateParser,delimiter:",",sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true,legend:"onmouseover",stepPlot:false,avoidMinZero:false,titleHeight:28,xLabelHeight:18,yLabelWidth:18,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.3,gridLineWidth:0.3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",interactionModel:null,animatedZooms:false,showRangeSelector:false,rangeSelectorHeight:40,rangeSelectorPlotStrokeColor:"#808FAB",rangeSelectorPlotFillColor:"#A7B1C4",axes:{x:{pixelsPerLabel:60,axisLabelFormatter:Dygraph.dateAxisFormatter,valueFormatter:Dygraph.dateString_,ticker:null},y:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null},y2:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null}}};Dygraph.HORIZONTAL=1;Dygraph.VERTICAL=2;Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!==null){var a=["Date"];for(var c=0;c<e.length;c++){a.push(e[c])}Dygraph.update(b,{labels:a})}this.__init__(f,d,b)};Dygraph.prototype.__init__=function(d,c,b){if(/MSIE/.test(navigator.userAgent)&&!window.opera&&typeof(G_vmlCanvasManager)!="undefined"&&document.readyState!="complete"){var a=this;setTimeout(function(){a.__init__(d,c,b)},100);return}if(b===null||b===undefined){b={}}b=Dygraph.mapLegacyOptions_(b);if(!d){Dygraph.error("Constructing dygraph with a non-existent div!");return}this.isUsingExcanvas_=typeof(G_vmlCanvasManager)!="undefined";this.maindiv_=d;this.file_=c;this.rollPeriod_=b.rollPeriod||Dygraph.DEFAULT_ROLL_PERIOD;this.previousVerticalX_=-1;this.fractions_=b.fractions||false;this.dateWindow_=b.dateWindow||null;this.is_initial_draw_=true;this.annotations_=[];this.zoomed_x_=false;this.zoomed_y_=false;d.innerHTML="";if(d.style.width===""&&b.width){d.style.width=b.width+"px"}if(d.style.height===""&&b.height){d.style.height=b.height+"px"}if(d.style.height===""&&d.clientHeight===0){d.style.height=Dygraph.DEFAULT_HEIGHT+"px";if(d.style.width===""){d.style.width=Dygraph.DEFAULT_WIDTH+"px"}}this.width_=d.clientWidth;this.height_=d.clientHeight;if(b.stackedGraph){b.fillGraph=true}this.user_attrs_={};Dygraph.update(this.user_attrs_,b);this.attrs_={};Dygraph.updateDeep(this.attrs_,Dygraph.DEFAULT_ATTRS);this.boundaryIds_=[];this.setIndexByName_={};this.createInterface_();this.start_()};Dygraph.prototype.isZoomed=function(a){if(a==null){return this.zoomed_x_||this.zoomed_y_}if(a==="x"){return this.zoomed_x_}if(a==="y"){return this.zoomed_y_}throw"axis parameter is ["+a+"] must be null, 'x' or 'y'."};Dygraph.prototype.toString=function(){var a=this.maindiv_;var b=(a&&a.id)?a.id:a;return"[Dygraph "+b+"]"};Dygraph.prototype.attr_=function(b,a){if(this.user_attrs_!==null&&a&&typeof(this.user_attrs_[a])!="undefined"&&this.user_attrs_[a]!==null&&typeof(this.user_attrs_[a][b])!="undefined"){return this.user_attrs_[a][b]}else{if(this.user_attrs_!==null&&typeof(this.user_attrs_[b])!="undefined"){return this.user_attrs_[b]}else{if(this.attrs_!==null&&typeof(this.attrs_[b])!="undefined"){return this.attrs_[b]}else{return null}}}};Dygraph.prototype.optionsViewForAxis_=function(b){var a=this;return function(c){var d=a.user_attrs_.axes;if(d&&d[b]&&d[b][c]){return d[b][c]}if(typeof(a.user_attrs_[c])!="undefined"){return a.user_attrs_[c]}d=a.attrs_.axes;if(d&&d[b]&&d[b][c]){return d[b][c]}if(b=="y"&&a.axes_[0].hasOwnProperty(c)){return a.axes_[0][c]}else{if(b=="y2"&&a.axes_[1].hasOwnProperty(c)){return a.axes_[1][c]}}return a.attr_(c)}};Dygraph.prototype.rollPeriod=function(){return this.rollPeriod_};Dygraph.prototype.xAxisRange=function(){return this.dateWindow_?this.dateWindow_:this.xAxisExtremes()};Dygraph.prototype.xAxisExtremes=function(){var b=this.rawData_[0][0];var a=this.rawData_[this.rawData_.length-1][0];return[b,a]};Dygraph.prototype.yAxisRange=function(a){if(typeof(a)=="undefined"){a=0}if(a<0||a>=this.axes_.length){return null}var b=this.axes_[a];return[b.computedValueRange[0],b.computedValueRange[1]]};Dygraph.prototype.yAxisRanges=function(){var a=[];for(var b=0;b<this.axes_.length;b++){a.push(this.yAxisRange(b))}return a};Dygraph.prototype.toDomCoords=function(a,c,b){return[this.toDomXCoord(a),this.toDomYCoord(c,b)]};Dygraph.prototype.toDomXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return c.x+(b-a[0])/(a[1]-a[0])*c.w};Dygraph.prototype.toDomYCoord=function(d,a){var c=this.toPercentYCoord(d,a);if(c===null){return null}var b=this.plotter_.area;return b.y+c*b.h};Dygraph.prototype.toDataCoords=function(a,c,b){return[this.toDataXCoord(a),this.toDataYCoord(c,b)]};Dygraph.prototype.toDataXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return a[0]+(b-c.x)/c.w*(a[1]-a[0])};Dygraph.prototype.toDataYCoord=function(h,b){if(h===null){return null}var c=this.plotter_.area;var g=this.yAxisRange(b);if(typeof(b)=="undefined"){b=0}if(!this.axes_[b].logscale){return g[0]+(c.y+c.h-h)/c.h*(g[1]-g[0])}else{var f=(h-c.y)/c.h;var a=Dygraph.log10(g[1]);var e=a-(f*(a-Dygraph.log10(g[0])));var d=Math.pow(Dygraph.LOG_SCALE,e);return d}};Dygraph.prototype.toPercentYCoord=function(e,b){if(e===null){return null}if(typeof(b)=="undefined"){b=0}var d=this.yAxisRange(b);var c;if(!this.axes_[b].logscale){c=(d[1]-e)/(d[1]-d[0])}else{var a=Dygraph.log10(d[1]);c=(a-Dygraph.log10(e))/(a-Dygraph.log10(d[0]))}return c};Dygraph.prototype.toPercentXCoord=function(b){if(b===null){return null}var a=this.xAxisRange();return(b-a[0])/(a[1]-a[0])};Dygraph.prototype.numColumns=function(){return this.rawData_[0]?this.rawData_[0].length:this.attr_("labels").length};Dygraph.prototype.numRows=function(){return this.rawData_.length};Dygraph.prototype.fullXRange_=function(){if(this.numRows()>0){return[this.rawData_[0][0],this.rawData_[this.numRows()-1][0]]}else{return[0,1]}};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";a.appendChild(this.graphDiv);this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.canvas_ctx_=Dygraph.getContext(this.canvas_);this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.hidden_ctx_=Dygraph.getContext(this.hidden_);if(this.attr_("showRangeSelector")){this.rangeSelector_=new DygraphRangeSelector(this)}this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.createMouseEventElement_();this.layout_=new DygraphLayout(this);if(this.rangeSelector_){this.rangeSelector_.addToGraph(this.graphDiv,this.layout_)}var b=this;this.mouseMoveHandler=function(c){b.mouseMove_(c)};Dygraph.addEvent(this.mouseEventElement_,"mousemove",this.mouseMoveHandler);this.mouseOutHandler=function(c){b.mouseOut_(c)};Dygraph.addEvent(this.mouseEventElement_,"mouseout",this.mouseOutHandler);this.createStatusMessage_();this.createDragInterface_();this.resizeHandler=function(c){b.resize()};Dygraph.addEvent(window,"resize",this.resizeHandler)};Dygraph.prototype.destroy=function(){var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};Dygraph.removeEvent(this.mouseEventElement_,"mouseout",this.mouseOutHandler);Dygraph.removeEvent(this.mouseEventElement_,"mousemove",this.mouseMoveHandler);a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};Dygraph.removeEvent(window,"resize",this.resizeHandler);this.resizeHandler=null;b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.prototype.createMouseEventElement_=function(){if(this.isUsingExcanvas_){var a=document.createElement("div");a.style.position="absolute";a.style.backgroundColor="white";a.style.filter="alpha(opacity=0)";a.style.width=this.width_+"px";a.style.height=this.height_+"px";this.graphDiv.appendChild(a);return a}else{return this.canvas_}};Dygraph.prototype.setColors_=function(){var e=this.attr_("labels").length-1;this.colors_=[];var a=this.attr_("colors");var d;if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var j=Math.ceil(e/2);for(d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var g=d%2?Math.ceil(d/2):(j+d/2);var f=(1*g/(1+e));this.colors_.push(Dygraph.hsvToRGB(f,c,b))}}else{for(d=0;d<e;d++){if(!this.visibility()[d]){continue}var h=a[d%a.length];this.colors_.push(h)}}this.plotter_.setColors(this.colors_)};Dygraph.prototype.getColors=function(){return this.colors_};Dygraph.prototype.createStatusMessage_=function(){var d=this.user_attrs_.labelsDiv;if(d&&null!==d&&(typeof(d)=="string"||d instanceof String)){this.user_attrs_.labelsDiv=document.getElementById(d)}if(!this.attr_("labelsDiv")){var a=this.attr_("labelsDivWidth");var c={position:"absolute",fontSize:"14px",zIndex:10,width:a+"px",top:"0px",left:(this.width_-a-2)+"px",background:"white",textAlign:"left",overflow:"hidden"};Dygraph.update(c,this.attr_("labelsDivStyles"));var e=document.createElement("div");e.className="dygraph-legend";for(var b in c){if(c.hasOwnProperty(b)){e.style[b]=c[b]}}this.graphDiv.appendChild(e);this.attrs_.labelsDiv=e}};Dygraph.prototype.positionLabelsDiv_=function(){if(this.user_attrs_.hasOwnProperty("labelsDiv")){return}var a=this.plotter_.area;var b=this.attr_("labelsDiv");b.style.left=a.x+a.w-this.attr_("labelsDivWidth")-1+"px";b.style.top=a.y+"px"};Dygraph.prototype.createRollInterface_=function(){if(!this.roller_){this.roller_=document.createElement("input");this.roller_.type="text";this.roller_.style.display="none";this.graphDiv.appendChild(this.roller_)}var e=this.attr_("showRoller")?"block":"none";var d=this.plotter_.area;var b={position:"absolute",zIndex:10,top:(d.y+d.h-25)+"px",left:(d.x+1)+"px",display:e};this.roller_.size="2";this.roller_.value=this.rollPeriod_;for(var a in b){if(b.hasOwnProperty(a)){this.roller_.style[a]=b[a]}}var c=this;this.roller_.onchange=function(){c.adjustRoll(c.roller_.value)}};Dygraph.prototype.dragGetX_=function(b,a){return Dygraph.pageX(b)-a.px};Dygraph.prototype.dragGetY_=function(b,a){return Dygraph.pageY(b)-a.py};Dygraph.prototype.createDragInterface_=function(){var c={isZooming:false,isPanning:false,is2DPan:false,dragStartX:null,dragStartY:null,dragEndX:null,dragEndY:null,dragDirection:null,prevEndX:null,prevEndY:null,prevDragDirection:null,initialLeftmostDate:null,xUnitsPerPixel:null,dateRange:null,px:0,py:0,boundedDates:null,boundedValues:null,initializeMouseDown:function(i,h,f){if(i.preventDefault){i.preventDefault()}else{i.returnValue=false;i.cancelBubble=true}f.px=Dygraph.findPosX(h.canvas_);f.py=Dygraph.findPosY(h.canvas_);f.dragStartX=h.dragGetX_(i,f);f.dragStartY=h.dragGetY_(i,f)}};var e=this.attr_("interactionModel");var b=this;var d=function(f){return function(g){f(g,b,c)}};for(var a in e){if(!e.hasOwnProperty(a)){continue}Dygraph.addEvent(this.mouseEventElement_,a,d(e[a]))}Dygraph.addEvent(document,"mouseup",function(g){if(c.isZooming||c.isPanning){c.isZooming=false;c.dragStartX=null;c.dragStartY=null}if(c.isPanning){c.isPanning=false;c.draggingDate=null;c.dateRange=null;for(var f=0;f<b.axes_.length;f++){delete b.axes_[f].draggingValue;delete b.axes_[f].dragValueRange}}})};Dygraph.prototype.drawZoomRect_=function(e,c,i,b,g,a,f,d){var h=this.canvas_ctx_;if(a==Dygraph.HORIZONTAL){h.clearRect(Math.min(c,f),this.layout_.getPlotArea().y,Math.abs(c-f),this.layout_.getPlotArea().h)}else{if(a==Dygraph.VERTICAL){h.clearRect(this.layout_.getPlotArea().x,Math.min(b,d),this.layout_.getPlotArea().w,Math.abs(b-d))}}if(e==Dygraph.HORIZONTAL){if(i&&c){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(Math.min(c,i),this.layout_.getPlotArea().y,Math.abs(i-c),this.layout_.getPlotArea().h)}}else{if(e==Dygraph.VERTICAL){if(g&&b){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(this.layout_.getPlotArea().x,Math.min(b,g),this.layout_.getPlotArea().w,Math.abs(g-b))}}}if(this.isUsingExcanvas_){this.currentZoomRectArgs_=[e,c,i,b,g,0,0,0]}};Dygraph.prototype.clearZoomRect_=function(){this.currentZoomRectArgs_=null;this.canvas_ctx_.clearRect(0,0,this.canvas_.width,this.canvas_.height)};Dygraph.prototype.doZoomX_=function(c,a){this.currentZoomRectArgs_=null;var b=this.toDataXCoord(c);var d=this.toDataXCoord(a);this.doZoomXDates_(b,d)};Dygraph.zoomAnimationFunction=function(c,b){var a=1.5;return(1-Math.pow(a,-c))/(1-Math.pow(a,-b))};Dygraph.prototype.doZoomXDates_=function(c,e){var a=this.xAxisRange();var d=[c,e];this.zoomed_x_=true;var b=this;this.doAnimatedZoom(a,d,null,null,function(){if(b.attr_("zoomCallback")){b.attr_("zoomCallback")(c,e,b.yAxisRanges())}})};Dygraph.prototype.doZoomY_=function(h,f){this.currentZoomRectArgs_=null;var c=this.yAxisRanges();var b=[];for(var e=0;e<this.axes_.length;e++){var d=this.toDataYCoord(h,e);var a=this.toDataYCoord(f,e);b.push([a,d])}this.zoomed_y_=true;var g=this;this.doAnimatedZoom(null,null,c,b,function(){if(g.attr_("zoomCallback")){var i=g.xAxisRange();g.attr_("zoomCallback")(i[0],i[1],g.yAxisRanges())}})};Dygraph.prototype.doUnzoom_=function(){var c=false,d=false,a=false;if(this.dateWindow_!==null){c=true;d=true}for(var g=0;g<this.axes_.length;g++){if(typeof(this.axes_[g].valueWindow)!=="undefined"&&this.axes_[g].valueWindow!==null){c=true;a=true}}this.clearSelection();if(c){this.zoomed_x_=false;this.zoomed_y_=false;var f=this.rawData_[0][0];var b=this.rawData_[this.rawData_.length-1][0];if(!this.attr_("animatedZooms")){this.dateWindow_=null;for(g=0;g<this.axes_.length;g++){if(this.axes_[g].valueWindow!==null){delete this.axes_[g].valueWindow}}this.drawGraph_();if(this.attr_("zoomCallback")){this.attr_("zoomCallback")(f,b,this.yAxisRanges())}return}var l=null,m=null,k=null,h=null;if(d){l=this.xAxisRange();m=[f,b]}if(a){k=this.yAxisRanges();var n=this.gatherDatasets_(this.rolledSeries_,null);var o=n[1];this.computeYAxisRanges_(o);h=[];for(g=0;g<this.axes_.length;g++){var e=this.axes_[g];h.push(e.valueRange!=null?e.valueRange:e.extremeRange)}}var j=this;this.doAnimatedZoom(l,m,k,h,function(){j.dateWindow_=null;for(var p=0;p<j.axes_.length;p++){if(j.axes_[p].valueWindow!==null){delete j.axes_[p].valueWindow}}if(j.attr_("zoomCallback")){j.attr_("zoomCallback")(f,b,j.yAxisRanges())}})}};Dygraph.prototype.doAnimatedZoom=function(a,e,b,c,m){var i=this.attr_("animatedZooms")?Dygraph.ANIMATION_STEPS:1;var l=[];var k=[];var f,d;if(a!==null&&e!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);l[f-1]=[a[0]*(1-d)+d*e[0],a[1]*(1-d)+d*e[1]]}}if(b!==null&&c!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);var n=[];for(var g=0;g<this.axes_.length;g++){n.push([b[g][0]*(1-d)+d*c[g][0],b[g][1]*(1-d)+d*c[g][1]])}k[f-1]=n}}var h=this;Dygraph.repeatAndCleanup(function(p){if(k.length){for(var o=0;o<h.axes_.length;o++){var j=k[p][o];h.axes_[o].valueWindow=[j[0],j[1]]}}if(l.length){h.dateWindow_=l[p]}h.drawGraph_()},i,Dygraph.ANIMATION_DURATION/i,m)};Dygraph.prototype.mouseMove_=function(b){var r=this.layout_.points;if(r===undefined){return}var a=Dygraph.pageX(b)-Dygraph.findPosX(this.mouseEventElement_);var j=-1;var f;var n=1e+100;var o=-1;for(f=0;f<r.length;f++){var q=r[f];if(q===null){continue}var h=Math.abs(q.canvasx-a);if(h>n){continue}n=h;o=f}if(o>=0){j=r[o].xval}this.selPoints_=[];var d=r.length;if(!this.attr_("stackedGraph")){for(f=0;f<d;f++){if(r[f].xval==j){this.selPoints_.push(r[f])}}}else{var g=0;for(f=d-1;f>=0;f--){if(r[f].xval==j){var c={};for(var e in r[f]){c[e]=r[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}this.selPoints_.reverse()}if(this.attr_("highlightCallback")){var m=this.lastx_;if(m!==null&&j!=m){this.attr_("highlightCallback")(b,j,this.selPoints_,this.idxToRow_(o))}}this.lastx_=j;this.updateSelection_()};Dygraph.prototype.idxToRow_=function(a){if(a<0){return -1}var c=-1;for(var b=0;b<this.boundaryIds_.length;b++){if(this.boundaryIds_[b]!==undefined){c=b;break}}if(c<0){return -1}for(var d=0;d<this.layout_.datasets.length;++d){var e=this.layout_.datasets[d];if(a<e.length){return this.boundaryIds_[c][0]+a}a-=e.length}return -1};Dygraph.prototype.generateLegendDashHTML_=function(o,d,n){var h="";var f,e,b,k;var c=0,m=0;var l=[];var g;var a=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(a){return"&mdash;"}if(!o||o.length<=1){h='<div style="display: inline-block; position: relative; bottom: .5ex; padding-left: 1em; height: 1px; border-bottom: 2px solid '+d+';"></div>'}else{for(f=0;f<=o.length;f++){c+=o[f%o.length]}g=Math.floor(n/(c-o[0]));if(g>1){for(f=0;f<o.length;f++){l[f]=o[f]/n}m=l.length}else{g=1;for(f=0;f<o.length;f++){l[f]=o[f]/c}m=l.length+1}for(e=0;e<g;e++){for(f=0;f<m;f+=2){b=l[f%l.length];if(f<o.length){k=l[(f+1)%l.length]}else{k=0}h+='<div style="display: inline-block; position: relative; bottom: .5ex; margin-right: '+k+"em; padding-left: "+b+"em; height: 1px; border-bottom: 2px solid "+d+';"></div>'}}}return h};Dygraph.prototype.generateLegendHTML_=function(k,f,b){var l,u,o,s,m,g;if(typeof(k)==="undefined"){if(this.attr_("legend")!="always"){return""}u=this.attr_("labelsSeparateLines");var r=this.attr_("labels");l="";for(o=1;o<r.length;o++){if(!this.visibility()[o-1]){continue}s=this.plotter_.colors[r[o]];if(l!==""){l+=(u?"<br/>":" ")}g=this.attr_("strokePattern",r[o]);m=this.generateLegendDashHTML_(g,s,b);l+="<span style='font-weight: bold; color: "+s+";'>"+m+" "+r[o]+"</span>"}return l}var t=this.optionsViewForAxis_("x");var h=t("valueFormatter");l=h(k,t,this.attr_("labels")[0],this)+":";var p=[];var d=this.numAxes();for(o=0;o<d;o++){p[o]=this.optionsViewForAxis_("y"+(o?1+o:""))}var e=this.attr_("labelsShowZeroValues");u=this.attr_("labelsSeparateLines");for(o=0;o<this.selPoints_.length;o++){var n=this.selPoints_[o];if(n.yval===0&&!e){continue}if(!Dygraph.isOK(n.canvasy)){continue}if(u){l+="<br/>"}var j=p[this.seriesToAxisMap_[n.name]];var q=j("valueFormatter");s=this.plotter_.colors[n.name];var a=q(n.yval,j,n.name,this);l+=" <b><span style='color: "+s+";'>"+n.name+"</span></b>:"+a}return l};Dygraph.prototype.setLegendHTML_=function(b,e){var c=this.attr_("labelsDiv");var f=document.createElement("span");f.setAttribute("style","margin: 0; padding: 0 0 0 1em; border: 0;");c.appendChild(f);var a=f.offsetWidth;var d=this.generateLegendHTML_(b,e,a);if(c!==null){c.innerHTML=d}else{if(typeof(this.shown_legend_error_)=="undefined"){this.error("labelsDiv is set to something nonexistent; legend will not be shown.");this.shown_legend_error_=true}}};Dygraph.prototype.updateSelection_=function(){var d;var h=this.canvas_ctx_;if(this.previousVerticalX_>=0){var e=0;var f=this.attr_("labels");for(d=1;d<f.length;d++){var b=this.attr_("highlightCircleSize",f[d]);if(b>e){e=b}}var g=this.previousVerticalX_;h.clearRect(g-e-1,0,2*e+2,this.height_)}if(this.isUsingExcanvas_&&this.currentZoomRectArgs_){Dygraph.prototype.drawZoomRect_.apply(this,this.currentZoomRectArgs_)}if(this.selPoints_.length>0){if(this.attr_("showLabelsOnHighlight")){this.setLegendHTML_(this.lastx_,this.selPoints_)}var c=this.selPoints_[0].canvasx;h.save();for(d=0;d<this.selPoints_.length;d++){var j=this.selPoints_[d];if(!Dygraph.isOK(j.canvasy)){continue}var a=this.attr_("highlightCircleSize",j.name);h.beginPath();h.fillStyle=this.plotter_.colors[j.name];h.arc(c,j.canvasy,a,0,2*Math.PI,false);h.fill()}h.restore();this.previousVerticalX_=c}};Dygraph.prototype.setSelection=function(c){this.selPoints_=[];var e=0;if(c!==false){c=c-this.boundaryIds_[0][0]}if(c!==false&&c>=0){for(var b=0;b<this.layout_.datasets.length;++b){var d=this.layout_.datasets[b];if(c<d.length){var a=this.layout_.points[e+c];if(this.attr_("stackedGraph")){a=this.layout_.unstackPointAtIndex(e+c)}this.selPoints_.push(a)}e+=d.length}}if(this.selPoints_.length){this.lastx_=this.selPoints_[0].xval;this.updateSelection_()}else{this.clearSelection()}};Dygraph.prototype.mouseOut_=function(a){if(this.attr_("unhighlightCallback")){this.attr_("unhighlightCallback")(a)}if(this.attr_("hideOverlayOnMouseOut")){this.clearSelection()}};Dygraph.prototype.clearSelection=function(){this.canvas_ctx_.clearRect(0,0,this.width_,this.height_);this.setLegendHTML_();this.selPoints_=[];this.lastx_=-1};Dygraph.prototype.getSelection=function(){if(!this.selPoints_||this.selPoints_.length<1){return -1}for(var a=0;a<this.layout_.points.length;a++){if(this.layout_.points[a].x==this.selPoints_[0].x){return a+this.boundaryIds_[0][0]}}return -1};Dygraph.prototype.loadedEvent_=function(a){this.rawData_=this.parseCSV_(a);this.predraw_()};Dygraph.prototype.addXTicks_=function(){var a;if(this.dateWindow_){a=[this.dateWindow_[0],this.dateWindow_[1]]}else{a=this.fullXRange_()}var c=this.optionsViewForAxis_("x");var b=c("ticker")(a[0],a[1],this.width_,c,this);this.layout_.setXTicks(b)};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null,c,g;var b=this.attr_("errorBars")||this.attr_("customBars");if(b){for(c=0;c<d.length;c++){g=d[c][1][0];if(!g){continue}var a=g-d[c][1][1];var e=g+d[c][1][2];if(a>g){a=g}if(e<g){e=g}if(f===null||e>f){f=e}if(h===null||a<h){h=a}}}else{for(c=0;c<d.length;c++){g=d[c][1];if(g===null||isNaN(g)){continue}if(f===null||g>f){f=g}if(h===null||g<h){h=g}}}return[h,f]};Dygraph.prototype.predraw_=function(){var f=new Date();this.computeYAxes_();if(this.plotter_){this.plotter_.clear()}this.plotter_=new DygraphCanvasRenderer(this,this.hidden_,this.hidden_ctx_,this.layout_);this.createRollInterface_();this.positionLabelsDiv_();if(this.rangeSelector_){this.rangeSelector_.renderStaticLayer()}this.rolledSeries_=[null];for(var c=1;c<this.numColumns();c++){var e=this.attr_("connectSeparatedPoints",c);var d=this.attr_("logscale",c);var b=this.extractSeries_(this.rawData_,c,d,e);b=this.rollingAverage(b,this.rollPeriod_);this.rolledSeries_.push(b)}this.drawGraph_();var a=new Date();this.drawingTimeMs_=(a-f)};Dygraph.prototype.gatherDatasets_=function(w,c){var s=[];var b=[];var e=[];var a={};var u,t,r;var m=w.length-1;for(u=m;u>=1;u--){if(!this.visibility()[u-1]){continue}var h=[];for(t=0;t<w[u].length;t++){h.push(w[u][t])}var o=this.attr_("errorBars")||this.attr_("customBars");if(c){var A=c[0];var f=c[1];var p=[];var d=null,z=null;for(r=0;r<h.length;r++){if(h[r][0]>=A&&d===null){d=r}if(h[r][0]<=f){z=r}}if(d===null){d=0}if(d>0){d--}if(z===null){z=h.length-1}if(z<h.length-1){z++}s[u-1]=[d,z];for(r=d;r<=z;r++){p.push(h[r])}h=p}else{s[u-1]=[0,h.length-1]}var n=this.extremeValues_(h);if(o){for(t=0;t<h.length;t++){h[t]=[h[t][0],h[t][1][0],h[t][1][1],h[t][1][2]]}}else{if(this.attr_("stackedGraph")){var q=h.length;var y;for(t=0;t<q;t++){var g=h[t][0];if(b[g]===undefined){b[g]=0}y=h[t][1];b[g]+=y;h[t]=[g,b[g]];if(b[g]>n[1]){n[1]=b[g]}if(b[g]<n[0]){n[0]=b[g]}}}}var v=this.attr_("labels")[u];a[v]=n;e[u]=h}return[e,a,s]};Dygraph.prototype.drawGraph_=function(k){var a=new Date();if(typeof(k)==="undefined"){k=true}var e=this.is_initial_draw_;this.is_initial_draw_=false;this.layout_.removeAllDatasets();this.setColors_();this.attrs_.pointSize=0.5*this.attr_("highlightCircleSize");var h=this.gatherDatasets_(this.rolledSeries_,this.dateWindow_);var d=h[0];var j=h[1];this.boundaryIds_=h[2];this.setIndexByName_={};var g=this.attr_("labels");if(g.length>0){this.setIndexByName_[g[0]]=0}for(var f=1;f<d.length;f++){this.setIndexByName_[g[f]]=f;if(!this.visibility()[f-1]){continue}this.layout_.addDataset(g[f],d[f])}this.computeYAxisRanges_(j);this.layout_.setYAxes(this.axes_);this.addXTicks_();var b=this.zoomed_x_;this.layout_.setDateWindow(this.dateWindow_);this.zoomed_x_=b;this.layout_.evaluateWithError();this.renderGraph_(e,false);if(this.attr_("timingName")){var c=new Date();if(console){console.log(this.attr_("timingName")+" - drawGraph: "+(c-a)+"ms")}}};Dygraph.prototype.renderGraph_=function(a,b){this.plotter_.clear();this.plotter_.render();this.canvas_.getContext("2d").clearRect(0,0,this.canvas_.width,this.canvas_.height);this.setLegendHTML_();if(!a){if(b){if(typeof(this.selPoints_)!=="undefined"&&this.selPoints_.length){this.clearSelection()}else{this.clearSelection()}}}if(this.rangeSelector_){this.rangeSelector_.renderInteractiveLayer()}if(this.attr_("drawCallback")!==null){this.attr_("drawCallback")(this,a)}};Dygraph.prototype.computeYAxes_=function(){var g,c,m,b,j,a,p;if(this.axes_!==undefined&&this.user_attrs_.hasOwnProperty("valueRange")===false){c=[];for(j=0;j<this.axes_.length;j++){c.push(this.axes_[j].valueWindow)}}this.axes_=[{yAxisId:0,g:this}];this.seriesToAxisMap_={};var h=this.attr_("labels");var f={};for(g=1;g<h.length;g++){f[h[g]]=(g-1)}var e=["includeZero","valueRange","labelsKMB","labelsKMG2","pixelsPerYLabel","yAxisLabelWidth","axisLabelFontSize","axisTickSize","logscale"];for(g=0;g<e.length;g++){var d=e[g];p=this.attr_(d);if(p){this.axes_[0][d]=p}}for(m in f){if(!f.hasOwnProperty(m)){continue}b=this.attr_("axis",m);if(b===null){this.seriesToAxisMap_[m]=0;continue}if(typeof(b)=="object"){a={};Dygraph.update(a,this.axes_[0]);Dygraph.update(a,{valueRange:null});var o=this.axes_.length;a.yAxisId=o;a.g=this;Dygraph.update(a,b);this.axes_.push(a);this.seriesToAxisMap_[m]=o}}for(m in f){if(!f.hasOwnProperty(m)){continue}b=this.attr_("axis",m);if(typeof(b)=="string"){if(!this.seriesToAxisMap_.hasOwnProperty(b)){this.error("Series "+m+" wants to share a y-axis with series "+b+", which does not define its own axis.");return null}var n=this.seriesToAxisMap_[b];this.seriesToAxisMap_[m]=n}}if(c!==undefined){for(j=0;j<c.length;j++){this.axes_[j].valueWindow=c[j]}}for(b=0;b<this.axes_.length;b++){if(b===0){a=this.optionsViewForAxis_("y"+(b?"2":""));p=a("valueRange");if(p){this.axes_[b].valueRange=p}}else{var l=this.user_attrs_.axes;if(l&&l.y2){p=l.y2.valueRange;if(p){this.axes_[b].valueRange=p}}}}};Dygraph.prototype.numAxes=function(){var c=0;for(var b in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(b)){continue}var a=this.seriesToAxisMap_[b];if(a>c){c=a}}return 1+c};Dygraph.prototype.axisPropertiesForSeries=function(a){return this.axes_[this.seriesToAxisMap_[a]]};Dygraph.prototype.computeYAxisRanges_=function(a){var g=[],h;for(h in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(h)){continue}var p=this.seriesToAxisMap_[h];while(g.length<=p){g.push([])}g[p].push(h)}for(var u=0;u<this.axes_.length;u++){var b=this.axes_[u];if(!g[u]){b.extremeRange=[0,1]}else{h=g[u];var x=Infinity;var w=-Infinity;var o,m;for(var s=0;s<h.length;s++){if(!a.hasOwnProperty(h[s])){continue}o=a[h[s]][0];if(o!==null){x=Math.min(o,x)}m=a[h[s]][1];if(m!==null){w=Math.max(m,w)}}if(b.includeZero&&x>0){x=0}if(x==Infinity){x=0}if(w==-Infinity){w=1}var t=w-x;if(t===0){t=w}var d,z;if(b.logscale){d=w+0.1*t;z=x}else{d=w+0.1*t;z=x-0.1*t;if(!this.attr_("avoidMinZero")){if(z<0&&x>=0){z=0}if(d>0&&w<=0){d=0}}if(this.attr_("includeZero")){if(w<0){d=0}if(x>0){z=0}}}b.extremeRange=[z,d]}if(b.valueWindow){b.computedValueRange=[b.valueWindow[0],b.valueWindow[1]]}else{if(b.valueRange){b.computedValueRange=[b.valueRange[0],b.valueRange[1]]}else{b.computedValueRange=b.extremeRange}}var n=this.optionsViewForAxis_("y"+(u?"2":""));var y=n("ticker");if(u===0||b.independentTicks){b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this)}else{var l=this.axes_[0];var e=l.ticks;var f=l.computedValueRange[1]-l.computedValueRange[0];var A=b.computedValueRange[1]-b.computedValueRange[0];var c=[];for(var r=0;r<e.length;r++){var q=(e[r].v-l.computedValueRange[0])/f;var v=b.computedValueRange[0]+q*A;c.push(v)}b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this,c)}}};Dygraph.prototype.extractSeries_=function(h,e,g,f){var d=[];for(var c=0;c<h.length;c++){var b=h[c][0];var a=h[c][e];if(g){if(a<=0){a=null}d.push([b,a])}else{if(a!==null||!f){d.push([b,a])}}}return d};Dygraph.prototype.rollingAverage=function(l,d){if(l.length<2){return l}d=Math.min(d,l.length);var b=[];var s=this.attr_("sigma");var E,o,w,v,m,c,D,x;if(this.fractions_){var k=0;var h=0;var e=100;for(w=0;w<l.length;w++){k+=l[w][1][0];h+=l[w][1][1];if(w-d>=0){k-=l[w-d][1][0];h-=l[w-d][1][1]}var A=l[w][0];var u=h?k/h:0;if(this.attr_("errorBars")){if(this.attr_("wilsonInterval")){if(h){var r=u<0?0:u,t=h;var z=s*Math.sqrt(r*(1-r)/t+s*s/(4*t*t));var a=1+s*s/h;E=(r+s*s/(2*h)-z)/a;o=(r+s*s/(2*h)+z)/a;b[w]=[A,[r*e,(r-E)*e,(o-r)*e]]}else{b[w]=[A,[0,0,0]]}}else{x=h?s*Math.sqrt(u*(1-u)/h):1;b[w]=[A,[e*u,e*x,e*x]]}}else{b[w]=[A,e*u]}}}else{if(this.attr_("customBars")){E=0;var B=0;o=0;var g=0;for(w=0;w<l.length;w++){var C=l[w][1];m=C[1];b[w]=[l[w][0],[m,m-C[0],C[2]-m]];if(m!==null&&!isNaN(m)){E+=C[0];B+=m;o+=C[2];g+=1}if(w-d>=0){var q=l[w-d];if(q[1][1]!==null&&!isNaN(q[1][1])){E-=q[1][0];B-=q[1][1];o-=q[1][2];g-=1}}if(g){b[w]=[l[w][0],[1*B/g,1*(B-E)/g,1*(o-B)/g]]}else{b[w]=[l[w][0],[null,null,null]]}}}else{if(!this.attr_("errorBars")){if(d==1){return l}for(w=0;w<l.length;w++){c=0;D=0;for(v=Math.max(0,w-d+1);v<w+1;v++){m=l[v][1];if(m===null||isNaN(m)){continue}D++;c+=l[v][1]}if(D){b[w]=[l[w][0],c/D]}else{b[w]=[l[w][0],null]}}}else{for(w=0;w<l.length;w++){c=0;var f=0;D=0;for(v=Math.max(0,w-d+1);v<w+1;v++){m=l[v][1][0];if(m===null||isNaN(m)){continue}D++;c+=l[v][1][0];f+=Math.pow(l[v][1][1],2)}if(D){x=Math.sqrt(f)/D;b[w]=[l[w][0],[c/D,s*x,s*x]]}else{b[w]=[l[w][0],[null,null,null]]}}}}}return b};Dygraph.prototype.detectTypeFromString_=function(b){var a=false;var c=b.indexOf("-");if((c>0&&(b[c-1]!="e"&&b[c-1]!="E"))||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}if(a){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueParser=function(d){return parseFloat(d)};this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}};Dygraph.prototype.parseFloat_=function(a,c,b){var e=parseFloat(a);if(!isNaN(e)){return e}if(/^ *$/.test(a)){return null}if(/^ *nan *$/i.test(a)){return NaN}var d="Unable to parse '"+a+"' as a number";if(b!==null&&c!==null){d+=" on line "+(1+c)+" ('"+b+"') of CSV."}this.error(d);return null};Dygraph.prototype.parseCSV_=function(s){var r=[];var a=s.split("\n");var g,k;var p=this.attr_("delimiter");if(a[0].indexOf(p)==-1&&a[0].indexOf("\t")>=0){p="\t"}var b=0;if(!("labels" in this.user_attrs_)){b=1;this.attrs_.labels=a[0].split(p)}var o=0;var m;var q=false;var c=this.attr_("labels").length;var f=false;for(var l=b;l<a.length;l++){var e=a[l];o=l;if(e.length===0){continue}if(e[0]=="#"){continue}var d=e.split(p);if(d.length<2){continue}var h=[];if(!q){this.detectTypeFromString_(d[0]);m=this.attr_("xValueParser");q=true}h[0]=m(d[0],this);if(this.fractions_){for(k=1;k<d.length;k++){g=d[k].split("/");if(g.length!=2){this.error('Expected fractional "num/den" values in CSV data but found a value \''+d[k]+"' on line "+(1+l)+" ('"+e+"') which is not of this form.");h[k]=[0,0]}else{h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e)]}}}else{if(this.attr_("errorBars")){if(d.length%2!=1){this.error("Expected alternating (value, stdev.) pairs in CSV data but line "+(1+l)+" has an odd number of values ("+(d.length-1)+"): '"+e+"'")}for(k=1;k<d.length;k+=2){h[(k+1)/2]=[this.parseFloat_(d[k],l,e),this.parseFloat_(d[k+1],l,e)]}}else{if(this.attr_("customBars")){for(k=1;k<d.length;k++){var t=d[k];if(/^ *$/.test(t)){h[k]=[null,null,null]}else{g=t.split(";");if(g.length==3){h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e),this.parseFloat_(g[2],l,e)]}else{this.warn('When using customBars, values must be either blank or "low;center;high" tuples (got "'+t+'" on line '+(1+l))}}}}else{for(k=1;k<d.length;k++){h[k]=this.parseFloat_(d[k],l,e)}}}}if(r.length>0&&h[0]<r[r.length-1][0]){f=true}if(h.length!=c){this.error("Number of columns in line "+l+" ("+h.length+") does not agree with number of labels ("+c+") "+e)}if(l===0&&this.attr_("labels")){var n=true;for(k=0;n&&k<h.length;k++){if(h[k]){n=false}}if(n){this.warn("The dygraphs 'labels' option is set, but the first row of CSV data ('"+e+"') appears to also contain labels. Will drop the CSV labels and use the option labels.");continue}}r.push(h)}if(f){this.warn("CSV is out of order; order it correctly to speed loading.");r.sort(function(j,i){return j[0]-i[0]})}return r};Dygraph.prototype.parseArray_=function(b){if(b.length===0){this.error("Can't plot empty data set");return null}if(b[0].length===0){this.error("Data set cannot contain an empty row");return null}var a;if(this.attr_("labels")===null){this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter");this.attrs_.labels=["X"];for(a=1;a<b[0].length;a++){this.attrs_.labels.push("Y"+a)}}if(Dygraph.isDateLike(b[0][0])){this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter;this.attrs_.axes.x.ticker=Dygraph.dateTicker;var c=Dygraph.clone(b);for(a=0;a<b.length;a++){if(c[a].length===0){this.error("Row "+(1+a)+" of data is empty");return null}if(c[a][0]===null||typeof(c[a][0].getTime)!="function"||isNaN(c[a][0].getTime())){this.error("x value in row "+(1+a)+" is not a Date");return null}c[a][0]=c[a][0].getTime()}return c}else{this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.axisLabelFormatter=Dygraph.numberAxisLabelFormatter;this.attrs_.axes.x.ticker=Dygraph.numericTicks;return b}};Dygraph.prototype.parseDataTable_=function(w){var d=function(i){var j=String.fromCharCode(65+i%26);i=Math.floor(i/26);while(i>0){j=String.fromCharCode(65+(i-1)%26)+j.toLowerCase();i=Math.floor((i-1)/26)}return j};var h=w.getNumberOfColumns();var g=w.getNumberOfRows();var f=w.getColumnType(0);if(f=="date"||f=="datetime"){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{if(f=="number"){this.attrs_.xValueParser=function(i){return parseFloat(i)};this.attrs_.axes.x.valueFormatter=function(i){return i};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+f+"')");return null}}var m=[];var t={};var s=false;var q,o;for(q=1;q<h;q++){var b=w.getColumnType(q);if(b=="number"){m.push(q)}else{if(b=="string"&&this.attr_("displayAnnotations")){var r=m[m.length-1];if(!t.hasOwnProperty(r)){t[r]=[q]}else{t[r].push(q)}s=true}else{this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true")}}}var u=[w.getColumnLabel(0)];for(q=0;q<m.length;q++){u.push(w.getColumnLabel(m[q]));if(this.attr_("errorBars")){q+=1}}this.attrs_.labels=u;h=u.length;var v=[];var l=false;var a=[];for(q=0;q<g;q++){var e=[];if(typeof(w.getValue(q,0))==="undefined"||w.getValue(q,0)===null){this.warn("Ignoring row "+q+" of DataTable because of undefined or null first column.");continue}if(f=="date"||f=="datetime"){e.push(w.getValue(q,0).getTime())}else{e.push(w.getValue(q,0))}if(!this.attr_("errorBars")){for(o=0;o<m.length;o++){var c=m[o];e.push(w.getValue(q,c));if(s&&t.hasOwnProperty(c)&&w.getValue(q,t[c][0])!==null){var p={};p.series=w.getColumnLabel(c);p.xval=e[0];p.shortText=d(a.length);p.text="";for(var n=0;n<t[c].length;n++){if(n){p.text+="\n"}p.text+=w.getValue(q,t[c][n])}a.push(p)}}for(o=0;o<e.length;o++){if(!isFinite(e[o])){e[o]=null}}}else{for(o=0;o<h-1;o++){e.push([w.getValue(q,1+2*o),w.getValue(q,2+2*o)])}}if(v.length>0&&e[0]<v[v.length-1][0]){l=true}v.push(e)}if(l){this.warn("DataTable is out of order; order it correctly to speed loading.");v.sort(function(j,i){return j[0]-i[0]})}this.rawData_=v;if(a.length>0){this.setAnnotations(a,true)}};Dygraph.prototype.start_=function(){var c=this.file_;if(typeof c=="function"){c=c()}if(Dygraph.isArrayLike(c)){this.rawData_=this.parseArray_(c);this.predraw_()}else{if(typeof c=="object"&&typeof c.getColumnRange=="function"){this.parseDataTable_(c);this.predraw_()}else{if(typeof c=="string"){if(c.indexOf("\n")>=0){this.loadedEvent_(c)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status===200||b.status===0){a.loadedEvent_(b.responseText)}}};b.open("GET",c,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof c))}}}};Dygraph.prototype.updateOptions=function(e,b){if(typeof(b)=="undefined"){b=false}var d=e.file;var c=Dygraph.mapLegacyOptions_(e);if("rollPeriod" in c){this.rollPeriod_=c.rollPeriod}if("dateWindow" in c){this.dateWindow_=c.dateWindow;if(!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_x_=(c.dateWindow!==null)}}if("valueRange" in c&&!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_y_=(c.valueRange!==null)}var a=Dygraph.isPixelChangingOptionList(this.attr_("labels"),c);Dygraph.updateDeep(this.user_attrs_,c);if(d){this.file_=d;if(!b){this.start_()}}else{if(!b){if(a){this.predraw_()}else{this.renderGraph_(false,false)}}}};Dygraph.mapLegacyOptions_=function(c){var a={};for(var b in c){if(b=="file"){continue}if(c.hasOwnProperty(b)){a[b]=c[b]}}var e=function(g,f,h){if(!a.axes){a.axes={}}if(!a.axes[g]){a.axes[g]={}}a.axes[g][f]=h};var d=function(f,g,h){if(typeof(c[f])!="undefined"){e(g,h,c[f]);delete a[f]}};d("xValueFormatter","x","valueFormatter");d("pixelsPerXLabel","x","pixelsPerLabel");d("xAxisLabelFormatter","x","axisLabelFormatter");d("xTicker","x","ticker");d("yValueFormatter","y","valueFormatter");d("pixelsPerYLabel","y","pixelsPerLabel");d("yAxisLabelFormatter","y","axisLabelFormatter");d("yTicker","y","ticker");return a};Dygraph.prototype.resize=function(d,b){if(this.resize_lock){return}this.resize_lock=true;if((d===null)!=(b===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");d=b=null}var a=this.width_;var c=this.height_;if(d){this.maindiv_.style.width=d+"px";this.maindiv_.style.height=b+"px";this.width_=d;this.height_=b}else{this.width_=this.maindiv_.clientWidth;this.height_=this.maindiv_.clientHeight}if(a!=this.width_||c!=this.height_){this.maindiv_.innerHTML="";this.roller_=null;this.attrs_.labelsDiv=null;this.createInterface_();if(this.annotations_.length){this.layout_.setAnnotations(this.annotations_)}this.predraw_()}this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.predraw_()};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length<this.numColumns()-1){this.attrs_.visibility.push(true)}return this.attr_("visibility")};Dygraph.prototype.setVisibility=function(b,c){var a=this.visibility();if(b<0||b>=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.predraw_()}};Dygraph.prototype.size=function(){return{width:this.width_,height:this.height_}};Dygraph.prototype.setAnnotations=function(b,a){Dygraph.addAnnotationRule();this.annotations_=b;this.layout_.setAnnotations(this.annotations_);if(!a){this.predraw_()}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.getLabels=function(a){return this.attr_("labels").slice()};Dygraph.prototype.indexFromSetName=function(a){return this.setIndexByName_[a]};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var f="border: 1px solid black; background-color: white; text-align: center;";var e=document.createElement("style");e.type="text/css";document.getElementsByTagName("head")[0].appendChild(e);for(var b=0;b<document.styleSheets.length;b++){if(document.styleSheets[b].disabled){continue}var d=document.styleSheets[b];try{if(d.insertRule){var a=d.cssRules?d.cssRules.length:0;d.insertRule(".dygraphDefaultAnnotation { "+f+" }",a)}else{if(d.addRule){d.addRule(".dygraphDefaultAnnotation",f)}}Dygraph.addedAnnotationCSS=true;return}catch(c){}}this.warn("Unable to add default annotation CSS rule; display may be off.")};var DateGraph=Dygraph;"use strict";Dygraph.LOG_SCALE=10;Dygraph.LN_TEN=Math.log(Dygraph.LOG_SCALE);Dygraph.log10=function(a){return Math.log(a)/Dygraph.LN_TEN};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.LOG_STACK_TRACES=false;Dygraph.DOTTED_LINE=[2,2];Dygraph.DASHED_LINE=[7,3];Dygraph.DOT_DASH_LINE=[7,2,2,2];Dygraph.log=function(b,d){var a;if(typeof(printStackTrace)!="undefined"){a=printStackTrace({guess:false});while(a[0].indexOf("stacktrace")!=-1){a.splice(0,1)}a.splice(0,2);for(var c=0;c<a.length;c++){a[c]=a[c].replace(/\([^)]*\/(.*)\)/,"@$1").replace(/\@.*\/([^\/]*)/,"@$1").replace("[object Object].","")}var e=a.splice(0,1)[0];d+=" ("+e.replace(/^.*@ ?/,"")+")"}if(typeof(console)!="undefined"){switch(b){case Dygraph.DEBUG:console.debug("dygraphs: "+d);break;case Dygraph.INFO:console.info("dygraphs: "+d);break;case Dygraph.WARNING:console.warn("dygraphs: "+d);break;case Dygraph.ERROR:console.error("dygraphs: "+d);break}}if(Dygraph.LOG_STACK_TRACES){console.log(a.join("\n"))}};Dygraph.info=function(a){Dygraph.log(Dygraph.INFO,a)};Dygraph.prototype.info=Dygraph.info;Dygraph.warn=function(a){Dygraph.log(Dygraph.WARNING,a)};Dygraph.prototype.warn=Dygraph.warn;Dygraph.error=function(a){Dygraph.log(Dygraph.ERROR,a)};Dygraph.prototype.error=Dygraph.error;Dygraph.getContext=function(a){return a.getContext("2d")};Dygraph.addEvent=function addEvent(c,b,a){if(c.addEventListener){c.addEventListener(b,a,false)}else{c[b+a]=function(){a(window.event)};c.attachEvent("on"+b,c[b+a])}};Dygraph.removeEvent=function addEvent(c,b,a){if(c.removeEventListener){c.removeEventListener(b,a,false)}else{c.detachEvent("on"+b,c[b+a]);c[b+a]=null}};Dygraph.cancelEvent=function(a){a=a?a:window.event;if(a.stopPropagation){a.stopPropagation()}if(a.preventDefault){a.preventDefault()}a.cancelBubble=true;a.cancel=true;a.returnValue=false;return false};Dygraph.hsvToRGB=function(h,g,k){var c;var d;var l;if(g===0){c=k;d=k;l=k}else{var e=Math.floor(h*6);var j=(h*6)-e;var b=k*(1-g);var a=k*(1-(g*j));var m=k*(1-(g*(1-j)));switch(e){case 1:c=a;d=k;l=b;break;case 2:c=b;d=k;l=m;break;case 3:c=b;d=a;l=k;break;case 4:c=m;d=b;l=k;break;case 5:c=k;d=b;l=a;break;case 6:case 0:c=k;d=m;l=b;break}}c=Math.floor(255*c+0.5);d=Math.floor(255*d+0.5);l=Math.floor(255*l+0.5);return"rgb("+c+","+d+","+l+")"};Dygraph.findPosX=function(b){var c=0;if(b.offsetParent){var a=b;while(1){c+=a.offsetLeft;if(!a.offsetParent){break}a=a.offsetParent}}else{if(b.x){c+=b.x}}while(b&&b!=document.body){c-=b.scrollLeft;b=b.parentNode}return c};Dygraph.findPosY=function(c){var b=0;if(c.offsetParent){var a=c;while(1){b+=a.offsetTop;if(!a.offsetParent){break}a=a.offsetParent}}else{if(c.y){b+=c.y}}while(c&&c!=document.body){b-=c.scrollTop;c=c.parentNode}return b};Dygraph.pageX=function(c){if(c.pageX){return(!c.pageX||c.pageX<0)?0:c.pageX}else{var d=document;var a=document.body;return c.clientX+(d.scrollLeft||a.scrollLeft)-(d.clientLeft||0)}};Dygraph.pageY=function(c){if(c.pageY){return(!c.pageY||c.pageY<0)?0:c.pageY}else{var d=document;var a=document.body;return c.clientY+(d.scrollTop||a.scrollTop)-(d.clientTop||0)}};Dygraph.isOK=function(a){return a&&!isNaN(a)};Dygraph.floatFormat=function(a,b){var c=Math.min(Math.max(1,b||2),21);return(Math.abs(a)<0.001&&a!==0)?a.toExponential(c-1):a.toPrecision(c)};Dygraph.zeropad=function(a){if(a<10){return"0"+a}else{return""+a}};Dygraph.hmsString_=function(a){var c=Dygraph.zeropad;var b=new Date(a);if(b.getSeconds()){return c(b.getHours())+":"+c(b.getMinutes())+":"+c(b.getSeconds())}else{return c(b.getHours())+":"+c(b.getMinutes())}};Dygraph.round_=function(c,b){var a=Math.pow(10,b);return Math.round(c*a)/a};Dygraph.binarySearch=function(a,d,i,e,b){if(e===null||e===undefined||b===null||b===undefined){e=0;b=d.length-1}if(e>b){return -1}if(i===null||i===undefined){i=0}var h=function(j){return j>=0&&j<d.length};var g=parseInt((e+b)/2,10);var c=d[g];if(c==a){return g}var f;if(c>a){if(i>0){f=g-1;if(h(f)&&d[f]<a){return g}}return Dygraph.binarySearch(a,d,i,e,g-1)}if(c<a){if(i<0){f=g+1;if(h(f)&&d[f]>a){return g}}return Dygraph.binarySearch(a,d,i,g+1,b)}};Dygraph.dateParser=function(a){var b;var c;c=Dygraph.dateStrToMillis(a);if(c&&!isNaN(c)){return c}if(a.search("-")!=-1){b=a.replace("-","/","g");while(b.search("-")!=-1){b=b.replace("-","/")}c=Dygraph.dateStrToMillis(b)}else{if(a.length==8){b=a.substr(0,4)+"/"+a.substr(4,2)+"/"+a.substr(6,2);c=Dygraph.dateStrToMillis(b)}else{c=Dygraph.dateStrToMillis(a)}}if(!c||isNaN(c)){Dygraph.error("Couldn't parse "+a+" as a date")}return c};Dygraph.dateStrToMillis=function(a){return new Date(a).getTime()};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.updateDeep=function(b,d){function c(e){return(typeof Node==="object"?e instanceof Node:typeof e==="object"&&typeof e.nodeType==="number"&&typeof e.nodeName==="string")}if(typeof(d)!="undefined"&&d!==null){for(var a in d){if(d.hasOwnProperty(a)){if(d[a]===null){b[a]=null}else{if(Dygraph.isArrayLike(d[a])){b[a]=d[a].slice()}else{if(c(d[a])){b[a]=d[a]}else{if(typeof(d[a])=="object"){if(typeof(b[a])!="object"){b[a]={}}Dygraph.updateDeep(b[a],d[a])}else{b[a]=d[a]}}}}}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a<c.length;a++){if(Dygraph.isArrayLike(c[a])){b.push(Dygraph.clone(c[a]))}else{b.push(c[a])}}return b};Dygraph.createCanvas=function(){var a=document.createElement("canvas");var b=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(b&&(typeof(G_vmlCanvasManager)!="undefined")){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.isAndroid=function(){return(/Android/).test(navigator.userAgent)};Dygraph.repeatAndCleanup=function(b,g,f,c){var e=0;var d=new Date().getTime();b(e);if(g==1){c();return}(function a(){if(e>=g){return}var h=d+(1+e)*f;setTimeout(function(){e++;b(e);if(e>=g-1){c()}else{a()}},h-new Date().getTime())})()};Dygraph.isPixelChangingOptionList=function(h,e){var d={annotationClickHandler:true,annotationDblClickHandler:true,annotationMouseOutHandler:true,annotationMouseOverHandler:true,axisLabelColor:true,axisLineColor:true,axisLineWidth:true,clickCallback:true,digitsAfterDecimal:true,drawCallback:true,drawPoints:true,drawXGrid:true,drawYGrid: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,maxNumberWidth:true,panEdgeFraction:true,pixelsPerYLabel:true,pointClickCallback:true,pointSize:true,rangeSelectorPlotFillColor:true,rangeSelectorPlotStrokeColor:true,showLabelsOnHighlight:true,showRoller:true,sigFigs:true,strokeWidth:true,underlayCallback:true,unhighlightCallback:true,xAxisLabelFormatter:true,xTicker:true,xValueFormatter:true,yAxisLabelFormatter:true,yValueFormatter:true,zoomCallback:true};var a=false;var b={};if(h){for(var f=1;f<h.length;f++){b[h[f]]=true}}for(var g in e){if(a){break}if(e.hasOwnProperty(g)){if(b[g]){for(var c in e[g]){if(a){break}if(e[g].hasOwnProperty(c)&&!d[c]){a=true}}}else{if(!d[g]){a=true}}}}return a};Dygraph.compareArrays=function(c,b){if(!Dygraph.isArrayLike(c)||!Dygraph.isArrayLike(b)){return false}if(c.length!==b.length){return false}for(var a=0;a<c.length;a++){if(c[a]!==b[a]){return false}}return true};"use strict";Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML="";if(typeof(this.date_graph)!="undefined"){this.date_graph.destroy()}this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var d=this.date_graph.getSelection();if(d<0){return b}var a=this.date_graph.layout_.datasets;for(var c=0;c<a.length;++c){b.push({row:d,column:c+1})}return b};"use strict";Dygraph.Interaction={};Dygraph.Interaction.startPan=function(n,s,c){var q,b;c.isPanning=true;var j=s.xAxisRange();c.dateRange=j[1]-j[0];c.initialLeftmostDate=j[0];c.xUnitsPerPixel=c.dateRange/(s.plotter_.area.w-1);if(s.attr_("panEdgeFraction")){var v=s.width_*s.attr_("panEdgeFraction");var d=s.xAxisExtremes();var h=s.toDomXCoord(d[0])-v;var k=s.toDomXCoord(d[1])+v;var t=s.toDataXCoord(h);var u=s.toDataXCoord(k);c.boundedDates=[t,u];var f=[];var a=s.height_*s.attr_("panEdgeFraction");for(q=0;q<s.axes_.length;q++){b=s.axes_[q];var o=b.extremeRange;var p=s.toDomYCoord(o[0],q)+a;var r=s.toDomYCoord(o[1],q)-a;var m=s.toDataYCoord(p);var e=s.toDataYCoord(r);f[q]=[m,e]}c.boundedValues=f}c.is2DPan=false;for(q=0;q<s.axes_.length;q++){b=s.axes_[q];var l=s.yAxisRange(q);if(b.logscale){b.initialTopValue=Dygraph.log10(l[1]);b.dragValueRange=Dygraph.log10(l[1])-Dygraph.log10(l[0])}else{b.initialTopValue=l[1];b.dragValueRange=l[1]-l[0]}b.unitsPerPixel=b.dragValueRange/(s.plotter_.area.h-1);if(b.valueWindow||b.valueRange){c.is2DPan=true}}};Dygraph.Interaction.movePan=function(b,k,c){c.dragEndX=k.dragGetX_(b,c);c.dragEndY=k.dragGetY_(b,c);var h=c.initialLeftmostDate-(c.dragEndX-c.dragStartX)*c.xUnitsPerPixel;if(c.boundedDates){h=Math.max(h,c.boundedDates[0])}var a=h+c.dateRange;if(c.boundedDates){if(a>c.boundedDates[1]){h=h-(a-c.boundedDates[1]);a=h+c.dateRange}}k.dateWindow_=[h,a];if(c.is2DPan){for(var j=0;j<k.axes_.length;j++){var e=k.axes_[j];var d=c.dragEndY-c.dragStartY;var n=d*e.unitsPerPixel;var f=c.boundedValues?c.boundedValues[j]:null;var l=e.initialTopValue+n;if(f){l=Math.min(l,f[1])}var m=l-e.dragValueRange;if(f){if(m<f[0]){l=l-(m-f[0]);m=l-e.dragValueRange}}if(e.logscale){e.valueWindow=[Math.pow(Dygraph.LOG_SCALE,m),Math.pow(Dygraph.LOG_SCALE,l)]}else{e.valueWindow=[m,l]}}}k.drawGraph_(false)};Dygraph.Interaction.endPan=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}a.isPanning=false;a.is2DPan=false;a.initialLeftmostDate=null;a.dateRange=null;a.valueRange=null;a.boundedDates=null;a.boundedValues=null};Dygraph.Interaction.startZoom=function(c,b,a){a.isZooming=true};Dygraph.Interaction.moveZoom=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragStartX-a.dragEndX);var d=Math.abs(a.dragStartY-a.dragEndY);a.dragDirection=(e<d/2)?Dygraph.VERTICAL:Dygraph.HORIZONTAL;b.drawZoomRect_(a.dragDirection,a.dragStartX,a.dragEndX,a.dragStartY,a.dragEndY,a.prevDragDirection,a.prevEndX,a.prevEndY);a.prevEndX=a.dragEndX;a.prevEndY=a.dragEndY;a.prevDragDirection=a.dragDirection};Dygraph.Interaction.treatMouseOpAsClick=function(f,b,d){var k=f.attr_("clickCallback");var n=f.attr_("pointClickCallback");var j=null;if(n){var l=-1;var m=Number.MAX_VALUE;for(var e=0;e<f.selPoints_.length;e++){var c=f.selPoints_[e];var a=Math.pow(c.canvasx-d.dragEndX,2)+Math.pow(c.canvasy-d.dragEndY,2);if(!isNaN(a)&&(l==-1||a<m)){m=a;l=e}}var h=f.attr_("highlightCircleSize")+2;if(m<=h*h){j=f.selPoints_[l]}}if(j){n(b,j)}if(k){k(b,f.lastx_,f.selPoints_)}};Dygraph.Interaction.endZoom=function(c,b,a){a.isZooming=false;a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}if(e>=10&&a.dragDirection==Dygraph.HORIZONTAL){b.doZoomX_(Math.min(a.dragStartX,a.dragEndX),Math.max(a.dragStartX,a.dragEndX))}else{if(d>=10&&a.dragDirection==Dygraph.VERTICAL){b.doZoomY_(Math.min(a.dragStartY,a.dragEndY),Math.max(a.dragStartY,a.dragEndY))}else{b.clearZoomRect_()}}a.dragStartX=null;a.dragStartY=null};Dygraph.Interaction.defaultModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);if(c.altKey||c.shiftKey){Dygraph.startPan(c,b,a)}else{Dygraph.startZoom(c,b,a)}},mousemove:function(c,b,a){if(a.isZooming){Dygraph.moveZoom(c,b,a)}else{if(a.isPanning){Dygraph.movePan(c,b,a)}}},mouseup:function(c,b,a){if(a.isZooming){Dygraph.endZoom(c,b,a)}else{if(a.isPanning){Dygraph.endPan(c,b,a)}}},mouseout:function(c,b,a){if(a.isZooming){a.dragEndX=null;a.dragEndY=null}},dblclick:function(c,b,a){if(c.altKey||c.shiftKey){return}b.doUnzoom_()}};Dygraph.DEFAULT_ATTRS.interactionModel=Dygraph.Interaction.defaultModel;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(c,b,a){a.initializeMouseDown(c,b,a)},mouseup:function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}}};Dygraph.Interaction.dragIsPanInteractionModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);Dygraph.startPan(c,b,a)},mousemove:function(c,b,a){if(a.isPanning){Dygraph.movePan(c,b,a)}},mouseup:function(c,b,a){if(a.isPanning){Dygraph.endPan(c,b,a)}}};"use strict";var DygraphRangeSelector=function(a){this.isIE_=/MSIE/.test(navigator.userAgent)&&!window.opera;this.isUsingExcanvas_=a.isUsingExcanvas_;this.dygraph_=a;this.createCanvases_();if(this.isUsingExcanvas_){this.createIEPanOverlay_()}this.createZoomHandles_();this.initInteraction_()};DygraphRangeSelector.prototype.addToGraph=function(a,b){this.layout_=b;this.resize_();a.appendChild(this.bgcanvas_);a.appendChild(this.fgcanvas_);a.appendChild(this.leftZoomHandle_);a.appendChild(this.rightZoomHandle_)};DygraphRangeSelector.prototype.renderStaticLayer=function(){this.resize_();this.drawStaticLayer_()};DygraphRangeSelector.prototype.renderInteractiveLayer=function(){if(this.isChangingRange_){return}this.placeZoomHandles_();this.drawInteractiveLayer_()};DygraphRangeSelector.prototype.resize_=function(){function c(d,e){d.style.top=e.y+"px";d.style.left=e.x+"px";d.width=e.w;d.height=e.h;d.style.width=d.width+"px";d.style.height=d.height+"px"}var b=this.layout_.getPlotArea();var a=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize");this.canvasRect_={x:b.x,y:b.y+b.h+a+4,w:b.w,h:this.attr_("rangeSelectorHeight")};c(this.bgcanvas_,this.canvasRect_);c(this.fgcanvas_,this.canvasRect_)};DygraphRangeSelector.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphRangeSelector.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_)};DygraphRangeSelector.prototype.createIEPanOverlay_=function(){this.iePanOverlay_=document.createElement("div");this.iePanOverlay_.style.position="absolute";this.iePanOverlay_.style.backgroundColor="white";this.iePanOverlay_.style.filter="alpha(opacity=0)";this.iePanOverlay_.style.display="none";this.iePanOverlay_.style.cursor="move";this.fgcanvas_.appendChild(this.iePanOverlay_)};DygraphRangeSelector.prototype.createZoomHandles_=function(){var a=new Image();a.className="dygraph-rangesel-zoomhandle";a.style.position="absolute";a.style.zIndex=10;a.style.visibility="hidden";a.style.cursor="col-resize";if(/MSIE 7/.test(navigator.userAgent)){a.width=7;a.height=14;a.style.backgroundColor="white";a.style.border="1px solid #333333"}else{a.width=9;a.height=16;a.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAAzwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7sqSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII="}this.leftZoomHandle_=a;this.rightZoomHandle_=a.cloneNode(false)};DygraphRangeSelector.prototype.initInteraction_=function(){var i=this;var f=this.isIE_?document:window;var k=0;var p=null;var n=false;var c=false;var j,d,m,g,q,e,r,o,l,b,h;j=function(w){var v=i.dygraph_.xAxisExtremes();var t=(v[1]-v[0])/i.canvasRect_.w;var u=v[0]+(w.leftHandlePos-i.canvasRect_.x)*t;var s=v[0]+(w.rightHandlePos-i.canvasRect_.x)*t;return[u,s]};d=function(s){Dygraph.cancelEvent(s);n=true;k=s.screenX;p=s.target?s.target:s.srcElement;Dygraph.addEvent(f,"mousemove",m);Dygraph.addEvent(f,"mouseup",g);i.fgcanvas_.style.cursor="col-resize"};m=function(w){if(!n){return}var t=w.screenX-k;if(Math.abs(t)<4){return}k=w.screenX;var v=i.getZoomHandleStatus_();var s;if(p==i.leftZoomHandle_){s=v.leftHandlePos+t;s=Math.min(s,v.rightHandlePos-p.width-3);s=Math.max(s,i.canvasRect_.x)}else{s=v.rightHandlePos+t;s=Math.min(s,i.canvasRect_.x+i.canvasRect_.w);s=Math.max(s,v.leftHandlePos+p.width+3)}var u=p.width/2;p.style.left=(s-u)+"px";i.drawInteractiveLayer_();if(!i.isUsingExcanvas_){q()}};g=function(s){if(!n){return}n=false;Dygraph.removeEvent(f,"mousemove",m);Dygraph.removeEvent(f,"mouseup",g);i.fgcanvas_.style.cursor="default";if(i.isUsingExcanvas_){q()}};q=function(){try{var t=i.getZoomHandleStatus_();i.isChangingRange_=true;if(!t.isZoomed){i.dygraph_.doUnzoom_()}else{var s=j(t);i.dygraph_.doZoomXDates_(s[0],s[1])}}finally{i.isChangingRange_=false}};e=function(u){if(i.isUsingExcanvas_){return u.srcElement==i.iePanOverlay_}else{var s;if(u.offsetX!=undefined){s=i.canvasRect_.x+u.offsetX}else{s=u.clientX}var t=i.getZoomHandleStatus_();return(s>t.leftHandlePos&&s<t.rightHandlePos)}};r=function(s){if(!c&&e(s)&&i.getZoomHandleStatus_().isZoomed){Dygraph.cancelEvent(s);c=true;k=s.screenX;Dygraph.addEvent(f,"mousemove",o);Dygraph.addEvent(f,"mouseup",l)}};o=function(w){if(!c){return}Dygraph.cancelEvent(w);var t=w.screenX-k;if(Math.abs(t)<4){return}k=w.screenX;var v=i.getZoomHandleStatus_();var y=v.leftHandlePos;var s=v.rightHandlePos;var x=s-y;if(y+t<=i.canvasRect_.x){y=i.canvasRect_.x;s=y+x}else{if(s+t>=i.canvasRect_.x+i.canvasRect_.w){s=i.canvasRect_.x+i.canvasRect_.w;y=s-x}else{y+=t;s+=t}}var u=i.leftZoomHandle_.width/2;i.leftZoomHandle_.style.left=(y-u)+"px";i.rightZoomHandle_.style.left=(s-u)+"px";i.drawInteractiveLayer_();if(!i.isUsingExcanvas_){b()}};l=function(s){if(!c){return}c=false;Dygraph.removeEvent(f,"mousemove",o);Dygraph.removeEvent(f,"mouseup",l);if(i.isUsingExcanvas_){b()}};b=function(){try{i.isChangingRange_=true;i.dygraph_.dateWindow_=j(i.getZoomHandleStatus_());i.dygraph_.drawGraph_(false)}finally{i.isChangingRange_=false}};h=function(s){if(n||c){return}var t=e(s)?"move":"default";if(t!=i.fgcanvas_.style.cursor){i.fgcanvas_.style.cursor=t}};this.dygraph_.attrs_.interactionModel=Dygraph.Interaction.dragIsPanInteractionModel;this.dygraph_.attrs_.panEdgeFraction=0.0001;var a=window.opera?"mousedown":"dragstart";Dygraph.addEvent(this.leftZoomHandle_,a,d);Dygraph.addEvent(this.rightZoomHandle_,a,d);if(this.isUsingExcanvas_){Dygraph.addEvent(this.iePanOverlay_,"mousedown",r)}else{Dygraph.addEvent(this.fgcanvas_,"mousedown",r);Dygraph.addEvent(this.fgcanvas_,"mousemove",h)}};DygraphRangeSelector.prototype.drawStaticLayer_=function(){var a=this.bgcanvas_ctx_;a.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);try{this.drawMiniPlot_()}catch(b){Dygraph.warn(b)}var c=0.5;this.bgcanvas_ctx_.lineWidth=1;a.strokeStyle="gray";a.beginPath();a.moveTo(c,c);a.lineTo(c,this.canvasRect_.h-c);a.lineTo(this.canvasRect_.w-c,this.canvasRect_.h-c);a.lineTo(this.canvasRect_.w-c,c);a.stroke()};DygraphRangeSelector.prototype.drawMiniPlot_=function(){var p=this.attr_("rangeSelectorPlotFillColor");var l=this.attr_("rangeSelectorPlotStrokeColor");if(!p&&!l){return}var m=this.computeCombinedSeriesAndLimits_();var e=m.yMax-m.yMin;var r=this.bgcanvas_ctx_;var f=0.5;var j=this.dygraph_.xAxisExtremes();var b=Math.max(j[1]-j[0],1e-30);var q=(this.canvasRect_.w-f)/b;var o=(this.canvasRect_.h-f)/e;var d=this.canvasRect_.w-f;var h=this.canvasRect_.h-f;r.beginPath();r.moveTo(f,h);for(var g=0;g<m.data.length;g++){var a=m.data[g];var n=(a[0]-j[0])*q;var k=h-(a[1]-m.yMin)*o;if(isFinite(n)&&isFinite(k)){r.lineTo(n,k)}}r.lineTo(d,h);r.closePath();if(p){var c=this.bgcanvas_ctx_.createLinearGradient(0,0,0,h);c.addColorStop(0,"white");c.addColorStop(1,p);this.bgcanvas_ctx_.fillStyle=c;r.fill()}if(l){this.bgcanvas_ctx_.strokeStyle=l;this.bgcanvas_ctx_.lineWidth=1.5;r.stroke()}};DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_=function(){var u=this.dygraph_.rawData_;var t=this.attr_("logscale");var p=[];var c;var g;var f,m;var l;var s,r,q;for(s=0;s<u.length;s++){if(u[s].length>1&&u[s][1]!=null){l=typeof u[s][1]!="number";if(l){c=[];g=[];for(q=0;q<u[s][1].length;q++){c.push(0);g.push(0)}}break}}for(s=0;s<u.length;s++){var h=u[s];var d=h[0];if(l){for(q=0;q<c.length;q++){c[q]=g[q]=0}}else{c=g=0}for(r=1;r<h.length;r++){if(this.dygraph_.visibility()[r-1]){if(l){for(q=0;q<c.length;q++){m=h[r][q];if(m===null||isNaN(m)){continue}c[q]+=m;g[q]++}}else{m=h[r];if(m===null||isNaN(m)){continue}c+=m;g++}}}if(l){for(q=0;q<c.length;q++){c[q]/=g[q]}f=c.slice(0)}else{f=c/g}p.push([d,f])}p=this.dygraph_.rollingAverage(p,this.dygraph_.rollPeriod_);if(typeof p[0][1]!="number"){for(s=0;s<p.length;s++){f=p[s][1];p[s][1]=f[0]}}var a=Number.MAX_VALUE;var b=-Number.MAX_VALUE;for(s=0;s<p.length;s++){f=p[s][1];if(f!==null&&isFinite(f)&&(!t||f>0)){a=Math.min(a,f);b=Math.max(b,f)}}var n=0.25;if(t){b=Dygraph.log10(b);b+=b*n;a=Dygraph.log10(a);for(s=0;s<p.length;s++){p[s][1]=Dygraph.log10(p[s][1])}}else{var e;var o=b-a;if(o<=Number.MIN_VALUE){e=b*n}else{e=o*n}b+=e;a-=e}return{data:p,yMin:a,yMax:b}};DygraphRangeSelector.prototype.placeZoomHandles_=function(){var g=this.dygraph_.xAxisExtremes();var a=this.dygraph_.xAxisRange();var b=g[1]-g[0];var i=Math.max(0,(a[0]-g[0])/b);var e=Math.max(0,(g[1]-a[1])/b);var h=this.canvasRect_.x+this.canvasRect_.w*i;var d=this.canvasRect_.x+this.canvasRect_.w*(1-e);var c=Math.max(this.canvasRect_.y,this.canvasRect_.y+(this.canvasRect_.h-this.leftZoomHandle_.height)/2);var f=this.leftZoomHandle_.width/2;this.leftZoomHandle_.style.left=(h-f)+"px";this.leftZoomHandle_.style.top=c+"px";this.rightZoomHandle_.style.left=(d-f)+"px";this.rightZoomHandle_.style.top=this.leftZoomHandle_.style.top;this.leftZoomHandle_.style.visibility="visible";this.rightZoomHandle_.style.visibility="visible"};DygraphRangeSelector.prototype.drawInteractiveLayer_=function(){var b=this.fgcanvas_ctx_;b.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);var e=1;var d=this.canvasRect_.w-e;var a=this.canvasRect_.h-e;var g=this.getZoomHandleStatus_();b.strokeStyle="black";if(!g.isZoomed){b.beginPath();b.moveTo(e,e);b.lineTo(e,a);b.lineTo(d,a);b.lineTo(d,e);b.stroke();if(this.iePanOverlay_){this.iePanOverlay_.style.display="none"}}else{var f=Math.max(e,g.leftHandlePos-this.canvasRect_.x);var c=Math.min(d,g.rightHandlePos-this.canvasRect_.x);b.fillStyle="rgba(240, 240, 240, 0.6)";b.fillRect(0,0,f,this.canvasRect_.h);b.fillRect(c,0,this.canvasRect_.w-c,this.canvasRect_.h);b.beginPath();b.moveTo(e,e);b.lineTo(f,e);b.lineTo(f,a);b.lineTo(c,a);b.lineTo(c,e);b.lineTo(d,e);b.stroke();if(this.isUsingExcanvas_){this.iePanOverlay_.style.width=(c-f)+"px";this.iePanOverlay_.style.left=f+"px";this.iePanOverlay_.style.height=a+"px";this.iePanOverlay_.style.display="inline"}}};DygraphRangeSelector.prototype.getZoomHandleStatus_=function(){var b=this.leftZoomHandle_.width/2;var c=parseInt(this.leftZoomHandle_.style.left,10)+b;var a=parseInt(this.rightZoomHandle_.style.left,10)+b;return{leftHandlePos:c,rightHandlePos:a,isZoomed:(c-1>this.canvasRect_.x||a+1<this.canvasRect_.x+this.canvasRect_.w)}};"use strict";Dygraph.numericTicks=function(I,H,w,r,d,s){var C=r("pixelsPerLabel");var J=[];var F,D,v,A;if(s){for(F=0;F<s.length;F++){J.push({v:s[F]})}}else{if(r("logscale")){A=Math.floor(w/C);var o=Dygraph.binarySearch(I,Dygraph.PREFERRED_LOG_TICK_VALUES,1);var K=Dygraph.binarySearch(H,Dygraph.PREFERRED_LOG_TICK_VALUES,-1);if(o==-1){o=0}if(K==-1){K=Dygraph.PREFERRED_LOG_TICK_VALUES.length-1}var u=null;if(K-o>=A/4){for(var t=K;t>=o;t--){var p=Dygraph.PREFERRED_LOG_TICK_VALUES[t];var m=Math.log(p/I)/Math.log(H/I)*w;var G={v:p};if(u===null){u={tickValue:p,pixel_coord:m}}else{if(Math.abs(m-u.pixel_coord)>=C){u={tickValue:p,pixel_coord:m}}else{G.label=""}}J.push(G)}J.reverse()}}if(J.length===0){var h=r("labelsKMG2");var q;if(h){q=[1,2,4,8]}else{q=[1,2,5]}var L,z,c;for(F=-10;F<50;F++){var g;if(h){g=Math.pow(16,F)}else{g=Math.pow(10,F)}var f=0;for(D=0;D<q.length;D++){L=g*q[D];z=Math.floor(I/L)*L;c=Math.ceil(H/L)*L;A=Math.abs(c-z)/L;f=w/A;if(f>C){break}}if(f>C){break}}if(z>c){L*=-1}for(F=0;F<A;F++){v=z+F*L;J.push({v:v})}}}var B;var y=[];if(r("labelsKMB")){B=1000;y=["K","M","B","T"]}if(r("labelsKMG2")){if(B){Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!")}B=1024;y=["k","M","G","T"]}var E=r("axisLabelFormatter");for(F=0;F<J.length;F++){if(J[F].label!==undefined){continue}v=J[F].v;var e=Math.abs(v);var l=E(v,0,r,d);if(y.length>0){var x=B*B*B*B;for(D=3;D>=0;D--,x/=B){if(e>=x){l=Dygraph.round_(v/x,r("digitsAfterDecimal"))+y[D];break}}}J[F].label=l}return J};Dygraph.dateTicker=function(e,c,i,g,f,h){var d=Dygraph.pickDateTickGranularity(e,c,i,g);if(d>=0){return Dygraph.getDateAxis(e,c,d,g,f)}else{return[]}};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.WEEKLY=14;Dygraph.MONTHLY=15;Dygraph.QUARTERLY=16;Dygraph.BIANNUAL=17;Dygraph.ANNUAL=18;Dygraph.DECADAL=19;Dygraph.CENTENNIAL=20;Dygraph.NUM_GRANULARITIES=21;Dygraph.SHORT_SPACINGS=[];Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]=1000*1;Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]=1000*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]=1000*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]=1000*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY]=1000*30;Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]=1000*60;Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]=1000*60*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]=1000*60*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]=1000*60*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY]=1000*60*30;Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]=1000*3600;Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]=1000*3600*2;Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]=1000*3600*6;Dygraph.SHORT_SPACINGS[Dygraph.DAILY]=1000*86400;Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]=1000*604800;Dygraph.PREFERRED_LOG_TICK_VALUES=function(){var c=[];for(var b=-39;b<=39;b++){var a=Math.pow(10,b);for(var d=1;d<=9;d++){var e=a*d;c.push(e)}}return c}();Dygraph.pickDateTickGranularity=function(d,c,j,h){var g=h("pixelsPerLabel");for(var f=0;f<Dygraph.NUM_GRANULARITIES;f++){var e=Dygraph.numDateTicks(d,c,f);if(j/e>=g){return f}}return -1};Dygraph.numDateTicks=function(e,b,g){if(g<Dygraph.MONTHLY){var h=Dygraph.SHORT_SPACINGS[g];return Math.floor(0.5+1*(b-e)/h)}else{var f=1;var d=12;if(g==Dygraph.QUARTERLY){d=3}if(g==Dygraph.BIANNUAL){d=2}if(g==Dygraph.ANNUAL){d=1}if(g==Dygraph.DECADAL){d=1;f=10}if(g==Dygraph.CENTENNIAL){d=1;f=100}var c=365.2524*24*3600*1000;var a=1*(b-e)/c;return Math.floor(0.5+1*a*d/f)}};Dygraph.getDateAxis=function(n,h,a,l,w){var u=l("axisLabelFormatter");var z=[];var k;if(a<Dygraph.MONTHLY){var c=Dygraph.SHORT_SPACINGS[a];var v=c/1000;var y=new Date(n);var f;if(v<=60){f=y.getSeconds();y.setSeconds(f-f%v)}else{y.setSeconds(0);v/=60;if(v<=60){f=y.getMinutes();y.setMinutes(f-f%v)}else{y.setMinutes(0);v/=60;if(v<=24){f=y.getHours();y.setHours(f-f%v)}else{y.setHours(0);v/=24;if(v==7){y.setDate(y.getDate()-y.getDay())}}}}n=y.getTime();for(k=n;k<=h;k+=c){z.push({v:k,label:u(new Date(k),a,l,w)})}}else{var e;var o=1;if(a==Dygraph.MONTHLY){e=[0,1,2,3,4,5,6,7,8,9,10,11]}else{if(a==Dygraph.QUARTERLY){e=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){e=[0,6]}else{if(a==Dygraph.ANNUAL){e=[0]}else{if(a==Dygraph.DECADAL){e=[0];o=10}else{if(a==Dygraph.CENTENNIAL){e=[0];o=100}else{Dygraph.warn("Span of dates is too long")}}}}}}var s=new Date(n).getFullYear();var p=new Date(h).getFullYear();var b=Dygraph.zeropad;for(var r=s;r<=p;r++){if(r%o!==0){continue}for(var q=0;q<e.length;q++){var m=r+"/"+b(1+e[q])+"/01";k=Dygraph.dateStrToMillis(m);if(k<n||k>h){continue}z.push({v:k,label:u(new Date(k),a,l,w)})}}}return z};Dygraph.DEFAULT_ATTRS.axes.x.ticker=Dygraph.dateTicker;Dygraph.DEFAULT_ATTRS.axes.y.ticker=Dygraph.numericTicks;Dygraph.DEFAULT_ATTRS.axes.y2.ticker=Dygraph.numericTicks;"use strict";function RGBColor(f){this.ok=false;if(f.charAt(0)=="#"){f=f.substr(1,6)}f=f.replace(/ /g,"");f=f.toLowerCase();var b={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"};for(var g in b){if(f==g){f=b[g]}}var e=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var c=0;c<e.length;c++){var j=e[c].re;var a=e[c].process;var h=j.exec(f);if(h){var d=a(h);this.r=d[0];this.g=d[1];this.b=d[2];this.ok=true}}this.r=(this.r<0||isNaN(this.r))?0:((this.r>255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var l=this.r.toString(16);var k=this.g.toString(16);var i=this.b.toString(16);if(l.length==1){l="0"+l}if(k.length==1){k="0"+k}if(i.length==1){i="0"+i}return"#"+l+k+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)=="undefined"){b=10}for(;parseInt(a,10)<b&&b>1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:"toLocaleString",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:["getHours","0"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(""+c.getFullYear()+"/1/1 GMT");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:["getMinutes","0"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(""+e.getFullYear()+"/1/1")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(b,0)},w:"getDay",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:"getFullYear",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?"-":"+")+a+e},Z:function(a){return a.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(a){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g=="locale"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)=="string"){return c[g]()}else{if(typeof(g)=="function"){return g.call(c,c)}else{if(typeof(g)=="object"&&typeof(g[0])=="string"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};
\ No newline at end of file
deleted file mode 100644 (file)
index 1638ad766459dc92d18e32f6324a4c7d6e7e953b..0000000000000000000000000000000000000000
+++ /dev/null
-/**
- * @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.
- */
-
-/*jshint globalstrict: true */
-/*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;
-  this.datasets = [];
-  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;
-};
-
-DygraphLayout.prototype.attr_ = function(name) {
-  return this.dygraph_.attr_(name);
-};
-
-DygraphLayout.prototype.addDataset = function(setname, set_xy) {
-  this.datasets[setname] = set_xy;
-};
-
-DygraphLayout.prototype.getPlotArea = function() {
-  return this.computePlotArea_();
-};
-
-// Compute the box which the chart should be drawn in. This is the canvas's
-// box, less space needed for axis and chart labels.
-DygraphLayout.prototype.computePlotArea_ = function() {
-  var area = {
-    // TODO(danvk): per-axis setting.
-    x: 0,
-    y: 0
-  };
-  if (this.attr_('drawYAxis')) {
-   area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
-  }
-
-  area.w = this.dygraph_.width_ - area.x - this.attr_('rightGap');
-  area.h = this.dygraph_.height_;
-  if (this.attr_('drawXAxis')) {
-    if (this.attr_('xAxisHeight')) {
-      area.h -= this.attr_('xAxisHeight');
-    } else {
-      area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
-    }
-  }
-
-  // Shrink the drawing area to accomodate additional y-axes.
-  if (this.dygraph_.numAxes() == 2) {
-    // TODO(danvk): per-axis setting.
-    area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
-  } else if (this.dygraph_.numAxes() > 2) {
-    this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
-                        "to use " + this.dygraph_.numAxes() + ")");
-  }
-
-  // Add space for chart labels: title, xlabel and ylabel.
-  if (this.attr_('title')) {
-    area.h -= this.attr_('titleHeight');
-    area.y += this.attr_('titleHeight');
-  }
-  if (this.attr_('xlabel')) {
-    area.h -= this.attr_('xLabelHeight');
-  }
-  if (this.attr_('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.
-  }
-
-  if (this.attr_('y2label')) {
-    // same logic applies here as for ylabel.
-    // TODO(danvk): make yAxisLabelWidth a per-axis property
-  }
-
-  // Add space for range selector, if needed.
-  if (this.attr_('showRangeSelector')) {
-    area.h -= this.attr_('rangeSelectorHeight') + 4;
-  }
-
-  return 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.attr_('xValueParser') || function(x) { return x; };
-  for (var i = 0; i < ann.length; i++) {
-    var a = {};
-    if (!ann[i].xval && !ann[i].x) {
-      this.dygraph_.error("Annotations must have an 'x' property");
-      return;
-    }
-    if (ann[i].icon &&
-        !(ann[i].hasOwnProperty('width') &&
-          ann[i].hasOwnProperty('height'))) {
-      this.dygraph_.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.setDateWindow = function(dateWindow) {
-  this.dateWindow_ = dateWindow;
-};
-
-DygraphLayout.prototype.evaluate = function() {
-  this._evaluateLimits();
-  this._evaluateLineCharts();
-  this._evaluateLineTicks();
-  this._evaluateAnnotations();
-};
-
-DygraphLayout.prototype._evaluateLimits = function() {
-  this.minxval = this.maxxval = null;
-  if (this.dateWindow_) {
-    this.minxval = this.dateWindow_[0];
-    this.maxxval = this.dateWindow_[1];
-  } else {
-    for (var name in this.datasets) {
-      if (!this.datasets.hasOwnProperty(name)) continue;
-      var series = this.datasets[name];
-      if (series.length > 1) {
-        var x1 = series[0][0];
-        if (!this.minxval || x1 < this.minxval) this.minxval = x1;
-
-        var x2 = series[series.length - 1][0];
-        if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
-      }
-    }
-  }
-  this.xrange = this.maxxval - this.minxval;
-  this.xscale = (this.xrange !== 0 ? 1/this.xrange : 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 (axis.g.attr_("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)) {
-        axis.g.error('axis ' + i + ' of graph at ' + axis.g +
-            ' can\'t be displayed in log scale for range [' +
-            axis.minyval + ' - ' + axis.maxyval + ']');
-      }
-    }
-  }
-};
-
-DygraphLayout._calcYNormal = function(axis, value) {
-  if (axis.logscale) {
-    return 1.0 - ((Dygraph.log10(value) - Dygraph.log10(axis.minyval)) * axis.ylogscale);
-  } else {
-    return 1.0 - ((value - axis.minyval) * axis.yscale);
-  }
-};
-
-DygraphLayout.prototype._evaluateLineCharts = function() {
-  // add all the rects
-  this.points = [];
-  // An array to keep track of how many points will be drawn for each set.
-  // This will allow for the canvas renderer to not have to check every point
-  // for every data set since the points are added in order of the sets in
-  // datasets.
-  this.setPointsLengths = [];
-
-  for (var setName in this.datasets) {
-    if (!this.datasets.hasOwnProperty(setName)) continue;
-
-    var dataset = this.datasets[setName];
-    var axis = this.dygraph_.axisPropertiesForSeries(setName);
-
-    var setPointsLength = 0;
-
-    for (var j = 0; j < dataset.length; j++) {
-      var item = dataset[j];
-      var xValue = parseFloat(item[0]);
-      var yValue = parseFloat(item[1]);
-
-      // Range from 0-1 where 0 represents left and 1 represents right.
-      var xNormal = (xValue - this.minxval) * this.xscale;
-      // Range from 0-1 where 0 represents top and 1 represents bottom
-      var yNormal = DygraphLayout._calcYNormal(axis, yValue);
-
-      var point = {
-        // TODO(danvk): here
-        x: xNormal,
-        y: yNormal,
-        xval: xValue,
-        yval: yValue,
-        name: setName
-      };
-      this.points.push(point);
-      setPointsLength += 1;
-    }
-    this.setPointsLengths.push(setPointsLength);
-  }
-};
-
-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.xscale * (tick.v - this.minxval);
-    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]);
-      }
-    }
-  }
-};
-
-
-/**
- * Behaves the same way as PlotKit.Layout, but also copies the errors
- * @private
- */
-DygraphLayout.prototype.evaluateWithError = function() {
-  this.evaluate();
-  if (!(this.attr_('errorBars') || this.attr_('customBars'))) return;
-
-  // Copy over the error terms
-  var i = 0;  // index in this.points
-  for (var setName in this.datasets) {
-    if (!this.datasets.hasOwnProperty(setName)) continue;
-    var j = 0;
-    var dataset = this.datasets[setName];
-    var axis = this.dygraph_.axisPropertiesForSeries(setName);
-    for (j = 0; j < dataset.length; j++, i++) {
-      var item = dataset[j];
-      var xv = parseFloat(item[0]);
-      var yv = parseFloat(item[1]);
-
-      if (xv == this.points[i].xval &&
-          yv == this.points[i].yval) {
-        var errorMinus = parseFloat(item[2]);
-        var errorPlus = parseFloat(item[3]);
-
-        var yv_minus = yv - errorMinus;
-        var yv_plus = yv + errorPlus;
-        this.points[i].y_top = DygraphLayout._calcYNormal(axis, yv_minus);
-        this.points[i].y_bottom = DygraphLayout._calcYNormal(axis, yv_plus);
-      }
-    }
-  }
-};
-
-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 (i = 0; i < this.points.length; i++) {
-    var p = this.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.datasets;
-  this.datasets = [];
-};
-
-/**
- * Return a copy of the point at the indicated index, with its yval unstacked.
- * @param int index of point in layout_.points
- */
-DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
-  var point = this.points[idx];
-
-  // Clone the point since we modify it
-  var unstackedPoint = {};
-  for (var pt in point) {
-    unstackedPoint[pt] = point[pt];
-  }
-
-  if (!this.attr_("stackedGraph")) {
-    return unstackedPoint;
-  }
-
-  // The unstacked yval is equal to the current yval minus the yval of the
-  // next point at the same xval.
-  for (var i = idx+1; i < this.points.length; i++) {
-    if (this.points[i].xval == point.xval) {
-      unstackedPoint.yval -= this.points[i].yval;
-      break;
-    }
-  }
-
-  return unstackedPoint;
-};
-/**
- * @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
- */
-
-/*jshint globalstrict: true */
-/*global Dygraph:false,RGBColor:false */
-"use strict";
-
-
-var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
-  this.dygraph_ = dygraph;
-
-  this.layout = layout;
-  this.element = element;
-  this.elementContext = elementContext;
-  this.container = this.element.parentNode;
-
-  this.height = this.element.height;
-  this.width = this.element.width;
-
-  // --- check whether everything is ok before we return
-  if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element)))
-      throw "Canvas is not supported.";
-
-  // internal state
-  this.xlabels = [];
-  this.ylabels = [];
-  this.annotations = [];
-  this.chartLabels = {};
-
-  this.area = layout.getPlotArea();
-  this.container.style.position = "relative";
-  this.container.style.width = this.width + "px";
-
-  // Set up a clipping area for the canvas (and the interaction canvas).
-  // This ensures that we don't overdraw.
-  if (this.dygraph_.isUsingExcanvas_) {
-    this._createIEClipArea();
-  } else {
-    // 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();
-    }
-  }
-};
-
-DygraphCanvasRenderer.prototype.attr_ = function(x) {
-  return this.dygraph_.attr_(x);
-};
-
-DygraphCanvasRenderer.prototype.clear = function() {
-  var context;
-  if (this.isIE) {
-    // VML takes a while to start up, so we just poll every this.IEDelay
-    try {
-      if (this.clearDelay) {
-        this.clearDelay.cancel();
-        this.clearDelay = null;
-      }
-      context = this.elementContext;
-    }
-    catch (e) {
-      // TODO(danvk): this is broken, since MochiKit.Async is gone.
-      // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
-      // this.clearDelay.addCallback(bind(this.clear, this));
-      return;
-    }
-  }
-
-  context = this.elementContext;
-  context.clearRect(0, 0, this.width, this.height);
-
-  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);
-  removeArray(this.annotations);
-
-  for (var k in this.chartLabels) {
-    if (!this.chartLabels.hasOwnProperty(k)) continue;
-    var el = this.chartLabels[k];
-    if (el.parentNode) el.parentNode.removeChild(el);
-  }
-  this.xlabels = [];
-  this.ylabels = [];
-  this.annotations = [];
-  this.chartLabels = {};
-};
-
-
-DygraphCanvasRenderer.isSupported = function(canvasName) {
-  var canvas = null;
-  try {
-    if (typeof(canvasName) == 'undefined' || canvasName === null) {
-      canvas = document.createElement("canvas");
-    } else {
-      canvas = canvasName;
-    }
-    canvas.getContext("2d");
-  }
-  catch (e) {
-    var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
-    var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
-    if ((!ie) || (ie[1] < 6) || (opera))
-      return false;
-    return true;
-  }
-  return true;
-};
-
-/**
- * @param { [String] } colors Array of color strings. Should have one entry for
- * each series to be rendered.
- */
-DygraphCanvasRenderer.prototype.setColors = function(colors) {
-  this.colorScheme_ = colors;
-};
-
-/**
- * Draw an X/Y grid on top of the existing plot
- */
-DygraphCanvasRenderer.prototype.render = function() {
-  // 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 ctx = this.elementContext;
-  function halfUp(x)  { return Math.round(x) + 0.5; }
-  function halfDown(y){ return Math.round(y) - 0.5; }
-
-  if (this.attr_('underlayCallback')) {
-    // NOTE: we pass the dygraph object to this callback twice to avoid breaking
-    // users who expect a deprecated form of this callback.
-    this.attr_('underlayCallback')(ctx, this.area, this.dygraph_, this.dygraph_);
-  }
-
-  var x, y, i, ticks;
-  if (this.attr_('drawYGrid')) {
-    ticks = this.layout.yticks;
-    // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
-    ctx.save();
-    ctx.strokeStyle = this.attr_('gridLineColor');
-    ctx.lineWidth = this.attr_('gridLineWidth');
-    for (i = 0; i < ticks.length; i++) {
-      // TODO(danvk): allow secondary axes to draw a grid, too.
-      if (ticks[i][0] !== 0) continue;
-      x = halfUp(this.area.x);
-      y = halfDown(this.area.y + ticks[i][1] * this.area.h);
-      ctx.beginPath();
-      ctx.moveTo(x, y);
-      ctx.lineTo(x + this.area.w, y);
-      ctx.closePath();
-      ctx.stroke();
-    }
-  }
-
-  if (this.attr_('drawXGrid')) {
-    ticks = this.layout.xticks;
-    ctx.save();
-    ctx.strokeStyle = this.attr_('gridLineColor');
-    ctx.lineWidth = this.attr_('gridLineWidth');
-    for (i=0; i<ticks.length; i++) {
-      x = halfUp(this.area.x + ticks[i][0] * this.area.w);
-      y = halfDown(this.area.y + this.area.h);
-      ctx.beginPath();
-      ctx.moveTo(x, y);
-      ctx.lineTo(x, this.area.y);
-      ctx.closePath();
-      ctx.stroke();
-    }
-  }
-
-  // Do the ordinary rendering, as before
-  this._renderLineChart();
-  this._renderAxis();
-  this._renderChartLabels();
-  this._renderAnnotations();
-};
-
-DygraphCanvasRenderer.prototype._createIEClipArea = function() {
-  var className = 'dygraph-clip-div';
-  var graphDiv = this.dygraph_.graphDiv;
-
-  // Remove old clip divs.
-  for (var i = graphDiv.childNodes.length-1; i >= 0; i--) {
-    if (graphDiv.childNodes[i].className == className) {
-      graphDiv.removeChild(graphDiv.childNodes[i]);
-    }
-  }
-
-  // Determine background color to give clip divs.
-  var backgroundColor = document.bgColor;
-  var element = this.dygraph_.graphDiv;
-  while (element != document) {
-    var bgcolor = element.currentStyle.backgroundColor;
-    if (bgcolor && bgcolor != 'transparent') {
-      backgroundColor = bgcolor;
-      break;
-    }
-    element = element.parentNode;
-  }
-
-  function createClipDiv(area) {
-    if (area.w === 0 || area.h === 0) {
-      return;
-    }
-    var elem = document.createElement('div');
-    elem.className = className;
-    elem.style.backgroundColor = backgroundColor;
-    elem.style.position = 'absolute';
-    elem.style.left = area.x + 'px';
-    elem.style.top = area.y + 'px';
-    elem.style.width = area.w + 'px';
-    elem.style.height = area.h + 'px';
-    graphDiv.appendChild(elem);
-  }
-
-  var plotArea = this.area;
-  // Left side
-  createClipDiv({
-    x:0, y:0,
-    w:plotArea.x,
-    h:this.height
-  });
-
-  // Top
-  createClipDiv({
-    x: plotArea.x, y: 0,
-    w: this.width - plotArea.x,
-    h: plotArea.y
-  });
-
-  // Right side
-  createClipDiv({
-    x: plotArea.x + plotArea.w, y: 0,
-    w: this.width-plotArea.x - plotArea.w,
-    h: this.height
-  });
-
-  // Bottom
-  createClipDiv({
-    x: plotArea.x,
-    y: plotArea.y + plotArea.h,
-    w: this.width - plotArea.x,
-    h: this.height - plotArea.h - plotArea.y
-  });
-};
-
-DygraphCanvasRenderer.prototype._renderAxis = function() {
-  if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) 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 = this.elementContext;
-
-  var label, x, y, tick, i;
-
-  var labelStyle = {
-    position: "absolute",
-    fontSize: this.attr_('axisLabelFontSize') + "px",
-    zIndex: 10,
-    color: this.attr_('axisLabelColor'),
-    width: this.attr_('axisLabelWidth') + "px",
-    // height: this.attr_('axisLabelFontSize') + 2 + "px",
-    lineHeight: "normal",  // Something other than "normal" line-height screws up label positioning.
-    overflow: "hidden"
-  };
-  var makeDiv = function(txt, axis, prec_axis) {
-    var div = document.createElement("div");
-    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();
-  context.strokeStyle = this.attr_('axisLineColor');
-  context.lineWidth = this.attr_('axisLineWidth');
-
-  if (this.attr_('drawYAxis')) {
-    if (this.layout.yticks && this.layout.yticks.length > 0) {
-      var num_axes = this.dygraph_.numAxes();
-      for (i = 0; i < this.layout.yticks.length; i++) {
-        tick = this.layout.yticks[i];
-        if (typeof(tick) == "function") return;
-        x = this.area.x;
-        var sgn = 1;
-        var prec_axis = 'y1';
-        if (tick[0] == 1) {  // right-side y-axis
-          x = this.area.x + this.area.w;
-          sgn = -1;
-          prec_axis = 'y2';
-        }
-        y = this.area.y + tick[1] * this.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 - this.attr_('axisLabelFontSize') / 2);
-        if (top < 0) top = 0;
-
-        if (top + this.attr_('axisLabelFontSize') + 3 > this.height) {
-          label.style.bottom = "0px";
-        } else {
-          label.style.top = top + "px";
-        }
-        if (tick[0] === 0) {
-          label.style.left = (this.area.x - this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
-          label.style.textAlign = "right";
-        } else if (tick[0] == 1) {
-          label.style.left = (this.area.x + this.area.w +
-                              this.attr_('axisTickSize')) + "px";
-          label.style.textAlign = "left";
-        }
-        label.style.width = this.attr_('yAxisLabelWidth') + "px";
-        this.container.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];
-      var fontSize = this.attr_('axisLabelFontSize');
-      var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
-      if (bottom > this.height - 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.
-    context.beginPath();
-    context.moveTo(halfUp(this.area.x), halfDown(this.area.y));
-    context.lineTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h));
-    context.closePath();
-    context.stroke();
-
-    // if there's a secondary y-axis, draw a vertical line for that, too.
-    if (this.dygraph_.numAxes() == 2) {
-      context.beginPath();
-      context.moveTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y));
-      context.lineTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
-      context.closePath();
-      context.stroke();
-    }
-  }
-
-  if (this.attr_('drawXAxis')) {
-    if (this.layout.xticks) {
-      for (i = 0; i < this.layout.xticks.length; i++) {
-        tick = this.layout.xticks[i];
-        x = this.area.x + tick[0] * this.area.w;
-        y = this.area.y + this.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 + this.attr_('axisTickSize')) + 'px';
-
-        var left = (x - this.attr_('axisLabelWidth')/2);
-        if (left + this.attr_('axisLabelWidth') > this.width) {
-          left = this.width - this.attr_('xAxisLabelWidth');
-          label.style.textAlign = "right";
-        }
-        if (left < 0) {
-          left = 0;
-          label.style.textAlign = "left";
-        }
-
-        label.style.left = left + "px";
-        label.style.width = this.attr_('xAxisLabelWidth') + "px";
-        this.container.appendChild(label);
-        this.xlabels.push(label);
-      }
-    }
-
-    context.beginPath();
-    context.moveTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h));
-    context.lineTo(halfUp(this.area.x + this.area.w), halfDown(this.area.y + this.area.h));
-    context.closePath();
-    context.stroke();
-  }
-
-  context.restore();
-};
-
-
-DygraphCanvasRenderer.prototype._renderChartLabels = function() {
-  var div, class_div;
-
-  // Generate divs for the chart title, xlabel and ylabel.
-  // Space for these divs has already been taken away from the charting area in
-  // the DygraphCanvasRenderer constructor.
-  if (this.attr_('title')) {
-    div = document.createElement("div");
-    div.style.position = 'absolute';
-    div.style.top = '0px';
-    div.style.left = this.area.x + 'px';
-    div.style.width = this.area.w + 'px';
-    div.style.height = this.attr_('titleHeight') + 'px';
-    div.style.textAlign = 'center';
-    div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px';
-    div.style.fontWeight = 'bold';
-    class_div = document.createElement("div");
-    class_div.className = 'dygraph-label dygraph-title';
-    class_div.innerHTML = this.attr_('title');
-    div.appendChild(class_div);
-    this.container.appendChild(div);
-    this.chartLabels.title = div;
-  }
-
-  if (this.attr_('xlabel')) {
-    div = document.createElement("div");
-    div.style.position = 'absolute';
-    div.style.bottom = 0;  // TODO(danvk): this is lazy. Calculate style.top.
-    div.style.left = this.area.x + 'px';
-    div.style.width = this.area.w + 'px';
-    div.style.height = this.attr_('xLabelHeight') + 'px';
-    div.style.textAlign = 'center';
-    div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px';
-
-    class_div = document.createElement("div");
-    class_div.className = 'dygraph-label dygraph-xlabel';
-    class_div.innerHTML = this.attr_('xlabel');
-    div.appendChild(class_div);
-    this.container.appendChild(div);
-    this.chartLabels.xlabel = div;
-  }
-
-  var that = this;
-  function createRotatedDiv(axis, classes, html) {
-    var box = {
-      left: 0,
-      top: that.area.y,
-      width: that.attr_('yLabelWidth'),
-      height: that.area.h
-    };
-    // TODO(danvk): is this outer div actually necessary?
-    div = document.createElement("div");
-    div.style.position = 'absolute';
-    if (axis == 1) {
-      div.style.left = box.left;
-    } else {
-      div.style.right = box.left;
-    }
-    div.style.top = box.top + 'px';
-    div.style.width = box.width + 'px';
-    div.style.height = box.height + 'px';
-    div.style.fontSize = (that.attr_('yLabelWidth') - 2) + 'px';
-
-    var inner_div = document.createElement("div");
-    inner_div.style.position = 'absolute';
-    inner_div.style.width = box.height + 'px';
-    inner_div.style.height = box.width + 'px';
-    inner_div.style.top = (box.height / 2 - box.width / 2) + 'px';
-    inner_div.style.left = (box.width / 2 - box.height / 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
-
-    if (typeof(document.documentMode) !== 'undefined' &&
-        document.documentMode < 9) {
-      // We're dealing w/ an old version of IE, so we have to rotate the text
-      // using a BasicImage transform. This uses a different origin of rotation
-      // than HTML5 rotation (top left of div vs. its center).
-      inner_div.style.filter =
-          'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
-          (axis == 1 ? '3' : '1') + ')';
-      inner_div.style.left = '0px';
-      inner_div.style.top = '0px';
-    }
-
-    class_div = document.createElement("div");
-    class_div.className = classes;
-    class_div.innerHTML = html;
-
-    inner_div.appendChild(class_div);
-    div.appendChild(inner_div);
-    return div;
-  }
-
-  var div;
-  if (this.attr_('ylabel')) {
-    div = createRotatedDiv(1, 'dygraph-label dygraph-ylabel',
-                           this.attr_('ylabel'));
-    this.container.appendChild(div);
-    this.chartLabels.ylabel = div;
-  }
-  if (this.attr_('y2label') && this.dygraph_.numAxes() == 2) {
-    div = createRotatedDiv(2, 'dygraph-label dygraph-y2label',
-                           this.attr_('y2label'));
-    this.container.appendChild(div);
-    this.chartLabels.y2label = div;
-  }
-};
-
-
-DygraphCanvasRenderer.prototype._renderAnnotations = function() {
-  var annotationStyle = {
-    "position": "absolute",
-    "fontSize": this.attr_('axisLabelFontSize') + "px",
-    "zIndex": 10,
-    "overflow": "hidden"
-  };
-
-  var bindEvt = function(eventName, classEventName, p, self) {
-    return function(e) {
-      var a = p.annotation;
-      if (a.hasOwnProperty(eventName)) {
-        a[eventName](a, p, self.dygraph_, e);
-      } else if (self.dygraph_.attr_(classEventName)) {
-        self.dygraph_.attr_(classEventName)(a, p, self.dygraph_,e );
-      }
-    };
-  };
-
-  // Get a list of point with annotations.
-  var points = this.layout.annotated_points;
-  for (var i = 0; i < points.length; i++) {
-    var p = points[i];
-    if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w) {
-      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));
-    }
-    div.style.left = (p.canvasx - width / 2) + "px";
-    if (a.attachAtBottom) {
-      div.style.top = (this.area.h - height - tick_height) + "px";
-    } else {
-      div.style.top = (p.canvasy - height - tick_height) + "px";
-    }
-    div.style.width = width + "px";
-    div.style.height = height + "px";
-    div.title = p.annotation.text;
-    div.style.color = this.colors[p.name];
-    div.style.borderColor = this.colors[p.name];
-    a.div = div;
-
-    Dygraph.addEvent(div, 'click',
-        bindEvt('clickHandler', 'annotationClickHandler', p, this));
-    Dygraph.addEvent(div, 'mouseover',
-        bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this));
-    Dygraph.addEvent(div, 'mouseout',
-        bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this));
-    Dygraph.addEvent(div, 'dblclick',
-        bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this));
-
-    this.container.appendChild(div);
-    this.annotations.push(div);
-
-    var ctx = this.elementContext;
-    ctx.strokeStyle = this.colors[p.name];
-    ctx.beginPath();
-    if (!a.attachAtBottom) {
-      ctx.moveTo(p.canvasx, p.canvasy);
-      ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height);
-    } else {
-      ctx.moveTo(p.canvasx, this.area.h);
-      ctx.lineTo(p.canvasx, this.area.h - 2 - tick_height);
-    }
-    ctx.closePath();
-    ctx.stroke();
-  }
-};
-
-
-/**
- * Actually draw the lines chart, including error bars.
- * TODO(danvk): split this into several smaller functions.
- * @private
- */
-DygraphCanvasRenderer.prototype._renderLineChart = function() {
-  var isNullOrNaN = function(x) {
-    return (x === null || isNaN(x));
-  };
-
-  // TODO(danvk): use this.attr_ for many of these.
-  var context = this.elementContext;
-  var fillAlpha = this.attr_('fillAlpha');
-  var errorBars = this.attr_("errorBars") || this.attr_("customBars");
-  var fillGraph = this.attr_("fillGraph");
-  var stackedGraph = this.attr_("stackedGraph");
-  var stepPlot = this.attr_("stepPlot");
-  var points = this.layout.points;
-  var pointsLength = points.length;
-  var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
-
-  var setNames = [];
-  for (var name in this.layout.datasets) {
-    if (this.layout.datasets.hasOwnProperty(name)) {
-      setNames.push(name);
-    }
-  }
-  var setCount = setNames.length;
-
-  // TODO(danvk): Move this mapping into Dygraph and get it out of here.
-  this.colors = {};
-  for (i = 0; i < setCount; i++) {
-    this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length];
-  }
-
-  // Update Points
-  // TODO(danvk): here
-  for (i = pointsLength; i--;) {
-    point = points[i];
-    point.canvasx = this.area.w * point.x + this.area.x;
-    point.canvasy = this.area.h * point.y + this.area.y;
-  }
-
-  // create paths
-  var ctx = context;
-  if (errorBars) {
-    if (fillGraph) {
-      this.dygraph_.warn("Can't use fillGraph option with error bars");
-    }
-
-    for (i = 0; i < setCount; i++) {
-      setName = setNames[i];
-      axis = this.dygraph_.axisPropertiesForSeries(setName);
-      color = this.colors[setName];
-
-      // setup graphics context
-      ctx.save();
-      prevX = NaN;
-      prevY = NaN;
-      prevYs = [-1, -1];
-      yscale = axis.yscale;
-      // should be same color as the lines but only 15% opaque.
-      rgb = new RGBColor(color);
-      err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
-                            fillAlpha + ')';
-      ctx.fillStyle = err_color;
-      ctx.beginPath();
-      for (j = 0; j < pointsLength; j++) {
-        point = points[j];
-        if (point.name == setName) {
-          if (!Dygraph.isOK(point.y)) {
-            prevX = NaN;
-            continue;
-          }
-
-          // TODO(danvk): here
-          if (stepPlot) {
-            newYs = [ point.y_bottom, point.y_top ];
-            prevY = point.y;
-          } else {
-            newYs = [ point.y_bottom, point.y_top ];
-          }
-          newYs[0] = this.area.h * newYs[0] + this.area.y;
-          newYs[1] = this.area.h * newYs[1] + this.area.y;
-          if (!isNaN(prevX)) {
-            if (stepPlot) {
-              ctx.moveTo(prevX, newYs[0]);
-            } else {
-              ctx.moveTo(prevX, prevYs[0]);
-            }
-            ctx.lineTo(point.canvasx, newYs[0]);
-            ctx.lineTo(point.canvasx, newYs[1]);
-            if (stepPlot) {
-              ctx.lineTo(prevX, newYs[1]);
-            } else {
-              ctx.lineTo(prevX, prevYs[1]);
-            }
-            ctx.closePath();
-          }
-          prevYs = newYs;
-          prevX = point.canvasx;
-        }
-      }
-      ctx.fill();
-    }
-  } else if (fillGraph) {
-    var baseline = [];  // for stacked graphs: baseline for filling
-
-    // process sets in reverse order (needed for stacked graphs)
-    for (i = setCount - 1; i >= 0; i--) {
-      setName = setNames[i];
-      color = this.colors[setName];
-      axis = this.dygraph_.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 = this.area.h * axisY + this.area.y;
-
-      // setup graphics context
-      ctx.save();
-      prevX = NaN;
-      prevYs = [-1, -1];
-      yscale = axis.yscale;
-      // should be same color as the lines but only 15% opaque.
-      rgb = new RGBColor(color);
-      err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
-                            fillAlpha + ')';
-      ctx.fillStyle = err_color;
-      ctx.beginPath();
-      for (j = 0; j < pointsLength; j++) {
-        point = points[j];
-        if (point.name == setName) {
-          if (!Dygraph.isOK(point.y)) {
-            prevX = NaN;
-            continue;
-          }
-          if (stackedGraph) {
-            var lastY = baseline[point.canvasx];
-            if (lastY === undefined) lastY = axisY;
-            baseline[point.canvasx] = point.canvasy;
-            newYs = [ point.canvasy, lastY ];
-          } else {
-            newYs = [ point.canvasy, axisY ];
-          }
-          if (!isNaN(prevX)) {
-            ctx.moveTo(prevX, prevYs[0]);
-            if (stepPlot) {
-              ctx.lineTo(point.canvasx, prevYs[0]);
-            } else {
-              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();
-    }
-  }
-
-  // Drawing the lines.
-  var firstIndexInSet = 0;
-  var afterLastIndexInSet = 0;
-  var setLength = 0;
-  for (i = 0; i < setCount; i += 1) {
-    setLength = this.layout.setPointsLengths[i];
-    afterLastIndexInSet += setLength;
-    setName = setNames[i];
-    color = this.colors[setName];
-    var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
-
-    // setup graphics context
-    context.save();
-    var pointSize = this.dygraph_.attr_("pointSize", setName);
-    prevX = null;
-    prevY = null;
-    var drawPoints = this.dygraph_.attr_("drawPoints", setName);
-    var strokePattern = this.dygraph_.attr_("strokePattern", setName);
-    if (!Dygraph.isArrayLike(strokePattern)) {
-      strokePattern = null;
-    }
-    for (j = firstIndexInSet; j < afterLastIndexInSet; j++) {
-      point = points[j];
-      if (isNullOrNaN(point.canvasy)) {
-        if (stepPlot && prevX !== null) {
-          // Draw a horizontal line to the start of the missing data
-          ctx.beginPath();
-          ctx.strokeStyle = color;
-          ctx.lineWidth = this.attr_('strokeWidth');
-          this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
-          ctx.stroke();
-        }
-        // this will make us move to the next point, not draw a line to it.
-        prevX = prevY = null;
-      } else {
-        // A point is "isolated" if it is non-null but both the previous
-        // and next points are null.
-        var isIsolated = (!prevX && (j == points.length - 1 ||
-                                     isNullOrNaN(points[j+1].canvasy)));
-        if (prevX === null) {
-          prevX = point.canvasx;
-          prevY = point.canvasy;
-        } else {
-          // Skip over points that will be drawn in the same pixel.
-          if (Math.round(prevX) == Math.round(point.canvasx) &&
-              Math.round(prevY) == Math.round(point.canvasy)) {
-            continue;
-          }
-          // TODO(antrob): skip over points that lie on a line that is already
-          // going to be drawn. There is no need to have more than 2
-          // consecutive points that are collinear.
-          if (strokeWidth) {
-            ctx.beginPath();
-            ctx.strokeStyle = color;
-            ctx.lineWidth = strokeWidth;
-            if (stepPlot) {
-              this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
-            }
-            this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
-            prevX = point.canvasx;
-            prevY = point.canvasy;
-            ctx.stroke();
-          }
-        }
-
-        if (drawPoints || isIsolated) {
-          ctx.beginPath();
-          ctx.fillStyle = color;
-          ctx.arc(point.canvasx, point.canvasy, pointSize,
-                  0, 2 * Math.PI, false);
-          ctx.fill();
-        }
-      }
-    }
-    firstIndexInSet = afterLastIndexInSet;
-  }
-
-  context.restore();
-};
-
-/**
- * This does dashed lines onto a canvas for a given pattern. You must call
- * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
- * the state of the line in regards to where we left off on drawing the pattern.
- * You can draw a dashed line in several function calls and the pattern will be
- * continous as long as you didn't call this function with a different pattern
- * in between.
- * @param ctx The canvas 2d context to draw on.
- * @param x The start of the line's x coordinate.
- * @param y The start of the line's y coordinate.
- * @param x2 The end of the line's x coordinate.
- * @param y2 The end of the line's y coordinate.
- * @param pattern The dash pattern to draw, an array of integers where even 
- * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
- * is drawn, 2 is the space between.). A null pattern, array of length one, or
- * empty array will do just a solid line.
- * @private
- */
-DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) {
-  // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
-  // Modified by Russell Valentine to keep line history and continue the pattern
-  // where it left off.
-  var dx, dy, len, rot, patternIndex, segment;
-
-  // If we don't have a pattern or it is an empty array or of size one just
-  // do a solid line.
-  if (!pattern || pattern.length <= 1) {
-    ctx.moveTo(x, y);
-    ctx.lineTo(x2, y2);
-    return;
-  }
-
-  // If we have a different dash pattern than the last time this was called we
-  // reset our dash history and start the pattern from the begging 
-  // regardless of state of the last pattern.
-  if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) {
-    this._dashedLineToHistoryPattern = pattern;
-    this._dashedLineToHistory = [0, 0];
-  }
-  ctx.save();
-
-  // Calculate transformation parameters
-  dx = (x2-x);
-  dy = (y2-y);
-  len = Math.sqrt(dx*dx + dy*dy);
-  rot = Math.atan2(dy, dx);
-
-  // Set transformation
-  ctx.translate(x, y);
-  ctx.moveTo(0, 0);
-  ctx.rotate(rot);
-
-  // Set last pattern index we used for this pattern.
-  patternIndex = this._dashedLineToHistory[0];
-  x = 0;
-  while (len > x) {
-    // Get the length of the pattern segment we are dealing with.
-    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 (this._dashedLineToHistory[1]) {
-      x += this._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.
-      this._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.
-      this._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) {
-      ctx.lineTo(x, 0);
-    } else {
-      ctx.moveTo(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;
-  }
-  ctx.restore();
-};
-/**
- * @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/
-
- */
-
-/*jshint globalstrict: true */
-/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager: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) {
-  if (arguments.length > 0) {
-    if (arguments.length == 4) {
-      // 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.
-      this.warn("Using deprecated four-argument dygraph constructor");
-      this.__old_init__(div, data, arguments[2], arguments[3]);
-    } else {
-      this.__init__(div, data, opts);
-    }
-  }
-};
-
-Dygraph.NAME = "Dygraph";
-Dygraph.VERSION = "1.2";
-Dygraph.__repr__ = function() {
-  return "[" + this.NAME + " " + this.VERSION + "]";
-};
-
-/**
- * Returns information about the Dygraph class.
- */
-Dygraph.toString = function() {
-  return this.__repr__();
-};
-
-// Various default values
-Dygraph.DEFAULT_ROLL_PERIOD = 1;
-Dygraph.DEFAULT_WIDTH = 480;
-Dygraph.DEFAULT_HEIGHT = 320;
-
-Dygraph.ANIMATION_STEPS = 10;
-Dygraph.ANIMATION_DURATION = 200;
-
-// 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
- * @param {String} name The name of the point's data series
- * @param {Dygraph} g The dygraph object
- */
-Dygraph.numberValueFormatter = function(x, opts, pt, g) {
-  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');
-
-  // 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))) {
-    return x.toExponential(digits);
-  } else {
-    return '' + Dygraph.round_(x, digits);
-  }
-};
-
-/**
- * variant for use as an axisLabelFormatter.
- * @private
- */
-Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) {
-  return Dygraph.numberValueFormatter(x, opts, g);
-};
-
-/**
- * Convert a JS date (millis since epoch) to YYYY/MM/DD
- * @param {Number} date The JavaScript date (ms since epoch)
- * @return {String} A date of the form "YYYY/MM/DD"
- * @private
- */
-Dygraph.dateString_ = function(date) {
-  var zeropad = Dygraph.zeropad;
-  var d = new Date(date);
-
-  // Get the year:
-  var year = "" + d.getFullYear();
-  // Get a 0 padded month string
-  var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
-  // Get a 0 padded day string
-  var day = zeropad(d.getDate());
-
-  var ret = "";
-  var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-  if (frac) ret = " " + Dygraph.hmsString_(date);
-
-  return year + "/" + month + "/" + day + ret;
-};
-
-/**
- * Convert a JS date to a string appropriate to display on an axis that
- * is displaying values at the stated granularity.
- * @param {Date} date The date to format
- * @param {Number} granularity One of the Dygraph granularity constants
- * @return {String} The formatted date
- * @private
- */
-Dygraph.dateAxisFormatter = function(date, granularity) {
-  if (granularity >= Dygraph.DECADAL) {
-    return date.strftime('%Y');
-  } else if (granularity >= Dygraph.MONTHLY) {
-    return date.strftime('%b %y');
-  } else {
-    var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
-    if (frac === 0 || granularity >= Dygraph.DAILY) {
-      return new Date(date.getTime() + 3600*1000).strftime('%d%b');
-    } else {
-      return Dygraph.hmsString_(date.getTime());
-    }
-  }
-};
-
-
-// Default attribute values.
-Dygraph.DEFAULT_ATTRS = {
-  highlightCircleSize: 3,
-
-  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,
-
-  axisTickSize: 3,
-  axisLabelFontSize: 14,
-  xAxisLabelWidth: 50,
-  yAxisLabelWidth: 50,
-  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,
-  hideOverlayOnMouseOut: true,
-
-  // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
-  legend: 'onmouseover',  // the only relevant value at the moment is 'always'.
-
-  stepPlot: false,
-  avoidMinZero: false,
-
-  // Sizes of the various chart labels.
-  titleHeight: 28,
-  xLabelHeight: 18,
-  yLabelWidth: 18,
-
-  drawXAxis: true,
-  drawYAxis: true,
-  axisLineColor: "black",
-  axisLineWidth: 0.3,
-  gridLineWidth: 0.3,
-  axisLabelColor: "black",
-  axisLabelFont: "Arial",  // TODO(danvk): is this implemented?
-  axisLabelWidth: 50,
-  drawYGrid: true,
-  drawXGrid: true,
-  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",
-  rangeSelectorPlotFillColor: "#A7B1C4",
-
-  // per-axis options
-  axes: {
-    x: {
-      pixelsPerLabel: 60,
-      axisLabelFormatter: Dygraph.dateAxisFormatter,
-      valueFormatter: Dygraph.dateString_,
-      ticker: null  // will be set in dygraph-tickers.js
-    },
-    y: {
-      pixelsPerLabel: 30,
-      valueFormatter: Dygraph.numberValueFormatter,
-      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
-      ticker: null  // will be set in dygraph-tickers.js
-    },
-    y2: {
-      pixelsPerLabel: 30,
-      valueFormatter: Dygraph.numberValueFormatter,
-      axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
-      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;
-
-// 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) {
-  // Hack for IE: if we're using excanvas and the document hasn't finished
-  // loading yet (and hence may not have initialized whatever it needs to
-  // initialize), then keep calling this routine periodically until it has.
-  if (/MSIE/.test(navigator.userAgent) && !window.opera &&
-      typeof(G_vmlCanvasManager) != 'undefined' &&
-      document.readyState != 'complete') {
-    var self = this;
-    setTimeout(function() { self.__init__(div, file, attrs); }, 100);
-    return;
-  }
-
-  // Support two-argument constructor
-  if (attrs === null || attrs === undefined) { attrs = {}; }
-
-  attrs = Dygraph.mapLegacyOptions_(attrs);
-
-  if (!div) {
-    Dygraph.error("Constructing dygraph with a non-existent div!");
-    return;
-  }
-
-  this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
-
-  // 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.is_initial_draw_ = true;
-  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.
-  this.width_ = div.clientWidth;
-  this.height_ = div.clientHeight;
-
-  // 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.
-  }
-
-  // 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_ = [];
-
-  // Create the containing DIV and other interactive elements
-  this.createInterface_();
-
-  this.start_();
-};
-
-/**
- * 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) 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 (seriesName &&
-      typeof(this.user_attrs_[seriesName]) != 'undefined' &&
-      this.user_attrs_[seriesName] !== null &&
-      typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
-    return this.user_attrs_[seriesName][name];
-  } else if (typeof(this.user_attrs_[name]) != 'undefined') {
-    return this.user_attrs_[name];
-  } else if (typeof(this.attrs_[name]) != 'undefined') {
-    return this.attrs_[name];
-  } else {
-    return null;
-  }
-};
-
-/**
- * @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][opt]) {
-      return axis_opts[axis][opt];
-    }
-    // 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][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 left = this.rawData_[0][0];
-  var right = this.rawData_[this.rawData_.length - 1][0];
-  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();
-  return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
-};
-
-/**
- * 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.axes_[axis].logscale) {
-    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 = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
-    //
-    // Move denominator to both sides:
-    // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
-    //
-    // subtract logr1, and take the negative value.
-    // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
-    //
-    // Swap both sides of the equation, and we can compute the log of the
-    // return value. Which means we just need to use that as the exponent in
-    // e^exponent.
-    // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
-
-    var logr1 = Dygraph.log10(yRange[1]);
-    var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
-    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;
-  if (!this.axes_[axis].logscale) {
-    // 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]);
-  } else {
-    var logr1 = Dygraph.log10(yRange[1]);
-    pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(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();
-  return (x - xRange[0]) / (xRange[1] - xRange[0]);
-};
-
-/**
- * Returns the number of columns (including the independent variable).
- * @return { Integer } The number of columns.
- */
-Dygraph.prototype.numColumns = function() {
-  return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
-};
-
-/**
- * Returns the number of rows (excluding any header/label row).
- * @return { Integer } The number of rows, less any header.
- */
-Dygraph.prototype.numRows = function() {
-  return this.rawData_.length;
-};
-
-/**
- * Returns the full range of the x-axis, as determined by the most extreme
- * values in the data set. Not affected by zooming, visibility, etc.
- * TODO(danvk): merge w/ xAxisExtremes
- * @return { Array<Number> } A [low, high] pair
- * @private
- */
-Dygraph.prototype.fullXRange_ = function() {
-  if (this.numRows() > 0) {
-    return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]];
-  } else {
-    return [0, 1];
-  }
-};
-
-/**
- * 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");
-  this.graphDiv.style.width = this.width_ + "px";
-  this.graphDiv.style.height = this.height_ + "px";
-  enclosing.appendChild(this.graphDiv);
-
-  // Create the canvas for interactive parts of the chart.
-  this.canvas_ = Dygraph.createCanvas();
-  this.canvas_.style.position = "absolute";
-  this.canvas_.width = this.width_;
-  this.canvas_.height = this.height_;
-  this.canvas_.style.width = this.width_ + "px";    // for IE
-  this.canvas_.style.height = this.height_ + "px";  // for IE
-
-  this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
-
-  // ... and for static parts of the chart.
-  this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
-  this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
-
-  if (this.attr_('showRangeSelector')) {
-    // The range selector must be created here so that its canvases and contexts get created here.
-    // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
-    // The range selector also sets xAxisHeight in order to reserve space.
-    this.rangeSelector_ = new DygraphRangeSelector(this);
-  }
-
-  // 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);
-
-  if (this.rangeSelector_) {
-    // This needs to happen after the graph canvases are added to the div and the layout object is created.
-    this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
-  }
-
-  var dygraph = this;
-  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
-    dygraph.mouseMove_(e);
-  });
-  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
-    dygraph.mouseOut_(e);
-  });
-
-  this.createStatusMessage_();
-  this.createDragInterface_();
-
-  this.resizeHandler = function(e) {
-    dygraph.resize();
-  }
-
-  // Update when the window is resized.
-  // TODO(danvk): drop frames depending on complexity of the chart.
-  Dygraph.addEvent(window, 'resize', this.resizeHandler);
-};
-
-/**
- * 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() {
-  var removeRecursive = function(node) {
-    while (node.hasChildNodes()) {
-      removeRecursive(node.firstChild);
-      node.removeChild(node.firstChild);
-    }
-  };
-  removeRecursive(this.maindiv_);
-
-  var nullOut = function(obj) {
-    for (var n in obj) {
-      if (typeof(obj[n]) === 'object') {
-        obj[n] = null;
-      }
-    }
-  };
-  // remove event handlers
-  Dygraph.removeEvent(window,'resize',this.resizeHandler);
-  this.resizeHandler = 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() {
-  if (this.isUsingExcanvas_) {
-    var elem = document.createElement("div");
-    elem.style.position = 'absolute';
-    elem.style.backgroundColor = 'white';
-    elem.style.filter = 'alpha(opacity=0)';
-    elem.style.width = this.width_ + "px";
-    elem.style.height = this.height_ + "px";
-    this.graphDiv.appendChild(elem);
-    return elem;
-  } else {
-    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 num = this.attr_("labels").length - 1;
-  this.colors_ = [];
-  var colors = this.attr_('colors');
-  var i;
-  if (!colors) {
-    var sat = this.attr_('colorSaturation') || 1.0;
-    var val = this.attr_('colorValue') || 0.5;
-    var half = Math.ceil(num / 2);
-    for (i = 1; i <= num; i++) {
-      if (!this.visibility()[i-1]) continue;
-      // alternate colors for high contrast.
-      var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
-      var hue = (1.0 * idx/ (1 + num));
-      this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
-    }
-  } else {
-    for (i = 0; i < num; i++) {
-      if (!this.visibility()[i]) continue;
-      var colorStr = colors[i % colors.length];
-      this.colors_.push(colorStr);
-    }
-  }
-
-  this.plotter_.setColors(this.colors_);
-};
-
-/**
- * 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.
- * @return {Array<string>} The list of colors.
- */
-Dygraph.prototype.getColors = function() {
-  return this.colors_;
-};
-
-/**
- * Create the div that contains information on the selected point(s)
- * This goes in the top right of the canvas, unless an external div has already
- * been specified.
- * @private
- */
-Dygraph.prototype.createStatusMessage_ = function() {
-  var userLabelsDiv = this.user_attrs_.labelsDiv;
-  if (userLabelsDiv && null !== userLabelsDiv &&
-      (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
-    this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv);
-  }
-  if (!this.attr_("labelsDiv")) {
-    var divWidth = this.attr_('labelsDivWidth');
-    var messagestyle = {
-      "position": "absolute",
-      "fontSize": "14px",
-      "zIndex": 10,
-      "width": divWidth + "px",
-      "top": "0px",
-      "left": (this.width_ - divWidth - 2) + "px",
-      "background": "white",
-      "textAlign": "left",
-      "overflow": "hidden"};
-    Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
-    var div = document.createElement("div");
-    div.className = "dygraph-legend";
-    for (var name in messagestyle) {
-      if (messagestyle.hasOwnProperty(name)) {
-        div.style[name] = messagestyle[name];
-      }
-    }
-    this.graphDiv.appendChild(div);
-    this.attrs_.labelsDiv = div;
-  }
-};
-
-/**
- * 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
- */
-Dygraph.prototype.positionLabelsDiv_ = function() {
-  // Don't touch a user-specified labelsDiv.
-  if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
-
-  var area = this.plotter_.area;
-  var div = this.attr_("labelsDiv");
-  div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
-  div.style.top = area.y + "px";
-};
-
-/**
- * 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.attr_('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); };
-};
-
-/**
- * @private
- * Converts page the x-coordinate of the event to pixel x-coordinates on the
- * canvas (i.e. DOM Coords).
- */
-Dygraph.prototype.dragGetX_ = function(e, context) {
-  return Dygraph.pageX(e) - context.px;
-};
-
-/**
- * @private
- * Converts page the y-coordinate of the event to pixel y-coordinates on the
- * canvas (i.e. DOM Coords).
- */
-Dygraph.prototype.dragGetY_ = function(e, context) {
-  return Dygraph.pageY(e) - context.py;
-};
-
-/**
- * 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,
-
-    // 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] ...]
-
-    initializeMouseDown: function(event, g, context) {
-      // prevents mouse drags from selecting page text.
-      if (event.preventDefault) {
-        event.preventDefault();  // Firefox, Chrome, etc.
-      } else {
-        event.returnValue = false;  // IE
-        event.cancelBubble = true;
-      }
-
-      context.px = Dygraph.findPosX(g.canvas_);
-      context.py = Dygraph.findPosY(g.canvas_);
-      context.dragStartX = g.dragGetX_(event, context);
-      context.dragStartY = g.dragGetY_(event, context);
-    }
-  };
-
-  var interactionModel = this.attr_("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;
-    Dygraph.addEvent(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.
-  Dygraph.addEvent(document, 'mouseup', function(event) {
-    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;
-      }
-    }
-  });
-};
-
-/**
- * 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));
-    }
-  }
-
-  if (this.isUsingExcanvas_) {
-    this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0];
-  }
-};
-
-/**
- * Clear the zoom rectangle (and perform no zoom).
- * @private
- */
-Dygraph.prototype.clearZoomRect_ = function() {
-  this.currentZoomRectArgs_ = null;
-  this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.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);
-};
-
-/**
- * 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));
-};
-
-/**
- * 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 yAxisRange is null (i.e. "fit to data", the animation
-  // can produce strange effects. Rather than the y-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.attr_("zoomCallback")) {
-      that.attr_("zoomCallback")(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.attr_("zoomCallback")) {
-      var xRange = that.xAxisRange();
-      that.attr_("zoomCallback")(xRange[0], xRange[1], that.yAxisRanges());
-    }
-  });
-};
-
-/**
- * Reset the zoom to the original view coordinates. This is the same as
- * double-clicking on the graph.
- *
- * @private
- */
-Dygraph.prototype.doUnzoom_ = 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 (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.attr_("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.attr_("zoomCallback")) {
-        this.attr_("zoomCallback")(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[1];
-
-      // 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++) {
-        newValueRanges.push(this.axes_[i].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.attr_("zoomCallback")) {
-            that.attr_("zoomCallback")(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.attr_("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);
-};
-
-/**
- * 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) return;
-
-  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
-
-  var lastx = -1;
-  var i;
-
-  // Loop through all the points and find the date nearest to our current
-  // location.
-  var minDist = 1e+100;
-  var idx = -1;
-  for (i = 0; i < points.length; i++) {
-    var point = points[i];
-    if (point === null) continue;
-    var dist = Math.abs(point.canvasx - canvasx);
-    if (dist > minDist) continue;
-    minDist = dist;
-    idx = i;
-  }
-  if (idx >= 0) lastx = points[idx].xval;
-
-  // Extract the points we've selected
-  this.selPoints_ = [];
-  var l = points.length;
-  if (!this.attr_("stackedGraph")) {
-    for (i = 0; i < l; i++) {
-      if (points[i].xval == lastx) {
-        this.selPoints_.push(points[i]);
-      }
-    }
-  } else {
-    // Need to 'unstack' points starting from the bottom
-    var cumulative_sum = 0;
-    for (i = l - 1; i >= 0; i--) {
-      if (points[i].xval == lastx) {
-        var p = {};  // Clone the point since we modify it
-        for (var k in points[i]) {
-          p[k] = points[i][k];
-        }
-        p.yval -= cumulative_sum;
-        cumulative_sum += p.yval;
-        this.selPoints_.push(p);
-      }
-    }
-    this.selPoints_.reverse();
-  }
-
-  if (this.attr_("highlightCallback")) {
-    var px = this.lastx_;
-    if (px !== null && lastx != px) {
-      // only fire if the selected point has changed.
-      this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
-    }
-  }
-
-  // Save last x position for callbacks.
-  this.lastx_ = lastx;
-
-  this.updateSelection_();
-};
-
-/**
- * Transforms layout_.points index into data row number.
- * @param int layout_.points index
- * @return int row number, or -1 if none could be found.
- * @private
- */
-Dygraph.prototype.idxToRow_ = function(idx) {
-  if (idx < 0) return -1;
-
-  // make sure that you get the boundaryIds record which is also defined (see bug #236)
-  var boundaryIdx = -1;
-  for (var i = 0; i < this.boundaryIds_.length; i++) {
-    if (this.boundaryIds_[i] !== undefined) {
-      boundaryIdx = i;
-      break;
-    }
-  }
-  if (boundaryIdx < 0) return -1;
-  for (var name in this.layout_.datasets) {
-    if (idx < this.layout_.datasets[name].length) {
-      return this.boundaryIds_[boundaryIdx][0] + idx;
-    }
-    idx -= this.layout_.datasets[name].length;
-  }
-  return -1;
-};
-
-/**
- * @private
- * Generates legend html dash for any stroke pattern. It will try to scale the
- * pattern to fit in 1em width. Or if small enough repeat the partern 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.
- */
-Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
-  var dash = "";
-  var i, j, paddingLeft, marginRight;
-  var strokePixelLength = 0, segmentLoop = 0;
-  var normalizedPattern = [];
-  var loop;
-  // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
-  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
-  if(isIE) {
-    return "&mdash;";
-  }
-  if (!strokePattern || strokePattern.length <= 1) {
-    // Solid line
-    dash = "<div style=\"display: inline-block; position: relative; " +
-    "bottom: .5ex; padding-left: 1em; height: 1px; " +
-    "border-bottom: 2px solid " + color + ";\"></div>";
-  } else {
-    // 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.
-    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;
-};
-
-/**
- * @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.
- */
-Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
-  // 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, c, dash, strokePattern;
-  if (typeof(x) === 'undefined') {
-    if (this.attr_('legend') != 'always') return '';
-
-    sepLines = this.attr_('labelsSeparateLines');
-    var labels = this.attr_('labels');
-    html = '';
-    for (i = 1; i < labels.length; i++) {
-      if (!this.visibility()[i - 1]) continue;
-      c = this.plotter_.colors[labels[i]];
-      if (html !== '') html += (sepLines ? '<br/>' : ' ');
-      strokePattern = this.attr_("strokePattern", labels[i]);
-      dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
-      html += "<span style='font-weight: bold; color: " + c + ";'>" + dash + 
-        " " + labels[i] + "</span>";
-    }
-    return html;
-  }
-
-  var xOptView = this.optionsViewForAxis_('x');
-  var xvf = xOptView('valueFormatter');
-  html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":";
-
-  var yOptViews = [];
-  var num_axes = this.numAxes();
-  for (i = 0; i < num_axes; i++) {
-    yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : ''));
-  }
-  var showZeros = this.attr_("labelsShowZeroValues");
-  sepLines = this.attr_("labelsSeparateLines");
-  for (i = 0; i < this.selPoints_.length; i++) {
-    var pt = this.selPoints_[i];
-    if (pt.yval === 0 && !showZeros) continue;
-    if (!Dygraph.isOK(pt.canvasy)) continue;
-    if (sepLines) html += "<br/>";
-
-    var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]];
-    var fmtFunc = yOptView('valueFormatter');
-    c = this.plotter_.colors[pt.name];
-    var yval = fmtFunc(pt.yval, yOptView, pt.name, this);
-
-    // TODO(danvk): use a template string here and make it an attribute.
-    html += " <b><span style='color: " + c + ";'>" + pt.name +
-        "</span></b>:" + yval;
-  }
-  return html;
-};
-
-/**
- * @private
- * Displays information about the selected points in the legend. If there is no
- * selection, the legend will be cleared.
- * @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'.
- */
-Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
-  var labelsDiv = this.attr_("labelsDiv");
-  var sizeSpan = document.createElement('span');
-  // Calculates the width of 1em in pixels for the legend.
-  sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
-  labelsDiv.appendChild(sizeSpan);
-  var oneEmWidth=sizeSpan.offsetWidth;
-
-  var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
-  if (labelsDiv !== null) {
-    labelsDiv.innerHTML = html;
-  } else {
-    if (typeof(this.shown_legend_error_) == 'undefined') {
-      this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
-      this.shown_legend_error_ = true;
-    }
-  }
-};
-
-/**
- * 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() {
-  // Clear the previously drawn vertical, if there is one
-  var i;
-  var ctx = this.canvas_ctx_;
-  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.attr_('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.isUsingExcanvas_ && this.currentZoomRectArgs_) {
-    Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_);
-  }
-
-  if (this.selPoints_.length > 0) {
-    // Set the status message to indicate the selected point(s)
-    if (this.attr_('showLabelsOnHighlight')) {
-      this.setLegendHTML_(this.lastx_, this.selPoints_);
-    }
-
-    // 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.attr_('highlightCircleSize', pt.name);
-      ctx.beginPath();
-      ctx.fillStyle = this.plotter_.colors[pt.name];
-      ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
-      ctx.fill();
-    }
-    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 { Integer } row number that should be highlighted (i.e. appear with
- * hover dots on the chart). Set to false to clear any selection.
- */
-Dygraph.prototype.setSelection = function(row) {
-  // Extract the points we've selected
-  this.selPoints_ = [];
-  var pos = 0;
-
-  if (row !== false) {
-    row = row - this.boundaryIds_[0][0];
-  }
-
-  if (row !== false && row >= 0) {
-    for (var i in this.layout_.datasets) {
-      if (row < this.layout_.datasets[i].length) {
-        var point = this.layout_.points[pos+row];
-
-        if (this.attr_("stackedGraph")) {
-          point = this.layout_.unstackPointAtIndex(pos+row);
-        }
-
-        this.selPoints_.push(point);
-      }
-      pos += this.layout_.datasets[i].length;
-    }
-  }
-
-  if (this.selPoints_.length) {
-    this.lastx_ = this.selPoints_[0].xval;
-    this.updateSelection_();
-  } else {
-    this.clearSelection();
-  }
-
-};
-
-/**
- * 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.attr_("unhighlightCallback")) {
-    this.attr_("unhighlightCallback")(event);
-  }
-
-  if (this.attr_("hideOverlayOnMouseOut")) {
-    this.clearSelection();
-  }
-};
-
-/**
- * Clears the current selection (i.e. points that were highlighted by moving
- * the mouse over the chart).
- */
-Dygraph.prototype.clearSelection = function() {
-  // Get rid of the overlay data
-  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
-  this.setLegendHTML_();
-  this.selPoints_ = [];
-  this.lastx_ = -1;
-};
-
-/**
- * Returns the number of the currently selected row. To get data for this row,
- * you can use the getValue method.
- * @return { Integer } row number, or -1 if nothing is selected
- */
-Dygraph.prototype.getSelection = function() {
-  if (!this.selPoints_ || this.selPoints_.length < 1) {
-    return -1;
-  }
-
-  for (var row=0; row<this.layout_.points.length; row++ ) {
-    if (this.layout_.points[row].x == this.selPoints_[0].x) {
-      return row + this.boundaryIds_[0][0];
-    }
-  }
-  return -1;
-};
-
-/**
- * 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.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.fullXRange_();
-  }
-
-  var xAxisOptionsView = this.optionsViewForAxis_('x');
-  var xTicks = xAxisOptionsView('ticker')(
-      range[0],
-      range[1],
-      this.width_,  // 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);
-};
-
-/**
- * @private
- * Computes the range of the data series (including confidence intervals).
- * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
- * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
- * @return [low, high]
- */
-Dygraph.prototype.extremeValues_ = function(series) {
-  var minY = null, maxY = null, j, y;
-
-  var bars = this.attr_("errorBars") || this.attr_("customBars");
-  if (bars) {
-    // With custom bars, maxY is the max of the high values.
-    for (j = 0; j < series.length; j++) {
-      y = series[j][1][0];
-      if (!y) continue;
-      var low = y - series[j][1][1];
-      var high = y + series[j][1][2];
-      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;
-      }
-    }
-  } else {
-    for (j = 0; j < series.length; 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];
-};
-
-/**
- * @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();
-
-  // TODO(danvk): move more computations out of drawGraph_ and into here.
-  this.computeYAxes_();
-
-  // Create a new plotter.
-  if (this.plotter_) this.plotter_.clear();
-  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_();
-
-  // Same thing applies for the labelsDiv. It's 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.
-  this.positionLabelsDiv_();
-
-  if (this.rangeSelector_) {
-    this.rangeSelector_.renderStaticLayer();
-  }
-
-  // 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 connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
-    var logScale = this.attr_('logscale', i);
-    var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints);
-    series = this.rollingAverage(series, this.rollPeriod_);
-    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);
-};
-
-/**
- * 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.
- *
- * TODO(danvk): make this more of a true function
- * @return [ datasets, seriesExtremes, boundaryIds ]
- * @private
- */
-Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
-  var boundaryIds = [];
-  var cumulative_y = [];  // For stacked series.
-  var datasets = [];
-  var extremes = {};  // series name -> [low, high]
-  var i, j, k;
-
-  // 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;
-  for (i = num_series; i >= 1; i--) {
-    if (!this.visibility()[i - 1]) continue;
-
-    // TODO(danvk): is this copy really necessary?
-    var series = [];
-    for (j = 0; j < rolledSeries[i].length; j++) {
-      series.push(rolledSeries[i][j]);
-    }
-
-    // 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.
-    var bars = this.attr_("errorBars") || this.attr_("customBars");
-    if (dateWindow) {
-      var low = dateWindow[0];
-      var high = dateWindow[1];
-      var pruned = [];
-      // TODO(danvk): do binary search instead of linear search.
-      // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
-      var firstIdx = null, lastIdx = null;
-      for (k = 0; k < series.length; k++) {
-        if (series[k][0] >= low && firstIdx === null) {
-          firstIdx = k;
-        }
-        if (series[k][0] <= high) {
-          lastIdx = k;
-        }
-      }
-      if (firstIdx === null) firstIdx = 0;
-      if (firstIdx > 0) firstIdx--;
-      if (lastIdx === null) lastIdx = series.length - 1;
-      if (lastIdx < series.length - 1) lastIdx++;
-      boundaryIds[i-1] = [firstIdx, lastIdx];
-      for (k = firstIdx; k <= lastIdx; k++) {
-        pruned.push(series[k]);
-      }
-      series = pruned;
-    } else {
-      boundaryIds[i-1] = [0, series.length-1];
-    }
-
-    var seriesExtremes = this.extremeValues_(series);
-
-    if (bars) {
-      for (j=0; j<series.length; j++) {
-        series[j] = [series[j][0],
-                     series[j][1][0],
-                     series[j][1][1],
-                     series[j][1][2]];
-      }
-    } else if (this.attr_("stackedGraph")) {
-      var l = series.length;
-      var actual_y;
-      for (j = 0; j < l; j++) {
-        // If one data set has a NaN, let all subsequent stacked
-        // sets inherit the NaN -- only start at 0 for the first set.
-        var x = series[j][0];
-        if (cumulative_y[x] === undefined) {
-          cumulative_y[x] = 0;
-        }
-
-        actual_y = series[j][1];
-        cumulative_y[x] += actual_y;
-
-        series[j] = [x, cumulative_y[x]];
-
-        if (cumulative_y[x] > seriesExtremes[1]) {
-          seriesExtremes[1] = cumulative_y[x];
-        }
-        if (cumulative_y[x] < seriesExtremes[0]) {
-          seriesExtremes[0] = cumulative_y[x];
-        }
-      }
-    }
-
-    var seriesName = this.attr_("labels")[i];
-    extremes[seriesName] = seriesExtremes;
-    datasets[i] = series;
-  }
-
-  return [ datasets, extremes, 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.
- *
- * clearSelection, when undefined or true, causes this.clearSelection to be
- * called at the end of the draw operation. This should rarely be defined,
- * and never true (that is it should be undefined most of the time, and
- * rarely false.)
- *
- * @private
- */
-Dygraph.prototype.drawGraph_ = function(clearSelection) {
-  var start = new Date();
-
-  if (typeof(clearSelection) === 'undefined') {
-    clearSelection = true;
-  }
-
-  // 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.attr_('highlightCircleSize');
-
-  var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
-  var datasets = packed[0];
-  var extremes = packed[1];
-  this.boundaryIds_ = packed[2];
-
-  for (var i = 1; i < datasets.length; i++) {
-    if (!this.visibility()[i - 1]) continue;
-    this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
-  }
-
-  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.layout_.setDateWindow(this.dateWindow_);
-  this.zoomed_x_ = tmp_zoomed_x;
-  this.layout_.evaluateWithError();
-  this.renderGraph_(is_initial_draw, false);
-
-  if (this.attr_("timingName")) {
-    var end = new Date();
-    if (console) {
-      console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
-    }
-  }
-};
-
-Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
-  this.plotter_.clear();
-  this.plotter_.render();
-  this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
-                                          this.canvas_.height);
-
-  // Generate a static legend before any particular point is selected.
-  this.setLegendHTML_();
-
-  if (!is_initial_draw) {
-    if (clearSelection) {
-      if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
-        // We should select the point nearest the page x/y here, but it's easier
-        // to just clear the selection. This prevents erroneous hover dots from
-        // being displayed.
-        this.clearSelection();
-      } else {
-        this.clearSelection();
-      }
-    }
-  }
-
-  if (this.rangeSelector_) {
-    this.rangeSelector_.renderInteractiveLayer();
-  }
-
-  if (this.attr_("drawCallback") !== null) {
-    this.attr_("drawCallback")(this, is_initial_draw);
-  }
-};
-
-/**
- * @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_ and this.seriesToAxisMap_.
- * axes_ = [ { options } ]
- * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
- *   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 i, valueWindows, seriesName, 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_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
-  this.seriesToAxisMap_ = {};
-
-  // Get a list of series names.
-  var labels = this.attr_("labels");
-  var series = {};
-  for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
-
-  // all options which could be applied per-axis:
-  var axisOptions = [
-    'includeZero',
-    'valueRange',
-    'labelsKMB',
-    'labelsKMG2',
-    'pixelsPerYLabel',
-    'yAxisLabelWidth',
-    'axisLabelFontSize',
-    'axisTickSize',
-    'logscale'
-  ];
-
-  // Copy global axis options over to the first axis.
-  for (i = 0; i < axisOptions.length; i++) {
-    var k = axisOptions[i];
-    v = this.attr_(k);
-    if (v) this.axes_[0][k] = v;
-  }
-
-  // Go through once and add all the axes.
-  for (seriesName in series) {
-    if (!series.hasOwnProperty(seriesName)) continue;
-    axis = this.attr_("axis", seriesName);
-    if (axis === null) {
-      this.seriesToAxisMap_[seriesName] = 0;
-      continue;
-    }
-    if (typeof(axis) == 'object') {
-      // Add a new axis, making a copy of its per-axis options.
-      opts = {};
-      Dygraph.update(opts, this.axes_[0]);
-      Dygraph.update(opts, { valueRange: null });  // shouldn't inherit this.
-      var yAxisId = this.axes_.length;
-      opts.yAxisId = yAxisId;
-      opts.g = this;
-      Dygraph.update(opts, axis);
-      this.axes_.push(opts);
-      this.seriesToAxisMap_[seriesName] = yAxisId;
-    }
-  }
-
-  // Go through one more time and assign series to an axis defined by another
-  // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
-  for (seriesName in series) {
-    if (!series.hasOwnProperty(seriesName)) continue;
-    axis = this.attr_("axis", seriesName);
-    if (typeof(axis) == 'string') {
-      if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
-        this.error("Series " + seriesName + " wants to share a y-axis with " +
-                   "series " + axis + ", which does not define its own axis.");
-        return null;
-      }
-      var idx = this.seriesToAxisMap_[axis];
-      this.seriesToAxisMap_[seriesName] = idx;
-    }
-  }
-
-  if (valueWindows !== undefined) {
-    // Restore valueWindow settings.
-    for (index = 0; index < valueWindows.length; index++) {
-      this.axes_[index].valueWindow = valueWindows[index];
-    }
-  }
-
-  // New axes options
-  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() {
-  var last_axis = 0;
-  for (var series in this.seriesToAxisMap_) {
-    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
-    var idx = this.seriesToAxisMap_[series];
-    if (idx > last_axis) last_axis = idx;
-  }
-  return 1 + last_axis;
-};
-
-/**
- * @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.seriesToAxisMap_[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) {
-  // Build a map from axis number -> [list of series names]
-  var seriesForAxis = [], series;
-  for (series in this.seriesToAxisMap_) {
-    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
-    var idx = this.seriesToAxisMap_[series];
-    while (seriesForAxis.length <= idx) seriesForAxis.push([]);
-    seriesForAxis[idx].push(series);
-  }
-
-  // Compute extreme values, a span and tick marks for each axis.
-  for (var i = 0; i < this.axes_.length; i++) {
-    var axis = this.axes_[i];
-
-    if (!seriesForAxis[i]) {
-      // If no series are defined or visible then use a reasonable default
-      axis.extremeRange = [0, 1];
-    } else {
-      // Calculate the extremes of extremes.
-      series = seriesForAxis[i];
-      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);
-        }
-      }
-      if (axis.includeZero && minY > 0) minY = 0;
-
-      // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
-      if (minY == Infinity) minY = 0;
-      if (maxY == -Infinity) maxY = 1;
-
-      // Add some padding and round up to an integer to be human-friendly.
-      var span = maxY - minY;
-      // special case: if we have no sense of scale, use +/-10% of the sole value.
-      if (span === 0) { span = maxY; }
-
-      var maxAxisY, minAxisY;
-      if (axis.logscale) {
-        maxAxisY = maxY + 0.1 * span;
-        minAxisY = minY;
-      } else {
-        maxAxisY = maxY + 0.1 * span;
-        minAxisY = minY - 0.1 * span;
-
-        // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
-        if (!this.attr_("avoidMinZero")) {
-          if (minAxisY < 0 && minY >= 0) minAxisY = 0;
-          if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
-        }
-
-        if (this.attr_("includeZero")) {
-          if (maxY < 0) maxAxisY = 0;
-          if (minY > 0) minAxisY = 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.
-      axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
-    } else {
-      axis.computedValueRange = axis.extremeRange;
-    }
-
-    // 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.
-    var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
-    var ticker = opts('ticker');
-    if (i === 0 || axis.independentTicks) {
-      axis.ticks = ticker(axis.computedValueRange[0],
-                          axis.computedValueRange[1],
-                          this.height_,  // TODO(danvk): should be area.height
-                          opts,
-                          this);
-    } else {
-      var p_axis = this.axes_[0];
-      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.height_,  // TODO(danvk): should be area.height
-                          opts,
-                          this,
-                          tick_values);
-    }
-  }
-};
-
-/**
- * Extracts one series from the raw data (a 2D array) into an array of (date,
- * value) tuples.
- *
- * This is where undesirable points (i.e. negative values on log scales and
- * missing values through which we wish to connect lines) are dropped.
- * 
- * @private
- */
-Dygraph.prototype.extractSeries_ = function(rawData, i, logScale, connectSeparatedPoints) {
-  var series = [];
-  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. Note that this ignores
-      // connectSeparatedPoints.
-      if (point <= 0) {
-        point = null;
-      }
-      series.push([x, point]);
-    } else {
-      if (point !== null || !connectSeparatedPoints) {
-        series.push([x, point]);
-      }
-    }
-  }
-  return series;
-};
-
-/**
- * @private
- * Calculates the rolling average of a data set.
- * If originalData is [label, val], rolls the average of those.
- * If originalData is [label, [, it's interpreted as [value, stddev]
- *   and the roll is returned in the same form, with appropriately reduced
- *   stddev for each value.
- * Note that this is where fractional input (i.e. '5/10') is converted into
- *   decimal values.
- * @param {Array} originalData The data in the appropriate format (see above)
- * @param {Number} rollPeriod The number of points over which to average the
- *                            data
- */
-Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
-  if (originalData.length < 2)
-    return originalData;
-  rollPeriod = Math.min(rollPeriod, originalData.length);
-  var rollingData = [];
-  var sigma = this.attr_("sigma");
-
-  var low, high, i, j, y, sum, num_ok, stddev;
-  if (this.fractions_) {
-    var num = 0;
-    var den = 0;  // numerator/denominator
-    var mult = 100.0;
-    for (i = 0; i < originalData.length; i++) {
-      num += originalData[i][1][0];
-      den += originalData[i][1][1];
-      if (i - rollPeriod >= 0) {
-        num -= originalData[i - rollPeriod][1][0];
-        den -= originalData[i - rollPeriod][1][1];
-      }
-
-      var date = originalData[i][0];
-      var value = den ? num / den : 0.0;
-      if (this.attr_("errorBars")) {
-        if (this.attr_("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, (p - low) * mult, (high - p) * 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 * stddev, mult * stddev]];
-        }
-      } else {
-        rollingData[i] = [date, mult * value];
-      }
-    }
-  } else if (this.attr_("customBars")) {
-    low = 0;
-    var mid = 0;
-    high = 0;
-    var count = 0;
-    for (i = 0; i < originalData.length; i++) {
-      var data = originalData[i][1];
-      y = data[1];
-      rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
-
-      if (y !== null && !isNaN(y)) {
-        low += data[0];
-        mid += y;
-        high += data[2];
-        count += 1;
-      }
-      if (i - rollPeriod >= 0) {
-        var prev = originalData[i - rollPeriod];
-        if (prev[1][1] !== null && !isNaN(prev[1][1])) {
-          low -= prev[1][0];
-          mid -= prev[1][1];
-          high -= prev[1][2];
-          count -= 1;
-        }
-      }
-      if (count) {
-        rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
-                                                1.0 * (mid - low) / count,
-                                                1.0 * (high - mid) / count ]];
-      } else {
-        rollingData[i] = [originalData[i][0], [null, null, null]];
-      }
-    }
-  } else {
-    // 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 (!this.attr_("errorBars")){
-      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];
-        }
-      }
-
-    } else {
-      for (i = 0; i < originalData.length; i++) {
-        sum = 0;
-        var variance = 0;
-        num_ok = 0;
-        for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
-          y = originalData[j][1][0];
-          if (y === null || isNaN(y)) continue;
-          num_ok++;
-          sum += originalData[j][1][0];
-          variance += Math.pow(originalData[j][1][1], 2);
-        }
-        if (num_ok) {
-          stddev = Math.sqrt(variance) / num_ok;
-          rollingData[i] = [originalData[i][0],
-                            [sum / num_ok, sigma * stddev, sigma * stddev]];
-        } else {
-          rollingData[i] = [originalData[i][0], [null, null, null]];
-        }
-      }
-    }
-  }
-
-  return rollingData;
-};
-
-/**
- * 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;
-  }
-
-  if (isDate) {
-    this.attrs_.xValueParser = Dygraph.dateParser;
-    this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
-    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
-  } 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;
-  }
-};
-
-/**
- * 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.
- * @private
- */
-
-// Parse the x as a float or return null if it's not a number.
-Dygraph.prototype.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 !== null && opt_line_no !== null) {
-    msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
-  }
-  this.error(msg);
-
-  return null;
-};
-
-/**
- * @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 lines = data.split("\n");
-  var vals, j;
-
-  // Use the default delimiter or fall back to a tab if that makes sense.
-  var delim = this.attr_('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_.
-  }
-  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.attr_("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) {
-          this.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] = [this.parseFloat_(vals[0], i, line),
-                       this.parseFloat_(vals[1], i, line)];
-        }
-      }
-    } else if (this.attr_("errorBars")) {
-      // If there are error bars, values are (value, stddev) pairs
-      if (inFields.length % 2 != 1) {
-        this.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] = [this.parseFloat_(inFields[j], i, line),
-                               this.parseFloat_(inFields[j + 1], i, line)];
-      }
-    } else if (this.attr_("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] = [ this.parseFloat_(vals[0], i, line),
-                          this.parseFloat_(vals[1], i, line),
-                          this.parseFloat_(vals[2], i, line) ];
-          } else {
-            this.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] = this.parseFloat_(inFields[j], i, line);
-      }
-    }
-    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
-      outOfOrder = true;
-    }
-
-    if (fields.length != expectedCols) {
-      this.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) {
-        this.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) {
-    this.warn("CSV is out of order; order it correctly to speed loading.");
-    ret.sort(function(a,b) { return a[0] - b[0]; });
-  }
-
-  return ret;
-};
-
-/**
- * @private
- * 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 {[Object]} data
- * @return {[Object]} data with numeric x values.
- */
-Dygraph.prototype.parseArray_ = function(data) {
-  // Peek at the first x value to see if it's numeric.
-  if (data.length === 0) {
-    this.error("Can't plot empty data set");
-    return null;
-  }
-  if (data[0].length === 0) {
-    this.error("Data set cannot contain an empty row");
-    return null;
-  }
-
-  var i;
-  if (this.attr_("labels") === null) {
-    this.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);
-    }
-  }
-
-  if (Dygraph.isDateLike(data[0][0])) {
-    // Some intelligent defaults for a date x-axis.
-    this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
-    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
-
-    // Assume they're all dates.
-    var parsedData = Dygraph.clone(data);
-    for (i = 0; i < data.length; i++) {
-      if (parsedData[i].length === 0) {
-        this.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())) {
-        this.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.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
-    this.attrs_.axes.x.ticker = Dygraph.numericTicks;
-    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 {[Object]} 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.dateString_;
-    this.attrs_.axes.x.ticker = Dygraph.dateTicker;
-    this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
-  } 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 {
-    this.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.attr_('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 {
-      this.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.attr_("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) {
-      this.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.attr_("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) {
-    this.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);
-  }
-};
-
-/**
- * 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.predraw_();
-  } else if (typeof data == 'object' &&
-             typeof data.getColumnRange == 'function') {
-    // must be a DataTable from gviz.
-    this.parseDataTable_(data);
-    this.predraw_();
-  } else if (typeof data == 'string') {
-    // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
-    if (data.indexOf('\n') >= 0) {
-      this.loadedEvent_(data);
-    } else {
-      var req = new XMLHttpRequest();
-      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 {
-    this.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} 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;
-
-  // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
-  var file = input_attrs.file;
-  var attrs = Dygraph.mapLegacyOptions_(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);
-
-  if (file) {
-    this.file_ = file;
-    if (!block_redraw) this.start_();
-  } else {
-    if (!block_redraw) {
-      if (requiresNewPoints) {
-        this.predraw_();
-      } else {
-        this.renderGraph_(false, false);
-      }
-    }
-  }
-};
-
-/**
- * Returns a copy of the options with deprecated names converted into current
- * names. Also drops the (potentially-large) 'file' attribute. If the caller is
- * interested in that, they should save a copy before calling this.
- * @private
- */
-Dygraph.mapLegacyOptions_ = function(attrs) {
-  var my_attrs = {};
-  for (var k in attrs) {
-    if (k == 'file') continue;
-    if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
-  }
-
-  var set = function(axis, opt, value) {
-    if (!my_attrs.axes) my_attrs.axes = {};
-    if (!my_attrs.axes[axis]) my_attrs.axes[axis] = {};
-    my_attrs.axes[axis][opt] = value;
-  };
-  var map = function(opt, axis, new_opt) {
-    if (typeof(attrs[opt]) != 'undefined') {
-      set(axis, new_opt, attrs[opt]);
-      delete my_attrs[opt];
-    }
-  };
-
-  // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
-  map('xValueFormatter', 'x', 'valueFormatter');
-  map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
-  map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
-  map('xTicker', 'x', 'ticker');
-  map('yValueFormatter', 'y', 'valueFormatter');
-  map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
-  map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
-  map('yTicker', 'y', 'ticker');
-  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)) {
-    this.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_) {
-    // TODO(danvk): there should be a clear() method.
-    this.maindiv_.innerHTML = "";
-    this.roller_ = null;
-    this.attrs_.labelsDiv = null;
-    this.createInterface_();
-    if (this.annotations_.length) {
-      // createInterface_ reset the layout, so we need to do this.
-      this.layout_.setAnnotations(this.annotations_);
-    }
-    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.attr_("visibility")) {
-    this.attrs_.visibility = [];
-  }
-  // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
-  while (this.attr_("visibility").length < this.numColumns() - 1) {
-    this.attrs_.visibility.push(true);
-  }
-  return this.attr_("visibility");
-};
-
-/**
- * Changes the visiblity of a series.
- */
-Dygraph.prototype.setVisibility = function(num, value) {
-  var x = this.visibility();
-  if (num < 0 || num >= x.length) {
-    this.warn("invalid series number in setVisibility: " + num);
-  } else {
-    x[num] = 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;
-  this.layout_.setAnnotations(this.annotations_);
-  if (!suppressDraw) {
-    this.predraw_();
-  }
-};
-
-/**
- * Return the list of annotations.
- */
-Dygraph.prototype.annotations = function() {
-  return this.annotations_;
-};
-
-/**
- * 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) {
-  var labels = this.attr_("labels");
-  for (var i = 0; i < labels.length; i++) {
-    if (labels[i] == name) return i;
-  }
-  return null;
-};
-
-/**
- * @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() {
-  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.
-    }
-  }
-
-  this.warn("Unable to add default annotation CSS rule; display may be off.");
-};
-
-// Older pages may still use this name.
-var DateGraph = Dygraph;
-/**
- * @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.
- */
-
-/*jshint globalstrict: true */
-/*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */
-"use strict";
-
-Dygraph.LOG_SCALE = 10;
-Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
-
-/** @private */
-Dygraph.log10 = function(x) {
-  return Math.log(x) / Dygraph.LN_TEN;
-};
-
-// Various logging levels.
-Dygraph.DEBUG = 1;
-Dygraph.INFO = 2;
-Dygraph.WARNING = 3;
-Dygraph.ERROR = 3;
-
-// Set this to log stack traces on warnings, etc.
-// This requires stacktrace.js, which is up to you to provide.
-// A copy can be found in the dygraphs repo, or at
-// https://github.com/eriwen/javascript-stacktrace
-Dygraph.LOG_STACK_TRACES = false;
-
-/** 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];
-
-/**
- * @private
- * Log an error on the JS console at the given severity.
- * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
- * @param { String } The message to log.
- */
-Dygraph.log = function(severity, message) {
-  var st;
-  if (typeof(printStackTrace) != 'undefined') {
-    // Remove uninteresting bits: logging functions and paths.
-    st = printStackTrace({guess:false});
-    while (st[0].indexOf("stacktrace") != -1) {
-      st.splice(0, 1);
-    }
-
-    st.splice(0, 2);
-    for (var i = 0; i < st.length; i++) {
-      st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1')
-          .replace(/\@.*\/([^\/]*)/, '@$1')
-          .replace('[object Object].', '');
-    }
-    var top_msg = st.splice(0, 1)[0];
-    message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')';
-  }
-
-  if (typeof(console) != 'undefined') {
-    switch (severity) {
-      case Dygraph.DEBUG:
-        console.debug('dygraphs: ' + message);
-        break;
-      case Dygraph.INFO:
-        console.info('dygraphs: ' + message);
-        break;
-      case Dygraph.WARNING:
-        console.warn('dygraphs: ' + message);
-        break;
-      case Dygraph.ERROR:
-        console.error('dygraphs: ' + message);
-        break;
-    }
-  }
-
-  if (Dygraph.LOG_STACK_TRACES) {
-    console.log(st.join('\n'));
-  }
-};
-
-/** @private */
-Dygraph.info = function(message) {
-  Dygraph.log(Dygraph.INFO, message);
-};
-/** @private */
-Dygraph.prototype.info = Dygraph.info;
-
-/** @private */
-Dygraph.warn = function(message) {
-  Dygraph.log(Dygraph.WARNING, message);
-};
-/** @private */
-Dygraph.prototype.warn = Dygraph.warn;
-
-/** @private */
-Dygraph.error = function(message) {
-  Dygraph.log(Dygraph.ERROR, message);
-};
-/** @private */
-Dygraph.prototype.error = Dygraph.error;
-
-/**
- * @private
- * 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);
- * };
- */
-Dygraph.getContext = function(canvas) {
-  return canvas.getContext("2d");
-};
-
-/**
- * @private
- * Add an event handler. This smooths a difference between IE and the rest of
- * the world.
- * @param { DOM element } elem The element to add the event to.
- * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
- * @param { Function } fn The function to call on the event. The function takes
- * one parameter: the event object.
- */
-Dygraph.addEvent = function addEvent(elem, type, fn) {
-  if (elem.addEventListener) {
-    elem.addEventListener(type, fn, false);
-  } else {
-    elem[type+fn] = function(){fn(window.event);};
-    elem.attachEvent('on'+type, elem[type+fn]);
-  }
-};
-
-/**
- * @private
- * Remove an event handler. This smooths a difference between IE and the rest of
- * the world.
- * @param { DOM element } elem The element to add the event to.
- * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
- * @param { Function } fn The function to call on the event. The function takes
- * one parameter: the event object.
- */
-Dygraph.removeEvent = function addEvent(elem, type, fn) {
-  if (elem.removeEventListener) {
-    elem.removeEventListener(type, fn, false);
-  } else {
-    elem.detachEvent('on'+type, elem[type+fn]);
-    elem[type+fn] = null;
-  }
-};
-
-/**
- * @private
- * 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.
- */
-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 x-coordinate of the supplied object relative to the left side
- * of the page.
- * @private
- */
-Dygraph.findPosX = function(obj) {
-  var curleft = 0;
-  if(obj.offsetParent) {
-    var copyObj = obj;
-    while(1) {
-      curleft += copyObj.offsetLeft;
-      if(!copyObj.offsetParent) {
-        break;
-      }
-      copyObj = copyObj.offsetParent;
-    }
-  } else if(obj.x) {
-    curleft += obj.x;
-  }
-  // This handles the case where the object is inside a scrolled div.
-  while(obj && obj != document.body) {
-    curleft -= obj.scrollLeft;
-    obj = obj.parentNode;
-  }
-  return curleft;
-};
-
-/**
- * Find the y-coordinate of the supplied object relative to the top of the
- * page.
- * @private
- */
-Dygraph.findPosY = function(obj) {
-  var curtop = 0;
-  if(obj.offsetParent) {
-    var copyObj = obj;
-    while(1) {
-      curtop += copyObj.offsetTop;
-      if(!copyObj.offsetParent) {
-        break;
-      }
-      copyObj = copyObj.offsetParent;
-    }
-  } else if(obj.y) {
-    curtop += obj.y;
-  }
-  // This handles the case where the object is inside a scrolled div.
-  while(obj && obj != document.body) {
-    curtop -= obj.scrollTop;
-    obj = obj.parentNode;
-  }
-  return curtop;
-};
-
-/**
- * @private
- * 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
- */
-Dygraph.pageX = function(e) {
-  if (e.pageX) {
-    return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
-  } else {
-    var de = document;
-    var b = document.body;
-    return e.clientX +
-        (de.scrollLeft || b.scrollLeft) -
-        (de.clientLeft || 0);
-  }
-};
-
-/**
- * @private
- * 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
- */
-Dygraph.pageY = function(e) {
-  if (e.pageY) {
-    return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
-  } else {
-    var de = document;
-    var b = document.body;
-    return e.clientY +
-        (de.scrollTop || b.scrollTop) -
-        (de.clientTop || 0);
-  }
-};
-
-/**
- * @private
- * @param { Number } x The number to consider.
- * @return { Boolean } Whether the number is zero or NaN.
- */
-// TODO(danvk): rename this function to something like 'isNonZeroNan'.
-// TODO(danvk): determine when else this returns false (e.g. for undefined or null)
-Dygraph.isOK = function(x) {
-  return x && !isNaN(x);
-};
-
-/**
- * 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);
-};
-
-/**
- * @private
- * Converts '9' to '09' (useful for dates)
- */
-Dygraph.zeropad = function(x) {
-  if (x < 10) return "0" + x; else return "" + x;
-};
-
-/**
- * Return a string version of the hours, minutes and seconds portion of a date.
- * @param {Number} date The JavaScript date (ms since epoch)
- * @return {String} A time of the form "HH:MM:SS"
- * @private
- */
-Dygraph.hmsString_ = function(date) {
-  var zeropad = Dygraph.zeropad;
-  var d = new Date(date);
-  if (d.getSeconds()) {
-    return zeropad(d.getHours()) + ":" +
-           zeropad(d.getMinutes()) + ":" +
-           zeropad(d.getSeconds());
-  } else {
-    return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
-  }
-};
-
-/**
- * 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;
-};
-
-/**
- * @private
- * Implementation of binary search over an array.
- * Currently does not work when val is outside the range of arry's values.
- * @param { Integer } val the value to search for
- * @param { Integer[] } arry is the value over which to search
- * @param { Integer } 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 { Integer } [low] The first index in arry to consider (optional)
- * @param { Integer } [high] The last index in arry to consider (optional)
- */
-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];
-  if (element == val) {
-    return mid;
-  }
-
-  var idx;
-  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);
-  }
-  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);
-  }
-};
-
-/**
- * @private
- * 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} A date in YYYYMMDD format.
- * @return {Number} Milliseconds since epoch.
- */
-Dygraph.dateParser = function(dateStr) {
-  var dateStrSlashed;
-  var d;
-
-  // Let the system try the format first.
-  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)) {
-    Dygraph.error("Couldn't parse " + dateStr + " as a date");
-  }
-  return d;
-};
-
-/**
- * @private
- * 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 { Integer } millis since epoch
- */
-Dygraph.dateStrToMillis = function(str) {
-  return new Date(str).getTime();
-};
-
-// These functions are all based on MochiKit.
-/**
- * Copies all the properties from o to self.
- *
- * @private
- */
-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.
- *
- * @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] = {};
-          }
-          Dygraph.updateDeep(self[k], o[k]);
-        } else {
-          self[k] = o[k];
-        }
-      }
-    }
-  }
-  return self;
-};
-
-/**
- * @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;
-};
-
-/**
- * @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.
- * @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;
-};
-
-/**
- * @private
- * Create a new canvas element. This is more complex than a simple
- * document.createElement("canvas") because of IE and excanvas.
- */
-Dygraph.createCanvas = function() {
-  var canvas = document.createElement("canvas");
-
-  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
-  if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
-    canvas = G_vmlCanvasManager.initElement(canvas);
-  }
-
-  return canvas;
-};
-
-/**
- * @private
- * Checks whether the user is on an Android browser.
- * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
- */
-Dygraph.isAndroid = function() {
-  return (/Android/).test(navigator.userAgent);
-};
-
-/**
- * @private
- * Call a function N times at a given interval, then call a cleanup function
- * once. repeat_fn is called once immediately, then (times - 1) times
- * asynchronously. If times=1, then cleanup_fn() is also called synchronously.
- * @param repeat_fn {Function} Called repeatedly -- takes the number of calls
- * (from 0 to times-1) as an argument.
- * @param times {number} The number of times to call repeat_fn
- * @param every_ms {number} Milliseconds between calls
- * @param cleanup_fn {Function} A function to call after all repeat_fn calls.
- * @private
- */
-Dygraph.repeatAndCleanup = function(repeat_fn, times, every_ms, cleanup_fn) {
-  var count = 0;
-  var start_time = new Date().getTime();
-  repeat_fn(count);
-  if (times == 1) {
-    cleanup_fn();
-    return;
-  }
-
-  (function loop() {
-    if (count >= times) return;
-    var target_time = start_time + (1 + count) * every_ms;
-    setTimeout(function() {
-      count++;
-      repeat_fn(count);
-      if (count >= times - 1) {
-        cleanup_fn();
-      } else {
-        loop();
-      }
-    }, target_time - new Date().getTime());
-    // TODO(danvk): adjust every_ms to produce evenly-timed function calls.
-  })();
-};
-
-/**
- * @private
- * This function will scan the option list and determine if they
- * require us to recalculate the pixel positions of each point.
- * @param { List } a list of options to check.
- * @return { Boolean } true if the graph needs new points else false.
- */
-Dygraph.isPixelChangingOptionList = function(labels, attrs) {
-  // 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,
-    'digitsAfterDecimal': true,
-    'drawCallback': true,
-    'drawPoints': true,
-    'drawXGrid': true,
-    'drawYGrid': 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,
-    'maxNumberWidth': true,
-    'panEdgeFraction': true,
-    'pixelsPerYLabel': true,
-    'pointClickCallback': true,
-    'pointSize': true,
-    'rangeSelectorPlotFillColor': true,
-    'rangeSelectorPlotStrokeColor': true,
-    'showLabelsOnHighlight': true,
-    'showRoller': true,
-    'sigFigs': true,
-    'strokeWidth': true,
-    'underlayCallback': true,
-    'unhighlightCallback': true,
-    'xAxisLabelFormatter': true,
-    'xTicker': true,
-    'xValueFormatter': true,
-    'yAxisLabelFormatter': true,
-    'yValueFormatter': true,
-    'zoomCallback': true
-  };
-
-  // Assume that we do not require new points.
-  // This will change to true if we actually do need new points.
-  var requiresNewPoints = false;
-
-  // 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;
-    }
-  }
-
-  // Iterate through the list of updated options.
-  for (var property in attrs) {
-    // Break early if we already know we need new points from a previous option.
-    if (requiresNewPoints) {
-      break;
-    }
-    if (attrs.hasOwnProperty(property)) {
-      // Find out of this field is actually a series specific options list.
-      if (seriesNamesDictionary[property]) {
-        // This property value is a list of options for this series.
-        // If any of these sub properties are not pixel safe, set the flag.
-        for (var subProperty in attrs[property]) {
-          // Break early if we already know we need new points from a previous option.
-          if (requiresNewPoints) {
-            break;
-          }
-          if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
-            requiresNewPoints = true;
-          }
-        }
-      // If this was not a series specific option list, check if its a pixel changing property.
-      } else if (!pixelSafeOptions[property]) {
-        requiresNewPoints = true;
-      }
-    }
-  }
-
-  return requiresNewPoints;
-};
-
-/**
- * Compares two arrays to see if they are equal. If either parameter is not an
- * array it will return false. Does a shallow compare 
- * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
- * @param array1 first array
- * @param array2 second array
- * @return True if both parameters are arrays, and contents are equal.
- */
-Dygraph.compareArrays = function(array1, array2) {
-  if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
-    return false;
-  }
-  if (array1.length !== array2.length) {
-    return false;
-  }
-  for (var i = 0; i < array1.length; i++) {
-    if (array1[i] !== array2[i]) {
-      return false;
-    }
-  }
-  return true;
-};
-/**
- * @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
- */
-
-/*jshint globalstrict: true */
-/*global Dygraph:false */
-"use strict";
-
-/**
- * A wrapper around Dygraph that implements the gviz API.
- * @param {Object} container The DOM object the visualization should live in.
- */
-Dygraph.GVizChart = function(container) {
-  this.container = container;
-};
-
-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} 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} 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 col = 1;
-  var datasets = this.date_graph.layout_.datasets;
-  for (var k in datasets) {
-    if (!datasets.hasOwnProperty(k)) continue;
-    selection.push({row: row, column: col});
-    col++;
-  }
-
-  return selection;
-};
-
-/**
- * @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)
- */
-
-/*jshint globalstrict: true */
-/*global Dygraph:false */
-"use strict";
-
-/**
- * A collection of functions to facilitate build custom interaction models.
- * @class
- */
-Dygraph.Interaction = {};
-
-/**
- * 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();
-  context.dateRange = xRange[1] - xRange[0];
-  context.initialLeftmostDate = xRange[0];
-  context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
-
-  if (g.attr_("panEdgeFraction")) {
-    var maxXPixelsToDraw = g.width_ * g.attr_("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.attr_("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);
-      var boundedBottomValue = g.toDataYCoord(boundedBottomY);
-
-      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.
-  context.is2DPan = false;
-  for (i = 0; i < g.axes_.length; i++) {
-    axis = g.axes_[i];
-    var yRange = g.yAxisRange(i);
-    // TODO(konigsberg): These values should be in |context|.
-    // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
-    if (axis.logscale) {
-      axis.initialTopValue = Dygraph.log10(yRange[1]);
-      axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
-    } else {
-      axis.initialTopValue = yRange[1];
-      axis.dragValueRange = yRange[1] - yRange[0];
-    }
-    axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
-
-    // 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 = g.dragGetX_(event, context);
-  context.dragEndY = g.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;
-    }
-  }
-
-  g.dateWindow_ = [minDate, maxDate];
-
-  // y-axis scaling is automatic unless this is a full 2D pan.
-  if (context.is2DPan) {
-    // Adjust each axis appropriately.
-    for (var i = 0; i < g.axes_.length; i++) {
-      var axis = g.axes_[i];
-
-      var pixelsDragged = context.dragEndY - context.dragStartY;
-      var unitsDragged = pixelsDragged * axis.unitsPerPixel;
-
-      var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
-
-      // In log scale, maxValue and minValue are the logs of those values.
-      var maxValue = axis.initialTopValue + unitsDragged;
-      if (boundedValue) {
-        maxValue = Math.min(maxValue, boundedValue[1]);
-      }
-      var minValue = maxValue - axis.dragValueRange;
-      if (boundedValue) {
-        if (minValue < boundedValue[0]) {
-          // Adjust maxValue, and recompute minValue.
-          maxValue = maxValue - (minValue - boundedValue[0]);
-          minValue = maxValue - axis.dragValueRange;
-        }
-      }
-      if (axis.logscale) {
-        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 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.endPan = function(event, g, context) {
-  context.dragEndX = g.dragGetX_(event, context);
-  context.dragEndY = g.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);
-  }
-
-  // TODO(konigsberg): Clear the context data from the axis.
-  // (replace with "context = {}" ?)
-  // TODO(konigsberg): mouseup should just delete the
-  // context object, and mousedown should create a new one.
-  context.isPanning = false;
-  context.is2DPan = false;
-  context.initialLeftmostDate = null;
-  context.dateRange = null;
-  context.valueRange = null;
-  context.boundedDates = null;
-  context.boundedValues = null;
-};
-
-/**
- * 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;
-};
-
-/**
- * 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.dragEndX = g.dragGetX_(event, context);
-  context.dragEndY = g.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;
-};
-
-Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
-  var clickCallback = g.attr_('clickCallback');
-  var pointClickCallback = g.attr_('pointClickCallback');
-
-  var selectedPoint = null;
-
-  // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
-  if (pointClickCallback) {
-    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.attr_('highlightCircleSize') + 2;
-    if (closestDistance <= radius * radius) {
-      selectedPoint = g.selPoints_[closestIdx];
-    }
-  }
-
-  if (selectedPoint) {
-    pointClickCallback(event, selectedPoint);
-  }
-
-  // TODO(danvk): pass along more info about the points, e.g. 'x'
-  if (clickCallback) {
-    clickCallback(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) {
-  context.isZooming = false;
-  context.dragEndX = g.dragGetX_(event, context);
-  context.dragEndY = g.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);
-  }
-
-  if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
-    g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
-               Math.max(context.dragStartX, context.dragEndX));
-  } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
-    g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
-               Math.max(context.dragStartY, context.dragEndY));
-  } else {
-    g.clearZoomRect_();
-  }
-  context.dragStartX = null;
-  context.dragStartY = null;
-};
-
-/**
- * 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) {
-    context.initializeMouseDown(event, g, context);
-
-    if (event.altKey || event.shiftKey) {
-      Dygraph.startPan(event, g, context);
-    } else {
-      Dygraph.startZoom(event, g, context);
-    }
-  },
-
-  // Draw zoom rectangles when the mouse is down and the user moves around
-  mousemove: function(event, g, context) {
-    if (context.isZooming) {
-      Dygraph.moveZoom(event, g, context);
-    } else if (context.isPanning) {
-      Dygraph.movePan(event, g, context);
-    }
-  },
-
-  mouseup: function(event, g, context) {
-    if (context.isZooming) {
-      Dygraph.endZoom(event, g, context);
-    } else if (context.isPanning) {
-      Dygraph.endPan(event, g, context);
-    }
-  },
-
-  // Temporarily cancel the dragging event when the mouse leaves the graph
-  mouseout: function(event, g, context) {
-    if (context.isZooming) {
-      context.dragEndX = null;
-      context.dragEndY = null;
-    }
-  },
-
-  // Disable zooming out if panning.
-  dblclick: function(event, g, context) {
-    if (event.altKey || event.shiftKey) {
-      return;
-    }
-    // TODO(konigsberg): replace g.doUnzoom()_ with something that is
-    // friendlier to public use.
-    g.doUnzoom_();
-  }
-};
-
-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: function(event, g, context) {
-    // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
-    context.dragEndX = g.dragGetX_(event, context);
-    context.dragEndY = g.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);
-    }
-  }
-};
-
-// 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);
-    }
-  }
-};
-// Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
-// All Rights Reserved.
-
-/**
- * @fileoverview This file contains the DygraphRangeSelector class used to provide
- * a timeline range selector widget for dygraphs.
- */
-
-/*jshint globalstrict: true */
-/*global Dygraph:false */
-"use strict";
-
-/**
- * The DygraphRangeSelector class provides a timeline range selector widget.
- * @param {Dygraph} dygraph The dygraph object
- * @constructor
- */
-var DygraphRangeSelector = function(dygraph) {
-  this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
-  this.isUsingExcanvas_ = dygraph.isUsingExcanvas_;
-  this.dygraph_ = dygraph;
-  this.createCanvases_();
-  if (this.isUsingExcanvas_) {
-    this.createIEPanOverlay_();
-  }
-  this.createZoomHandles_();
-  this.initInteraction_();
-};
-
-/**
- * Adds the range selector to the dygraph.
- * @param {Object} graphDiv The container div for the range selector.
- * @param {DygraphLayout} layout The DygraphLayout object for this graph.
- */
-DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) {
-  this.layout_ = layout;
-  this.resize_();
-  graphDiv.appendChild(this.bgcanvas_);
-  graphDiv.appendChild(this.fgcanvas_);
-  graphDiv.appendChild(this.leftZoomHandle_);
-  graphDiv.appendChild(this.rightZoomHandle_);
-};
-
-/**
- * Renders the static background portion of the range selector.
- */
-DygraphRangeSelector.prototype.renderStaticLayer = function() {
-  this.resize_();
-  this.drawStaticLayer_();
-};
-
-/**
- * Renders the interactive foreground portion of the range selector.
- */
-DygraphRangeSelector.prototype.renderInteractiveLayer = function() {
-  if (this.isChangingRange_) {
-    return;
-  }
-  this.placeZoomHandles_();
-  this.drawInteractiveLayer_();
-};
-
-/**
- * @private
- * Resizes the range selector.
- */
-DygraphRangeSelector.prototype.resize_ = function() {
-  function setElementRect(canvas, rect) {
-    canvas.style.top = rect.y + 'px';
-    canvas.style.left = rect.x + 'px';
-    canvas.width = rect.w;
-    canvas.height = rect.h;
-    canvas.style.width = canvas.width + 'px';    // for IE
-    canvas.style.height = canvas.height + 'px';  // for IE
-  }
-
-  var plotArea = this.layout_.getPlotArea();
-  var xAxisLabelHeight = this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
-  this.canvasRect_ = {
-    x: plotArea.x,
-    y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
-    w: plotArea.w,
-    h: this.attr_('rangeSelectorHeight')
-  };
-
-  setElementRect(this.bgcanvas_, this.canvasRect_);
-  setElementRect(this.fgcanvas_, this.canvasRect_);
-};
-
-DygraphRangeSelector.prototype.attr_ = function(name) {
-  return this.dygraph_.attr_(name);
-};
-
-/**
- * @private
- * Creates the background and foreground canvases.
- */
-DygraphRangeSelector.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 overlay divs for IE/Excanvas so that mouse events are handled properly.
- */
-DygraphRangeSelector.prototype.createIEPanOverlay_ = function() {
-  this.iePanOverlay_ = document.createElement("div");
-  this.iePanOverlay_.style.position = 'absolute';
-  this.iePanOverlay_.style.backgroundColor = 'white';
-  this.iePanOverlay_.style.filter = 'alpha(opacity=0)';
-  this.iePanOverlay_.style.display = 'none';
-  this.iePanOverlay_.style.cursor = 'move';
-  this.fgcanvas_.appendChild(this.iePanOverlay_);
-};
-
-/**
- * @private
- * Creates the zoom handle elements.
- */
-DygraphRangeSelector.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';
-  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=';
-  }
-
-  this.leftZoomHandle_ = img;
-  this.rightZoomHandle_ = img.cloneNode(false);
-};
-
-/**
- * @private
- * Sets up the interaction for the range selector.
- */
-DygraphRangeSelector.prototype.initInteraction_ = function() {
-  var self = this;
-  var topElem = this.isIE_ ? document : window;
-  var xLast = 0;
-  var handle = null;
-  var isZooming = false;
-  var isPanning = false;
-
-  // 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, onCanvasMouseMove;
-
-  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;
-    xLast = e.screenX;
-    handle = e.target ? e.target : e.srcElement;
-    Dygraph.addEvent(topElem, 'mousemove', onZoom);
-    Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
-    self.fgcanvas_.style.cursor = 'col-resize';
-  };
-
-  onZoom = function(e) {
-    if (!isZooming) {
-      return;
-    }
-    var delX = e.screenX - xLast;
-    if (Math.abs(delX) < 4) {
-      return;
-    }
-    xLast = e.screenX;
-    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 not using excanvas).
-    if (!self.isUsingExcanvas_) {
-      doZoom();
-    }
-  };
-
-  onZoomEnd = function(e) {
-    if (!isZooming) {
-      return;
-    }
-    isZooming = false;
-    Dygraph.removeEvent(topElem, 'mousemove', onZoom);
-    Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
-    self.fgcanvas_.style.cursor = 'default';
-
-    // If using excanvas, Zoom now.
-    if (self.isUsingExcanvas_) {
-      doZoom();
-    }
-  };
-
-  doZoom = function() {
-    try {
-      var zoomHandleStatus = self.getZoomHandleStatus_();
-      self.isChangingRange_ = true;
-      if (!zoomHandleStatus.isZoomed) {
-        self.dygraph_.doUnzoom_();
-      } else {
-        var xDataWindow = toXDataWindow(zoomHandleStatus);
-        self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
-      }
-    } finally {
-      self.isChangingRange_ = false;
-    }
-  };
-
-  isMouseInPanZone = function(e) {
-    if (self.isUsingExcanvas_) {
-        return e.srcElement == self.iePanOverlay_;
-    } else {
-      // Getting clientX directly from the event is not accurate enough :(
-      var clientX;
-      if (e.offsetX != undefined) {
-        clientX = self.canvasRect_.x + e.offsetX;
-      } else {
-        clientX = e.clientX;
-      }
-      var zoomHandleStatus = self.getZoomHandleStatus_();
-      return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
-    }
-  };
-
-  onPanStart = function(e) {
-    if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
-      Dygraph.cancelEvent(e);
-      isPanning = true;
-      xLast = e.screenX;
-      Dygraph.addEvent(topElem, 'mousemove', onPan);
-      Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
-    }
-  };
-
-  onPan = function(e) {
-    if (!isPanning) {
-      return;
-    }
-    Dygraph.cancelEvent(e);
-
-    var delX = e.screenX - xLast;
-    if (Math.abs(delX) < 4) {
-      return;
-    }
-    xLast = e.screenX;
-
-    // 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 not using excanvas).
-    if (!self.isUsingExcanvas_) {
-      doPan();
-    }
-  };
-
-  onPanEnd = function(e) {
-    if (!isPanning) {
-      return;
-    }
-    isPanning = false;
-    Dygraph.removeEvent(topElem, 'mousemove', onPan);
-    Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
-    // If using excanvas, do pan now.
-    if (self.isUsingExcanvas_) {
-      doPan();
-    }
-  };
-
-  doPan = function() {
-    try {
-      self.isChangingRange_ = true;
-      self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
-      self.dygraph_.drawGraph_(false);
-    } finally {
-      self.isChangingRange_ = false;
-    }
-  };
-
-  onCanvasMouseMove = function(e) {
-    if (isZooming || isPanning) {
-      return;
-    }
-    var cursor = isMouseInPanZone(e) ? 'move' : 'default';
-    if (cursor != self.fgcanvas_.style.cursor) {
-      self.fgcanvas_.style.cursor = cursor;
-    }
-  };
-
-  this.dygraph_.attrs_.interactionModel =
-      Dygraph.Interaction.dragIsPanInteractionModel;
-  this.dygraph_.attrs_.panEdgeFraction = 0.0001;
-
-  var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
-  Dygraph.addEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
-  Dygraph.addEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
-
-  if (this.isUsingExcanvas_) {
-    Dygraph.addEvent(this.iePanOverlay_, 'mousedown', onPanStart);
-  } else {
-    Dygraph.addEvent(this.fgcanvas_, 'mousedown', onPanStart);
-    Dygraph.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove);
-  }
-};
-
-/**
- * @private
- * Draws the static layer in the background canvas.
- */
-DygraphRangeSelector.prototype.drawStaticLayer_ = function() {
-  var ctx = this.bgcanvas_ctx_;
-  ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
-  try {
-    this.drawMiniPlot_();
-  } catch(ex) {
-    Dygraph.warn(ex);
-  }
-
-  var margin = 0.5;
-  this.bgcanvas_ctx_.lineWidth = 1;
-  ctx.strokeStyle = 'gray';
-  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.
- */
-DygraphRangeSelector.prototype.drawMiniPlot_ = function() {
-  var fillStyle = this.attr_('rangeSelectorPlotFillColor');
-  var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor');
-  if (!fillStyle && !strokeStyle) {
-    return;
-  }
-
-  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;
-
-  ctx.beginPath();
-  ctx.moveTo(margin, canvasHeight);
-  for (var i = 0; i < combinedSeriesData.data.length; i++) {
-    var dataPoint = combinedSeriesData.data[i];
-    var x = (dataPoint[0] - xExtremes[0])*xFact;
-    var y = canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact;
-    if (isFinite(x) && isFinite(y)) {
-      ctx.lineTo(x, y);
-    }
-  }
-  ctx.lineTo(canvasWidth, canvasHeight);
-  ctx.closePath();
-
-  if (fillStyle) {
-    var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
-    lingrad.addColorStop(0, 'white');
-    lingrad.addColorStop(1, fillStyle);
-    this.bgcanvas_ctx_.fillStyle = lingrad;
-    ctx.fill();
-  }
-
-  if (strokeStyle) {
-    this.bgcanvas_ctx_.strokeStyle = strokeStyle;
-    this.bgcanvas_ctx_.lineWidth = 1.5;
-    ctx.stroke();
-  }
-};
-
-/**
- * @private
- * Computes and returns the combinded series data along with min/max for the mini plot.
- * @return {Object} An object containing combinded series array, ymin, ymax.
- */
-DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
-  var data = this.dygraph_.rawData_;
-  var logscale = this.attr_('logscale');
-
-  // Create a combined series (average of all series values).
-  var combinedSeries = [];
-  var sum;
-  var count;
-  var yVal, y;
-  var mutipleValues;
-  var i, j, k;
-
-  // Find out if data has multiple values per datapoint.
-  // Go to first data point that actually has values (see http://code.google.com/p/dygraphs/issues/detail?id=246)
-  for (i = 0; i < data.length; i++) {
-    if (data[i].length > 1 && data[i][1] != null) {
-      mutipleValues = typeof data[i][1] != 'number';
-      if (mutipleValues) {
-        sum = [];
-        count = [];
-        for (k = 0; k < data[i][1].length; k++) {
-          sum.push(0);
-          count.push(0);
-        }
-      }
-      break;
-    }
-  }
-
-  for (i = 0; i < data.length; i++) {
-    var dataPoint = data[i];
-    var xVal = dataPoint[0];
-
-    if (mutipleValues) {
-      for (k = 0; k < sum.length; k++) {
-        sum[k] = count[k] = 0;
-      }
-    } else {
-      sum = count = 0;
-    }
-
-    for (j = 1; j < dataPoint.length; j++) {
-      if (this.dygraph_.visibility()[j-1]) {
-        if (mutipleValues) {
-          for (k = 0; k < sum.length; k++) {
-            y = dataPoint[j][k];
-            if (y === null || isNaN(y)) continue;
-            sum[k] += y;
-            count[k]++;
-          }
-        } else {
-          y = dataPoint[j];
-          if (y === null || isNaN(y)) continue;
-          sum += y;
-          count++;
-        }
-      }
-    }
-
-    if (mutipleValues) {
-      for (k = 0; k < sum.length; k++) {
-        sum[k] /= count[k];
-      }
-      yVal = sum.slice(0);
-    } else {
-      yVal = sum/count;
-    }
-
-    combinedSeries.push([xVal, yVal]);
-  }
-
-  // Account for roll period, fractions.
-  combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
-
-  if (typeof combinedSeries[0][1] != 'number') {
-    for (i = 0; i < combinedSeries.length; i++) {
-      yVal = combinedSeries[i][1];
-      combinedSeries[i][1] = yVal[0];
-    }
-  }
-
-  // Compute the y range.
-  var yMin = Number.MAX_VALUE;
-  var yMax = -Number.MAX_VALUE;
-  for (i = 0; i < combinedSeries.length; i++) {
-    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.
- */
-DygraphRangeSelector.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.
- */
-DygraphRangeSelector.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 = 'black';
-  if (!zoomHandleStatus.isZoomed) {
-    ctx.beginPath();
-    ctx.moveTo(margin, margin);
-    ctx.lineTo(margin, height);
-    ctx.lineTo(width, height);
-    ctx.lineTo(width, margin);
-    ctx.stroke();
-    if (this.iePanOverlay_) {
-      this.iePanOverlay_.style.display = 'none';
-    }
-  } 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, 0.6)';
-    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();
-
-    if (this.isUsingExcanvas_) {
-      this.iePanOverlay_.style.width = (rightHandleCanvasPos - leftHandleCanvasPos) + 'px';
-      this.iePanOverlay_.style.left = leftHandleCanvasPos + 'px';
-      this.iePanOverlay_.style.height = height + 'px';
-      this.iePanOverlay_.style.display = 'inline';
-    }
-  }
-};
-
-/**
- * @private
- * Returns the current zoom handle position information.
- * @return {Object} The zoom handle status.
- */
-DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() {
-  var halfHandleWidth = this.leftZoomHandle_.width/2;
-  var leftHandlePos = parseInt(this.leftZoomHandle_.style.left, 10) + halfHandleWidth;
-  var rightHandlePos = parseInt(this.rightZoomHandle_.style.left, 10) + halfHandleWidth;
-  return {
-      leftHandlePos: leftHandlePos,
-      rightHandlePos: rightHandlePos,
-      isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
-  };
-};
-/**
- * @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 globalstrict: true */
-/*global Dygraph:false */
-"use strict";
-
-Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) {
-  var pixels_per_tick = 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;
-      if (kmg2) {
-        mults = [1, 2, 4, 8];
-      } else {
-        mults = [1, 2, 5];
-      }
-      var scale, low_val, high_val;
-      for (i = -10; i < 50; i++) {
-        var base_scale;
-        if (kmg2) {
-          base_scale = Math.pow(16, i);
-        } else {
-          base_scale = Math.pow(10, i);
-        }
-        var spacing = 0;
-        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;
-          // wish I could break out of both loops at once...
-          if (spacing > pixels_per_tick) break;
-        }
-        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} );
-      }
-    }
-  }
-
-  // Add formatted labels to the ticks.
-  var k;
-  var k_labels = [];
-  if (opts("labelsKMB")) {
-    k = 1000;
-    k_labels = [ "K", "M", "B", "T" ];
-  }
-  if (opts("labelsKMG2")) {
-    if (k) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
-    k = 1024;
-    k_labels = [ "k", "M", "G", "T" ];
-  }
-
-  var formatter = opts('axisLabelFormatter');
-
-  // Add labels to the ticks.
-  for (i = 0; i < ticks.length; i++) {
-    if (ticks[i].label !== undefined) continue;  // Use current label.
-    tickV = ticks[i].v;
-    var absTickV = Math.abs(tickV);
-    // TODO(danvk): set granularity to something appropriate here.
-    var label = formatter(tickV, 0, opts, dygraph);
-    if (k_labels.length > 0) {
-      // TODO(danvk): should this be integrated into the axisLabelFormatter?
-      // Round up to an appropriate unit.
-      var n = k*k*k*k;
-      for (j = 3; j >= 0; j--, n /= k) {
-        if (absTickV >= n) {
-          label = Dygraph.round_(tickV / n, opts('digitsAfterDecimal')) +
-              k_labels[j];
-          break;
-        }
-      }
-    }
-    ticks[i].label = label;
-  }
-
-  return ticks;
-};
-
-
-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
-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.WEEKLY = 14;
-Dygraph.MONTHLY = 15;
-Dygraph.QUARTERLY = 16;
-Dygraph.BIANNUAL = 17;
-Dygraph.ANNUAL = 18;
-Dygraph.DECADAL = 19;
-Dygraph.CENTENNIAL = 20;
-Dygraph.NUM_GRANULARITIES = 21;
-
-Dygraph.SHORT_SPACINGS = [];
-Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
-Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]    = 1000 * 2;
-Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]   = 1000 * 5;
-Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]    = 1000 * 10;
-Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
-Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]        = 1000 * 60;
-Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]    = 1000 * 60 * 2;
-Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]   = 1000 * 60 * 5;
-Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]    = 1000 * 60 * 10;
-Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
-Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]          = 1000 * 3600;
-Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]      = 1000 * 3600 * 2;
-Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]      = 1000 * 3600 * 6;
-Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
-Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
-
-/**
- * @private
- * 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.
- */
-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} opts Function mapping from option name -> 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 = 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;
-};
-
-Dygraph.numDateTicks = function(start_time, end_time, granularity) {
-  if (granularity < Dygraph.MONTHLY) {
-    // Generate one tick mark for every fixed interval of time.
-    var spacing = Dygraph.SHORT_SPACINGS[granularity];
-    return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
-  } else {
-    var year_mod = 1;  // e.g. to only print one point every 10 years.
-    var num_months = 12;
-    if (granularity == Dygraph.QUARTERLY) num_months = 3;
-    if (granularity == Dygraph.BIANNUAL) num_months = 2;
-    if (granularity == Dygraph.ANNUAL) num_months = 1;
-    if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
-    if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
-
-    var msInYear = 365.2524 * 24 * 3600 * 1000;
-    var num_years = 1.0 * (end_time - start_time) / msInYear;
-    return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
-  }
-};
-
-Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
-  var formatter = opts("axisLabelFormatter");
-  var ticks = [];
-  var t;
-
-  if (granularity < Dygraph.MONTHLY) {
-    // Generate one tick mark for every fixed interval of time.
-    var spacing = Dygraph.SHORT_SPACINGS[granularity];
-
-    // Find a time less than start_time which occurs on a "nice" time boundary
-    // for this granularity.
-    var g = spacing / 1000;
-    var d = new Date(start_time);
-    var x;
-    if (g <= 60) {  // seconds
-      x = d.getSeconds(); d.setSeconds(x - x % g);
-    } else {
-      d.setSeconds(0);
-      g /= 60;
-      if (g <= 60) {  // minutes
-        x = d.getMinutes(); d.setMinutes(x - x % g);
-      } else {
-        d.setMinutes(0);
-        g /= 60;
-
-        if (g <= 24) {  // days
-          x = d.getHours(); d.setHours(x - x % g);
-        } else {
-          d.setHours(0);
-          g /= 24;
-
-          if (g == 7) {  // one week
-            d.setDate(d.getDate() - d.getDay());
-          }
-        }
-      }
-    }
-    start_time = d.getTime();
-
-    for (t = start_time; t <= end_time; t += spacing) {
-      ticks.push({ v:t,
-                   label: formatter(new Date(t), granularity, opts, dg)
-                 });
-    }
-  } else {
-    // Display a tick mark on the first of a set of months of each year.
-    // Years get a tick mark iff y % year_mod == 0. This is useful for
-    // displaying a tick mark once every 10 years, say, on long time scales.
-    var months;
-    var year_mod = 1;  // e.g. to only print one point every 10 years.
-
-    if (granularity == Dygraph.MONTHLY) {
-      months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
-    } else if (granularity == Dygraph.QUARTERLY) {
-      months = [ 0, 3, 6, 9 ];
-    } else if (granularity == Dygraph.BIANNUAL) {
-      months = [ 0, 6 ];
-    } else if (granularity == Dygraph.ANNUAL) {
-      months = [ 0 ];
-    } else if (granularity == Dygraph.DECADAL) {
-      months = [ 0 ];
-      year_mod = 10;
-    } else if (granularity == Dygraph.CENTENNIAL) {
-      months = [ 0 ];
-      year_mod = 100;
-    } else {
-      Dygraph.warn("Span of dates is too long");
-    }
-
-    var start_year = new Date(start_time).getFullYear();
-    var end_year   = new Date(end_time).getFullYear();
-    var zeropad = Dygraph.zeropad;
-    for (var i = start_year; i <= end_year; i++) {
-      if (i % year_mod !== 0) continue;
-      for (var j = 0; j < months.length; j++) {
-        var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
-        t = Dygraph.dateStrToMillis(date_str);
-        if (t < start_time || t > end_time) continue;
-        ticks.push({ v:t,
-                     label: formatter(new Date(t), granularity, opts, dg)
-                   });
-      }
-    }
-  }
-
-  return ticks;
-};
-
-// These are set here so that this file can be included after dygraph.js.
-Dygraph.DEFAULT_ATTRS.axes.x.ticker = Dygraph.dateTicker;
-Dygraph.DEFAULT_ATTRS.axes.y.ticker = Dygraph.numericTicks;
-Dygraph.DEFAULT_ATTRS.axes.y2.ticker = Dygraph.numericTicks;
-/**
- * A class to parse color values
- *
- * NOTE: modified by danvk. I removed the "getHelpXML" function to reduce the
- * file size, added "use strict" and a few "var" declarations where needed.
- *
- * @author Stoyan Stefanov <sstoo@gmail.com>
- * @link   http://www.phpied.com/rgb-color-parser-in-javascript/
- * @license Use it if you like it
- */
-"use strict";
-
-function RGBColor(color_string)
-{
-    this.ok = false;
-
-    // strip any leading #
-    if (color_string.charAt(0) == '#') { // remove # if any
-        color_string = color_string.substr(1,6);
-    }
-
-    color_string = color_string.replace(/ /g,'');
-    color_string = color_string.toLowerCase();
-
-    // before getting into regexps, try simple matches
-    // and overwrite the input
-    var simple_colors = {
-        aliceblue: 'f0f8ff',
-        antiquewhite: 'faebd7',
-        aqua: '00ffff',
-        aquamarine: '7fffd4',
-        azure: 'f0ffff',
-        beige: 'f5f5dc',
-        bisque: 'ffe4c4',
-        black: '000000',
-        blanchedalmond: 'ffebcd',
-        blue: '0000ff',
-        blueviolet: '8a2be2',
-        brown: 'a52a2a',
-        burlywood: 'deb887',
-        cadetblue: '5f9ea0',
-        chartreuse: '7fff00',
-        chocolate: 'd2691e',
-        coral: 'ff7f50',
-        cornflowerblue: '6495ed',
-        cornsilk: 'fff8dc',
-        crimson: 'dc143c',
-        cyan: '00ffff',
-        darkblue: '00008b',
-        darkcyan: '008b8b',
-        darkgoldenrod: 'b8860b',
-        darkgray: 'a9a9a9',
-        darkgreen: '006400',
-        darkkhaki: 'bdb76b',
-        darkmagenta: '8b008b',
-        darkolivegreen: '556b2f',
-        darkorange: 'ff8c00',
-        darkorchid: '9932cc',
-        darkred: '8b0000',
-        darksalmon: 'e9967a',
-        darkseagreen: '8fbc8f',
-        darkslateblue: '483d8b',
-        darkslategray: '2f4f4f',
-        darkturquoise: '00ced1',
-        darkviolet: '9400d3',
-        deeppink: 'ff1493',
-        deepskyblue: '00bfff',
-        dimgray: '696969',
-        dodgerblue: '1e90ff',
-        feldspar: 'd19275',
-        firebrick: 'b22222',
-        floralwhite: 'fffaf0',
-        forestgreen: '228b22',
-        fuchsia: 'ff00ff',
-        gainsboro: 'dcdcdc',
-        ghostwhite: 'f8f8ff',
-        gold: 'ffd700',
-        goldenrod: 'daa520',
-        gray: '808080',
-        green: '008000',
-        greenyellow: 'adff2f',
-        honeydew: 'f0fff0',
-        hotpink: 'ff69b4',
-        indianred : 'cd5c5c',
-        indigo : '4b0082',
-        ivory: 'fffff0',
-        khaki: 'f0e68c',
-        lavender: 'e6e6fa',
-        lavenderblush: 'fff0f5',
-        lawngreen: '7cfc00',
-        lemonchiffon: 'fffacd',
-        lightblue: 'add8e6',
-        lightcoral: 'f08080',
-        lightcyan: 'e0ffff',
-        lightgoldenrodyellow: 'fafad2',
-        lightgrey: 'd3d3d3',
-        lightgreen: '90ee90',
-        lightpink: 'ffb6c1',
-        lightsalmon: 'ffa07a',
-        lightseagreen: '20b2aa',
-        lightskyblue: '87cefa',
-        lightslateblue: '8470ff',
-        lightslategray: '778899',
-        lightsteelblue: 'b0c4de',
-        lightyellow: 'ffffe0',
-        lime: '00ff00',
-        limegreen: '32cd32',
-        linen: 'faf0e6',
-        magenta: 'ff00ff',
-        maroon: '800000',
-        mediumaquamarine: '66cdaa',
-        mediumblue: '0000cd',
-        mediumorchid: 'ba55d3',
-        mediumpurple: '9370d8',
-        mediumseagreen: '3cb371',
-        mediumslateblue: '7b68ee',
-        mediumspringgreen: '00fa9a',
-        mediumturquoise: '48d1cc',
-        mediumvioletred: 'c71585',
-        midnightblue: '191970',
-        mintcream: 'f5fffa',
-        mistyrose: 'ffe4e1',
-        moccasin: 'ffe4b5',
-        navajowhite: 'ffdead',
-        navy: '000080',
-        oldlace: 'fdf5e6',
-        olive: '808000',
-        olivedrab: '6b8e23',
-        orange: 'ffa500',
-        orangered: 'ff4500',
-        orchid: 'da70d6',
-        palegoldenrod: 'eee8aa',
-        palegreen: '98fb98',
-        paleturquoise: 'afeeee',
-        palevioletred: 'd87093',
-        papayawhip: 'ffefd5',
-        peachpuff: 'ffdab9',
-        peru: 'cd853f',
-        pink: 'ffc0cb',
-        plum: 'dda0dd',
-        powderblue: 'b0e0e6',
-        purple: '800080',
-        red: 'ff0000',
-        rosybrown: 'bc8f8f',
-        royalblue: '4169e1',
-        saddlebrown: '8b4513',
-        salmon: 'fa8072',
-        sandybrown: 'f4a460',
-        seagreen: '2e8b57',
-        seashell: 'fff5ee',
-        sienna: 'a0522d',
-        silver: 'c0c0c0',
-        skyblue: '87ceeb',
-        slateblue: '6a5acd',
-        slategray: '708090',
-        snow: 'fffafa',
-        springgreen: '00ff7f',
-        steelblue: '4682b4',
-        tan: 'd2b48c',
-        teal: '008080',
-        thistle: 'd8bfd8',
-        tomato: 'ff6347',
-        turquoise: '40e0d0',
-        violet: 'ee82ee',
-        violetred: 'd02090',
-        wheat: 'f5deb3',
-        white: 'ffffff',
-        whitesmoke: 'f5f5f5',
-        yellow: 'ffff00',
-        yellowgreen: '9acd32'
-    };
-    for (var key in simple_colors) {
-        if (color_string == key) {
-            color_string = simple_colors[key];
-        }
-    }
-    // emd of simple type-in colors
-
-    // array of color definition objects
-    var color_defs = [
-        {
-            re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
-            example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
-            process: function (bits){
-                return [
-                    parseInt(bits[1]),
-                    parseInt(bits[2]),
-                    parseInt(bits[3])
-                ];
-            }
-        },
-        {
-            re: /^(\w{2})(\w{2})(\w{2})$/,
-            example: ['#00ff00', '336699'],
-            process: function (bits){
-                return [
-                    parseInt(bits[1], 16),
-                    parseInt(bits[2], 16),
-                    parseInt(bits[3], 16)
-                ];
-            }
-        },
-        {
-            re: /^(\w{1})(\w{1})(\w{1})$/,
-            example: ['#fb0', 'f0f'],
-            process: function (bits){
-                return [
-                    parseInt(bits[1] + bits[1], 16),
-                    parseInt(bits[2] + bits[2], 16),
-                    parseInt(bits[3] + bits[3], 16)
-                ];
-            }
-        }
-    ];
-
-    // search through the definitions to find a match
-    for (var i = 0; i < color_defs.length; i++) {
-        var re = color_defs[i].re;
-        var processor = color_defs[i].process;
-        var bits = re.exec(color_string);
-        if (bits) {
-            var channels = processor(bits);
-            this.r = channels[0];
-            this.g = channels[1];
-            this.b = channels[2];
-            this.ok = true;
-        }
-
-    }
-
-    // validate/cleanup values
-    this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
-    this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
-    this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
-
-    // some getters
-    this.toRGB = function () {
-        return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
-    }
-    this.toHex = function () {
-        var r = this.r.toString(16);
-        var g = this.g.toString(16);
-        var b = this.b.toString(16);
-        if (r.length == 1) r = '0' + r;
-        if (g.length == 1) g = '0' + g;
-        if (b.length == 1) b = '0' + b;
-        return '#' + r + g + b;
-    }
-
-
-}
-
-Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(x,pad,r){if(typeof (r)=="undefined"){r=10}for(;parseInt(x,10)<r&&r>1;r/=10){x=pad.toString()+x}return x.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(d){return Date.ext.locales[d.locale].a[d.getDay()]},A:function(d){return Date.ext.locales[d.locale].A[d.getDay()]},b:function(d){return Date.ext.locales[d.locale].b[d.getMonth()]},B:function(d){return Date.ext.locales[d.locale].B[d.getMonth()]},c:"toLocaleString",C:function(d){return Date.ext.util.xPad(parseInt(d.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(d){return Date.ext.util.xPad(parseInt(Date.ext.util.G(d)/100,10),0)},G:function(d){var y=d.getFullYear();var V=parseInt(Date.ext.formats.V(d),10);var W=parseInt(Date.ext.formats.W(d),10);if(W>V){y++}else{if(W===0&&V>=52){y--}}return y},H:["getHours","0"],I:function(d){var I=d.getHours()%12;return Date.ext.util.xPad(I===0?12:I,0)},j:function(d){var ms=d-new Date(""+d.getFullYear()+"/1/1 GMT");ms+=d.getTimezoneOffset()*60000;var doy=parseInt(ms/60000/60/24,10)+1;return Date.ext.util.xPad(doy,0,100)},m:function(d){return Date.ext.util.xPad(d.getMonth()+1,0)},M:["getMinutes","0"],p:function(d){return Date.ext.locales[d.locale].p[d.getHours()>=12?1:0]},P:function(d){return Date.ext.locales[d.locale].P[d.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(d){var dow=d.getDay();return dow===0?7:dow},U:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=6-d.getDay();var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0)},V:function(d){var woy=parseInt(Date.ext.formats.W(d),10);var dow1_1=(new Date(""+d.getFullYear()+"/1/1")).getDay();var idow=woy+(dow1_1>4||dow1_1<=1?0:1);if(idow==53&&(new Date(""+d.getFullYear()+"/12/31")).getDay()<4){idow=1}else{if(idow===0){idow=Date.ext.formats.V(new Date(""+(d.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(idow,0)},w:"getDay",W:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=7-Date.ext.formats.u(d);var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0,10)},y:function(d){return Date.ext.util.xPad(d.getFullYear()%100,0)},Y:"getFullYear",z:function(d){var o=d.getTimezoneOffset();var H=Date.ext.util.xPad(parseInt(Math.abs(o/60),10),0);var M=Date.ext.util.xPad(o%60,0);return(o>0?"-":"+")+H+M},Z:function(d){return d.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(d){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(fmt){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var d=this;while(fmt.match(/%[cDhnrRtTxXzZ]/)){fmt=fmt.replace(/%([cDhnrRtTxXzZ])/g,function(m0,m1){var f=Date.ext.aggregates[m1];return(f=="locale"?Date.ext.locales[d.locale][m1]:f)})}var str=fmt.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(m0,m1){var f=Date.ext.formats[m1];if(typeof (f)=="string"){return d[f]()}else{if(typeof (f)=="function"){return f.call(d,d)}else{if(typeof (f)=="object"&&typeof (f[0])=="string"){return Date.ext.util.xPad(d[f[0]](),f[1])}else{return m1}}}});d=null;return str};
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..c20ea8749db308943b47db554e61a6f2d789b0e7
--- /dev/null
@@ -0,0 +1 @@
+dygraph-1.2dev.js
\ No newline at end of file
deleted file mode 100644 (file)
index 245583492925e5406d5daae8aee7a5f583a75d63..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1 +0,0 @@
-"use strict";var DygraphLayout=function(a){this.dygraph_=a;this.datasets=[];this.annotations=[];this.yAxes_=null;this.xTicks_=null;this.yTicks_=null};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.getPlotArea=function(){return this.computePlotArea_()};DygraphLayout.prototype.computePlotArea_=function(){var a={x:0,y:0};if(this.attr_("drawYAxis")){a.x=this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize")}a.w=this.dygraph_.width_-a.x-this.attr_("rightGap");a.h=this.dygraph_.height_;if(this.attr_("drawXAxis")){if(this.attr_("xAxisHeight")){a.h-=this.attr_("xAxisHeight")}else{a.h-=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize")}}if(this.dygraph_.numAxes()==2){a.w-=(this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize"))}else{if(this.dygraph_.numAxes()>2){this.dygraph_.error("Only two y-axes are supported at this time. (Trying to use "+this.dygraph_.numAxes()+")")}}if(this.attr_("title")){a.h-=this.attr_("titleHeight");a.y+=this.attr_("titleHeight")}if(this.attr_("xlabel")){a.h-=this.attr_("xLabelHeight")}if(this.attr_("ylabel")){}if(this.attr_("y2label")){}if(this.attr_("showRangeSelector")){a.h-=this.attr_("rangeSelectorHeight")+4}return a};DygraphLayout.prototype.setAnnotations=function(d){this.annotations=[];var e=this.attr_("xValueParser")||function(a){return a};for(var c=0;c<d.length;c++){var b={};if(!d[c].xval&&!d[c].x){this.dygraph_.error("Annotations must have an 'x' property");return}if(d[c].icon&&!(d[c].hasOwnProperty("width")&&d[c].hasOwnProperty("height"))){this.dygraph_.error("Must set width and height when setting annotation.icon property");return}Dygraph.update(b,d[c]);if(!b.xval){b.xval=e(b.x)}this.annotations.push(b)}};DygraphLayout.prototype.setXTicks=function(a){this.xTicks_=a};DygraphLayout.prototype.setYAxes=function(a){this.yAxes_=a};DygraphLayout.prototype.setDateWindow=function(a){this.dateWindow_=a};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks();this._evaluateAnnotations()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.dateWindow_){this.minxval=this.dateWindow_[0];this.maxxval=this.dateWindow_[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var e=this.datasets[c];if(e.length>1){var b=e[0][0];if(!this.minxval||b<this.minxval){this.minxval=b}var a=e[e.length-1][0];if(!this.maxxval||a>this.maxxval){this.maxxval=a}}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!==0?1/this.xrange:1);for(var d=0;d<this.yAxes_.length;d++){var f=this.yAxes_[d];f.minyval=f.computedValueRange[0];f.maxyval=f.computedValueRange[1];f.yrange=f.maxyval-f.minyval;f.yscale=(f.yrange!==0?1/f.yrange:1);if(f.g.attr_("logscale")){f.ylogrange=Dygraph.log10(f.maxyval)-Dygraph.log10(f.minyval);f.ylogscale=(f.ylogrange!==0?1/f.ylogrange:1);if(!isFinite(f.ylogrange)||isNaN(f.ylogrange)){f.g.error("axis "+d+" of graph at "+f.g+" can't be displayed in log scale for range ["+f.minyval+" - "+f.maxyval+"]")}}}};DygraphLayout._calcYNormal=function(a,b){if(a.logscale){return 1-((Dygraph.log10(b)-Dygraph.log10(a.minyval))*a.ylogscale)}else{return 1-((b-a.minyval)*a.yscale)}};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=[];this.setPointsLengths=[];for(var f in this.datasets){if(!this.datasets.hasOwnProperty(f)){continue}var b=this.datasets[f];var a=this.dygraph_.axisPropertiesForSeries(f);var g=0;for(var d=0;d<b.length;d++){var l=b[d];var c=parseFloat(l[0]);var h=parseFloat(l[1]);var k=(c-this.minxval)*this.xscale;var e=DygraphLayout._calcYNormal(a,h);var i={x:k,y:e,xval:c,yval:h,name:f};this.points.push(i);g+=1}this.setPointsLengths.push(g)}};DygraphLayout.prototype._evaluateLineTicks=function(){var d,c,b,f;this.xticks=[];for(d=0;d<this.xTicks_.length;d++){c=this.xTicks_[d];b=c.label;f=this.xscale*(c.v-this.minxval);if((f>=0)&&(f<=1)){this.xticks.push([f,b])}}this.yticks=[];for(d=0;d<this.yAxes_.length;d++){var e=this.yAxes_[d];for(var a=0;a<e.ticks.length;a++){c=e.ticks[a];b=c.label;f=this.dygraph_.toPercentYCoord(c.v,d);if((f>=0)&&(f<=1)){this.yticks.push([d,f,b])}}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!(this.attr_("errorBars")||this.attr_("customBars"))){return}var g=0;for(var k in this.datasets){if(!this.datasets.hasOwnProperty(k)){continue}var f=0;var e=this.datasets[k];var d=this.dygraph_.axisPropertiesForSeries(k);for(f=0;f<e.length;f++,g++){var n=e[f];var b=parseFloat(n[0]);var l=parseFloat(n[1]);if(b==this.points[g].xval&&l==this.points[g].yval){var h=parseFloat(n[2]);var c=parseFloat(n[3]);var m=l-h;var a=l+c;this.points[g].y_top=DygraphLayout._calcYNormal(d,m);this.points[g].y_bottom=DygraphLayout._calcYNormal(d,a)}}}};DygraphLayout.prototype._evaluateAnnotations=function(){var d;var f={};for(d=0;d<this.annotations.length;d++){var b=this.annotations[d];f[b.xval+","+b.series]=b}this.annotated_points=[];if(!this.annotations||!this.annotations.length){return}for(d=0;d<this.points.length;d++){var e=this.points[d];var c=e.xval+","+e.name;if(c in f){e.annotation=f[c];this.annotated_points.push(e)}}};DygraphLayout.prototype.removeAllDatasets=function(){delete this.datasets;this.datasets=[]};DygraphLayout.prototype.unstackPointAtIndex=function(b){var a=this.points[b];var d={};for(var e in a){d[e]=a[e]}if(!this.attr_("stackedGraph")){return d}for(var c=b+1;c<this.points.length;c++){if(this.points[c].xval==a.xval){d.yval-=this.points[c].yval;break}}return d};"use strict";var DygraphCanvasRenderer=function(d,c,b,e){this.dygraph_=d;this.layout=e;this.element=c;this.elementContext=b;this.container=this.element.parentNode;this.height=this.element.height;this.width=this.element.width;if(!this.isIE&&!(DygraphCanvasRenderer.isSupported(this.element))){throw"Canvas is not supported."}this.xlabels=[];this.ylabels=[];this.annotations=[];this.chartLabels={};this.area=e.getPlotArea();this.container.style.position="relative";this.container.style.width=this.width+"px";if(this.dygraph_.isUsingExcanvas_){this._createIEClipArea()}else{if(!Dygraph.isAndroid()){var a=this.dygraph_.canvas_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip();a=this.dygraph_.hidden_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip()}}};DygraphCanvasRenderer.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphCanvasRenderer.prototype.clear=function(){var c;if(this.isIE){try{if(this.clearDelay){this.clearDelay.cancel();this.clearDelay=null}c=this.elementContext}catch(f){return}}c=this.elementContext;c.clearRect(0,0,this.width,this.height);function a(g){for(var e=0;e<g.length;e++){var h=g[e];if(h.parentNode){h.parentNode.removeChild(h)}}}a(this.xlabels);a(this.ylabels);a(this.annotations);for(var b in this.chartLabels){if(!this.chartLabels.hasOwnProperty(b)){continue}var d=this.chartLabels[b];if(d.parentNode){d.parentNode.removeChild(d)}}this.xlabels=[];this.ylabels=[];this.annotations=[];this.chartLabels={}};DygraphCanvasRenderer.isSupported=function(f){var b=null;try{if(typeof(f)=="undefined"||f===null){b=document.createElement("canvas")}else{b=f}b.getContext("2d")}catch(c){var d=navigator.appVersion.match(/MSIE (\d\.\d)/);var a=(navigator.userAgent.toLowerCase().indexOf("opera")!=-1);if((!d)||(d[1]<6)||(a)){return false}return true}return true};DygraphCanvasRenderer.prototype.setColors=function(a){this.colorScheme_=a};DygraphCanvasRenderer.prototype.render=function(){var b=this.elementContext;function c(h){return Math.round(h)+0.5}function g(h){return Math.round(h)-0.5}if(this.attr_("underlayCallback")){this.attr_("underlayCallback")(b,this.area,this.dygraph_,this.dygraph_)}var a,f,d,e;if(this.attr_("drawYGrid")){e=this.layout.yticks;b.save();b.strokeStyle=this.attr_("gridLineColor");b.lineWidth=this.attr_("gridLineWidth");for(d=0;d<e.length;d++){if(e[d][0]!==0){continue}a=c(this.area.x);f=g(this.area.y+e[d][1]*this.area.h);b.beginPath();b.moveTo(a,f);b.lineTo(a+this.area.w,f);b.closePath();b.stroke()}}if(this.attr_("drawXGrid")){e=this.layout.xticks;b.save();b.strokeStyle=this.attr_("gridLineColor");b.lineWidth=this.attr_("gridLineWidth");for(d=0;d<e.length;d++){a=c(this.area.x+e[d][0]*this.area.w);f=g(this.area.y+this.area.h);b.beginPath();b.moveTo(a,f);b.lineTo(a,this.area.y);b.closePath();b.stroke()}}this._renderLineChart();this._renderAxis();this._renderChartLabels();this._renderAnnotations()};DygraphCanvasRenderer.prototype._createIEClipArea=function(){var g="dygraph-clip-div";var f=this.dygraph_.graphDiv;for(var e=f.childNodes.length-1;e>=0;e--){if(f.childNodes[e].className==g){f.removeChild(f.childNodes[e])}}var c=document.bgColor;var d=this.dygraph_.graphDiv;while(d!=document){var a=d.currentStyle.backgroundColor;if(a&&a!="transparent"){c=a;break}d=d.parentNode}function b(j){if(j.w===0||j.h===0){return}var i=document.createElement("div");i.className=g;i.style.backgroundColor=c;i.style.position="absolute";i.style.left=j.x+"px";i.style.top=j.y+"px";i.style.width=j.w+"px";i.style.height=j.h+"px";f.appendChild(i)}var h=this.area;b({x:0,y:0,w:h.x,h:this.height});b({x:h.x,y:0,w:this.width-h.x,h:h.y});b({x:h.x+h.w,y:0,w:this.width-h.x-h.w,h:this.height});b({x:h.x,y:h.y+h.h,w:this.width-h.x,h:this.height-h.h-h.y})};DygraphCanvasRenderer.prototype._renderAxis=function(){if(!this.attr_("drawXAxis")&&!this.attr_("drawYAxis")){return}function q(i){return Math.round(i)+0.5}function p(i){return Math.round(i)-0.5}var d=this.elementContext;var l,n,m,s,r;var a={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,color:this.attr_("axisLabelColor"),width:this.attr_("axisLabelWidth")+"px",lineHeight:"normal",overflow:"hidden"};var g=function(i,v,w){var x=document.createElement("div");for(var u in a){if(a.hasOwnProperty(u)){x.style[u]=a[u]}}var t=document.createElement("div");t.className="dygraph-axis-label dygraph-axis-label-"+v+(w?" dygraph-axis-label-"+w:"");t.innerHTML=i;x.appendChild(t);return x};d.save();d.strokeStyle=this.attr_("axisLineColor");d.lineWidth=this.attr_("axisLineWidth");if(this.attr_("drawYAxis")){if(this.layout.yticks&&this.layout.yticks.length>0){var b=this.dygraph_.numAxes();for(r=0;r<this.layout.yticks.length;r++){s=this.layout.yticks[r];if(typeof(s)=="function"){return}n=this.area.x;var j=1;var c="y1";if(s[0]==1){n=this.area.x+this.area.w;j=-1;c="y2"}m=this.area.y+s[1]*this.area.h;l=g(s[2],"y",b==2?c:null);var o=(m-this.attr_("axisLabelFontSize")/2);if(o<0){o=0}if(o+this.attr_("axisLabelFontSize")+3>this.height){l.style.bottom="0px"}else{l.style.top=o+"px"}if(s[0]===0){l.style.left=(this.area.x-this.attr_("yAxisLabelWidth")-this.attr_("axisTickSize"))+"px";l.style.textAlign="right"}else{if(s[0]==1){l.style.left=(this.area.x+this.area.w+this.attr_("axisTickSize"))+"px";l.style.textAlign="left"}}l.style.width=this.attr_("yAxisLabelWidth")+"px";this.container.appendChild(l);this.ylabels.push(l)}var h=this.ylabels[0];var e=this.attr_("axisLabelFontSize");var k=parseInt(h.style.top,10)+e;if(k>this.height-e){h.style.top=(parseInt(h.style.top,10)-e/2)+"px"}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y));d.lineTo(q(this.area.x),p(this.area.y+this.area.h));d.closePath();d.stroke();if(this.dygraph_.numAxes()==2){d.beginPath();d.moveTo(p(this.area.x+this.area.w),p(this.area.y));d.lineTo(p(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}}if(this.attr_("drawXAxis")){if(this.layout.xticks){for(r=0;r<this.layout.xticks.length;r++){s=this.layout.xticks[r];n=this.area.x+s[0]*this.area.w;m=this.area.y+this.area.h;l=g(s[1],"x");l.style.textAlign="center";l.style.top=(m+this.attr_("axisTickSize"))+"px";var f=(n-this.attr_("axisLabelWidth")/2);if(f+this.attr_("axisLabelWidth")>this.width){f=this.width-this.attr_("xAxisLabelWidth");l.style.textAlign="right"}if(f<0){f=0;l.style.textAlign="left"}l.style.left=f+"px";l.style.width=this.attr_("xAxisLabelWidth")+"px";this.container.appendChild(l);this.xlabels.push(l)}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y+this.area.h));d.lineTo(q(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}d.restore()};DygraphCanvasRenderer.prototype._renderChartLabels=function(){var d,a;if(this.attr_("title")){d=document.createElement("div");d.style.position="absolute";d.style.top="0px";d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("titleHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("titleHeight")-8)+"px";d.style.fontWeight="bold";a=document.createElement("div");a.className="dygraph-label dygraph-title";a.innerHTML=this.attr_("title");d.appendChild(a);this.container.appendChild(d);this.chartLabels.title=d}if(this.attr_("xlabel")){d=document.createElement("div");d.style.position="absolute";d.style.bottom=0;d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("xLabelHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("xLabelHeight")-2)+"px";a=document.createElement("div");a.className="dygraph-label dygraph-xlabel";a.innerHTML=this.attr_("xlabel");d.appendChild(a);this.container.appendChild(d);this.chartLabels.xlabel=d}var c=this;function b(h,g,f){var i={left:0,top:c.area.y,width:c.attr_("yLabelWidth"),height:c.area.h};d=document.createElement("div");d.style.position="absolute";if(h==1){d.style.left=i.left}else{d.style.right=i.left}d.style.top=i.top+"px";d.style.width=i.width+"px";d.style.height=i.height+"px";d.style.fontSize=(c.attr_("yLabelWidth")-2)+"px";var e=document.createElement("div");e.style.position="absolute";e.style.width=i.height+"px";e.style.height=i.width+"px";e.style.top=(i.height/2-i.width/2)+"px";e.style.left=(i.width/2-i.height/2)+"px";e.style.textAlign="center";var j="rotate("+(h==1?"-":"")+"90deg)";e.style.transform=j;e.style.WebkitTransform=j;e.style.MozTransform=j;e.style.OTransform=j;e.style.msTransform=j;if(typeof(document.documentMode)!=="undefined"&&document.documentMode<9){e.style.filter="progid:DXImageTransform.Microsoft.BasicImage(rotation="+(h==1?"3":"1")+")";e.style.left="0px";e.style.top="0px"}a=document.createElement("div");a.className=g;a.innerHTML=f;e.appendChild(a);d.appendChild(e);return d}var d;if(this.attr_("ylabel")){d=b(1,"dygraph-label dygraph-ylabel",this.attr_("ylabel"));this.container.appendChild(d);this.chartLabels.ylabel=d}if(this.attr_("y2label")&&this.dygraph_.numAxes()==2){d=b(2,"dygraph-label dygraph-y2label",this.attr_("y2label"));this.container.appendChild(d);this.chartLabels.y2label=d}};DygraphCanvasRenderer.prototype._renderAnnotations=function(){var h={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,overflow:"hidden"};var j=function(i,q,r,a){return function(s){var p=r.annotation;if(p.hasOwnProperty(i)){p[i](p,r,a.dygraph_,s)}else{if(a.dygraph_.attr_(q)){a.dygraph_.attr_(q)(p,r,a.dygraph_,s)}}}};var m=this.layout.annotated_points;for(var g=0;g<m.length;g++){var e=m[g];if(e.canvasx<this.area.x||e.canvasx>this.area.x+this.area.w){continue}var k=e.annotation;var l=6;if(k.hasOwnProperty("tickHeight")){l=k.tickHeight}var c=document.createElement("div");for(var b in h){if(h.hasOwnProperty(b)){c.style[b]=h[b]}}if(!k.hasOwnProperty("icon")){c.className="dygraphDefaultAnnotation"}if(k.hasOwnProperty("cssClass")){c.className+=" "+k.cssClass}var d=k.hasOwnProperty("width")?k.width:16;var n=k.hasOwnProperty("height")?k.height:16;if(k.hasOwnProperty("icon")){var f=document.createElement("img");f.src=k.icon;f.width=d;f.height=n;c.appendChild(f)}else{if(e.annotation.hasOwnProperty("shortText")){c.appendChild(document.createTextNode(e.annotation.shortText))}}c.style.left=(e.canvasx-d/2)+"px";if(k.attachAtBottom){c.style.top=(this.area.h-n-l)+"px"}else{c.style.top=(e.canvasy-n-l)+"px"}c.style.width=d+"px";c.style.height=n+"px";c.title=e.annotation.text;c.style.color=this.colors[e.name];c.style.borderColor=this.colors[e.name];k.div=c;Dygraph.addEvent(c,"click",j("clickHandler","annotationClickHandler",e,this));Dygraph.addEvent(c,"mouseover",j("mouseOverHandler","annotationMouseOverHandler",e,this));Dygraph.addEvent(c,"mouseout",j("mouseOutHandler","annotationMouseOutHandler",e,this));Dygraph.addEvent(c,"dblclick",j("dblClickHandler","annotationDblClickHandler",e,this));this.container.appendChild(c);this.annotations.push(c);var o=this.elementContext;o.strokeStyle=this.colors[e.name];o.beginPath();if(!k.attachAtBottom){o.moveTo(e.canvasx,e.canvasy);o.lineTo(e.canvasx,e.canvasy-2-l)}else{o.moveTo(e.canvasx,this.area.h);o.lineTo(e.canvasx,this.area.h-2-l)}o.closePath();o.stroke()}};DygraphCanvasRenderer.prototype._renderLineChart=function(){var G=function(i){return(i===null||isNaN(i))};var M=this.elementContext;var m=this.attr_("fillAlpha");var C=this.attr_("errorBars")||this.attr_("customBars");var k=this.attr_("fillGraph");var w=this.attr_("stackedGraph");var v=this.attr_("stepPlot");var A=this.layout.points;var z=A.length;var u,K,I,b,a,f,t,F,n,D,h,d,r;var E=[];for(var l in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(l)){E.push(l)}}var x=E.length;this.colors={};for(K=0;K<x;K++){this.colors[E[K]]=this.colorScheme_[K%this.colorScheme_.length]}for(K=z;K--;){u=A[K];u.canvasx=this.area.w*u.x+this.area.x;u.canvasy=this.area.h*u.y+this.area.y}var q=M;if(C){if(k){this.dygraph_.warn("Can't use fillGraph option with error bars")}for(K=0;K<x;K++){F=E[K];r=this.dygraph_.axisPropertiesForSeries(F);t=this.colors[F];q.save();b=NaN;a=NaN;f=[-1,-1];d=r.yscale;h=new RGBColor(t);D="rgba("+h.r+","+h.g+","+h.b+","+m+")";q.fillStyle=D;q.beginPath();for(I=0;I<z;I++){u=A[I];if(u.name==F){if(!Dygraph.isOK(u.y)){b=NaN;continue}if(v){n=[u.y_bottom,u.y_top];a=u.y}else{n=[u.y_bottom,u.y_top]}n[0]=this.area.h*n[0]+this.area.y;n[1]=this.area.h*n[1]+this.area.y;if(!isNaN(b)){if(v){q.moveTo(b,n[0])}else{q.moveTo(b,f[0])}q.lineTo(u.canvasx,n[0]);q.lineTo(u.canvasx,n[1]);if(v){q.lineTo(b,n[1])}else{q.lineTo(b,f[1])}q.closePath()}f=n;b=u.canvasx}}q.fill()}}else{if(k){var H=[];for(K=x-1;K>=0;K--){F=E[K];t=this.colors[F];r=this.dygraph_.axisPropertiesForSeries(F);var e=1+r.minyval*r.yscale;if(e<0){e=0}else{if(e>1){e=1}}e=this.area.h*e+this.area.y;q.save();b=NaN;f=[-1,-1];d=r.yscale;h=new RGBColor(t);D="rgba("+h.r+","+h.g+","+h.b+","+m+")";q.fillStyle=D;q.beginPath();for(I=0;I<z;I++){u=A[I];if(u.name==F){if(!Dygraph.isOK(u.y)){b=NaN;continue}if(w){var g=H[u.canvasx];if(g===undefined){g=e}H[u.canvasx]=u.canvasy;n=[u.canvasy,g]}else{n=[u.canvasy,e]}if(!isNaN(b)){q.moveTo(b,f[0]);if(v){q.lineTo(u.canvasx,f[0])}else{q.lineTo(u.canvasx,n[0])}q.lineTo(u.canvasx,n[1]);q.lineTo(b,f[1]);q.closePath()}f=n;b=u.canvasx}}q.fill()}}}var L=0;var o=0;var c=0;for(K=0;K<x;K+=1){c=this.layout.setPointsLengths[K];o+=c;F=E[K];t=this.colors[F];var y=this.dygraph_.attr_("strokeWidth",F);M.save();var B=this.dygraph_.attr_("pointSize",F);b=null;a=null;var p=this.dygraph_.attr_("drawPoints",F);var s=this.dygraph_.attr_("strokePattern",F);if(!Dygraph.isArrayLike(s)){s=null}for(I=L;I<o;I++){u=A[I];if(G(u.canvasy)){if(v&&b!==null){q.beginPath();q.strokeStyle=t;q.lineWidth=this.attr_("strokeWidth");this._dashedLine(q,b,a,u.canvasx,a,s);q.stroke()}b=a=null}else{var J=(!b&&(I==A.length-1||G(A[I+1].canvasy)));if(b===null){b=u.canvasx;a=u.canvasy}else{if(Math.round(b)==Math.round(u.canvasx)&&Math.round(a)==Math.round(u.canvasy)){continue}if(y){q.beginPath();q.strokeStyle=t;q.lineWidth=y;if(v){this._dashedLine(q,b,a,u.canvasx,a,s)}this._dashedLine(q,b,a,u.canvasx,u.canvasy,s);b=u.canvasx;a=u.canvasy;q.stroke()}}if(p||J){q.beginPath();q.fillStyle=t;q.arc(u.canvasx,u.canvasy,B,0,2*Math.PI,false);q.fill()}}}L=o}M.restore()};DygraphCanvasRenderer.prototype._dashedLine=function(j,i,g,a,h,f){var l,k,e,b,c,d;if(!f||f.length<=1){j.moveTo(i,g);j.lineTo(a,h);return}if(!Dygraph.compareArrays(f,this._dashedLineToHistoryPattern)){this._dashedLineToHistoryPattern=f;this._dashedLineToHistory=[0,0]}j.save();l=(a-i);k=(h-g);e=Math.sqrt(l*l+k*k);b=Math.atan2(k,l);j.translate(i,g);j.moveTo(0,0);j.rotate(b);c=this._dashedLineToHistory[0];i=0;while(e>i){d=f[c];if(this._dashedLineToHistory[1]){i+=this._dashedLineToHistory[1]}else{i+=d}if(i>e){this._dashedLineToHistory=[c,i-e];i=e}else{this._dashedLineToHistory=[(c+1)%f.length,0]}if(c%2===0){j.lineTo(i,0)}else{j.moveTo(i,0)}c=(c+1)%f.length}j.restore()};"use strict";var Dygraph=function(c,b,a){if(arguments.length>0){if(arguments.length==4){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.2";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.ANIMATION_STEPS=10;Dygraph.ANIMATION_DURATION=200;Dygraph.numberValueFormatter=function(a,e,h,d){var b=e("sigFigs");if(b!==null){return Dygraph.floatFormat(a,b)}var f=e("digitsAfterDecimal");var c=e("maxNumberWidth");if(a!==0&&(Math.abs(a)>=Math.pow(10,c)||Math.abs(a)<Math.pow(10,-f))){return a.toExponential(f)}else{return""+Dygraph.round_(a,f)}};Dygraph.numberAxisLabelFormatter=function(a,d,c,b){return Dygraph.numberValueFormatter(a,c,b)};Dygraph.dateString_=function(e){var i=Dygraph.zeropad;var h=new Date(e);var f=""+h.getFullYear();var g=i(h.getMonth()+1);var a=i(h.getDate());var c="";var b=h.getHours()*3600+h.getMinutes()*60+h.getSeconds();if(b){c=" "+Dygraph.hmsString_(e)}return f+"/"+g+"/"+a+c};Dygraph.dateAxisFormatter=function(b,c){if(c>=Dygraph.DECADAL){return b.strftime("%Y")}else{if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a===0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}}};Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,digitsAfterDecimal:2,maxNumberWidth:6,sigFigs:null,strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueParser:Dygraph.dateParser,delimiter:",",sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true,legend:"onmouseover",stepPlot:false,avoidMinZero:false,titleHeight:28,xLabelHeight:18,yLabelWidth:18,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.3,gridLineWidth:0.3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",interactionModel:null,animatedZooms:false,showRangeSelector:false,rangeSelectorHeight:40,rangeSelectorPlotStrokeColor:"#808FAB",rangeSelectorPlotFillColor:"#A7B1C4",axes:{x:{pixelsPerLabel:60,axisLabelFormatter:Dygraph.dateAxisFormatter,valueFormatter:Dygraph.dateString_,ticker:null},y:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null},y2:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null}}};Dygraph.HORIZONTAL=1;Dygraph.VERTICAL=2;Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!==null){var a=["Date"];for(var c=0;c<e.length;c++){a.push(e[c])}Dygraph.update(b,{labels:a})}this.__init__(f,d,b)};Dygraph.prototype.__init__=function(d,c,b){if(/MSIE/.test(navigator.userAgent)&&!window.opera&&typeof(G_vmlCanvasManager)!="undefined"&&document.readyState!="complete"){var a=this;setTimeout(function(){a.__init__(d,c,b)},100);return}if(b===null||b===undefined){b={}}b=Dygraph.mapLegacyOptions_(b);if(!d){Dygraph.error("Constructing dygraph with a non-existent div!");return}this.isUsingExcanvas_=typeof(G_vmlCanvasManager)!="undefined";this.maindiv_=d;this.file_=c;this.rollPeriod_=b.rollPeriod||Dygraph.DEFAULT_ROLL_PERIOD;this.previousVerticalX_=-1;this.fractions_=b.fractions||false;this.dateWindow_=b.dateWindow||null;this.is_initial_draw_=true;this.annotations_=[];this.zoomed_x_=false;this.zoomed_y_=false;d.innerHTML="";if(d.style.width===""&&b.width){d.style.width=b.width+"px"}if(d.style.height===""&&b.height){d.style.height=b.height+"px"}if(d.style.height===""&&d.clientHeight===0){d.style.height=Dygraph.DEFAULT_HEIGHT+"px";if(d.style.width===""){d.style.width=Dygraph.DEFAULT_WIDTH+"px"}}this.width_=d.clientWidth;this.height_=d.clientHeight;if(b.stackedGraph){b.fillGraph=true}this.user_attrs_={};Dygraph.update(this.user_attrs_,b);this.attrs_={};Dygraph.updateDeep(this.attrs_,Dygraph.DEFAULT_ATTRS);this.boundaryIds_=[];this.createInterface_();this.start_()};Dygraph.prototype.isZoomed=function(a){if(a==null){return this.zoomed_x_||this.zoomed_y_}if(a==="x"){return this.zoomed_x_}if(a==="y"){return this.zoomed_y_}throw"axis parameter is ["+a+"] must be null, 'x' or 'y'."};Dygraph.prototype.toString=function(){var a=this.maindiv_;var b=(a&&a.id)?a.id:a;return"[Dygraph "+b+"]"};Dygraph.prototype.attr_=function(b,a){if(a&&typeof(this.user_attrs_[a])!="undefined"&&this.user_attrs_[a]!==null&&typeof(this.user_attrs_[a][b])!="undefined"){return this.user_attrs_[a][b]}else{if(typeof(this.user_attrs_[b])!="undefined"){return this.user_attrs_[b]}else{if(typeof(this.attrs_[b])!="undefined"){return this.attrs_[b]}else{return null}}}};Dygraph.prototype.optionsViewForAxis_=function(b){var a=this;return function(c){var d=a.user_attrs_.axes;if(d&&d[b]&&d[b][c]){return d[b][c]}if(typeof(a.user_attrs_[c])!="undefined"){return a.user_attrs_[c]}d=a.attrs_.axes;if(d&&d[b]&&d[b][c]){return d[b][c]}if(b=="y"&&a.axes_[0].hasOwnProperty(c)){return a.axes_[0][c]}else{if(b=="y2"&&a.axes_[1].hasOwnProperty(c)){return a.axes_[1][c]}}return a.attr_(c)}};Dygraph.prototype.rollPeriod=function(){return this.rollPeriod_};Dygraph.prototype.xAxisRange=function(){return this.dateWindow_?this.dateWindow_:this.xAxisExtremes()};Dygraph.prototype.xAxisExtremes=function(){var b=this.rawData_[0][0];var a=this.rawData_[this.rawData_.length-1][0];return[b,a]};Dygraph.prototype.yAxisRange=function(a){if(typeof(a)=="undefined"){a=0}if(a<0||a>=this.axes_.length){return null}var b=this.axes_[a];return[b.computedValueRange[0],b.computedValueRange[1]]};Dygraph.prototype.yAxisRanges=function(){var a=[];for(var b=0;b<this.axes_.length;b++){a.push(this.yAxisRange(b))}return a};Dygraph.prototype.toDomCoords=function(a,c,b){return[this.toDomXCoord(a),this.toDomYCoord(c,b)]};Dygraph.prototype.toDomXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return c.x+(b-a[0])/(a[1]-a[0])*c.w};Dygraph.prototype.toDomYCoord=function(d,a){var c=this.toPercentYCoord(d,a);if(c===null){return null}var b=this.plotter_.area;return b.y+c*b.h};Dygraph.prototype.toDataCoords=function(a,c,b){return[this.toDataXCoord(a),this.toDataYCoord(c,b)]};Dygraph.prototype.toDataXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return a[0]+(b-c.x)/c.w*(a[1]-a[0])};Dygraph.prototype.toDataYCoord=function(h,b){if(h===null){return null}var c=this.plotter_.area;var g=this.yAxisRange(b);if(typeof(b)=="undefined"){b=0}if(!this.axes_[b].logscale){return g[0]+(c.y+c.h-h)/c.h*(g[1]-g[0])}else{var f=(h-c.y)/c.h;var a=Dygraph.log10(g[1]);var e=a-(f*(a-Dygraph.log10(g[0])));var d=Math.pow(Dygraph.LOG_SCALE,e);return d}};Dygraph.prototype.toPercentYCoord=function(e,b){if(e===null){return null}if(typeof(b)=="undefined"){b=0}var d=this.yAxisRange(b);var c;if(!this.axes_[b].logscale){c=(d[1]-e)/(d[1]-d[0])}else{var a=Dygraph.log10(d[1]);c=(a-Dygraph.log10(e))/(a-Dygraph.log10(d[0]))}return c};Dygraph.prototype.toPercentXCoord=function(b){if(b===null){return null}var a=this.xAxisRange();return(b-a[0])/(a[1]-a[0])};Dygraph.prototype.numColumns=function(){return this.rawData_[0]?this.rawData_[0].length:this.attr_("labels").length};Dygraph.prototype.numRows=function(){return this.rawData_.length};Dygraph.prototype.fullXRange_=function(){if(this.numRows()>0){return[this.rawData_[0][0],this.rawData_[this.numRows()-1][0]]}else{return[0,1]}};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";a.appendChild(this.graphDiv);this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.canvas_ctx_=Dygraph.getContext(this.canvas_);this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.hidden_ctx_=Dygraph.getContext(this.hidden_);if(this.attr_("showRangeSelector")){this.rangeSelector_=new DygraphRangeSelector(this)}this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.createMouseEventElement_();this.layout_=new DygraphLayout(this);if(this.rangeSelector_){this.rangeSelector_.addToGraph(this.graphDiv,this.layout_)}var b=this;Dygraph.addEvent(this.mouseEventElement_,"mousemove",function(c){b.mouseMove_(c)});Dygraph.addEvent(this.mouseEventElement_,"mouseout",function(c){b.mouseOut_(c)});this.createStatusMessage_();this.createDragInterface_();this.resizeHandler=function(c){b.resize()};Dygraph.addEvent(window,"resize",this.resizeHandler)};Dygraph.prototype.destroy=function(){var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};Dygraph.removeEvent(window,"resize",this.resizeHandler);this.resizeHandler=null;b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.prototype.createMouseEventElement_=function(){if(this.isUsingExcanvas_){var a=document.createElement("div");a.style.position="absolute";a.style.backgroundColor="white";a.style.filter="alpha(opacity=0)";a.style.width=this.width_+"px";a.style.height=this.height_+"px";this.graphDiv.appendChild(a);return a}else{return this.canvas_}};Dygraph.prototype.setColors_=function(){var e=this.attr_("labels").length-1;this.colors_=[];var a=this.attr_("colors");var d;if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var j=Math.ceil(e/2);for(d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var g=d%2?Math.ceil(d/2):(j+d/2);var f=(1*g/(1+e));this.colors_.push(Dygraph.hsvToRGB(f,c,b))}}else{for(d=0;d<e;d++){if(!this.visibility()[d]){continue}var h=a[d%a.length];this.colors_.push(h)}}this.plotter_.setColors(this.colors_)};Dygraph.prototype.getColors=function(){return this.colors_};Dygraph.prototype.createStatusMessage_=function(){var d=this.user_attrs_.labelsDiv;if(d&&null!==d&&(typeof(d)=="string"||d instanceof String)){this.user_attrs_.labelsDiv=document.getElementById(d)}if(!this.attr_("labelsDiv")){var a=this.attr_("labelsDivWidth");var c={position:"absolute",fontSize:"14px",zIndex:10,width:a+"px",top:"0px",left:(this.width_-a-2)+"px",background:"white",textAlign:"left",overflow:"hidden"};Dygraph.update(c,this.attr_("labelsDivStyles"));var e=document.createElement("div");e.className="dygraph-legend";for(var b in c){if(c.hasOwnProperty(b)){e.style[b]=c[b]}}this.graphDiv.appendChild(e);this.attrs_.labelsDiv=e}};Dygraph.prototype.positionLabelsDiv_=function(){if(this.user_attrs_.hasOwnProperty("labelsDiv")){return}var a=this.plotter_.area;var b=this.attr_("labelsDiv");b.style.left=a.x+a.w-this.attr_("labelsDivWidth")-1+"px";b.style.top=a.y+"px"};Dygraph.prototype.createRollInterface_=function(){if(!this.roller_){this.roller_=document.createElement("input");this.roller_.type="text";this.roller_.style.display="none";this.graphDiv.appendChild(this.roller_)}var e=this.attr_("showRoller")?"block":"none";var d=this.plotter_.area;var b={position:"absolute",zIndex:10,top:(d.y+d.h-25)+"px",left:(d.x+1)+"px",display:e};this.roller_.size="2";this.roller_.value=this.rollPeriod_;for(var a in b){if(b.hasOwnProperty(a)){this.roller_.style[a]=b[a]}}var c=this;this.roller_.onchange=function(){c.adjustRoll(c.roller_.value)}};Dygraph.prototype.dragGetX_=function(b,a){return Dygraph.pageX(b)-a.px};Dygraph.prototype.dragGetY_=function(b,a){return Dygraph.pageY(b)-a.py};Dygraph.prototype.createDragInterface_=function(){var c={isZooming:false,isPanning:false,is2DPan:false,dragStartX:null,dragStartY:null,dragEndX:null,dragEndY:null,dragDirection:null,prevEndX:null,prevEndY:null,prevDragDirection:null,initialLeftmostDate:null,xUnitsPerPixel:null,dateRange:null,px:0,py:0,boundedDates:null,boundedValues:null,initializeMouseDown:function(i,h,f){if(i.preventDefault){i.preventDefault()}else{i.returnValue=false;i.cancelBubble=true}f.px=Dygraph.findPosX(h.canvas_);f.py=Dygraph.findPosY(h.canvas_);f.dragStartX=h.dragGetX_(i,f);f.dragStartY=h.dragGetY_(i,f)}};var e=this.attr_("interactionModel");var b=this;var d=function(f){return function(g){f(g,b,c)}};for(var a in e){if(!e.hasOwnProperty(a)){continue}Dygraph.addEvent(this.mouseEventElement_,a,d(e[a]))}Dygraph.addEvent(document,"mouseup",function(g){if(c.isZooming||c.isPanning){c.isZooming=false;c.dragStartX=null;c.dragStartY=null}if(c.isPanning){c.isPanning=false;c.draggingDate=null;c.dateRange=null;for(var f=0;f<b.axes_.length;f++){delete b.axes_[f].draggingValue;delete b.axes_[f].dragValueRange}}})};Dygraph.prototype.drawZoomRect_=function(e,c,i,b,g,a,f,d){var h=this.canvas_ctx_;if(a==Dygraph.HORIZONTAL){h.clearRect(Math.min(c,f),this.layout_.getPlotArea().y,Math.abs(c-f),this.layout_.getPlotArea().h)}else{if(a==Dygraph.VERTICAL){h.clearRect(this.layout_.getPlotArea().x,Math.min(b,d),this.layout_.getPlotArea().w,Math.abs(b-d))}}if(e==Dygraph.HORIZONTAL){if(i&&c){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(Math.min(c,i),this.layout_.getPlotArea().y,Math.abs(i-c),this.layout_.getPlotArea().h)}}else{if(e==Dygraph.VERTICAL){if(g&&b){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(this.layout_.getPlotArea().x,Math.min(b,g),this.layout_.getPlotArea().w,Math.abs(g-b))}}}if(this.isUsingExcanvas_){this.currentZoomRectArgs_=[e,c,i,b,g,0,0,0]}};Dygraph.prototype.clearZoomRect_=function(){this.currentZoomRectArgs_=null;this.canvas_ctx_.clearRect(0,0,this.canvas_.width,this.canvas_.height)};Dygraph.prototype.doZoomX_=function(c,a){this.currentZoomRectArgs_=null;var b=this.toDataXCoord(c);var d=this.toDataXCoord(a);this.doZoomXDates_(b,d)};Dygraph.zoomAnimationFunction=function(c,b){var a=1.5;return(1-Math.pow(a,-c))/(1-Math.pow(a,-b))};Dygraph.prototype.doZoomXDates_=function(c,e){var a=this.xAxisRange();var d=[c,e];this.zoomed_x_=true;var b=this;this.doAnimatedZoom(a,d,null,null,function(){if(b.attr_("zoomCallback")){b.attr_("zoomCallback")(c,e,b.yAxisRanges())}})};Dygraph.prototype.doZoomY_=function(h,f){this.currentZoomRectArgs_=null;var c=this.yAxisRanges();var b=[];for(var e=0;e<this.axes_.length;e++){var d=this.toDataYCoord(h,e);var a=this.toDataYCoord(f,e);b.push([a,d])}this.zoomed_y_=true;var g=this;this.doAnimatedZoom(null,null,c,b,function(){if(g.attr_("zoomCallback")){var i=g.xAxisRange();g.attr_("zoomCallback")(i[0],i[1],g.yAxisRanges())}})};Dygraph.prototype.doUnzoom_=function(){var c=false,d=false,a=false;if(this.dateWindow_!==null){c=true;d=true}for(var f=0;f<this.axes_.length;f++){if(this.axes_[f].valueWindow!==null){c=true;a=true}}this.clearSelection();if(c){this.zoomed_x_=false;this.zoomed_y_=false;var e=this.rawData_[0][0];var b=this.rawData_[this.rawData_.length-1][0];if(!this.attr_("animatedZooms")){this.dateWindow_=null;for(f=0;f<this.axes_.length;f++){if(this.axes_[f].valueWindow!==null){delete this.axes_[f].valueWindow}}this.drawGraph_();if(this.attr_("zoomCallback")){this.attr_("zoomCallback")(e,b,this.yAxisRanges())}return}var k=null,l=null,j=null,g=null;if(d){k=this.xAxisRange();l=[e,b]}if(a){j=this.yAxisRanges();var m=this.gatherDatasets_(this.rolledSeries_,null);var n=m[1];this.computeYAxisRanges_(n);g=[];for(f=0;f<this.axes_.length;f++){g.push(this.axes_[f].extremeRange)}}var h=this;this.doAnimatedZoom(k,l,j,g,function(){h.dateWindow_=null;for(var o=0;o<h.axes_.length;o++){if(h.axes_[o].valueWindow!==null){delete h.axes_[o].valueWindow}}if(h.attr_("zoomCallback")){h.attr_("zoomCallback")(e,b,h.yAxisRanges())}})}};Dygraph.prototype.doAnimatedZoom=function(a,e,b,c,m){var i=this.attr_("animatedZooms")?Dygraph.ANIMATION_STEPS:1;var l=[];var k=[];var f,d;if(a!==null&&e!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);l[f-1]=[a[0]*(1-d)+d*e[0],a[1]*(1-d)+d*e[1]]}}if(b!==null&&c!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);var n=[];for(var g=0;g<this.axes_.length;g++){n.push([b[g][0]*(1-d)+d*c[g][0],b[g][1]*(1-d)+d*c[g][1]])}k[f-1]=n}}var h=this;Dygraph.repeatAndCleanup(function(p){if(k.length){for(var o=0;o<h.axes_.length;o++){var j=k[p][o];h.axes_[o].valueWindow=[j[0],j[1]]}}if(l.length){h.dateWindow_=l[p]}h.drawGraph_()},i,Dygraph.ANIMATION_DURATION/i,m)};Dygraph.prototype.mouseMove_=function(b){var r=this.layout_.points;if(r===undefined){return}var a=Dygraph.pageX(b)-Dygraph.findPosX(this.mouseEventElement_);var j=-1;var f;var n=1e+100;var o=-1;for(f=0;f<r.length;f++){var q=r[f];if(q===null){continue}var h=Math.abs(q.canvasx-a);if(h>n){continue}n=h;o=f}if(o>=0){j=r[o].xval}this.selPoints_=[];var d=r.length;if(!this.attr_("stackedGraph")){for(f=0;f<d;f++){if(r[f].xval==j){this.selPoints_.push(r[f])}}}else{var g=0;for(f=d-1;f>=0;f--){if(r[f].xval==j){var c={};for(var e in r[f]){c[e]=r[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}this.selPoints_.reverse()}if(this.attr_("highlightCallback")){var m=this.lastx_;if(m!==null&&j!=m){this.attr_("highlightCallback")(b,j,this.selPoints_,this.idxToRow_(o))}}this.lastx_=j;this.updateSelection_()};Dygraph.prototype.idxToRow_=function(a){if(a<0){return -1}var d=-1;for(var c=0;c<this.boundaryIds_.length;c++){if(this.boundaryIds_[c]!==undefined){d=c;break}}if(d<0){return -1}for(var b in this.layout_.datasets){if(a<this.layout_.datasets[b].length){return this.boundaryIds_[d][0]+a}a-=this.layout_.datasets[b].length}return -1};Dygraph.prototype.generateLegendDashHTML_=function(o,d,n){var h="";var f,e,b,k;var c=0,m=0;var l=[];var g;var a=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(a){return"&mdash;"}if(!o||o.length<=1){h='<div style="display: inline-block; position: relative; bottom: .5ex; padding-left: 1em; height: 1px; border-bottom: 2px solid '+d+';"></div>'}else{for(f=0;f<=o.length;f++){c+=o[f%o.length]}g=Math.floor(n/(c-o[0]));if(g>1){for(f=0;f<o.length;f++){l[f]=o[f]/n}m=l.length}else{g=1;for(f=0;f<o.length;f++){l[f]=o[f]/c}m=l.length+1}for(e=0;e<g;e++){for(f=0;f<m;f+=2){b=l[f%l.length];if(f<o.length){k=l[(f+1)%l.length]}else{k=0}h+='<div style="display: inline-block; position: relative; bottom: .5ex; margin-right: '+k+"em; padding-left: "+b+"em; height: 1px; border-bottom: 2px solid "+d+';"></div>'}}}return h};Dygraph.prototype.generateLegendHTML_=function(k,f,b){var l,u,o,s,m,g;if(typeof(k)==="undefined"){if(this.attr_("legend")!="always"){return""}u=this.attr_("labelsSeparateLines");var r=this.attr_("labels");l="";for(o=1;o<r.length;o++){if(!this.visibility()[o-1]){continue}s=this.plotter_.colors[r[o]];if(l!==""){l+=(u?"<br/>":" ")}g=this.attr_("strokePattern",r[o]);m=this.generateLegendDashHTML_(g,s,b);l+="<span style='font-weight: bold; color: "+s+";'>"+m+" "+r[o]+"</span>"}return l}var t=this.optionsViewForAxis_("x");var h=t("valueFormatter");l=h(k,t,this.attr_("labels")[0],this)+":";var p=[];var d=this.numAxes();for(o=0;o<d;o++){p[o]=this.optionsViewForAxis_("y"+(o?1+o:""))}var e=this.attr_("labelsShowZeroValues");u=this.attr_("labelsSeparateLines");for(o=0;o<this.selPoints_.length;o++){var n=this.selPoints_[o];if(n.yval===0&&!e){continue}if(!Dygraph.isOK(n.canvasy)){continue}if(u){l+="<br/>"}var j=p[this.seriesToAxisMap_[n.name]];var q=j("valueFormatter");s=this.plotter_.colors[n.name];var a=q(n.yval,j,n.name,this);l+=" <b><span style='color: "+s+";'>"+n.name+"</span></b>:"+a}return l};Dygraph.prototype.setLegendHTML_=function(b,e){var c=this.attr_("labelsDiv");var f=document.createElement("span");f.setAttribute("style","margin: 0; padding: 0 0 0 1em; border: 0;");c.appendChild(f);var a=f.offsetWidth;var d=this.generateLegendHTML_(b,e,a);if(c!==null){c.innerHTML=d}else{if(typeof(this.shown_legend_error_)=="undefined"){this.error("labelsDiv is set to something nonexistent; legend will not be shown.");this.shown_legend_error_=true}}};Dygraph.prototype.updateSelection_=function(){var d;var h=this.canvas_ctx_;if(this.previousVerticalX_>=0){var e=0;var f=this.attr_("labels");for(d=1;d<f.length;d++){var b=this.attr_("highlightCircleSize",f[d]);if(b>e){e=b}}var g=this.previousVerticalX_;h.clearRect(g-e-1,0,2*e+2,this.height_)}if(this.isUsingExcanvas_&&this.currentZoomRectArgs_){Dygraph.prototype.drawZoomRect_.apply(this,this.currentZoomRectArgs_)}if(this.selPoints_.length>0){if(this.attr_("showLabelsOnHighlight")){this.setLegendHTML_(this.lastx_,this.selPoints_)}var c=this.selPoints_[0].canvasx;h.save();for(d=0;d<this.selPoints_.length;d++){var j=this.selPoints_[d];if(!Dygraph.isOK(j.canvasy)){continue}var a=this.attr_("highlightCircleSize",j.name);h.beginPath();h.fillStyle=this.plotter_.colors[j.name];h.arc(c,j.canvasy,a,0,2*Math.PI,false);h.fill()}h.restore();this.previousVerticalX_=c}};Dygraph.prototype.setSelection=function(c){this.selPoints_=[];var d=0;if(c!==false){c=c-this.boundaryIds_[0][0]}if(c!==false&&c>=0){for(var b in this.layout_.datasets){if(c<this.layout_.datasets[b].length){var a=this.layout_.points[d+c];if(this.attr_("stackedGraph")){a=this.layout_.unstackPointAtIndex(d+c)}this.selPoints_.push(a)}d+=this.layout_.datasets[b].length}}if(this.selPoints_.length){this.lastx_=this.selPoints_[0].xval;this.updateSelection_()}else{this.clearSelection()}};Dygraph.prototype.mouseOut_=function(a){if(this.attr_("unhighlightCallback")){this.attr_("unhighlightCallback")(a)}if(this.attr_("hideOverlayOnMouseOut")){this.clearSelection()}};Dygraph.prototype.clearSelection=function(){this.canvas_ctx_.clearRect(0,0,this.width_,this.height_);this.setLegendHTML_();this.selPoints_=[];this.lastx_=-1};Dygraph.prototype.getSelection=function(){if(!this.selPoints_||this.selPoints_.length<1){return -1}for(var a=0;a<this.layout_.points.length;a++){if(this.layout_.points[a].x==this.selPoints_[0].x){return a+this.boundaryIds_[0][0]}}return -1};Dygraph.prototype.loadedEvent_=function(a){this.rawData_=this.parseCSV_(a);this.predraw_()};Dygraph.prototype.addXTicks_=function(){var a;if(this.dateWindow_){a=[this.dateWindow_[0],this.dateWindow_[1]]}else{a=this.fullXRange_()}var c=this.optionsViewForAxis_("x");var b=c("ticker")(a[0],a[1],this.width_,c,this);this.layout_.setXTicks(b)};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null,c,g;var b=this.attr_("errorBars")||this.attr_("customBars");if(b){for(c=0;c<d.length;c++){g=d[c][1][0];if(!g){continue}var a=g-d[c][1][1];var e=g+d[c][1][2];if(a>g){a=g}if(e<g){e=g}if(f===null||e>f){f=e}if(h===null||a<h){h=a}}}else{for(c=0;c<d.length;c++){g=d[c][1];if(g===null||isNaN(g)){continue}if(f===null||g>f){f=g}if(h===null||g<h){h=g}}}return[h,f]};Dygraph.prototype.predraw_=function(){var f=new Date();this.computeYAxes_();if(this.plotter_){this.plotter_.clear()}this.plotter_=new DygraphCanvasRenderer(this,this.hidden_,this.hidden_ctx_,this.layout_);this.createRollInterface_();this.positionLabelsDiv_();if(this.rangeSelector_){this.rangeSelector_.renderStaticLayer()}this.rolledSeries_=[null];for(var c=1;c<this.numColumns();c++){var e=this.attr_("connectSeparatedPoints",c);var d=this.attr_("logscale",c);var b=this.extractSeries_(this.rawData_,c,d,e);b=this.rollingAverage(b,this.rollPeriod_);this.rolledSeries_.push(b)}this.drawGraph_();var a=new Date();this.drawingTimeMs_=(a-f)};Dygraph.prototype.gatherDatasets_=function(w,c){var s=[];var b=[];var e=[];var a={};var u,t,r;var m=w.length-1;for(u=m;u>=1;u--){if(!this.visibility()[u-1]){continue}var h=[];for(t=0;t<w[u].length;t++){h.push(w[u][t])}var o=this.attr_("errorBars")||this.attr_("customBars");if(c){var A=c[0];var f=c[1];var p=[];var d=null,z=null;for(r=0;r<h.length;r++){if(h[r][0]>=A&&d===null){d=r}if(h[r][0]<=f){z=r}}if(d===null){d=0}if(d>0){d--}if(z===null){z=h.length-1}if(z<h.length-1){z++}s[u-1]=[d,z];for(r=d;r<=z;r++){p.push(h[r])}h=p}else{s[u-1]=[0,h.length-1]}var n=this.extremeValues_(h);if(o){for(t=0;t<h.length;t++){h[t]=[h[t][0],h[t][1][0],h[t][1][1],h[t][1][2]]}}else{if(this.attr_("stackedGraph")){var q=h.length;var y;for(t=0;t<q;t++){var g=h[t][0];if(b[g]===undefined){b[g]=0}y=h[t][1];b[g]+=y;h[t]=[g,b[g]];if(b[g]>n[1]){n[1]=b[g]}if(b[g]<n[0]){n[0]=b[g]}}}}var v=this.attr_("labels")[u];a[v]=n;e[u]=h}return[e,a,s]};Dygraph.prototype.drawGraph_=function(j){var a=new Date();if(typeof(j)==="undefined"){j=true}var e=this.is_initial_draw_;this.is_initial_draw_=false;this.layout_.removeAllDatasets();this.setColors_();this.attrs_.pointSize=0.5*this.attr_("highlightCircleSize");var g=this.gatherDatasets_(this.rolledSeries_,this.dateWindow_);var d=g[0];var h=g[1];this.boundaryIds_=g[2];for(var f=1;f<d.length;f++){if(!this.visibility()[f-1]){continue}this.layout_.addDataset(this.attr_("labels")[f],d[f])}this.computeYAxisRanges_(h);this.layout_.setYAxes(this.axes_);this.addXTicks_();var b=this.zoomed_x_;this.layout_.setDateWindow(this.dateWindow_);this.zoomed_x_=b;this.layout_.evaluateWithError();this.renderGraph_(e,false);if(this.attr_("timingName")){var c=new Date();if(console){console.log(this.attr_("timingName")+" - drawGraph: "+(c-a)+"ms")}}};Dygraph.prototype.renderGraph_=function(a,b){this.plotter_.clear();this.plotter_.render();this.canvas_.getContext("2d").clearRect(0,0,this.canvas_.width,this.canvas_.height);this.setLegendHTML_();if(!a){if(b){if(typeof(this.selPoints_)!=="undefined"&&this.selPoints_.length){this.clearSelection()}else{this.clearSelection()}}}if(this.rangeSelector_){this.rangeSelector_.renderInteractiveLayer()}if(this.attr_("drawCallback")!==null){this.attr_("drawCallback")(this,a)}};Dygraph.prototype.computeYAxes_=function(){var g,c,m,b,j,a,p;if(this.axes_!==undefined&&this.user_attrs_.hasOwnProperty("valueRange")===false){c=[];for(j=0;j<this.axes_.length;j++){c.push(this.axes_[j].valueWindow)}}this.axes_=[{yAxisId:0,g:this}];this.seriesToAxisMap_={};var h=this.attr_("labels");var f={};for(g=1;g<h.length;g++){f[h[g]]=(g-1)}var e=["includeZero","valueRange","labelsKMB","labelsKMG2","pixelsPerYLabel","yAxisLabelWidth","axisLabelFontSize","axisTickSize","logscale"];for(g=0;g<e.length;g++){var d=e[g];p=this.attr_(d);if(p){this.axes_[0][d]=p}}for(m in f){if(!f.hasOwnProperty(m)){continue}b=this.attr_("axis",m);if(b===null){this.seriesToAxisMap_[m]=0;continue}if(typeof(b)=="object"){a={};Dygraph.update(a,this.axes_[0]);Dygraph.update(a,{valueRange:null});var o=this.axes_.length;a.yAxisId=o;a.g=this;Dygraph.update(a,b);this.axes_.push(a);this.seriesToAxisMap_[m]=o}}for(m in f){if(!f.hasOwnProperty(m)){continue}b=this.attr_("axis",m);if(typeof(b)=="string"){if(!this.seriesToAxisMap_.hasOwnProperty(b)){this.error("Series "+m+" wants to share a y-axis with series "+b+", which does not define its own axis.");return null}var n=this.seriesToAxisMap_[b];this.seriesToAxisMap_[m]=n}}if(c!==undefined){for(j=0;j<c.length;j++){this.axes_[j].valueWindow=c[j]}}for(b=0;b<this.axes_.length;b++){if(b===0){a=this.optionsViewForAxis_("y"+(b?"2":""));p=a("valueRange");if(p){this.axes_[b].valueRange=p}}else{var l=this.user_attrs_.axes;if(l&&l.y2){p=l.y2.valueRange;if(p){this.axes_[b].valueRange=p}}}}};Dygraph.prototype.numAxes=function(){var c=0;for(var b in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(b)){continue}var a=this.seriesToAxisMap_[b];if(a>c){c=a}}return 1+c};Dygraph.prototype.axisPropertiesForSeries=function(a){return this.axes_[this.seriesToAxisMap_[a]]};Dygraph.prototype.computeYAxisRanges_=function(a){var g=[],h;for(h in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(h)){continue}var p=this.seriesToAxisMap_[h];while(g.length<=p){g.push([])}g[p].push(h)}for(var u=0;u<this.axes_.length;u++){var b=this.axes_[u];if(!g[u]){b.extremeRange=[0,1]}else{h=g[u];var x=Infinity;var w=-Infinity;var o,m;for(var s=0;s<h.length;s++){if(!a.hasOwnProperty(h[s])){continue}o=a[h[s]][0];if(o!==null){x=Math.min(o,x)}m=a[h[s]][1];if(m!==null){w=Math.max(m,w)}}if(b.includeZero&&x>0){x=0}if(x==Infinity){x=0}if(w==-Infinity){w=1}var t=w-x;if(t===0){t=w}var d,z;if(b.logscale){d=w+0.1*t;z=x}else{d=w+0.1*t;z=x-0.1*t;if(!this.attr_("avoidMinZero")){if(z<0&&x>=0){z=0}if(d>0&&w<=0){d=0}}if(this.attr_("includeZero")){if(w<0){d=0}if(x>0){z=0}}}b.extremeRange=[z,d]}if(b.valueWindow){b.computedValueRange=[b.valueWindow[0],b.valueWindow[1]]}else{if(b.valueRange){b.computedValueRange=[b.valueRange[0],b.valueRange[1]]}else{b.computedValueRange=b.extremeRange}}var n=this.optionsViewForAxis_("y"+(u?"2":""));var y=n("ticker");if(u===0||b.independentTicks){b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this)}else{var l=this.axes_[0];var e=l.ticks;var f=l.computedValueRange[1]-l.computedValueRange[0];var A=b.computedValueRange[1]-b.computedValueRange[0];var c=[];for(var r=0;r<e.length;r++){var q=(e[r].v-l.computedValueRange[0])/f;var v=b.computedValueRange[0]+q*A;c.push(v)}b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this,c)}}};Dygraph.prototype.extractSeries_=function(h,e,g,f){var d=[];for(var c=0;c<h.length;c++){var b=h[c][0];var a=h[c][e];if(g){if(a<=0){a=null}d.push([b,a])}else{if(a!==null||!f){d.push([b,a])}}}return d};Dygraph.prototype.rollingAverage=function(l,d){if(l.length<2){return l}d=Math.min(d,l.length);var b=[];var s=this.attr_("sigma");var E,o,w,v,m,c,D,x;if(this.fractions_){var k=0;var h=0;var e=100;for(w=0;w<l.length;w++){k+=l[w][1][0];h+=l[w][1][1];if(w-d>=0){k-=l[w-d][1][0];h-=l[w-d][1][1]}var A=l[w][0];var u=h?k/h:0;if(this.attr_("errorBars")){if(this.attr_("wilsonInterval")){if(h){var r=u<0?0:u,t=h;var z=s*Math.sqrt(r*(1-r)/t+s*s/(4*t*t));var a=1+s*s/h;E=(r+s*s/(2*h)-z)/a;o=(r+s*s/(2*h)+z)/a;b[w]=[A,[r*e,(r-E)*e,(o-r)*e]]}else{b[w]=[A,[0,0,0]]}}else{x=h?s*Math.sqrt(u*(1-u)/h):1;b[w]=[A,[e*u,e*x,e*x]]}}else{b[w]=[A,e*u]}}}else{if(this.attr_("customBars")){E=0;var B=0;o=0;var g=0;for(w=0;w<l.length;w++){var C=l[w][1];m=C[1];b[w]=[l[w][0],[m,m-C[0],C[2]-m]];if(m!==null&&!isNaN(m)){E+=C[0];B+=m;o+=C[2];g+=1}if(w-d>=0){var q=l[w-d];if(q[1][1]!==null&&!isNaN(q[1][1])){E-=q[1][0];B-=q[1][1];o-=q[1][2];g-=1}}if(g){b[w]=[l[w][0],[1*B/g,1*(B-E)/g,1*(o-B)/g]]}else{b[w]=[l[w][0],[null,null,null]]}}}else{if(!this.attr_("errorBars")){if(d==1){return l}for(w=0;w<l.length;w++){c=0;D=0;for(v=Math.max(0,w-d+1);v<w+1;v++){m=l[v][1];if(m===null||isNaN(m)){continue}D++;c+=l[v][1]}if(D){b[w]=[l[w][0],c/D]}else{b[w]=[l[w][0],null]}}}else{for(w=0;w<l.length;w++){c=0;var f=0;D=0;for(v=Math.max(0,w-d+1);v<w+1;v++){m=l[v][1][0];if(m===null||isNaN(m)){continue}D++;c+=l[v][1][0];f+=Math.pow(l[v][1][1],2)}if(D){x=Math.sqrt(f)/D;b[w]=[l[w][0],[c/D,s*x,s*x]]}else{b[w]=[l[w][0],[null,null,null]]}}}}}return b};Dygraph.prototype.detectTypeFromString_=function(b){var a=false;var c=b.indexOf("-");if((c>0&&(b[c-1]!="e"&&b[c-1]!="E"))||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}if(a){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueParser=function(d){return parseFloat(d)};this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}};Dygraph.prototype.parseFloat_=function(a,c,b){var e=parseFloat(a);if(!isNaN(e)){return e}if(/^ *$/.test(a)){return null}if(/^ *nan *$/i.test(a)){return NaN}var d="Unable to parse '"+a+"' as a number";if(b!==null&&c!==null){d+=" on line "+(1+c)+" ('"+b+"') of CSV."}this.error(d);return null};Dygraph.prototype.parseCSV_=function(s){var r=[];var a=s.split("\n");var g,k;var p=this.attr_("delimiter");if(a[0].indexOf(p)==-1&&a[0].indexOf("\t")>=0){p="\t"}var b=0;if(!("labels" in this.user_attrs_)){b=1;this.attrs_.labels=a[0].split(p)}var o=0;var m;var q=false;var c=this.attr_("labels").length;var f=false;for(var l=b;l<a.length;l++){var e=a[l];o=l;if(e.length===0){continue}if(e[0]=="#"){continue}var d=e.split(p);if(d.length<2){continue}var h=[];if(!q){this.detectTypeFromString_(d[0]);m=this.attr_("xValueParser");q=true}h[0]=m(d[0],this);if(this.fractions_){for(k=1;k<d.length;k++){g=d[k].split("/");if(g.length!=2){this.error('Expected fractional "num/den" values in CSV data but found a value \''+d[k]+"' on line "+(1+l)+" ('"+e+"') which is not of this form.");h[k]=[0,0]}else{h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e)]}}}else{if(this.attr_("errorBars")){if(d.length%2!=1){this.error("Expected alternating (value, stdev.) pairs in CSV data but line "+(1+l)+" has an odd number of values ("+(d.length-1)+"): '"+e+"'")}for(k=1;k<d.length;k+=2){h[(k+1)/2]=[this.parseFloat_(d[k],l,e),this.parseFloat_(d[k+1],l,e)]}}else{if(this.attr_("customBars")){for(k=1;k<d.length;k++){var t=d[k];if(/^ *$/.test(t)){h[k]=[null,null,null]}else{g=t.split(";");if(g.length==3){h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e),this.parseFloat_(g[2],l,e)]}else{this.warn('When using customBars, values must be either blank or "low;center;high" tuples (got "'+t+'" on line '+(1+l))}}}}else{for(k=1;k<d.length;k++){h[k]=this.parseFloat_(d[k],l,e)}}}}if(r.length>0&&h[0]<r[r.length-1][0]){f=true}if(h.length!=c){this.error("Number of columns in line "+l+" ("+h.length+") does not agree with number of labels ("+c+") "+e)}if(l===0&&this.attr_("labels")){var n=true;for(k=0;n&&k<h.length;k++){if(h[k]){n=false}}if(n){this.warn("The dygraphs 'labels' option is set, but the first row of CSV data ('"+e+"') appears to also contain labels. Will drop the CSV labels and use the option labels.");continue}}r.push(h)}if(f){this.warn("CSV is out of order; order it correctly to speed loading.");r.sort(function(j,i){return j[0]-i[0]})}return r};Dygraph.prototype.parseArray_=function(b){if(b.length===0){this.error("Can't plot empty data set");return null}if(b[0].length===0){this.error("Data set cannot contain an empty row");return null}var a;if(this.attr_("labels")===null){this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter");this.attrs_.labels=["X"];for(a=1;a<b[0].length;a++){this.attrs_.labels.push("Y"+a)}}if(Dygraph.isDateLike(b[0][0])){this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter;this.attrs_.axes.x.ticker=Dygraph.dateTicker;var c=Dygraph.clone(b);for(a=0;a<b.length;a++){if(c[a].length===0){this.error("Row "+(1+a)+" of data is empty");return null}if(c[a][0]===null||typeof(c[a][0].getTime)!="function"||isNaN(c[a][0].getTime())){this.error("x value in row "+(1+a)+" is not a Date");return null}c[a][0]=c[a][0].getTime()}return c}else{this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.axisLabelFormatter=Dygraph.numberAxisLabelFormatter;this.attrs_.axes.x.ticker=Dygraph.numericTicks;return b}};Dygraph.prototype.parseDataTable_=function(w){var d=function(i){var j=String.fromCharCode(65+i%26);i=Math.floor(i/26);while(i>0){j=String.fromCharCode(65+(i-1)%26)+j.toLowerCase();i=Math.floor((i-1)/26)}return j};var h=w.getNumberOfColumns();var g=w.getNumberOfRows();var f=w.getColumnType(0);if(f=="date"||f=="datetime"){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{if(f=="number"){this.attrs_.xValueParser=function(i){return parseFloat(i)};this.attrs_.axes.x.valueFormatter=function(i){return i};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+f+"')");return null}}var m=[];var t={};var s=false;var q,o;for(q=1;q<h;q++){var b=w.getColumnType(q);if(b=="number"){m.push(q)}else{if(b=="string"&&this.attr_("displayAnnotations")){var r=m[m.length-1];if(!t.hasOwnProperty(r)){t[r]=[q]}else{t[r].push(q)}s=true}else{this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true")}}}var u=[w.getColumnLabel(0)];for(q=0;q<m.length;q++){u.push(w.getColumnLabel(m[q]));if(this.attr_("errorBars")){q+=1}}this.attrs_.labels=u;h=u.length;var v=[];var l=false;var a=[];for(q=0;q<g;q++){var e=[];if(typeof(w.getValue(q,0))==="undefined"||w.getValue(q,0)===null){this.warn("Ignoring row "+q+" of DataTable because of undefined or null first column.");continue}if(f=="date"||f=="datetime"){e.push(w.getValue(q,0).getTime())}else{e.push(w.getValue(q,0))}if(!this.attr_("errorBars")){for(o=0;o<m.length;o++){var c=m[o];e.push(w.getValue(q,c));if(s&&t.hasOwnProperty(c)&&w.getValue(q,t[c][0])!==null){var p={};p.series=w.getColumnLabel(c);p.xval=e[0];p.shortText=d(a.length);p.text="";for(var n=0;n<t[c].length;n++){if(n){p.text+="\n"}p.text+=w.getValue(q,t[c][n])}a.push(p)}}for(o=0;o<e.length;o++){if(!isFinite(e[o])){e[o]=null}}}else{for(o=0;o<h-1;o++){e.push([w.getValue(q,1+2*o),w.getValue(q,2+2*o)])}}if(v.length>0&&e[0]<v[v.length-1][0]){l=true}v.push(e)}if(l){this.warn("DataTable is out of order; order it correctly to speed loading.");v.sort(function(j,i){return j[0]-i[0]})}this.rawData_=v;if(a.length>0){this.setAnnotations(a,true)}};Dygraph.prototype.start_=function(){var c=this.file_;if(typeof c=="function"){c=c()}if(Dygraph.isArrayLike(c)){this.rawData_=this.parseArray_(c);this.predraw_()}else{if(typeof c=="object"&&typeof c.getColumnRange=="function"){this.parseDataTable_(c);this.predraw_()}else{if(typeof c=="string"){if(c.indexOf("\n")>=0){this.loadedEvent_(c)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status===200||b.status===0){a.loadedEvent_(b.responseText)}}};b.open("GET",c,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof c))}}}};Dygraph.prototype.updateOptions=function(e,b){if(typeof(b)=="undefined"){b=false}var d=e.file;var c=Dygraph.mapLegacyOptions_(e);if("rollPeriod" in c){this.rollPeriod_=c.rollPeriod}if("dateWindow" in c){this.dateWindow_=c.dateWindow;if(!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_x_=(c.dateWindow!==null)}}if("valueRange" in c&&!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_y_=(c.valueRange!==null)}var a=Dygraph.isPixelChangingOptionList(this.attr_("labels"),c);Dygraph.updateDeep(this.user_attrs_,c);if(d){this.file_=d;if(!b){this.start_()}}else{if(!b){if(a){this.predraw_()}else{this.renderGraph_(false,false)}}}};Dygraph.mapLegacyOptions_=function(c){var a={};for(var b in c){if(b=="file"){continue}if(c.hasOwnProperty(b)){a[b]=c[b]}}var e=function(g,f,h){if(!a.axes){a.axes={}}if(!a.axes[g]){a.axes[g]={}}a.axes[g][f]=h};var d=function(f,g,h){if(typeof(c[f])!="undefined"){e(g,h,c[f]);delete a[f]}};d("xValueFormatter","x","valueFormatter");d("pixelsPerXLabel","x","pixelsPerLabel");d("xAxisLabelFormatter","x","axisLabelFormatter");d("xTicker","x","ticker");d("yValueFormatter","y","valueFormatter");d("pixelsPerYLabel","y","pixelsPerLabel");d("yAxisLabelFormatter","y","axisLabelFormatter");d("yTicker","y","ticker");return a};Dygraph.prototype.resize=function(d,b){if(this.resize_lock){return}this.resize_lock=true;if((d===null)!=(b===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");d=b=null}var a=this.width_;var c=this.height_;if(d){this.maindiv_.style.width=d+"px";this.maindiv_.style.height=b+"px";this.width_=d;this.height_=b}else{this.width_=this.maindiv_.clientWidth;this.height_=this.maindiv_.clientHeight}if(a!=this.width_||c!=this.height_){this.maindiv_.innerHTML="";this.roller_=null;this.attrs_.labelsDiv=null;this.createInterface_();if(this.annotations_.length){this.layout_.setAnnotations(this.annotations_)}this.predraw_()}this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.predraw_()};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length<this.numColumns()-1){this.attrs_.visibility.push(true)}return this.attr_("visibility")};Dygraph.prototype.setVisibility=function(b,c){var a=this.visibility();if(b<0||b>=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.predraw_()}};Dygraph.prototype.size=function(){return{width:this.width_,height:this.height_}};Dygraph.prototype.setAnnotations=function(b,a){Dygraph.addAnnotationRule();this.annotations_=b;this.layout_.setAnnotations(this.annotations_);if(!a){this.predraw_()}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.indexFromSetName=function(a){var c=this.attr_("labels");for(var b=0;b<c.length;b++){if(c[b]==a){return b}}return null};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var f="border: 1px solid black; background-color: white; text-align: center;";var e=document.createElement("style");e.type="text/css";document.getElementsByTagName("head")[0].appendChild(e);for(var b=0;b<document.styleSheets.length;b++){if(document.styleSheets[b].disabled){continue}var d=document.styleSheets[b];try{if(d.insertRule){var a=d.cssRules?d.cssRules.length:0;d.insertRule(".dygraphDefaultAnnotation { "+f+" }",a)}else{if(d.addRule){d.addRule(".dygraphDefaultAnnotation",f)}}Dygraph.addedAnnotationCSS=true;return}catch(c){}}this.warn("Unable to add default annotation CSS rule; display may be off.")};var DateGraph=Dygraph;"use strict";Dygraph.LOG_SCALE=10;Dygraph.LN_TEN=Math.log(Dygraph.LOG_SCALE);Dygraph.log10=function(a){return Math.log(a)/Dygraph.LN_TEN};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.LOG_STACK_TRACES=false;Dygraph.DOTTED_LINE=[2,2];Dygraph.DASHED_LINE=[7,3];Dygraph.DOT_DASH_LINE=[7,2,2,2];Dygraph.log=function(b,d){var a;if(typeof(printStackTrace)!="undefined"){a=printStackTrace({guess:false});while(a[0].indexOf("stacktrace")!=-1){a.splice(0,1)}a.splice(0,2);for(var c=0;c<a.length;c++){a[c]=a[c].replace(/\([^)]*\/(.*)\)/,"@$1").replace(/\@.*\/([^\/]*)/,"@$1").replace("[object Object].","")}var e=a.splice(0,1)[0];d+=" ("+e.replace(/^.*@ ?/,"")+")"}if(typeof(console)!="undefined"){switch(b){case Dygraph.DEBUG:console.debug("dygraphs: "+d);break;case Dygraph.INFO:console.info("dygraphs: "+d);break;case Dygraph.WARNING:console.warn("dygraphs: "+d);break;case Dygraph.ERROR:console.error("dygraphs: "+d);break}}if(Dygraph.LOG_STACK_TRACES){console.log(a.join("\n"))}};Dygraph.info=function(a){Dygraph.log(Dygraph.INFO,a)};Dygraph.prototype.info=Dygraph.info;Dygraph.warn=function(a){Dygraph.log(Dygraph.WARNING,a)};Dygraph.prototype.warn=Dygraph.warn;Dygraph.error=function(a){Dygraph.log(Dygraph.ERROR,a)};Dygraph.prototype.error=Dygraph.error;Dygraph.getContext=function(a){return a.getContext("2d")};Dygraph.addEvent=function addEvent(c,b,a){if(c.addEventListener){c.addEventListener(b,a,false)}else{c[b+a]=function(){a(window.event)};c.attachEvent("on"+b,c[b+a])}};Dygraph.removeEvent=function addEvent(c,b,a){if(c.removeEventListener){c.removeEventListener(b,a,false)}else{c.detachEvent("on"+b,c[b+a]);c[b+a]=null}};Dygraph.cancelEvent=function(a){a=a?a:window.event;if(a.stopPropagation){a.stopPropagation()}if(a.preventDefault){a.preventDefault()}a.cancelBubble=true;a.cancel=true;a.returnValue=false;return false};Dygraph.hsvToRGB=function(h,g,k){var c;var d;var l;if(g===0){c=k;d=k;l=k}else{var e=Math.floor(h*6);var j=(h*6)-e;var b=k*(1-g);var a=k*(1-(g*j));var m=k*(1-(g*(1-j)));switch(e){case 1:c=a;d=k;l=b;break;case 2:c=b;d=k;l=m;break;case 3:c=b;d=a;l=k;break;case 4:c=m;d=b;l=k;break;case 5:c=k;d=b;l=a;break;case 6:case 0:c=k;d=m;l=b;break}}c=Math.floor(255*c+0.5);d=Math.floor(255*d+0.5);l=Math.floor(255*l+0.5);return"rgb("+c+","+d+","+l+")"};Dygraph.findPosX=function(b){var c=0;if(b.offsetParent){var a=b;while(1){c+=a.offsetLeft;if(!a.offsetParent){break}a=a.offsetParent}}else{if(b.x){c+=b.x}}while(b&&b!=document.body){c-=b.scrollLeft;b=b.parentNode}return c};Dygraph.findPosY=function(c){var b=0;if(c.offsetParent){var a=c;while(1){b+=a.offsetTop;if(!a.offsetParent){break}a=a.offsetParent}}else{if(c.y){b+=c.y}}while(c&&c!=document.body){b-=c.scrollTop;c=c.parentNode}return b};Dygraph.pageX=function(c){if(c.pageX){return(!c.pageX||c.pageX<0)?0:c.pageX}else{var d=document;var a=document.body;return c.clientX+(d.scrollLeft||a.scrollLeft)-(d.clientLeft||0)}};Dygraph.pageY=function(c){if(c.pageY){return(!c.pageY||c.pageY<0)?0:c.pageY}else{var d=document;var a=document.body;return c.clientY+(d.scrollTop||a.scrollTop)-(d.clientTop||0)}};Dygraph.isOK=function(a){return a&&!isNaN(a)};Dygraph.floatFormat=function(a,b){var c=Math.min(Math.max(1,b||2),21);return(Math.abs(a)<0.001&&a!==0)?a.toExponential(c-1):a.toPrecision(c)};Dygraph.zeropad=function(a){if(a<10){return"0"+a}else{return""+a}};Dygraph.hmsString_=function(a){var c=Dygraph.zeropad;var b=new Date(a);if(b.getSeconds()){return c(b.getHours())+":"+c(b.getMinutes())+":"+c(b.getSeconds())}else{return c(b.getHours())+":"+c(b.getMinutes())}};Dygraph.round_=function(c,b){var a=Math.pow(10,b);return Math.round(c*a)/a};Dygraph.binarySearch=function(a,d,i,e,b){if(e===null||e===undefined||b===null||b===undefined){e=0;b=d.length-1}if(e>b){return -1}if(i===null||i===undefined){i=0}var h=function(j){return j>=0&&j<d.length};var g=parseInt((e+b)/2,10);var c=d[g];if(c==a){return g}var f;if(c>a){if(i>0){f=g-1;if(h(f)&&d[f]<a){return g}}return Dygraph.binarySearch(a,d,i,e,g-1)}if(c<a){if(i<0){f=g+1;if(h(f)&&d[f]>a){return g}}return Dygraph.binarySearch(a,d,i,g+1,b)}};Dygraph.dateParser=function(a){var b;var c;c=Dygraph.dateStrToMillis(a);if(c&&!isNaN(c)){return c}if(a.search("-")!=-1){b=a.replace("-","/","g");while(b.search("-")!=-1){b=b.replace("-","/")}c=Dygraph.dateStrToMillis(b)}else{if(a.length==8){b=a.substr(0,4)+"/"+a.substr(4,2)+"/"+a.substr(6,2);c=Dygraph.dateStrToMillis(b)}else{c=Dygraph.dateStrToMillis(a)}}if(!c||isNaN(c)){Dygraph.error("Couldn't parse "+a+" as a date")}return c};Dygraph.dateStrToMillis=function(a){return new Date(a).getTime()};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.updateDeep=function(b,d){function c(e){return(typeof Node==="object"?e instanceof Node:typeof e==="object"&&typeof e.nodeType==="number"&&typeof e.nodeName==="string")}if(typeof(d)!="undefined"&&d!==null){for(var a in d){if(d.hasOwnProperty(a)){if(d[a]===null){b[a]=null}else{if(Dygraph.isArrayLike(d[a])){b[a]=d[a].slice()}else{if(c(d[a])){b[a]=d[a]}else{if(typeof(d[a])=="object"){if(typeof(b[a])!="object"){b[a]={}}Dygraph.updateDeep(b[a],d[a])}else{b[a]=d[a]}}}}}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a<c.length;a++){if(Dygraph.isArrayLike(c[a])){b.push(Dygraph.clone(c[a]))}else{b.push(c[a])}}return b};Dygraph.createCanvas=function(){var a=document.createElement("canvas");var b=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(b&&(typeof(G_vmlCanvasManager)!="undefined")){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.isAndroid=function(){return(/Android/).test(navigator.userAgent)};Dygraph.repeatAndCleanup=function(b,g,f,c){var e=0;var d=new Date().getTime();b(e);if(g==1){c();return}(function a(){if(e>=g){return}var h=d+(1+e)*f;setTimeout(function(){e++;b(e);if(e>=g-1){c()}else{a()}},h-new Date().getTime())})()};Dygraph.isPixelChangingOptionList=function(h,e){var d={annotationClickHandler:true,annotationDblClickHandler:true,annotationMouseOutHandler:true,annotationMouseOverHandler:true,axisLabelColor:true,axisLineColor:true,axisLineWidth:true,clickCallback:true,digitsAfterDecimal:true,drawCallback:true,drawPoints:true,drawXGrid:true,drawYGrid: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,maxNumberWidth:true,panEdgeFraction:true,pixelsPerYLabel:true,pointClickCallback:true,pointSize:true,rangeSelectorPlotFillColor:true,rangeSelectorPlotStrokeColor:true,showLabelsOnHighlight:true,showRoller:true,sigFigs:true,strokeWidth:true,underlayCallback:true,unhighlightCallback:true,xAxisLabelFormatter:true,xTicker:true,xValueFormatter:true,yAxisLabelFormatter:true,yValueFormatter:true,zoomCallback:true};var a=false;var b={};if(h){for(var f=1;f<h.length;f++){b[h[f]]=true}}for(var g in e){if(a){break}if(e.hasOwnProperty(g)){if(b[g]){for(var c in e[g]){if(a){break}if(e[g].hasOwnProperty(c)&&!d[c]){a=true}}}else{if(!d[g]){a=true}}}}return a};Dygraph.compareArrays=function(c,b){if(!Dygraph.isArrayLike(c)||!Dygraph.isArrayLike(b)){return false}if(c.length!==b.length){return false}for(var a=0;a<c.length;a++){if(c[a]!==b[a]){return false}}return true};"use strict";Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML="";if(typeof(this.date_graph)!="undefined"){this.date_graph.destroy()}this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var d=[];var e=this.date_graph.getSelection();if(e<0){return d}var b=1;var c=this.date_graph.layout_.datasets;for(var a in c){if(!c.hasOwnProperty(a)){continue}d.push({row:e,column:b});b++}return d};"use strict";Dygraph.Interaction={};Dygraph.Interaction.startPan=function(n,s,c){var q,b;c.isPanning=true;var j=s.xAxisRange();c.dateRange=j[1]-j[0];c.initialLeftmostDate=j[0];c.xUnitsPerPixel=c.dateRange/(s.plotter_.area.w-1);if(s.attr_("panEdgeFraction")){var v=s.width_*s.attr_("panEdgeFraction");var d=s.xAxisExtremes();var h=s.toDomXCoord(d[0])-v;var k=s.toDomXCoord(d[1])+v;var t=s.toDataXCoord(h);var u=s.toDataXCoord(k);c.boundedDates=[t,u];var f=[];var a=s.height_*s.attr_("panEdgeFraction");for(q=0;q<s.axes_.length;q++){b=s.axes_[q];var o=b.extremeRange;var p=s.toDomYCoord(o[0],q)+a;var r=s.toDomYCoord(o[1],q)-a;var m=s.toDataYCoord(p);var e=s.toDataYCoord(r);f[q]=[m,e]}c.boundedValues=f}c.is2DPan=false;for(q=0;q<s.axes_.length;q++){b=s.axes_[q];var l=s.yAxisRange(q);if(b.logscale){b.initialTopValue=Dygraph.log10(l[1]);b.dragValueRange=Dygraph.log10(l[1])-Dygraph.log10(l[0])}else{b.initialTopValue=l[1];b.dragValueRange=l[1]-l[0]}b.unitsPerPixel=b.dragValueRange/(s.plotter_.area.h-1);if(b.valueWindow||b.valueRange){c.is2DPan=true}}};Dygraph.Interaction.movePan=function(b,k,c){c.dragEndX=k.dragGetX_(b,c);c.dragEndY=k.dragGetY_(b,c);var h=c.initialLeftmostDate-(c.dragEndX-c.dragStartX)*c.xUnitsPerPixel;if(c.boundedDates){h=Math.max(h,c.boundedDates[0])}var a=h+c.dateRange;if(c.boundedDates){if(a>c.boundedDates[1]){h=h-(a-c.boundedDates[1]);a=h+c.dateRange}}k.dateWindow_=[h,a];if(c.is2DPan){for(var j=0;j<k.axes_.length;j++){var e=k.axes_[j];var d=c.dragEndY-c.dragStartY;var n=d*e.unitsPerPixel;var f=c.boundedValues?c.boundedValues[j]:null;var l=e.initialTopValue+n;if(f){l=Math.min(l,f[1])}var m=l-e.dragValueRange;if(f){if(m<f[0]){l=l-(m-f[0]);m=l-e.dragValueRange}}if(e.logscale){e.valueWindow=[Math.pow(Dygraph.LOG_SCALE,m),Math.pow(Dygraph.LOG_SCALE,l)]}else{e.valueWindow=[m,l]}}}k.drawGraph_(false)};Dygraph.Interaction.endPan=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}a.isPanning=false;a.is2DPan=false;a.initialLeftmostDate=null;a.dateRange=null;a.valueRange=null;a.boundedDates=null;a.boundedValues=null};Dygraph.Interaction.startZoom=function(c,b,a){a.isZooming=true};Dygraph.Interaction.moveZoom=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragStartX-a.dragEndX);var d=Math.abs(a.dragStartY-a.dragEndY);a.dragDirection=(e<d/2)?Dygraph.VERTICAL:Dygraph.HORIZONTAL;b.drawZoomRect_(a.dragDirection,a.dragStartX,a.dragEndX,a.dragStartY,a.dragEndY,a.prevDragDirection,a.prevEndX,a.prevEndY);a.prevEndX=a.dragEndX;a.prevEndY=a.dragEndY;a.prevDragDirection=a.dragDirection};Dygraph.Interaction.treatMouseOpAsClick=function(f,b,d){var k=f.attr_("clickCallback");var n=f.attr_("pointClickCallback");var j=null;if(n){var l=-1;var m=Number.MAX_VALUE;for(var e=0;e<f.selPoints_.length;e++){var c=f.selPoints_[e];var a=Math.pow(c.canvasx-d.dragEndX,2)+Math.pow(c.canvasy-d.dragEndY,2);if(!isNaN(a)&&(l==-1||a<m)){m=a;l=e}}var h=f.attr_("highlightCircleSize")+2;if(m<=h*h){j=f.selPoints_[l]}}if(j){n(b,j)}if(k){k(b,f.lastx_,f.selPoints_)}};Dygraph.Interaction.endZoom=function(c,b,a){a.isZooming=false;a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}if(e>=10&&a.dragDirection==Dygraph.HORIZONTAL){b.doZoomX_(Math.min(a.dragStartX,a.dragEndX),Math.max(a.dragStartX,a.dragEndX))}else{if(d>=10&&a.dragDirection==Dygraph.VERTICAL){b.doZoomY_(Math.min(a.dragStartY,a.dragEndY),Math.max(a.dragStartY,a.dragEndY))}else{b.clearZoomRect_()}}a.dragStartX=null;a.dragStartY=null};Dygraph.Interaction.defaultModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);if(c.altKey||c.shiftKey){Dygraph.startPan(c,b,a)}else{Dygraph.startZoom(c,b,a)}},mousemove:function(c,b,a){if(a.isZooming){Dygraph.moveZoom(c,b,a)}else{if(a.isPanning){Dygraph.movePan(c,b,a)}}},mouseup:function(c,b,a){if(a.isZooming){Dygraph.endZoom(c,b,a)}else{if(a.isPanning){Dygraph.endPan(c,b,a)}}},mouseout:function(c,b,a){if(a.isZooming){a.dragEndX=null;a.dragEndY=null}},dblclick:function(c,b,a){if(c.altKey||c.shiftKey){return}b.doUnzoom_()}};Dygraph.DEFAULT_ATTRS.interactionModel=Dygraph.Interaction.defaultModel;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(c,b,a){a.initializeMouseDown(c,b,a)},mouseup:function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}}};Dygraph.Interaction.dragIsPanInteractionModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);Dygraph.startPan(c,b,a)},mousemove:function(c,b,a){if(a.isPanning){Dygraph.movePan(c,b,a)}},mouseup:function(c,b,a){if(a.isPanning){Dygraph.endPan(c,b,a)}}};"use strict";var DygraphRangeSelector=function(a){this.isIE_=/MSIE/.test(navigator.userAgent)&&!window.opera;this.isUsingExcanvas_=a.isUsingExcanvas_;this.dygraph_=a;this.createCanvases_();if(this.isUsingExcanvas_){this.createIEPanOverlay_()}this.createZoomHandles_();this.initInteraction_()};DygraphRangeSelector.prototype.addToGraph=function(a,b){this.layout_=b;this.resize_();a.appendChild(this.bgcanvas_);a.appendChild(this.fgcanvas_);a.appendChild(this.leftZoomHandle_);a.appendChild(this.rightZoomHandle_)};DygraphRangeSelector.prototype.renderStaticLayer=function(){this.resize_();this.drawStaticLayer_()};DygraphRangeSelector.prototype.renderInteractiveLayer=function(){if(this.isChangingRange_){return}this.placeZoomHandles_();this.drawInteractiveLayer_()};DygraphRangeSelector.prototype.resize_=function(){function c(d,e){d.style.top=e.y+"px";d.style.left=e.x+"px";d.width=e.w;d.height=e.h;d.style.width=d.width+"px";d.style.height=d.height+"px"}var b=this.layout_.getPlotArea();var a=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize");this.canvasRect_={x:b.x,y:b.y+b.h+a+4,w:b.w,h:this.attr_("rangeSelectorHeight")};c(this.bgcanvas_,this.canvasRect_);c(this.fgcanvas_,this.canvasRect_)};DygraphRangeSelector.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphRangeSelector.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_)};DygraphRangeSelector.prototype.createIEPanOverlay_=function(){this.iePanOverlay_=document.createElement("div");this.iePanOverlay_.style.position="absolute";this.iePanOverlay_.style.backgroundColor="white";this.iePanOverlay_.style.filter="alpha(opacity=0)";this.iePanOverlay_.style.display="none";this.iePanOverlay_.style.cursor="move";this.fgcanvas_.appendChild(this.iePanOverlay_)};DygraphRangeSelector.prototype.createZoomHandles_=function(){var a=new Image();a.className="dygraph-rangesel-zoomhandle";a.style.position="absolute";a.style.zIndex=10;a.style.visibility="hidden";a.style.cursor="col-resize";if(/MSIE 7/.test(navigator.userAgent)){a.width=7;a.height=14;a.style.backgroundColor="white";a.style.border="1px solid #333333"}else{a.width=9;a.height=16;a.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAAzwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7sqSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII="}this.leftZoomHandle_=a;this.rightZoomHandle_=a.cloneNode(false)};DygraphRangeSelector.prototype.initInteraction_=function(){var i=this;var f=this.isIE_?document:window;var k=0;var p=null;var n=false;var c=false;var j,d,m,g,q,e,r,o,l,b,h;j=function(w){var v=i.dygraph_.xAxisExtremes();var t=(v[1]-v[0])/i.canvasRect_.w;var u=v[0]+(w.leftHandlePos-i.canvasRect_.x)*t;var s=v[0]+(w.rightHandlePos-i.canvasRect_.x)*t;return[u,s]};d=function(s){Dygraph.cancelEvent(s);n=true;k=s.screenX;p=s.target?s.target:s.srcElement;Dygraph.addEvent(f,"mousemove",m);Dygraph.addEvent(f,"mouseup",g);i.fgcanvas_.style.cursor="col-resize"};m=function(w){if(!n){return}var t=w.screenX-k;if(Math.abs(t)<4){return}k=w.screenX;var v=i.getZoomHandleStatus_();var s;if(p==i.leftZoomHandle_){s=v.leftHandlePos+t;s=Math.min(s,v.rightHandlePos-p.width-3);s=Math.max(s,i.canvasRect_.x)}else{s=v.rightHandlePos+t;s=Math.min(s,i.canvasRect_.x+i.canvasRect_.w);s=Math.max(s,v.leftHandlePos+p.width+3)}var u=p.width/2;p.style.left=(s-u)+"px";i.drawInteractiveLayer_();if(!i.isUsingExcanvas_){q()}};g=function(s){if(!n){return}n=false;Dygraph.removeEvent(f,"mousemove",m);Dygraph.removeEvent(f,"mouseup",g);i.fgcanvas_.style.cursor="default";if(i.isUsingExcanvas_){q()}};q=function(){try{var t=i.getZoomHandleStatus_();i.isChangingRange_=true;if(!t.isZoomed){i.dygraph_.doUnzoom_()}else{var s=j(t);i.dygraph_.doZoomXDates_(s[0],s[1])}}finally{i.isChangingRange_=false}};e=function(u){if(i.isUsingExcanvas_){return u.srcElement==i.iePanOverlay_}else{var s;if(u.offsetX!=undefined){s=i.canvasRect_.x+u.offsetX}else{s=u.clientX}var t=i.getZoomHandleStatus_();return(s>t.leftHandlePos&&s<t.rightHandlePos)}};r=function(s){if(!c&&e(s)&&i.getZoomHandleStatus_().isZoomed){Dygraph.cancelEvent(s);c=true;k=s.screenX;Dygraph.addEvent(f,"mousemove",o);Dygraph.addEvent(f,"mouseup",l)}};o=function(w){if(!c){return}Dygraph.cancelEvent(w);var t=w.screenX-k;if(Math.abs(t)<4){return}k=w.screenX;var v=i.getZoomHandleStatus_();var y=v.leftHandlePos;var s=v.rightHandlePos;var x=s-y;if(y+t<=i.canvasRect_.x){y=i.canvasRect_.x;s=y+x}else{if(s+t>=i.canvasRect_.x+i.canvasRect_.w){s=i.canvasRect_.x+i.canvasRect_.w;y=s-x}else{y+=t;s+=t}}var u=i.leftZoomHandle_.width/2;i.leftZoomHandle_.style.left=(y-u)+"px";i.rightZoomHandle_.style.left=(s-u)+"px";i.drawInteractiveLayer_();if(!i.isUsingExcanvas_){b()}};l=function(s){if(!c){return}c=false;Dygraph.removeEvent(f,"mousemove",o);Dygraph.removeEvent(f,"mouseup",l);if(i.isUsingExcanvas_){b()}};b=function(){try{i.isChangingRange_=true;i.dygraph_.dateWindow_=j(i.getZoomHandleStatus_());i.dygraph_.drawGraph_(false)}finally{i.isChangingRange_=false}};h=function(s){if(n||c){return}var t=e(s)?"move":"default";if(t!=i.fgcanvas_.style.cursor){i.fgcanvas_.style.cursor=t}};this.dygraph_.attrs_.interactionModel=Dygraph.Interaction.dragIsPanInteractionModel;this.dygraph_.attrs_.panEdgeFraction=0.0001;var a=window.opera?"mousedown":"dragstart";Dygraph.addEvent(this.leftZoomHandle_,a,d);Dygraph.addEvent(this.rightZoomHandle_,a,d);if(this.isUsingExcanvas_){Dygraph.addEvent(this.iePanOverlay_,"mousedown",r)}else{Dygraph.addEvent(this.fgcanvas_,"mousedown",r);Dygraph.addEvent(this.fgcanvas_,"mousemove",h)}};DygraphRangeSelector.prototype.drawStaticLayer_=function(){var a=this.bgcanvas_ctx_;a.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);try{this.drawMiniPlot_()}catch(b){Dygraph.warn(b)}var c=0.5;this.bgcanvas_ctx_.lineWidth=1;a.strokeStyle="gray";a.beginPath();a.moveTo(c,c);a.lineTo(c,this.canvasRect_.h-c);a.lineTo(this.canvasRect_.w-c,this.canvasRect_.h-c);a.lineTo(this.canvasRect_.w-c,c);a.stroke()};DygraphRangeSelector.prototype.drawMiniPlot_=function(){var p=this.attr_("rangeSelectorPlotFillColor");var l=this.attr_("rangeSelectorPlotStrokeColor");if(!p&&!l){return}var m=this.computeCombinedSeriesAndLimits_();var e=m.yMax-m.yMin;var r=this.bgcanvas_ctx_;var f=0.5;var j=this.dygraph_.xAxisExtremes();var b=Math.max(j[1]-j[0],1e-30);var q=(this.canvasRect_.w-f)/b;var o=(this.canvasRect_.h-f)/e;var d=this.canvasRect_.w-f;var h=this.canvasRect_.h-f;r.beginPath();r.moveTo(f,h);for(var g=0;g<m.data.length;g++){var a=m.data[g];var n=(a[0]-j[0])*q;var k=h-(a[1]-m.yMin)*o;if(isFinite(n)&&isFinite(k)){r.lineTo(n,k)}}r.lineTo(d,h);r.closePath();if(p){var c=this.bgcanvas_ctx_.createLinearGradient(0,0,0,h);c.addColorStop(0,"white");c.addColorStop(1,p);this.bgcanvas_ctx_.fillStyle=c;r.fill()}if(l){this.bgcanvas_ctx_.strokeStyle=l;this.bgcanvas_ctx_.lineWidth=1.5;r.stroke()}};DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_=function(){var u=this.dygraph_.rawData_;var t=this.attr_("logscale");var p=[];var c;var g;var f,m;var l;var s,r,q;for(s=0;s<u.length;s++){if(u[s].length>1&&u[s][1]!=null){l=typeof u[s][1]!="number";if(l){c=[];g=[];for(q=0;q<u[s][1].length;q++){c.push(0);g.push(0)}}break}}for(s=0;s<u.length;s++){var h=u[s];var d=h[0];if(l){for(q=0;q<c.length;q++){c[q]=g[q]=0}}else{c=g=0}for(r=1;r<h.length;r++){if(this.dygraph_.visibility()[r-1]){if(l){for(q=0;q<c.length;q++){m=h[r][q];if(m===null||isNaN(m)){continue}c[q]+=m;g[q]++}}else{m=h[r];if(m===null||isNaN(m)){continue}c+=m;g++}}}if(l){for(q=0;q<c.length;q++){c[q]/=g[q]}f=c.slice(0)}else{f=c/g}p.push([d,f])}p=this.dygraph_.rollingAverage(p,this.dygraph_.rollPeriod_);if(typeof p[0][1]!="number"){for(s=0;s<p.length;s++){f=p[s][1];p[s][1]=f[0]}}var a=Number.MAX_VALUE;var b=-Number.MAX_VALUE;for(s=0;s<p.length;s++){f=p[s][1];if(f!==null&&isFinite(f)&&(!t||f>0)){a=Math.min(a,f);b=Math.max(b,f)}}var n=0.25;if(t){b=Dygraph.log10(b);b+=b*n;a=Dygraph.log10(a);for(s=0;s<p.length;s++){p[s][1]=Dygraph.log10(p[s][1])}}else{var e;var o=b-a;if(o<=Number.MIN_VALUE){e=b*n}else{e=o*n}b+=e;a-=e}return{data:p,yMin:a,yMax:b}};DygraphRangeSelector.prototype.placeZoomHandles_=function(){var g=this.dygraph_.xAxisExtremes();var a=this.dygraph_.xAxisRange();var b=g[1]-g[0];var i=Math.max(0,(a[0]-g[0])/b);var e=Math.max(0,(g[1]-a[1])/b);var h=this.canvasRect_.x+this.canvasRect_.w*i;var d=this.canvasRect_.x+this.canvasRect_.w*(1-e);var c=Math.max(this.canvasRect_.y,this.canvasRect_.y+(this.canvasRect_.h-this.leftZoomHandle_.height)/2);var f=this.leftZoomHandle_.width/2;this.leftZoomHandle_.style.left=(h-f)+"px";this.leftZoomHandle_.style.top=c+"px";this.rightZoomHandle_.style.left=(d-f)+"px";this.rightZoomHandle_.style.top=this.leftZoomHandle_.style.top;this.leftZoomHandle_.style.visibility="visible";this.rightZoomHandle_.style.visibility="visible"};DygraphRangeSelector.prototype.drawInteractiveLayer_=function(){var b=this.fgcanvas_ctx_;b.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);var e=1;var d=this.canvasRect_.w-e;var a=this.canvasRect_.h-e;var g=this.getZoomHandleStatus_();b.strokeStyle="black";if(!g.isZoomed){b.beginPath();b.moveTo(e,e);b.lineTo(e,a);b.lineTo(d,a);b.lineTo(d,e);b.stroke();if(this.iePanOverlay_){this.iePanOverlay_.style.display="none"}}else{var f=Math.max(e,g.leftHandlePos-this.canvasRect_.x);var c=Math.min(d,g.rightHandlePos-this.canvasRect_.x);b.fillStyle="rgba(240, 240, 240, 0.6)";b.fillRect(0,0,f,this.canvasRect_.h);b.fillRect(c,0,this.canvasRect_.w-c,this.canvasRect_.h);b.beginPath();b.moveTo(e,e);b.lineTo(f,e);b.lineTo(f,a);b.lineTo(c,a);b.lineTo(c,e);b.lineTo(d,e);b.stroke();if(this.isUsingExcanvas_){this.iePanOverlay_.style.width=(c-f)+"px";this.iePanOverlay_.style.left=f+"px";this.iePanOverlay_.style.height=a+"px";this.iePanOverlay_.style.display="inline"}}};DygraphRangeSelector.prototype.getZoomHandleStatus_=function(){var b=this.leftZoomHandle_.width/2;var c=parseInt(this.leftZoomHandle_.style.left,10)+b;var a=parseInt(this.rightZoomHandle_.style.left,10)+b;return{leftHandlePos:c,rightHandlePos:a,isZoomed:(c-1>this.canvasRect_.x||a+1<this.canvasRect_.x+this.canvasRect_.w)}};"use strict";Dygraph.numericTicks=function(I,H,w,r,d,s){var C=r("pixelsPerLabel");var J=[];var F,D,v,A;if(s){for(F=0;F<s.length;F++){J.push({v:s[F]})}}else{if(r("logscale")){A=Math.floor(w/C);var o=Dygraph.binarySearch(I,Dygraph.PREFERRED_LOG_TICK_VALUES,1);var K=Dygraph.binarySearch(H,Dygraph.PREFERRED_LOG_TICK_VALUES,-1);if(o==-1){o=0}if(K==-1){K=Dygraph.PREFERRED_LOG_TICK_VALUES.length-1}var u=null;if(K-o>=A/4){for(var t=K;t>=o;t--){var p=Dygraph.PREFERRED_LOG_TICK_VALUES[t];var m=Math.log(p/I)/Math.log(H/I)*w;var G={v:p};if(u===null){u={tickValue:p,pixel_coord:m}}else{if(Math.abs(m-u.pixel_coord)>=C){u={tickValue:p,pixel_coord:m}}else{G.label=""}}J.push(G)}J.reverse()}}if(J.length===0){var h=r("labelsKMG2");var q;if(h){q=[1,2,4,8]}else{q=[1,2,5]}var L,z,c;for(F=-10;F<50;F++){var g;if(h){g=Math.pow(16,F)}else{g=Math.pow(10,F)}var f=0;for(D=0;D<q.length;D++){L=g*q[D];z=Math.floor(I/L)*L;c=Math.ceil(H/L)*L;A=Math.abs(c-z)/L;f=w/A;if(f>C){break}}if(f>C){break}}if(z>c){L*=-1}for(F=0;F<A;F++){v=z+F*L;J.push({v:v})}}}var B;var y=[];if(r("labelsKMB")){B=1000;y=["K","M","B","T"]}if(r("labelsKMG2")){if(B){Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!")}B=1024;y=["k","M","G","T"]}var E=r("axisLabelFormatter");for(F=0;F<J.length;F++){if(J[F].label!==undefined){continue}v=J[F].v;var e=Math.abs(v);var l=E(v,0,r,d);if(y.length>0){var x=B*B*B*B;for(D=3;D>=0;D--,x/=B){if(e>=x){l=Dygraph.round_(v/x,r("digitsAfterDecimal"))+y[D];break}}}J[F].label=l}return J};Dygraph.dateTicker=function(e,c,i,g,f,h){var d=Dygraph.pickDateTickGranularity(e,c,i,g);if(d>=0){return Dygraph.getDateAxis(e,c,d,g,f)}else{return[]}};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.WEEKLY=14;Dygraph.MONTHLY=15;Dygraph.QUARTERLY=16;Dygraph.BIANNUAL=17;Dygraph.ANNUAL=18;Dygraph.DECADAL=19;Dygraph.CENTENNIAL=20;Dygraph.NUM_GRANULARITIES=21;Dygraph.SHORT_SPACINGS=[];Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]=1000*1;Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]=1000*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]=1000*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]=1000*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY]=1000*30;Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]=1000*60;Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]=1000*60*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]=1000*60*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]=1000*60*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY]=1000*60*30;Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]=1000*3600;Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]=1000*3600*2;Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]=1000*3600*6;Dygraph.SHORT_SPACINGS[Dygraph.DAILY]=1000*86400;Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]=1000*604800;Dygraph.PREFERRED_LOG_TICK_VALUES=function(){var c=[];for(var b=-39;b<=39;b++){var a=Math.pow(10,b);for(var d=1;d<=9;d++){var e=a*d;c.push(e)}}return c}();Dygraph.pickDateTickGranularity=function(d,c,j,h){var g=h("pixelsPerLabel");for(var f=0;f<Dygraph.NUM_GRANULARITIES;f++){var e=Dygraph.numDateTicks(d,c,f);if(j/e>=g){return f}}return -1};Dygraph.numDateTicks=function(e,b,g){if(g<Dygraph.MONTHLY){var h=Dygraph.SHORT_SPACINGS[g];return Math.floor(0.5+1*(b-e)/h)}else{var f=1;var d=12;if(g==Dygraph.QUARTERLY){d=3}if(g==Dygraph.BIANNUAL){d=2}if(g==Dygraph.ANNUAL){d=1}if(g==Dygraph.DECADAL){d=1;f=10}if(g==Dygraph.CENTENNIAL){d=1;f=100}var c=365.2524*24*3600*1000;var a=1*(b-e)/c;return Math.floor(0.5+1*a*d/f)}};Dygraph.getDateAxis=function(n,h,a,l,w){var u=l("axisLabelFormatter");var z=[];var k;if(a<Dygraph.MONTHLY){var c=Dygraph.SHORT_SPACINGS[a];var v=c/1000;var y=new Date(n);var f;if(v<=60){f=y.getSeconds();y.setSeconds(f-f%v)}else{y.setSeconds(0);v/=60;if(v<=60){f=y.getMinutes();y.setMinutes(f-f%v)}else{y.setMinutes(0);v/=60;if(v<=24){f=y.getHours();y.setHours(f-f%v)}else{y.setHours(0);v/=24;if(v==7){y.setDate(y.getDate()-y.getDay())}}}}n=y.getTime();for(k=n;k<=h;k+=c){z.push({v:k,label:u(new Date(k),a,l,w)})}}else{var e;var o=1;if(a==Dygraph.MONTHLY){e=[0,1,2,3,4,5,6,7,8,9,10,11]}else{if(a==Dygraph.QUARTERLY){e=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){e=[0,6]}else{if(a==Dygraph.ANNUAL){e=[0]}else{if(a==Dygraph.DECADAL){e=[0];o=10}else{if(a==Dygraph.CENTENNIAL){e=[0];o=100}else{Dygraph.warn("Span of dates is too long")}}}}}}var s=new Date(n).getFullYear();var p=new Date(h).getFullYear();var b=Dygraph.zeropad;for(var r=s;r<=p;r++){if(r%o!==0){continue}for(var q=0;q<e.length;q++){var m=r+"/"+b(1+e[q])+"/01";k=Dygraph.dateStrToMillis(m);if(k<n||k>h){continue}z.push({v:k,label:u(new Date(k),a,l,w)})}}}return z};Dygraph.DEFAULT_ATTRS.axes.x.ticker=Dygraph.dateTicker;Dygraph.DEFAULT_ATTRS.axes.y.ticker=Dygraph.numericTicks;Dygraph.DEFAULT_ATTRS.axes.y2.ticker=Dygraph.numericTicks;"use strict";function RGBColor(f){this.ok=false;if(f.charAt(0)=="#"){f=f.substr(1,6)}f=f.replace(/ /g,"");f=f.toLowerCase();var b={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"};for(var g in b){if(f==g){f=b[g]}}var e=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var c=0;c<e.length;c++){var j=e[c].re;var a=e[c].process;var h=j.exec(f);if(h){var d=a(h);this.r=d[0];this.g=d[1];this.b=d[2];this.ok=true}}this.r=(this.r<0||isNaN(this.r))?0:((this.r>255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var l=this.r.toString(16);var k=this.g.toString(16);var i=this.b.toString(16);if(l.length==1){l="0"+l}if(k.length==1){k="0"+k}if(i.length==1){i="0"+i}return"#"+l+k+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)=="undefined"){b=10}for(;parseInt(a,10)<b&&b>1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:"toLocaleString",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:["getHours","0"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(""+c.getFullYear()+"/1/1 GMT");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:["getMinutes","0"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(""+e.getFullYear()+"/1/1")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(b,0)},w:"getDay",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:"getFullYear",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?"-":"+")+a+e},Z:function(a){return a.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(a){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g=="locale"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)=="string"){return c[g]()}else{if(typeof(g)=="function"){return g.call(c,c)}else{if(typeof(g)=="object"&&typeof(g[0])=="string"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};
\ No newline at end of file
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..ec236d289c1e0c16a1267a1bdf2dcb3c0acbe94b
--- /dev/null
@@ -0,0 +1 @@
+dygraph-1.2dev.min.js
\ No newline at end of file