IE8 updates
authorAndrew Tridgell <tridge@samba.org>
Sun, 10 Oct 2010 00:12:48 +0000 (11:12 +1100)
committerAndrew Tridgell <tridge@samba.org>
Sun, 10 Oct 2010 00:12:48 +0000 (11:12 +1100)
live/graphs.js
live/index.html
live/lib/debug.js [moved from live/debug.js with 100% similarity]
live/lib/dygraph-canvas.js [new file with mode: 0644]
live/lib/dygraph-combined.js [moved from live/dygraph-combined.js with 100% similarity]
live/lib/dygraph.js [new file with mode: 0644]
live/lib/excanvas.js [new file with mode: 0644]
live/lib/rgbcolor.js [new file with mode: 0644]
live/lib/strftime-min.js [new file with mode: 0644]

index d33dc0d6854361f148d42e8b837595bbe5804173..fbcf7e42483d69b74866172e9a6cac6af0aa5553 100644 (file)
@@ -19,7 +19,9 @@ function heading(level, h) {
   create a div for a graph
  */
 function graph_div(divname) {
-  document.write('<div id="' + divname + '" style="width:700px; height:350px;"></div>');
+  if (document.getElementById(divname) == null) {
+    document.write('<div id="' + divname + '" style="width:700px; height:350px;"></div>');
+  }
 }
 
 /*
@@ -251,8 +253,23 @@ function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs)
   /* add the labels to the given graph attributes */
   attrs.labels = d.labels;
 
+  defaultAttrs = {
+  width: 700,
+  height: 350,
+  rollPeriod: 1,
+  strokeWidth: 1,
+  showRoller: true
+  }
+
+  for (a in defaultAttrs) {
+    if (attrs[a] == undefined) {
+      attrs[a] = defaultAttrs[a];
+    }
+  }
+
   graph_div(divname);
-  return new Dygraph(document.getElementById(divname), d.data, attrs);
+  var g = new Dygraph(document.getElementById(divname), d.data, attrs);
+  return g;
 }
 
 
@@ -301,57 +318,36 @@ function show_graphs() {
   graph_sum_csv_files("Total AC Power", 
                      todays_csv_files(),
                       "Pac",
-                      { includeZero: true, 
-                       showRoller: true, 
-                       rollPeriod: 5,
-                       strokeWidth: 2 });
+                      { includeZero: true });
 
   heading(3, "AC Power from each inverter (W)");
 
   graph_csv_files("AC Power", 
                  todays_csv_files(),
                  "Pac",
-                 { includeZero: true, 
-                    showRoller: true, 
-                   rollPeriod: 5,
-                    strokeWidth: 2 });
+                 { includeZero: true });
 
   heading(3, "DC Voltage for each inverter (V)");
 
   graph_csv_files("DC Voltage", 
                  todays_csv_files(),
                  "Upv-Soll",
-                 { 
-                 missingValue: 666,
-                     includeZero: false, 
-                     showRoller: true, 
-                     rollPeriod: 5,
-                     strokeWidth: 2 
-                     });
+                 { includeZero: true,
+                   missingValue: 666 });
 
   heading(3, "Total DC current (A)");
 
   graph_sum_csv_files("Total Current", 
                      todays_csv_files(),
                      "Ipv",
-                      { includeZero: true, 
-                       showRoller: true, 
-                       rollPeriod: 5,
-                       strokeWidth: 2 
-                     }
-                     );
+                      { includeZero: true });
 
   heading(3, "DC Current for each inverter (A)");
 
   graph_csv_files("DC Current", 
                  todays_csv_files(),
                  "Ipv",
-                 { 
-                 includeZero: false, 
-                     showRoller: true, 
-                     rollPeriod : 5,
-                     strokeWidth: 2 
-                     });
+                 { includeZero: false });
 
   heading(3, "DC Power for each inverter (W)");
 
@@ -360,12 +356,7 @@ function show_graphs() {
                       todays_csv_files(),
                       [ "Ipv", "Upv-Soll" ],
                       product, null,
-                      { 
-                      includeZero: true, 
-                      showRoller: true, 
-                      rollPeriod : 5,
-                      strokeWidth: 2 
-                     });
+                      { includeZero: true });
 
   heading(3, "Total DC Power (W)");
 
@@ -373,12 +364,7 @@ function show_graphs() {
                       todays_csv_files(),
                       [ "Ipv", "Upv-Soll" ],
                       product, sum,
-                      { 
-                      includeZero: true, 
-                      showRoller: true, 
-                      rollPeriod : 5,
-                      strokeWidth: 2 
-                     });
+                      { includeZero: true });
 
 
   heading(3, "Inverter efficiencies (%)");
@@ -395,24 +381,33 @@ function show_graphs() {
                       todays_csv_files(),
                       [ "Pac", "Ipv", "Upv-Soll" ],
                       efficiency, null,
-                      { 
-                      includeZero: false, 
-                      showRoller: true, 
-                      rollPeriod : 5,
-                      strokeWidth: 2 
-                     });
+                      { includeZero: false });
 
   heading(3, "AC Voltage for each inverter (V)");
 
   graph_csv_files("AC Voltage", 
                  todays_csv_files(),
                  "Uac",
-                 { 
-                 includeZero: false, 
-                     showRoller: true, 
-                     rollPeriod : 5,
-                     strokeWidth: 2 
-                     });
+                 { includeZero: false });
+
+  heading(3, "Lifetime Power for each inverter (kWh)");
 
+  graph_csv_files("Lifetime Power", 
+                 todays_csv_files(),
+                 "E-Total",
+                 { includeZero: false });
+
+}
+
+function set_date() {
+  show_graphs();
 }
 
+function setup_datepicker() {
+  
+  document.getElementById("sd").value = 
+    midnight.getFullYear() + "-" + intLength(midnight.getMonth()+1, 2) + "-" + intLength(midnight.getDate(),2);
+
+  // Add the onchange event handler to the start date input
+  datePickerController.addEvent(document.getElementById("sd"), "change", set_date);
+}
index 055286bb3ea54e3c4b5b316d30cfe02bffc0e745..3b24cd3ab99524aa605e7c8af3ba89625220af96 100755 (executable)
@@ -1,24 +1,46 @@
-<html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
 <head>
-<title>Todays Data</title>
-<script type="text/javascript"
-       src="debug.js"></script>
-<script type="text/javascript"
-       src="dygraph-combined.js"></script>
-<script type="text/javascript"
-       src="graphs.js"></script>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<title>Daily Inverter Data</title>
+<!--[if IE]>
+<script type="text/javascript" src="lib/excanvas.js"></script>
+<![endif]-->
+<script type="text/javascript" src="lib/strftime.js"></script>
+<script type="text/javascript" src="lib/rgbcolor.js"></script>
+<script type="text/javascript" src="lib/dygraph-canvas.js"></script>
+<script type="text/javascript" src="lib/dygraph.js"></script>
+<script type="text/javascript" src="lib/debug.js"></script>
+<script type="text/javascript" src="graphs.js"></script>
+<!--
+<script type="text/javascript" src="lib/date-picker/js/datepicker.js"></script>
+<link href="lib/date-picker/css/datepicker.css" rel="stylesheet" --
+  --type="text/css" />
+-->
 <!-- meta http-equiv="Refresh" content="300" -->
 </head>
 <body>
-<h1>Todays Data</h1>
+<h1>Daily Inverter Data</h1>
+
+<!--
+<form id="date" method="post" action="">
+    <fieldset>
+      <label for="sd">Start Date : </label>
+      <p><input type="text" class="w8em format-d-m-y highlight-days-67
+      range-high-today range-low-2010-10-08" name="sd" id="sd" value="" maxlength="10" /></p>
+    </fieldset>
+</form>
+-->
 
 <script type="text/javascript">
+/* setup_datepicker(); */
 show_graphs();
 </script>
 
 <p>
 <small>Thanks to <a href="http://danvk.org/dygraphs/">dygraphs</a> for
-  the graphs</small>
+  the graphs</small></p>
 
 </body>
 </html>
similarity index 100%
rename from live/debug.js
rename to live/lib/debug.js
diff --git a/live/lib/dygraph-canvas.js b/live/lib/dygraph-canvas.js
new file mode 100644 (file)
index 0000000..fb724e1
--- /dev/null
@@ -0,0 +1,850 @@
+// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
+// All Rights Reserved.
+
+/**
+ * @fileoverview Based on PlotKit, but modified to meet the needs of dygraphs.
+ * In particular, support for:
+ * - grid overlays 
+ * - error bars
+ * - dygraphs attribute system
+ */
+
+/**
+ * Creates a new DygraphLayout object.
+ * @param {Object} options Options for PlotKit.Layout
+ * @return {Object} The DygraphLayout object
+ */
+DygraphLayout = function(dygraph, options) {
+  this.dygraph_ = dygraph;
+  this.options = {};  // TODO(danvk): remove, use attr_ instead.
+  Dygraph.update(this.options, options ? options : {});
+  this.datasets = new Array();
+  this.annotations = new Array()
+};
+
+DygraphLayout.prototype.attr_ = function(name) {
+  return this.dygraph_.attr_(name);
+};
+
+DygraphLayout.prototype.addDataset = function(setname, set_xy) {
+  this.datasets[setname] = set_xy;
+};
+
+DygraphLayout.prototype.setAnnotations = function(ann) {
+  // The Dygraph object's annotations aren't parsed. We parse them here and
+  // save a copy.
+  var parse = this.attr_('xValueParser');
+  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.evaluate = function() {
+  this._evaluateLimits();
+  this._evaluateLineCharts();
+  this._evaluateLineTicks();
+  this._evaluateAnnotations();
+};
+
+DygraphLayout.prototype._evaluateLimits = function() {
+  this.minxval = this.maxxval = null;
+  if (this.options.dateWindow) {
+    this.minxval = this.options.dateWindow[0];
+    this.maxxval = this.options.dateWindow[1];
+  } else {
+    for (var name in this.datasets) {
+      if (!this.datasets.hasOwnProperty(name)) continue;
+      var series = this.datasets[name];
+      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);
+
+  this.minyval = this.options.yAxis[0];
+  this.maxyval = this.options.yAxis[1];
+  this.yrange = this.maxyval - this.minyval;
+  this.yscale = (this.yrange != 0 ? 1/this.yrange : 1.0);
+};
+
+DygraphLayout.prototype._evaluateLineCharts = function() {
+  // add all the rects
+  this.points = new Array();
+  for (var setName in this.datasets) {
+    if (!this.datasets.hasOwnProperty(setName)) continue;
+
+    var dataset = this.datasets[setName];
+    for (var j = 0; j < dataset.length; j++) {
+      var item = dataset[j];
+      var point = {
+        // TODO(danvk): here
+        x: ((parseFloat(item[0]) - this.minxval) * this.xscale),
+        y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale),
+        xval: parseFloat(item[0]),
+        yval: parseFloat(item[1]),
+        name: setName
+      };
+
+      // limit the x, y values so they do not overdraw
+      if (point.y <= 0.0) {
+        point.y = 0.0;
+      }
+      if (point.y >= 1.0) {
+        point.y = 1.0;
+      }
+      this.points.push(point);
+    }
+  }
+};
+
+DygraphLayout.prototype._evaluateLineTicks = function() {
+  this.xticks = new Array();
+  for (var i = 0; i < this.options.xTicks.length; i++) {
+    var tick = this.options.xTicks[i];
+    var label = tick.label;
+    var pos = this.xscale * (tick.v - this.minxval);
+    if ((pos >= 0.0) && (pos <= 1.0)) {
+      this.xticks.push([pos, label]);
+    }
+  }
+
+  this.yticks = new Array();
+  for (var i = 0; i < this.options.yTicks.length; i++) {
+    var tick = this.options.yTicks[i];
+    var label = tick.label;
+    var pos = 1.0 - (this.yscale * (tick.v - this.minyval));
+    if ((pos >= 0.0) && (pos <= 1.0)) {
+      this.yticks.push([pos, label]);
+    }
+  }
+};
+
+
+/**
+ * Behaves the same way as PlotKit.Layout, but also copies the errors
+ * @private
+ */
+DygraphLayout.prototype.evaluateWithError = function() {
+  this.evaluate();
+  if (!this.options.errorBars) 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];
+    for (var 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) {
+        this.points[i].errorMinus = parseFloat(item[2]);
+        this.points[i].errorPlus = parseFloat(item[3]);
+      }
+    }
+  }
+};
+
+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 annotations = {};
+  for (var i = 0; i < this.annotations.length; i++) {
+    var a = this.annotations[i];
+    annotations[a.xval + "," + a.series] = a;
+  }
+
+  this.annotated_points = [];
+  for (var 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 = new Array();
+};
+
+/**
+ * Change the values of various layout options
+ * @param {Object} new_options an associative array of new properties
+ */
+DygraphLayout.prototype.updateOptions = function(new_options) {
+  Dygraph.update(this.options, new_options ? new_options : {});
+};
+
+/**
+ * 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 i in point) {
+    unstackedPoint[i] = point[i];
+  }
+  
+  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;
+}  
+
+// Subclass PlotKit.CanvasRenderer to add:
+// 1. X/Y grid overlay
+// 2. Ability to draw error bars (if required)
+
+/**
+ * Sets some PlotKit.CanvasRenderer options
+ * @param {Object} element The canvas to attach to
+ * @param {Layout} layout The DygraphLayout object for this graph.
+ * @param {Object} options Options to pass on to CanvasRenderer
+ */
+DygraphCanvasRenderer = function(dygraph, element, layout, options) {
+  // TODO(danvk): remove options, just use dygraph.attr_.
+  this.dygraph_ = dygraph;
+
+  // default options
+  this.options = {
+    "strokeWidth": 0.5,
+    "drawXAxis": true,
+    "drawYAxis": true,
+    "axisLineColor": "black",
+    "axisLineWidth": 0.5,
+    "axisTickSize": 3,
+    "axisLabelColor": "black",
+    "axisLabelFont": "Arial",
+    "axisLabelFontSize": 9,
+    "axisLabelWidth": 50,
+    "drawYGrid": true,
+    "drawXGrid": true,
+    "gridLineColor": "rgb(128,128,128)",
+    "fillAlpha": 0.15,
+    "underlayCallback": null
+  };
+  Dygraph.update(this.options, options);
+
+  this.layout = layout;
+  this.element = element;
+  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 = new Array();
+  this.ylabels = new Array();
+  this.annotations = new Array();
+
+  this.area = {
+    x: this.options.yAxisLabelWidth + 2 * this.options.axisTickSize,
+    y: 0
+  };
+  this.area.w = this.width - this.area.x - this.options.rightGap;
+  this.area.h = this.height - this.options.axisLabelFontSize -
+                2 * this.options.axisTickSize;
+
+  this.container.style.position = "relative";
+  this.container.style.width = this.width + "px";
+};
+
+DygraphCanvasRenderer.prototype.clear = function() {
+  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;
+      }
+      var context = this.element.getContext("2d");
+    }
+    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;
+    }
+  }
+
+  var context = this.element.getContext("2d");
+  context.clearRect(0, 0, this.width, this.height);
+
+  for (var i = 0; i < this.xlabels.length; i++) {
+    var el = this.xlabels[i];
+    el.parentNode.removeChild(el);
+  }
+  for (var i = 0; i < this.ylabels.length; i++) {
+    var el = this.ylabels[i];
+    el.parentNode.removeChild(el);
+  }
+  for (var i = 0; i < this.annotations.length; i++) {
+    var el = this.annotations[i];
+    el.parentNode.removeChild(el);
+  }
+  this.xlabels = new Array();
+  this.ylabels = new Array();
+  this.annotations = new Array();
+};
+
+
+DygraphCanvasRenderer.isSupported = function(canvasName) {
+  var canvas = null;
+  try {
+    if (typeof(canvasName) == 'undefined' || canvasName == null)
+      canvas = document.createElement("canvas");
+    else
+      canvas = canvasName;
+    var context = 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;
+};
+
+/**
+ * Draw an X/Y grid on top of the existing plot
+ */
+DygraphCanvasRenderer.prototype.render = function() {
+  // Draw the new X/Y grid
+  var ctx = this.element.getContext("2d");
+
+  if (this.options.underlayCallback) {
+    this.options.underlayCallback(ctx, this.area, this.layout, this.dygraph_);
+  }
+
+  if (this.options.drawYGrid) {
+    var ticks = this.layout.yticks;
+    ctx.save();
+    ctx.strokeStyle = this.options.gridLineColor;
+    ctx.lineWidth = this.options.axisLineWidth;
+    for (var i = 0; i < ticks.length; i++) {
+      var x = this.area.x;
+      var y = this.area.y + ticks[i][0] * this.area.h;
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x + this.area.w, y);
+      ctx.closePath();
+      ctx.stroke();
+    }
+  }
+
+  if (this.options.drawXGrid) {
+    var ticks = this.layout.xticks;
+    ctx.save();
+    ctx.strokeStyle = this.options.gridLineColor;
+    ctx.lineWidth = this.options.axisLineWidth;
+    for (var i=0; i<ticks.length; i++) {
+      var x = this.area.x + ticks[i][0] * this.area.w;
+      var y = 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._renderAnnotations();
+};
+
+
+DygraphCanvasRenderer.prototype._renderAxis = function() {
+  if (!this.options.drawXAxis && !this.options.drawYAxis)
+    return;
+
+  var context = this.element.getContext("2d");
+
+  var labelStyle = {
+    "position": "absolute",
+    "fontSize": this.options.axisLabelFontSize + "px",
+    "zIndex": 10,
+    "color": this.options.axisLabelColor,
+    "width": this.options.axisLabelWidth + "px",
+    "overflow": "hidden"
+  };
+  var makeDiv = function(txt) {
+    var div = document.createElement("div");
+    for (var name in labelStyle) {
+      if (labelStyle.hasOwnProperty(name)) {
+        div.style[name] = labelStyle[name];
+      }
+    }
+    div.appendChild(document.createTextNode(txt));
+    return div;
+  };
+
+  // axis lines
+  context.save();
+  context.strokeStyle = this.options.axisLineColor;
+  context.lineWidth = this.options.axisLineWidth;
+
+  if (this.options.drawYAxis) {
+    if (this.layout.yticks && this.layout.yticks.length > 0) {
+      for (var i = 0; i < this.layout.yticks.length; i++) {
+        var tick = this.layout.yticks[i];
+        if (typeof(tick) == "function") return;
+        var x = this.area.x;
+        var y = this.area.y + tick[0] * this.area.h;
+        context.beginPath();
+        context.moveTo(x, y);
+        context.lineTo(x - this.options.axisTickSize, y);
+        context.closePath();
+        context.stroke();
+
+        var label = makeDiv(tick[1]);
+        var top = (y - this.options.axisLabelFontSize / 2);
+        if (top < 0) top = 0;
+
+        if (top + this.options.axisLabelFontSize + 3 > this.height) {
+          label.style.bottom = "0px";
+        } else {
+          label.style.top = top + "px";
+        }
+        label.style.left = "0px";
+        label.style.textAlign = "right";
+        label.style.width = this.options.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.options.axisLabelFontSize;
+      var bottom = parseInt(bottomTick.style.top) + fontSize;
+      if (bottom > this.height - fontSize) {
+        bottomTick.style.top = (parseInt(bottomTick.style.top) -
+            fontSize / 2) + "px";
+      }
+    }
+
+    context.beginPath();
+    context.moveTo(this.area.x, this.area.y);
+    context.lineTo(this.area.x, this.area.y + this.area.h);
+    context.closePath();
+    context.stroke();
+  }
+
+  if (this.options.drawXAxis) {
+    if (this.layout.xticks) {
+      for (var i = 0; i < this.layout.xticks.length; i++) {
+        var tick = this.layout.xticks[i];
+        if (typeof(dataset) == "function") return;
+
+        var x = this.area.x + tick[0] * this.area.w;
+        var y = this.area.y + this.area.h;
+        context.beginPath();
+        context.moveTo(x, y);
+        context.lineTo(x, y + this.options.axisTickSize);
+        context.closePath();
+        context.stroke();
+
+        var label = makeDiv(tick[1]);
+        label.style.textAlign = "center";
+        label.style.bottom = "0px";
+
+        var left = (x - this.options.axisLabelWidth/2);
+        if (left + this.options.axisLabelWidth > this.width) {
+          left = this.width - this.options.xAxisLabelWidth;
+          label.style.textAlign = "right";
+        }
+        if (left < 0) {
+          left = 0;
+          label.style.textAlign = "left";
+        }
+
+        label.style.left = left + "px";
+        label.style.width = this.options.xAxisLabelWidth + "px";
+        this.container.appendChild(label);
+        this.xlabels.push(label);
+      }
+    }
+
+    context.beginPath();
+    context.moveTo(this.area.x, this.area.y + this.area.h);
+    context.lineTo(this.area.x + this.area.w, this.area.y + this.area.h);
+    context.closePath();
+    context.stroke();
+  }
+
+  context.restore();
+};
+
+
+DygraphCanvasRenderer.prototype._renderAnnotations = function() {
+  var annotationStyle = {
+    "position": "absolute",
+    "fontSize": this.options.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.element.getContext("2d");
+    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();
+  }
+};
+
+
+/**
+ * Overrides the CanvasRenderer method to draw error bars
+ */
+DygraphCanvasRenderer.prototype._renderLineChart = function() {
+  var context = this.element.getContext("2d");
+  var colorCount = this.options.colorScheme.length;
+  var colorScheme = this.options.colorScheme;
+  var fillAlpha = this.options.fillAlpha;
+  var errorBars = this.layout.options.errorBars;
+  var fillGraph = this.layout.options.fillGraph;
+  var stackedGraph = this.layout.options.stackedGraph;
+  var stepPlot = this.layout.options.stepPlot;
+
+  var setNames = [];
+  for (var name in this.layout.datasets) {
+    if (this.layout.datasets.hasOwnProperty(name)) {
+      setNames.push(name);
+    }
+  }
+  var setCount = setNames.length;
+
+  this.colors = {}
+  for (var i = 0; i < setCount; i++) {
+    this.colors[setNames[i]] = colorScheme[i % colorCount];
+  }
+
+  // Update Points
+  // TODO(danvk): here
+  for (var i = 0; i < this.layout.points.length; i++) {
+    var point = this.layout.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 isOK = function(x) { return x && !isNaN(x); };
+
+  var ctx = context;
+  if (errorBars) {
+    if (fillGraph) {
+      this.dygraph_.warn("Can't use fillGraph option with error bars");
+    }
+
+    for (var i = 0; i < setCount; i++) {
+      var setName = setNames[i];
+      var color = this.colors[setName];
+
+      // setup graphics context
+      ctx.save();
+      var prevX = NaN;
+      var prevY = NaN;
+      var prevYs = [-1, -1];
+      var yscale = this.layout.yscale;
+      // should be same color as the lines but only 15% opaque.
+      var rgb = new RGBColor(color);
+      var err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
+                            fillAlpha + ')';
+      ctx.fillStyle = err_color;
+      ctx.beginPath();
+      for (var j = 0; j < this.layout.points.length; j++) {
+        var point = this.layout.points[j];
+        if (point.name == setName) {
+          if (!isOK(point.y)) {
+            prevX = NaN;
+            continue;
+          }
+
+          // TODO(danvk): here
+          if (stepPlot) {
+            var newYs = [ prevY - point.errorPlus * yscale,
+                          prevY + point.errorMinus * yscale ];
+            prevY = point.y;
+          } else {
+            var newYs = [ point.y - point.errorPlus * yscale,
+                          point.y + point.errorMinus * yscale ];
+          }
+          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 axisY = 1.0 + this.layout.minyval * this.layout.yscale;
+    if (axisY < 0.0) axisY = 0.0;
+    else if (axisY > 1.0) axisY = 1.0;
+    axisY = this.area.h * axisY + this.area.y;
+
+    var baseline = []  // for stacked graphs: baseline for filling
+
+    // process sets in reverse order (needed for stacked graphs)
+    for (var i = setCount - 1; i >= 0; i--) {
+      var setName = setNames[i];
+      var color = this.colors[setName];
+
+      // setup graphics context
+      ctx.save();
+      var prevX = NaN;
+      var prevYs = [-1, -1];
+      var yscale = this.layout.yscale;
+      // should be same color as the lines but only 15% opaque.
+      var rgb = new RGBColor(color);
+      var err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' +
+                            fillAlpha + ')';
+      ctx.fillStyle = err_color;
+      ctx.beginPath();
+      for (var j = 0; j < this.layout.points.length; j++) {
+        var point = this.layout.points[j];
+        if (point.name == setName) {
+          if (!isOK(point.y)) {
+            prevX = NaN;
+            continue;
+          }
+          var newYs;
+          if (stackedGraph) {
+            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();
+    }
+  }
+
+  for (var i = 0; i < setCount; i++) {
+    var setName = setNames[i];
+    var color = this.colors[setName];
+    var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
+
+    // setup graphics context
+    context.save();
+    var point = this.layout.points[0];
+    var pointSize = this.dygraph_.attr_("pointSize", setName);
+    var prevX = null, prevY = null;
+    var drawPoints = this.dygraph_.attr_("drawPoints", setName);
+    var points = this.layout.points;
+    for (var j = 0; j < points.length; j++) {
+      var point = points[j];
+      if (point.name == setName) {
+        if (!isOK(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.options.strokeWidth;
+            ctx.moveTo(prevX, prevY);
+            ctx.lineTo(point.canvasx, prevY);
+            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 ||
+                                       !isOK(points[j+1].canvasy)));
+
+          if (!prevX) {
+            prevX = point.canvasx;
+            prevY = point.canvasy;
+          } else {
+            // TODO(danvk): figure out why this conditional is necessary.
+            if (strokeWidth) {
+              ctx.beginPath();
+              ctx.strokeStyle = color;
+              ctx.lineWidth = strokeWidth;
+              ctx.moveTo(prevX, prevY);
+              if (stepPlot) {
+                ctx.lineTo(point.canvasx, prevY);
+              }
+              prevX = point.canvasx;
+              prevY = point.canvasy;
+              ctx.lineTo(prevX, prevY);
+              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();
+};
diff --git a/live/lib/dygraph.js b/live/lib/dygraph.js
new file mode 100644 (file)
index 0000000..1883458
--- /dev/null
@@ -0,0 +1,2568 @@
+// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
+// All Rights Reserved.
+
+/**
+ * @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://www.danvk.org/dygraphs
+
+ */
+
+/**
+ * An interactive, zoomable graph
+ * @param {String | Function} file A file containing CSV data or a function that
+ * returns this data. The expected format for each line is
+ * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
+ * YYYYMMDD,val1,stddev1,val2,stddev2,...
+ * @param {Object} attrs Various other attributes, e.g. errorBars determines
+ * whether the input data contains error ranges.
+ */
+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 + "]";
+};
+Dygraph.toString = function() {
+  return this.__repr__();
+};
+
+// Various default values
+Dygraph.DEFAULT_ROLL_PERIOD = 1;
+Dygraph.DEFAULT_WIDTH = 480;
+Dygraph.DEFAULT_HEIGHT = 320;
+Dygraph.AXIS_LINE_WIDTH = 0.3;
+
+// Default attribute values.
+Dygraph.DEFAULT_ATTRS = {
+  highlightCircleSize: 3,
+  pixelsPerXLabel: 60,
+  pixelsPerYLabel: 30,
+
+  labelsDivWidth: 250,
+  labelsDivStyles: {
+    // TODO(danvk): move defaults from createStatusMessage_ here.
+  },
+  labelsSeparateLines: false,
+  labelsShowZeroValues: true,
+  labelsKMB: false,
+  labelsKMG2: false,
+  showLabelsOnHighlight: true,
+
+  yValueFormatter: function(x) { return Dygraph.round_(x, 2); },
+
+  strokeWidth: 1.0,
+
+  axisTickSize: 3,
+  axisLabelFontSize: 14,
+  xAxisLabelWidth: 50,
+  yAxisLabelWidth: 50,
+  xAxisLabelFormatter: Dygraph.dateAxisFormatter,
+  rightGap: 5,
+
+  showRoller: false,
+  xValueFormatter: Dygraph.dateString_,
+  xValueParser: Dygraph.dateParser,
+  xTicker: Dygraph.dateTicker,
+
+  delimiter: ',',
+
+  logScale: false,
+  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,
+
+  stepPlot: false,
+  avoidMinZero: false
+};
+
+// Various logging levels.
+Dygraph.DEBUG = 1;
+Dygraph.INFO = 2;
+Dygraph.WARNING = 3;
+Dygraph.ERROR = 3;
+
+// 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 interaction &lt;canvas&gt; inside of it. See the constructor for details
+ * on the parameters.
+ * @param {Element} div the Element to render the graph into.
+ * @param {String | Function} file Source data
+ * @param {Object} attrs Miscellaneous other options
+ * @private
+ */
+Dygraph.prototype.__init__ = function(div, file, attrs) {
+  // Support two-argument constructor
+  if (attrs == null) { attrs = {}; }
+
+  // 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.valueRange_ = attrs.valueRange || null;
+  this.wilsonInterval_ = attrs.wilsonInterval || true;
+  this.is_initial_draw_ = true;
+  this.annotations_ = [];
+
+  // Clear the div. This ensure that, if multiple dygraphs are passed the same
+  // div, then only one will be drawn.
+  div.innerHTML = "";
+
+  // If the div isn't already sized then inherit from our attrs or
+  // give it a default size.
+  if (div.style.width == '') {
+    div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
+  }
+  if (div.style.height == '') {
+    div.style.height = attrs.height || Dygraph.DEFAULT_HEIGHT + "px";
+  }
+  this.width_ = parseInt(div.style.width, 10);
+  this.height_ = parseInt(div.style.height, 10);
+  // The div might have been specified as percent of the current window size,
+  // convert that to an appropriate number of pixels.
+  if (div.style.width.indexOf("%") == div.style.width.length - 1) {
+    this.width_ = div.offsetWidth;
+  }
+  if (div.style.height.indexOf("%") == div.style.height.length - 1) {
+    this.height_ = div.offsetHeight;
+  }
+
+  if (this.width_ == 0) {
+    this.error("dygraph has zero width. Please specify a width in pixels.");
+  }
+  if (this.height_ == 0) {
+    this.error("dygraph has zero height. Please specify a height in pixels.");
+  }
+
+  // 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.attrs_ = {};
+  Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
+
+  this.boundaryIds_ = [];
+
+  // Make a note of whether labels will be pulled from the CSV file.
+  this.labelsFromCSV_ = (this.attr_("labels") == null);
+
+  Dygraph.addAnnotationRule();
+
+  // Create the containing DIV and other interactive elements
+  this.createInterface_();
+
+  this.start_();
+};
+
+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;
+  }
+};
+
+// TODO(danvk): any way I can get the line numbers to be this.warn call?
+Dygraph.prototype.log = function(severity, message) {
+  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;
+    }
+  }
+}
+Dygraph.prototype.info = function(message) {
+  this.log(Dygraph.INFO, message);
+}
+Dygraph.prototype.warn = function(message) {
+  this.log(Dygraph.WARNING, message);
+}
+Dygraph.prototype.error = function(message) {
+  this.log(Dygraph.ERROR, message);
+}
+
+/**
+ * Returns the current rolling period, as set by the user or an option.
+ * @return {Number} The number of days 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() {
+  if (this.dateWindow_) return this.dateWindow_;
+
+  // The entire chart is visible.
+  var left = this.rawData_[0][0];
+  var right = this.rawData_[this.rawData_.length - 1][0];
+  return [left, right];
+};
+
+/**
+ * Returns the currently-visible y-range. This can be affected by zooming,
+ * panning or a call to updateOptions.
+ * Returns a two-element array: [bottom, top].
+ */
+Dygraph.prototype.yAxisRange = function() {
+  return this.displayedYRange_;
+};
+
+/**
+ * Convert from data coordinates to canvas/div X/Y coordinates.
+ * Returns a two-element array: [X, Y]
+ */
+Dygraph.prototype.toDomCoords = function(x, y) {
+  var ret = [null, null];
+  var area = this.plotter_.area;
+  if (x !== null) {
+    var xRange = this.xAxisRange();
+    ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
+  }
+
+  if (y !== null) {
+    var yRange = this.yAxisRange();
+    ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
+  }
+
+  return ret;
+};
+
+// TODO(danvk): use these functions throughout dygraphs.
+/**
+ * Convert from canvas/div coords to data coordinates.
+ * Returns a two-element array: [X, Y]
+ */
+Dygraph.prototype.toDataCoords = function(x, y) {
+  var ret = [null, null];
+  var area = this.plotter_.area;
+  if (x !== null) {
+    var xRange = this.xAxisRange();
+    ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+  }
+
+  if (y !== null) {
+    var yRange = this.yAxisRange();
+    ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
+  }
+
+  return ret;
+};
+
+/**
+ * Returns the number of columns (including the independent variable).
+ */
+Dygraph.prototype.numColumns = function() {
+  return this.rawData_[0].length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ */
+Dygraph.prototype.numRows = function() {
+  return this.rawData_.length;
+};
+
+/**
+ * Returns the value in the given row and column. If the row and column exceed
+ * the bounds on the data, returns null. Also returns null if the value is
+ * missing.
+ */
+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];
+};
+
+Dygraph.addEvent = function(el, evt, fn) {
+  var normed_fn = function(e) {
+    if (!e) var e = window.event;
+    fn(e);
+  };
+  if (window.addEventListener) {  // Mozilla, Netscape, Firefox
+    el.addEventListener(evt, normed_fn, false);
+  } else {  // IE
+    el.attachEvent('on' + evt, normed_fn);
+  }
+};
+
+Dygraph.clipCanvas_ = function(cnv, clip) {
+  var ctx = cnv.getContext("2d");
+  ctx.beginPath();
+  ctx.rect(clip.left, clip.top, clip.width, clip.height);
+  ctx.clip();
+};
+
+/**
+ * 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);
+
+  var clip = {
+    top: 0,
+    left: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
+  };
+  clip.width = this.width_ - clip.left - this.attr_("rightGap");
+  clip.height = this.height_ - this.attr_("axisLabelFontSize")
+      - 2 * this.attr_("axisTickSize");
+  this.clippingArea_ = clip;
+
+  // 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
+
+  // ... and for static parts of the chart.
+  this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
+
+  // 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.canvas_;
+
+  // Make sure we don't overdraw.
+  Dygraph.clipCanvas_(this.hidden_, this.clippingArea_);
+  Dygraph.clipCanvas_(this.canvas_, this.clippingArea_);
+
+  var dygraph = this;
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
+    dygraph.mouseMove_(e);
+  });
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
+    dygraph.mouseOut_(e);
+  });
+
+  // Create the grapher
+  // TODO(danvk): why does the Layout need its own set of options?
+  this.layoutOptions_ = { 'xOriginIsZero': false };
+  Dygraph.update(this.layoutOptions_, this.attrs_);
+  Dygraph.update(this.layoutOptions_, this.user_attrs_);
+  Dygraph.update(this.layoutOptions_, {
+    'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
+
+  this.layout_ = new DygraphLayout(this, this.layoutOptions_);
+
+  // TODO(danvk): why does the Renderer need its own set of options?
+  this.renderOptions_ = { colorScheme: this.colors_,
+                          strokeColor: null,
+                          axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
+  Dygraph.update(this.renderOptions_, this.attrs_);
+  Dygraph.update(this.renderOptions_, this.user_attrs_);
+  this.plotter_ = new DygraphCanvasRenderer(this,
+                                            this.hidden_, this.layout_,
+                                            this.renderOptions_);
+
+  this.createStatusMessage_();
+  this.createRollInterface_();
+  this.createDragInterface_();
+};
+
+/**
+ * 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;
+      }
+    }
+  };
+
+  // These may not all be necessary, but it can't hurt...
+  nullOut(this.layout_);
+  nullOut(this.plotter_);
+  nullOut(this);
+};
+
+/**
+ * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
+ * this particular canvas. All Dygraph work 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;
+};
+
+// Taken from MochiKit.Color
+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 + ')';
+};
+
+
+/**
+ * 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() {
+  // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
+  // away with this.renderOptions_.
+  var num = this.attr_("labels").length - 1;
+  this.colors_ = [];
+  var colors = this.attr_('colors');
+  if (!colors) {
+    var sat = this.attr_('colorSaturation') || 1.0;
+    var val = this.attr_('colorValue') || 0.5;
+    var half = Math.ceil(num / 2);
+    for (var 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 (var i = 0; i < num; i++) {
+      if (!this.visibility()[i]) continue;
+      var colorStr = colors[i % colors.length];
+      this.colors_.push(colorStr);
+    }
+  }
+
+  // TODO(danvk): update this w/r/t/ the new options system.
+  this.renderOptions_.colorScheme = this.colors_;
+  Dygraph.update(this.plotter_.options, this.renderOptions_);
+  Dygraph.update(this.layoutOptions_, this.user_attrs_);
+  Dygraph.update(this.layoutOptions_, this.attrs_);
+}
+
+/**
+ * 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_;
+};
+
+// 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
+Dygraph.findPosX = function(obj) {
+  var curleft = 0;
+  if(obj.offsetParent)
+    while(1)
+    {
+      curleft += obj.offsetLeft;
+      if(!obj.offsetParent)
+        break;
+      obj = obj.offsetParent;
+    }
+  else if(obj.x)
+    curleft += obj.x;
+  return curleft;
+};
+
+Dygraph.findPosY = function(obj) {
+  var curtop = 0;
+  if(obj.offsetParent)
+    while(1)
+    {
+      curtop += obj.offsetTop;
+      if(!obj.offsetParent)
+        break;
+      obj = obj.offsetParent;
+    }
+  else if(obj.y)
+    curtop += obj.y;
+  return curtop;
+};
+
+
+
+/**
+ * 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");
+    for (var name in messagestyle) {
+      if (messagestyle.hasOwnProperty(name)) {
+        div.style[name] = messagestyle[name];
+      }
+    }
+    this.graphDiv.appendChild(div);
+    this.attrs_.labelsDiv = div;
+  }
+};
+
+/**
+ * Create the text box to adjust the averaging period
+ * @return {Object} The newly-created text box
+ * @private
+ */
+Dygraph.prototype.createRollInterface_ = function() {
+  var display = this.attr_('showRoller') ? "block" : "none";
+  var textAttr = { "position": "absolute",
+                   "zIndex": 10,
+                   "top": (this.plotter_.area.h - 25) + "px",
+                   "left": (this.plotter_.area.x + 1) + "px",
+                   "display": display
+                  };
+  var roller = document.createElement("input");
+  roller.type = "text";
+  roller.size = "2";
+  roller.value = this.rollPeriod_;
+  for (var name in textAttr) {
+    if (textAttr.hasOwnProperty(name)) {
+      roller.style[name] = textAttr[name];
+    }
+  }
+
+  var pa = this.graphDiv;
+  pa.appendChild(roller);
+  var dygraph = this;
+  roller.onchange = function() { dygraph.adjustRoll(roller.value); };
+  return roller;
+};
+
+// These functions are 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);
+  }
+};
+
+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);
+  }
+};
+
+/**
+ * Set up all the mouse handlers needed to capture dragging behavior for zoom
+ * events.
+ * @private
+ */
+Dygraph.prototype.createDragInterface_ = function() {
+  var self = this;
+
+  // Tracks whether the mouse is down right now
+  var isZooming = false;
+  var isPanning = false;
+  var dragStartX = null;
+  var dragStartY = null;
+  var dragEndX = null;
+  var dragEndY = null;
+  var prevEndX = null;
+  var draggingDate = null;
+  var dateRange = null;
+
+  // Utility function to convert page-wide coordinates to canvas coords
+  var px = 0;
+  var py = 0;
+  var getX = function(e) { return Dygraph.pageX(e) - px };
+  var getY = function(e) { return Dygraph.pageY(e) - py };
+
+  // Draw zoom rectangles when the mouse is down and the user moves around
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
+    if (isZooming) {
+      dragEndX = getX(event);
+      dragEndY = getY(event);
+
+      self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
+      prevEndX = dragEndX;
+    } else if (isPanning) {
+      dragEndX = getX(event);
+      dragEndY = getY(event);
+
+      // Want to have it so that:
+      // 1. draggingDate appears at dragEndX
+      // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
+
+      self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
+      self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
+      self.drawGraph_(self.rawData_);
+    }
+  });
+
+  // Track the beginning of drag events
+  Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
+    px = Dygraph.findPosX(self.canvas_);
+    py = Dygraph.findPosY(self.canvas_);
+    dragStartX = getX(event);
+    dragStartY = getY(event);
+
+    if (event.altKey || event.shiftKey) {
+      if (!self.dateWindow_) return;  // have to be zoomed in to pan.
+      isPanning = true;
+      dateRange = self.dateWindow_[1] - self.dateWindow_[0];
+      draggingDate = (dragStartX / self.width_) * dateRange +
+        self.dateWindow_[0];
+    } else {
+      isZooming = true;
+    }
+  });
+
+  // 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 (isZooming || isPanning) {
+      isZooming = false;
+      dragStartX = null;
+      dragStartY = null;
+    }
+
+    if (isPanning) {
+      isPanning = false;
+      draggingDate = null;
+      dateRange = null;
+    }
+  });
+
+  // Temporarily cancel the dragging event when the mouse leaves the graph
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
+    if (isZooming) {
+      dragEndX = null;
+      dragEndY = null;
+    }
+  });
+
+  // If the mouse is released on the canvas during a drag event, then it's a
+  // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
+    if (isZooming) {
+      isZooming = false;
+      dragEndX = getX(event);
+      dragEndY = getY(event);
+      var regionWidth = Math.abs(dragEndX - dragStartX);
+      var regionHeight = Math.abs(dragEndY - dragStartY);
+
+      if (regionWidth < 2 && regionHeight < 2 &&
+          self.lastx_ != undefined && self.lastx_ != -1) {
+        // TODO(danvk): pass along more info about the points, e.g. 'x'
+        if (self.attr_('clickCallback') != null) {
+          self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
+        }
+        if (self.attr_('pointClickCallback')) {
+          // check if the click was on a particular point.
+          var closestIdx = -1;
+          var closestDistance = 0;
+          for (var i = 0; i < self.selPoints_.length; i++) {
+            var p = self.selPoints_[i];
+            var distance = Math.pow(p.canvasx - dragEndX, 2) +
+                           Math.pow(p.canvasy - dragEndY, 2);
+            if (closestIdx == -1 || distance < closestDistance) {
+              closestDistance = distance;
+              closestIdx = i;
+            }
+          }
+
+          // Allow any click within two pixels of the dot.
+          var radius = self.attr_('highlightCircleSize') + 2;
+          if (closestDistance <= 5 * 5) {
+            self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
+          }
+        }
+      }
+
+      if (regionWidth >= 10) {
+        self.doZoom_(Math.min(dragStartX, dragEndX),
+                     Math.max(dragStartX, dragEndX));
+      } else {
+        self.canvas_.getContext("2d").clearRect(0, 0,
+                                           self.canvas_.width,
+                                           self.canvas_.height);
+      }
+
+      dragStartX = null;
+      dragStartY = null;
+    }
+
+    if (isPanning) {
+      isPanning = false;
+      draggingDate = null;
+      dateRange = null;
+    }
+  });
+
+  // Double-clicking zooms back out
+  Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
+    if (self.dateWindow_ == null) return;
+    self.dateWindow_ = null;
+    self.drawGraph_(self.rawData_);
+    var minDate = self.rawData_[0][0];
+    var maxDate = self.rawData_[self.rawData_.length - 1][0];
+    if (self.attr_("zoomCallback")) {
+      self.attr_("zoomCallback")(minDate, maxDate);
+    }
+  });
+};
+
+/**
+ * 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} 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} prevEndX The value of endX on the previous call to this
+ * function. Used to avoid excess redrawing
+ * @private
+ */
+Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
+  var ctx = this.canvas_.getContext("2d");
+
+  // Clean up from the previous rect if necessary
+  if (prevEndX) {
+    ctx.clearRect(Math.min(startX, prevEndX), 0,
+                  Math.abs(startX - prevEndX), this.height_);
+  }
+
+  // Draw a light-grey rectangle to show the new viewing area
+  if (endX && startX) {
+    ctx.fillStyle = "rgba(128,128,128,0.33)";
+    ctx.fillRect(Math.min(startX, endX), 0,
+                 Math.abs(endX - startX), this.height_);
+  }
+};
+
+/**
+ * Zoom to something containing [lowX, highX]. These are pixel coordinates
+ * in the canvas. The exact zoom window may be slightly larger if there are no
+ * data points near lowX or highX. 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.doZoom_ = function(lowX, highX) {
+  // Find the earliest and latest dates contained in this canvasx range.
+  var r = this.toDataCoords(lowX, null);
+  var minDate = r[0];
+  r = this.toDataCoords(highX, null);
+  var maxDate = r[0];
+
+  this.dateWindow_ = [minDate, maxDate];
+  this.drawGraph_(this.rawData_);
+  if (this.attr_("zoomCallback")) {
+    this.attr_("zoomCallback")(minDate, maxDate);
+  }
+};
+
+/**
+ * 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) {
+  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+  var points = this.layout_.points;
+
+  var lastx = -1;
+  var lasty = -1;
+
+  // Loop through all the points and find the date nearest to our current
+  // location.
+  var minDist = 1e+100;
+  var idx = -1;
+  for (var i = 0; i < points.length; i++) {
+    var dist = Math.abs(points[i].canvasx - canvasx);
+    if (dist > minDist) continue;
+    minDist = dist;
+    idx = i;
+  }
+  if (idx >= 0) lastx = points[idx].xval;
+  // Check that you can really highlight the last day's data
+  if (canvasx > points[points.length-1].canvasx)
+    lastx = points[points.length-1].xval;
+
+  // Extract the points we've selected
+  this.selPoints_ = [];
+  var l = points.length;
+  if (!this.attr_("stackedGraph")) {
+    for (var 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 (var 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_);
+    }
+  }
+
+  // Save last x position for callbacks.
+  this.lastx_ = lastx;
+
+  this.updateSelection_();
+};
+
+/**
+ * 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 ctx = this.canvas_.getContext("2d");
+  if (this.previousVerticalX_ >= 0) {
+    // Determine the maximum highlight circle size.
+    var maxCircleSize = 0;
+    var labels = this.attr_('labels');
+    for (var 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_);
+  }
+
+  var isOK = function(x) { return x && !isNaN(x); };
+
+  if (this.selPoints_.length > 0) {
+    var canvasx = this.selPoints_[0].canvasx;
+
+    // Set the status message to indicate the selected point(s)
+    var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
+    var fmtFunc = this.attr_('yValueFormatter');
+    var clen = this.colors_.length;
+
+    if (this.attr_('showLabelsOnHighlight')) {
+      // Set the status message to indicate the selected point(s)
+      for (var i = 0; i < this.selPoints_.length; i++) {
+        if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
+        if (!isOK(this.selPoints_[i].canvasy)) continue;
+        if (this.attr_("labelsSeparateLines")) {
+          replace += "<br/>";
+        }
+        var point = this.selPoints_[i];
+        var c = new RGBColor(this.plotter_.colors[point.name]);
+        var yval = fmtFunc(point.yval);
+        replace += " <b><font color='" + c.toHex() + "'>"
+                + point.name + "</font></b>:"
+                + yval;
+      }
+
+      this.attr_("labelsDiv").innerHTML = replace;
+    }
+
+    // Draw colored circles over the center of each selected point
+    ctx.save();
+    for (var i = 0; i < this.selPoints_.length; i++) {
+      if (!isOK(this.selPoints_[i].canvasy)) continue;
+      var circleSize =
+        this.attr_('highlightCircleSize', this.selPoints_[i].name);
+      ctx.beginPath();
+      ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
+      ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
+              0, 2 * Math.PI, false);
+      ctx.fill();
+    }
+    ctx.restore();
+
+    this.previousVerticalX_ = canvasx;
+  }
+};
+
+/**
+ * Set manually set selected dots, and display information about them
+ * @param int row number that should by highlighted
+ *            false value clears the selection
+ * @public
+ */
+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.lastx_ = -1;
+    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();
+  }
+};
+
+/**
+ * Remove all selection from the canvas
+ * @public
+ */
+Dygraph.prototype.clearSelection = function() {
+  // Get rid of the overlay data
+  var ctx = this.canvas_.getContext("2d");
+  ctx.clearRect(0, 0, this.width_, this.height_);
+  this.attr_("labelsDiv").innerHTML = "";
+  this.selPoints_ = [];
+  this.lastx_ = -1;
+}
+
+/**
+ * Returns the number of the currently selected row
+ * @return int row number, of -1 if nothing is selected
+ * @public
+ */
+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;
+}
+
+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());
+  }
+}
+
+/**
+ * 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.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());
+    }
+  }
+}
+
+/**
+ * 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, self) {
+  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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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.drawGraph_(this.rawData_);
+};
+
+Dygraph.prototype.months =  ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
+                             "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
+
+/**
+ * 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 startDate, endDate;
+  if (this.dateWindow_) {
+    startDate = this.dateWindow_[0];
+    endDate = this.dateWindow_[1];
+  } else {
+    startDate = this.rawData_[0][0];
+    endDate   = this.rawData_[this.rawData_.length - 1][0];
+  }
+
+  var xTicks = this.attr_('xTicker')(startDate, endDate, this);
+  this.layout_.updateOptions({xTicks: xTicks});
+};
+
+// 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.NUM_GRANULARITIES = 20;
+
+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;
+
+// NumXTicks()
+//
+//   If we used this time granularity, how many ticks would there be?
+//   This is only an approximation, but it's generally good enough.
+//
+Dygraph.prototype.NumXTicks = 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; }
+
+    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);
+  }
+};
+
+// GetXAxis()
+//
+//   Construct an x-axis of nicely-formatted times on meaningful boundaries
+//   (e.g. 'Jan 09' rather than 'Jan 22, 2009').
+//
+//   Returns an array containing {v: millis, label: label} dictionaries.
+//
+Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
+  var formatter = this.attr_("xAxisLabelFormatter");
+  var ticks = [];
+  if (granularity < Dygraph.MONTHLY) {
+    // Generate one tick mark for every fixed interval of time.
+    var spacing = Dygraph.SHORT_SPACINGS[granularity];
+    var format = '%d%b';  // e.g. "1Jan"
+
+    // 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);
+    if (g <= 60) {  // seconds
+      var x = d.getSeconds(); d.setSeconds(x - x % g);
+    } else {
+      d.setSeconds(0);
+      g /= 60;
+      if (g <= 60) {  // minutes
+        var x = d.getMinutes(); d.setMinutes(x - x % g);
+      } else {
+        d.setMinutes(0);
+        g /= 60;
+
+        if (g <= 24) {  // days
+          var 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 (var t = start_time; t <= end_time; t += spacing) {
+      ticks.push({ v:t, label: formatter(new Date(t), granularity) });
+    }
+  } 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, 12 ];
+    } 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;
+    }
+
+    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";
+        var t = Date.parse(date_str);
+        if (t < start_time || t > end_time) continue;
+        ticks.push({ v:t, label: formatter(new Date(t), granularity) });
+      }
+    }
+  }
+
+  return ticks;
+};
+
+
+/**
+ * Add ticks to the x-axis based on a date range.
+ * @param {Number} startDate Start of the date window (millis since epoch)
+ * @param {Number} endDate End of the date window (millis since epoch)
+ * @return {Array.<Object>} Array of {label, value} tuples.
+ * @public
+ */
+Dygraph.dateTicker = function(startDate, endDate, self) {
+  var chosen = -1;
+  for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
+    var num_ticks = self.NumXTicks(startDate, endDate, i);
+    if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
+      chosen = i;
+      break;
+    }
+  }
+
+  if (chosen >= 0) {
+    return self.GetXAxis(startDate, endDate, chosen);
+  } else {
+    // TODO(danvk): signal error.
+  }
+};
+
+/**
+ * Add ticks when the x axis has numbers on it (instead of dates)
+ * @param {Number} startDate Start of the date window (millis since epoch)
+ * @param {Number} endDate End of the date window (millis since epoch)
+ * @param self
+ * @param {function} formatter: Optional formatter to use for each tick value
+ * @return {Array.<Object>} Array of {label, value} tuples.
+ * @public
+ */
+Dygraph.numericTicks = function(minV, maxV, self, formatter) {
+  // 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.
+  if (self.attr_("labelsKMG2")) {
+    var mults = [1, 2, 4, 8];
+  } else {
+    var mults = [1, 2, 5];
+  }
+  var scale, low_val, high_val, nTicks;
+  // TODO(danvk): make it possible to set this for x- and y-axes independently.
+  var pixelsPerTick = self.attr_('pixelsPerYLabel');
+  for (var i = -10; i < 50; i++) {
+    if (self.attr_("labelsKMG2")) {
+      var base_scale = Math.pow(16, i);
+    } else {
+      var base_scale = Math.pow(10, i);
+    }
+    for (var j = 0; j < mults.length; j++) {
+      scale = base_scale * mults[j];
+      low_val = Math.floor(minV / scale) * scale;
+      high_val = Math.ceil(maxV / scale) * scale;
+      nTicks = Math.abs(high_val - low_val) / scale;
+      var spacing = self.height_ / nTicks;
+      // wish I could break out of both loops at once...
+      if (spacing > pixelsPerTick) break;
+    }
+    if (spacing > pixelsPerTick) break;
+  }
+
+  // Construct labels for the ticks
+  var ticks = [];
+  var k;
+  var k_labels = [];
+  if (self.attr_("labelsKMB")) {
+    k = 1000;
+    k_labels = [ "K", "M", "B", "T" ];
+  }
+  if (self.attr_("labelsKMG2")) {
+    if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+    k = 1024;
+    k_labels = [ "k", "M", "G", "T" ];
+  }
+
+  // Allow reverse y-axis if it's explicitly requested.
+  if (low_val > high_val) scale *= -1;
+
+  for (var i = 0; i < nTicks; i++) {
+    var tickV = low_val + i * scale;
+    var absTickV = Math.abs(tickV);
+    var label;
+    if (formatter != undefined) {
+      label = formatter(tickV);
+    } else {
+      label = Dygraph.round_(tickV, 2);
+    }
+    if (k_labels.length) {
+      // Round up to an appropriate unit.
+      var n = k*k*k*k;
+      for (var j = 3; j >= 0; j--, n /= k) {
+        if (absTickV >= n) {
+          label = Dygraph.round_(tickV / n, 1) + k_labels[j];
+          break;
+        }
+      }
+    }
+    ticks.push( {label: label, v: tickV} );
+  }
+  return ticks;
+};
+
+/**
+ * Adds appropriate ticks on the y-axis
+ * @param {Number} minY The minimum Y value in the data set
+ * @param {Number} maxY The maximum Y value in the data set
+ * @private
+ */
+Dygraph.prototype.addYTicks_ = function(minY, maxY) {
+  // Set the number of ticks so that the labels are human-friendly.
+  // TODO(danvk): make this an attribute as well.
+  var formatter = this.attr_('yAxisLabelFormatter') ? this.attr_('yAxisLabelFormatter') : this.attr_('yValueFormatter');
+  var ticks = Dygraph.numericTicks(minY, maxY, this, formatter);
+  this.layout_.updateOptions( { yAxis: [minY, maxY],
+                                yTicks: ticks } );
+};
+
+// Computes the range of the data series (including confidence intervals).
+// series is either [ [x1, y1], [x2, y2], ... ] or
+// [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
+// Returns [low, high]
+Dygraph.prototype.extremeValues_ = function(series) {
+  var minY = null, maxY = null;
+
+  var bars = this.attr_("errorBars") || this.attr_("customBars");
+  if (bars) {
+    // With custom bars, maxY is the max of the high values.
+    for (var j = 0; j < series.length; j++) {
+      var 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 (var j = 0; j < series.length; j++) {
+      var 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];
+};
+
+/**
+ * Update the graph with new data. Data is in the format
+ * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
+ * or, if errorBars=true,
+ * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
+ * @param {Array.<Object>} data The data (see above)
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function(data) {
+  // This is used to set the second parameter to drawCallback, below.
+  var is_initial_draw = this.is_initial_draw_;
+  this.is_initial_draw_ = false;
+
+  var minY = null, maxY = null;
+  this.layout_.removeAllDatasets();
+  this.setColors_();
+  this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
+
+  // 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 cumulative_y = [];  // For stacked series.
+  var datasets = [];
+
+  // Loop over all fields and create datasets
+  for (var i = data[0].length - 1; i >= 1; i--) {
+    if (!this.visibility()[i - 1]) continue;
+
+    var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
+
+    var series = [];
+    for (var j = 0; j < data.length; j++) {
+      if (data[j][i] != null || !connectSeparatedPoints) {
+        var date = data[j][0];
+        series.push([date, data[j][i]]);
+      }
+    }
+    series = this.rollingAverage(series, this.rollPeriod_);
+
+    // 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 (this.dateWindow_) {
+      var low = this.dateWindow_[0];
+      var high= this.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 (var 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++;
+      this.boundaryIds_[i-1] = [firstIdx, lastIdx];
+      for (var k = firstIdx; k <= lastIdx; k++) {
+        pruned.push(series[k]);
+      }
+      series = pruned;
+    } else {
+      this.boundaryIds_[i-1] = [0, series.length-1];
+    }
+
+    var extremes = this.extremeValues_(series);
+    var thisMinY = extremes[0];
+    var thisMaxY = extremes[1];
+    if (minY === null || (thisMinY != null && thisMinY < minY)) minY = thisMinY;
+    if (maxY === null || (thisMaxY != null && thisMaxY > maxY)) maxY = thisMaxY;
+
+    if (bars) {
+      for (var j=0; j<series.length; j++) {
+        val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
+        series[j] = val;
+      }
+    } else if (this.attr_("stackedGraph")) {
+      var l = series.length;
+      var actual_y;
+      for (var 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 (!maxY || cumulative_y[x] > maxY)
+          maxY = cumulative_y[x];
+      }
+    }
+
+    datasets[i] = series;
+  }
+
+  for (var i = 1; i < datasets.length; i++) {
+    if (!this.visibility()[i - 1]) continue;
+    this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
+  }
+
+  // Use some heuristics to come up with a good maxY value, unless it's been
+  // set explicitly by the user.
+  if (this.valueRange_ != null) {
+    this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
+    this.displayedYRange_ = this.valueRange_;
+  } else {
+    // This affects the calculation of span, below.
+    if (this.attr_("includeZero") && minY > 0) {
+      minY = 0;
+    }
+
+    // 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 = maxY + 0.1 * span;
+    var 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;
+    }
+
+    this.addYTicks_(minAxisY, maxAxisY);
+    this.displayedYRange_ = [minAxisY, maxAxisY];
+  }
+
+  this.addXTicks_();
+
+  // Tell PlotKit to use this new data and render itself
+  this.layout_.updateOptions({dateWindow: this.dateWindow_});
+  this.layout_.evaluateWithError();
+  this.plotter_.clear();
+  this.plotter_.render();
+  this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+                                         this.canvas_.height);
+
+  if (this.attr_("drawCallback") !== null) {
+    this.attr_("drawCallback")(this, is_initial_draw);
+  }
+};
+
+/**
+ * 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 days over which to average the data
+ */
+Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
+  if (originalData.length < 2)
+    return originalData;
+  var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
+  var rollingData = [];
+  var sigma = this.attr_("sigma");
+
+  if (this.fractions_) {
+    var num = 0;
+    var den = 0;  // numerator/denominator
+    var mult = 100.0;
+    for (var 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.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;
+            var low  = (p + sigma * sigma / (2 * den) - pm) / denom;
+            var 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 {
+          var 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")) {
+    var low = 0;
+    var mid = 0;
+    var high = 0;
+    var count = 0;
+    for (var i = 0; i < originalData.length; i++) {
+      var data = originalData[i][1];
+      var 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;
+        }
+      }
+      rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
+                                              1.0 * (mid - low) / count,
+                                              1.0 * (high - mid) / count ]];
+    }
+  } else {
+    // Calculate the rolling average for the first rollPeriod - 1 points where
+    // there is not enough data to roll over the full number of days
+    var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
+    if (!this.attr_("errorBars")){
+      if (rollPeriod == 1) {
+        return originalData;
+      }
+
+      for (var i = 0; i < originalData.length; i++) {
+        var sum = 0;
+        var num_ok = 0;
+        for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+          var 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 (var i = 0; i < originalData.length; i++) {
+        var sum = 0;
+        var variance = 0;
+        var num_ok = 0;
+        for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+          var 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) {
+          var 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;
+};
+
+/**
+ * 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.
+ * @public
+ */
+Dygraph.dateParser = function(dateStr, self) {
+  var dateStrSlashed;
+  var 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 = Date.parse(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 = Date.parse(dateStrSlashed);
+  } else {
+    // Any format that Date.parse will accept, e.g. "2009/07/12" or
+    // "2009/07/12 12:34:56"
+    d = Date.parse(dateStr);
+  }
+
+  if (!d || isNaN(d)) {
+    self.error("Couldn't parse " + dateStr + " as a date");
+  }
+  return d;
+};
+
+/**
+ * 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;
+  if (str.indexOf('-') >= 0 ||
+      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_.xValueFormatter = Dygraph.dateString_;
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.xTicker = Dygraph.dateTicker;
+    this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
+  } else {
+    this.attrs_.xValueFormatter = function(x) { return x; };
+    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
+    this.attrs_.xTicker = Dygraph.numericTicks;
+    this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
+  }
+};
+
+/**
+ * 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 {Array.<Object>} data See above.
+ * @private
+ *
+ * @return Array.<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");
+
+  // 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 (this.labelsFromCSV_) {
+    start = 1;
+    this.attrs_.labels = lines[0].split(delim);
+  }
+
+  // Parse the x as a float or return null if it's not a number.
+  var parseFloatOrNull = function(x) {
+    var val = parseFloat(x);
+    return isNaN(val) ? null : val;
+  };
+
+  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];
+    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 (var j = 1; j < inFields.length; j++) {
+        // TODO(danvk): figure out an appropriate way to flag parse errors.
+        var vals = inFields[j].split("/");
+        fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
+      }
+    } else if (this.attr_("errorBars")) {
+      // If there are error bars, values are (value, stddev) pairs
+      for (var j = 1; j < inFields.length; j += 2)
+        fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
+                               parseFloatOrNull(inFields[j + 1])];
+    } else if (this.attr_("customBars")) {
+      // Bars are a low;center;high tuple
+      for (var j = 1; j < inFields.length; j++) {
+        var vals = inFields[j].split(";");
+        fields[j] = [ parseFloatOrNull(vals[0]),
+                      parseFloatOrNull(vals[1]),
+                      parseFloatOrNull(vals[2]) ];
+      }
+    } else {
+      // Values are just numbers
+      for (var j = 1; j < inFields.length; j++) {
+        fields[j] = parseFloatOrNull(inFields[j]);
+      }
+    }
+    if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
+      outOfOrder = true;
+    }
+    ret.push(fields);
+
+    if (fields.length != expectedCols) {
+      this.error("Number of columns in line " + i + " (" + fields.length +
+                 ") does not agree with number of labels (" + expectedCols +
+                 ") " + line);
+    }
+  }
+
+  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;
+};
+
+/**
+ * The user has provided their data as a pre-packaged JS array. If the x values
+ * are numeric, this is the same as dygraphs' internal format. If the x values
+ * are dates, we need to convert them from Date objects to ms since epoch.
+ * @param {Array.<Object>} data
+ * @return {Array.<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;
+  }
+
+  if (this.attr_("labels") == null) {
+    this.warn("Using default labels. Set labels explicitly via 'labels' " +
+              "in the options parameter");
+    this.attrs_.labels = [ "X" ];
+    for (var 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_.xValueFormatter = Dygraph.dateString_;
+    this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
+    this.attrs_.xTicker = Dygraph.dateTicker;
+
+    // Assume they're all dates.
+    var parsedData = Dygraph.clone(data);
+    for (var 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.
+    this.attrs_.xValueFormatter = function(x) { return x; };
+    this.attrs_.xTicker = 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 {Array.<Object>} data See above.
+ * @private
+ */
+Dygraph.prototype.parseDataTable_ = function(data) {
+  var cols = data.getNumberOfColumns();
+  var rows = data.getNumberOfRows();
+
+  var indepType = data.getColumnType(0);
+  if (indepType == 'date' || indepType == 'datetime') {
+    this.attrs_.xValueFormatter = Dygraph.dateString_;
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.xTicker = Dygraph.dateTicker;
+    this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
+  } else if (indepType == 'number') {
+    this.attrs_.xValueFormatter = function(x) { return x; };
+    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
+    this.attrs_.xTicker = Dygraph.numericTicks;
+    this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
+  } 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;
+  for (var 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 (var 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 (var 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 (var 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 = String.fromCharCode(65 /* A */ + 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);
+        }
+      }
+    } else {
+      for (var 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);
+  }
+}
+
+// These functions are all based on MochiKit.
+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;
+};
+
+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;
+};
+
+Dygraph.isDateLike = function (o) {
+  if (typeof(o) != "object" || o === null ||
+      typeof(o.getTime) != 'function') {
+    return false;
+  }
+  return true;
+};
+
+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;
+};
+
+
+/**
+ * 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() {
+  if (typeof this.file_ == 'function') {
+    // CSV string. Pretend we got it via XHR.
+    this.loadedEvent_(this.file_());
+  } else if (Dygraph.isArrayLike(this.file_)) {
+    this.rawData_ = this.parseArray_(this.file_);
+    this.drawGraph_(this.rawData_);
+  } else if (typeof this.file_ == 'object' &&
+             typeof this.file_.getColumnRange == 'function') {
+    // must be a DataTable from gviz.
+    this.parseDataTable_(this.file_);
+    this.drawGraph_(this.rawData_);
+  } else if (typeof this.file_ == 'string') {
+    // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
+    if (this.file_.indexOf('\n') >= 0) {
+      this.loadedEvent_(this.file_);
+    } else {
+      var req = new XMLHttpRequest();
+      var caller = this;
+      req.onreadystatechange = function () {
+        if (req.readyState == 4) {
+          if (req.status == 200) {
+            caller.loadedEvent_(req.responseText);
+          }
+        }
+      };
+
+      req.open("GET", this.file_, true);
+      req.send(null);
+    }
+  } else {
+    this.error("Unknown data format: " + (typeof this.file_));
+  }
+};
+
+/**
+ * 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>
+ * @param {Object} attrs The new properties and values
+ */
+Dygraph.prototype.updateOptions = function(attrs) {
+  // TODO(danvk): this is a mess. Rethink this function.
+  if (attrs.rollPeriod) {
+    this.rollPeriod_ = attrs.rollPeriod;
+  }
+  if (attrs.dateWindow) {
+    this.dateWindow_ = attrs.dateWindow;
+  }
+  if (attrs.valueRange) {
+    this.valueRange_ = attrs.valueRange;
+  }
+
+  // TODO(danvk): validate per-series options.
+  // Supported:
+  // strokeWidth
+  // pointSize
+  // drawPoints
+  // highlightCircleSize
+
+  Dygraph.update(this.user_attrs_, attrs);
+  Dygraph.update(this.renderOptions_, attrs);
+
+  this.labelsFromCSV_ = (this.attr_("labels") == null);
+
+  // TODO(danvk): this doesn't match the constructor logic
+  this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
+  if (attrs['file']) {
+    this.file_ = attrs['file'];
+    this.start_();
+  } else {
+    this.drawGraph_(this.rawData_);
+  }
+};
+
+/**
+ * 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;
+  }
+
+  // TODO(danvk): there should be a clear() method.
+  this.maindiv_.innerHTML = "";
+  this.attrs_.labelsDiv = null;
+
+  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_.offsetWidth;
+    this.height_ = this.maindiv_.offsetHeight;
+  }
+
+  this.createInterface_();
+  this.drawGraph_(this.rawData_);
+
+  this.resize_lock = false;
+};
+
+/**
+ * Adjusts the number of days in the rolling average. Updates the graph to
+ * reflect the new averaging period.
+ * @param {Number} length Number of days over which to average the data.
+ */
+Dygraph.prototype.adjustRoll = function(length) {
+  this.rollPeriod_ = length;
+  this.drawGraph_(this.rawData_);
+};
+
+/**
+ * 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"] = [];
+  }
+  while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
+    this.attr_("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.drawGraph_(this.rawData_);
+  }
+};
+
+/**
+ * Update the list of annotations and redraw the chart.
+ */
+Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
+  this.annotations_ = ann;
+  this.layout_.setAnnotations(this.annotations_);
+  if (!suppressDraw) {
+    this.drawGraph_(this.rawData_);
+  }
+};
+
+/**
+ * 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;
+};
+
+Dygraph.addAnnotationRule = function() {
+  if (Dygraph.addedAnnotationCSS) return;
+
+  var mysheet;
+  if (document.styleSheets.length > 0) {
+    mysheet = document.styleSheets[0];
+  } else {
+    var styleSheetElement = document.createElement("style");
+    styleSheetElement.type = "text/css";
+    document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
+    for(i = 0; i < document.styleSheets.length; i++) {
+      if (document.styleSheets[i].disabled) continue;
+      mysheet = document.styleSheets[i];
+    }
+  }
+
+  var rule = "border: 1px solid black; " +
+             "background-color: white; " +
+             "text-align: center;";
+  if (mysheet.insertRule) {  // Firefox
+    mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", 0);
+  } else if (mysheet.addRule) {  // IE
+    mysheet.addRule(".dygraphDefaultAnnotation", rule);
+  }
+
+  Dygraph.addedAnnotationCSS = true;
+}
+
+/**
+ * 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");
+
+  isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
+    canvas = G_vmlCanvasManager.initElement(canvas);
+  }
+
+  return canvas;
+};
+
+
+/**
+ * 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) {
+  this.container.innerHTML = '';
+  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;
+
+  col = 1;
+  for (var i in this.date_graph.layout_.datasets) {
+    selection.push({row: row, column: col});
+    col++;
+  }
+
+  return selection;
+}
+
+// Older pages may still use this name.
+DateGraph = Dygraph;
diff --git a/live/lib/excanvas.js b/live/lib/excanvas.js
new file mode 100644 (file)
index 0000000..1d9ddb2
--- /dev/null
@@ -0,0 +1,876 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+//   different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+//   width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+//   Quirks mode will draw the canvas using border-box. Either change your
+//   doctype to HTML5
+//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+//   or use Box Sizing Behavior from WebFX
+//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+  // alias some functions to make (compiled) code shorter
+  var m = Math;
+  var mr = m.round;
+  var ms = m.sin;
+  var mc = m.cos;
+  var abs = m.abs;
+  var sqrt = m.sqrt;
+
+  // this is used for sub pixel precision
+  var Z = 10;
+  var Z2 = Z / 2;
+
+  /**
+   * This funtion is assigned to the <canvas> elements as element.getContext().
+   * @this {HTMLElement}
+   * @return {CanvasRenderingContext2D_}
+   */
+  function getContext() {
+    return this.context_ ||
+        (this.context_ = new CanvasRenderingContext2D_(this));
+  }
+
+  var slice = Array.prototype.slice;
+
+  /**
+   * Binds a function to an object. The returned function will always use the
+   * passed in {@code obj} as {@code this}.
+   *
+   * Example:
+   *
+   *   g = bind(f, obj, a, b)
+   *   g(c, d) // will do f.call(obj, a, b, c, d)
+   *
+   * @param {Function} f The function to bind the object to
+   * @param {Object} obj The object that should act as this when the function
+   *     is called
+   * @param {*} var_args Rest arguments that will be used as the initial
+   *     arguments when the function is called
+   * @return {Function} A new function that has bound this
+   */
+  function bind(f, obj, var_args) {
+    var a = slice.call(arguments, 2);
+    return function() {
+      return f.apply(obj, a.concat(slice.call(arguments)));
+    };
+  }
+
+  var G_vmlCanvasManager_ = {
+    init: function(opt_doc) {
+      if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+        var doc = opt_doc || document;
+        // Create a dummy element so that IE will allow canvas elements to be
+        // recognized.
+        doc.createElement('canvas');
+        doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
+      }
+    },
+
+    init_: function(doc) {
+      // create xmlns
+      if (!doc.namespaces['g_vml_']) {
+        doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
+                           '#default#VML');
+
+      }
+      if (!doc.namespaces['g_o_']) {
+        doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
+                           '#default#VML');
+      }
+
+      // Setup default CSS.  Only add one style sheet per document
+      if (!doc.styleSheets['ex_canvas_']) {
+        var ss = doc.createStyleSheet();
+        ss.owningElement.id = 'ex_canvas_';
+        ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
+            // default size is 300x150 in Gecko and Opera
+            'text-align:left;width:300px;height:150px}' +
+            'g_vml_\\:*{behavior:url(#default#VML)}' +
+            'g_o_\\:*{behavior:url(#default#VML)}';
+
+      }
+
+      // find all canvas elements
+      var els = doc.getElementsByTagName('canvas');
+      for (var i = 0; i < els.length; i++) {
+        this.initElement(els[i]);
+      }
+    },
+
+    /**
+     * Public initializes a canvas element so that it can be used as canvas
+     * element from now on. This is called automatically before the page is
+     * loaded but if you are creating elements using createElement you need to
+     * make sure this is called on the element.
+     * @param {HTMLElement} el The canvas element to initialize.
+     * @return {HTMLElement} the element that was created.
+     */
+    initElement: function(el) {
+      if (!el.getContext) {
+
+        el.getContext = getContext;
+
+        // do not use inline function because that will leak memory
+        el.attachEvent('onpropertychange', onPropertyChange);
+        el.attachEvent('onresize', onResize);
+
+        var attrs = el.attributes;
+        if (attrs.width && attrs.width.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setWidth_(attrs.width.nodeValue);
+          el.style.width = attrs.width.nodeValue + 'px';
+        } else {
+          el.width = el.clientWidth;
+        }
+        if (attrs.height && attrs.height.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setHeight_(attrs.height.nodeValue);
+          el.style.height = attrs.height.nodeValue + 'px';
+        } else {
+          el.height = el.clientHeight;
+        }
+        //el.getContext().setCoordsize_()
+      }
+      return el;
+    }
+  };
+
+  function onPropertyChange(e) {
+    var el = e.srcElement;
+
+    switch (e.propertyName) {
+      case 'width':
+        el.style.width = el.attributes.width.nodeValue + 'px';
+        el.getContext().clearRect();
+        break;
+      case 'height':
+        el.style.height = el.attributes.height.nodeValue + 'px';
+        el.getContext().clearRect();
+        break;
+    }
+  }
+
+  function onResize(e) {
+    var el = e.srcElement;
+    if (el.firstChild) {
+      el.firstChild.style.width =  el.clientWidth + 'px';
+      el.firstChild.style.height = el.clientHeight + 'px';
+    }
+  }
+
+  G_vmlCanvasManager_.init();
+
+  // precompute "00" to "FF"
+  var dec2hex = [];
+  for (var i = 0; i < 16; i++) {
+    for (var j = 0; j < 16; j++) {
+      dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
+    }
+  }
+
+  function createMatrixIdentity() {
+    return [
+      [1, 0, 0],
+      [0, 1, 0],
+      [0, 0, 1]
+    ];
+  }
+
+  function matrixMultiply(m1, m2) {
+    var result = createMatrixIdentity();
+
+    for (var x = 0; x < 3; x++) {
+      for (var y = 0; y < 3; y++) {
+        var sum = 0;
+
+        for (var z = 0; z < 3; z++) {
+          sum += m1[x][z] * m2[z][y];
+        }
+
+        result[x][y] = sum;
+      }
+    }
+    return result;
+  }
+
+  function copyState(o1, o2) {
+    o2.fillStyle     = o1.fillStyle;
+    o2.lineCap       = o1.lineCap;
+    o2.lineJoin      = o1.lineJoin;
+    o2.lineWidth     = o1.lineWidth;
+    o2.miterLimit    = o1.miterLimit;
+    o2.shadowBlur    = o1.shadowBlur;
+    o2.shadowColor   = o1.shadowColor;
+    o2.shadowOffsetX = o1.shadowOffsetX;
+    o2.shadowOffsetY = o1.shadowOffsetY;
+    o2.strokeStyle   = o1.strokeStyle;
+    o2.globalAlpha   = o1.globalAlpha;
+    o2.arcScaleX_    = o1.arcScaleX_;
+    o2.arcScaleY_    = o1.arcScaleY_;
+    o2.lineScale_    = o1.lineScale_;
+  }
+
+  function processStyle(styleString) {
+    var str, alpha = 1;
+
+    styleString = String(styleString);
+    if (styleString.substring(0, 3) == 'rgb') {
+      var start = styleString.indexOf('(', 3);
+      var end = styleString.indexOf(')', start + 1);
+      var guts = styleString.substring(start + 1, end).split(',');
+
+      str = '#';
+      for (var i = 0; i < 3; i++) {
+        str += dec2hex[Number(guts[i])];
+      }
+
+      if (guts.length == 4 && styleString.substr(3, 1) == 'a') {
+        alpha = guts[3];
+      }
+    } else {
+      str = styleString;
+    }
+
+    return {color: str, alpha: alpha};
+  }
+
+  function processLineCap(lineCap) {
+    switch (lineCap) {
+      case 'butt':
+        return 'flat';
+      case 'round':
+        return 'round';
+      case 'square':
+      default:
+        return 'square';
+    }
+  }
+
+  /**
+   * This class implements CanvasRenderingContext2D interface as described by
+   * the WHATWG.
+   * @param {HTMLElement} surfaceElement The element that the 2D context should
+   * be associated with
+   */
+  function CanvasRenderingContext2D_(surfaceElement) {
+    this.m_ = createMatrixIdentity();
+
+    this.mStack_ = [];
+    this.aStack_ = [];
+    this.currentPath_ = [];
+
+    // Canvas context properties
+    this.strokeStyle = '#000';
+    this.fillStyle = '#000';
+
+    this.lineWidth = 1;
+    this.lineJoin = 'miter';
+    this.lineCap = 'butt';
+    this.miterLimit = Z * 1;
+    this.globalAlpha = 1;
+    this.canvas = surfaceElement;
+
+    var el = surfaceElement.ownerDocument.createElement('div');
+    el.style.width =  surfaceElement.clientWidth + 'px';
+    el.style.height = surfaceElement.clientHeight + 'px';
+    el.style.overflow = 'hidden';
+    el.style.position = 'absolute';
+    surfaceElement.appendChild(el);
+
+    this.element_ = el;
+    this.arcScaleX_ = 1;
+    this.arcScaleY_ = 1;
+    this.lineScale_ = 1;
+  }
+
+  var contextPrototype = CanvasRenderingContext2D_.prototype;
+  contextPrototype.clearRect = function() {
+    this.element_.innerHTML = '';
+    this.currentPath_ = [];
+  };
+
+  contextPrototype.beginPath = function() {
+    // TODO: Branch current matrix so that save/restore has no effect
+    //       as per safari docs.
+    this.currentPath_ = [];
+  };
+
+  contextPrototype.moveTo = function(aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
+    this.currentX_ = p.x;
+    this.currentY_ = p.y;
+  };
+
+  contextPrototype.lineTo = function(aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
+
+    this.currentX_ = p.x;
+    this.currentY_ = p.y;
+  };
+
+  contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+                                            aCP2x, aCP2y,
+                                            aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    var cp1 = this.getCoords_(aCP1x, aCP1y);
+    var cp2 = this.getCoords_(aCP2x, aCP2y);
+    bezierCurveTo(this, cp1, cp2, p);
+  };
+
+  // Helper function that takes the already fixed cordinates.
+  function bezierCurveTo(self, cp1, cp2, p) {
+    self.currentPath_.push({
+      type: 'bezierCurveTo',
+      cp1x: cp1.x,
+      cp1y: cp1.y,
+      cp2x: cp2.x,
+      cp2y: cp2.y,
+      x: p.x,
+      y: p.y
+    });
+    self.currentX_ = p.x;
+    self.currentY_ = p.y;
+  }
+
+  contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+    // the following is lifted almost directly from
+    // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+
+    var cp = this.getCoords_(aCPx, aCPy);
+    var p = this.getCoords_(aX, aY);
+
+    var cp1 = {
+      x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
+      y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
+    };
+    var cp2 = {
+      x: cp1.x + (p.x - this.currentX_) / 3.0,
+      y: cp1.y + (p.y - this.currentY_) / 3.0
+    };
+
+    bezierCurveTo(this, cp1, cp2, p);
+  };
+
+  contextPrototype.arc = function(aX, aY, aRadius,
+                                  aStartAngle, aEndAngle, aClockwise) {
+    aRadius *= Z;
+    var arcType = aClockwise ? 'at' : 'wa';
+
+    var xStart = aX + mc(aStartAngle) * aRadius - Z2;
+    var yStart = aY + ms(aStartAngle) * aRadius - Z2;
+
+    var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
+    var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
+
+    // IE won't render arches drawn counter clockwise if xStart == xEnd.
+    if (xStart == xEnd && !aClockwise) {
+      xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+                       // that can be represented in binary
+    }
+
+    var p = this.getCoords_(aX, aY);
+    var pStart = this.getCoords_(xStart, yStart);
+    var pEnd = this.getCoords_(xEnd, yEnd);
+
+    this.currentPath_.push({type: arcType,
+                           x: p.x,
+                           y: p.y,
+                           radius: aRadius,
+                           xStart: pStart.x,
+                           yStart: pStart.y,
+                           xEnd: pEnd.x,
+                           yEnd: pEnd.y});
+
+  };
+
+  contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+  };
+
+  contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+    // Will destroy any existing path (same as FF behaviour)
+    this.beginPath();
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+    this.stroke();
+    this.currentPath_ = [];
+  };
+
+  contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+    // Will destroy any existing path (same as FF behaviour)
+    this.beginPath();
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+    this.fill();
+    this.currentPath_ = [];
+  };
+
+  contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+    var gradient = new CanvasGradient_('gradient');
+    gradient.x0_ = aX0;
+    gradient.y0_ = aY0;
+    gradient.x1_ = aX1;
+    gradient.y1_ = aY1;
+    return gradient;
+  };
+
+  contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
+                                                   aX1, aY1, aR1) {
+    var gradient = new CanvasGradient_('gradientradial');
+    gradient.x0_ = aX0;
+    gradient.y0_ = aY0;
+    gradient.r0_ = aR0;
+    gradient.x1_ = aX1;
+    gradient.y1_ = aY1;
+    gradient.r1_ = aR1;
+    return gradient;
+  };
+
+  contextPrototype.drawImage = function(image, var_args) {
+    var dx, dy, dw, dh, sx, sy, sw, sh;
+
+    // to find the original width we overide the width and height
+    var oldRuntimeWidth = image.runtimeStyle.width;
+    var oldRuntimeHeight = image.runtimeStyle.height;
+    image.runtimeStyle.width = 'auto';
+    image.runtimeStyle.height = 'auto';
+
+    // get the original size
+    var w = image.width;
+    var h = image.height;
+
+    // and remove overides
+    image.runtimeStyle.width = oldRuntimeWidth;
+    image.runtimeStyle.height = oldRuntimeHeight;
+
+    if (arguments.length == 3) {
+      dx = arguments[1];
+      dy = arguments[2];
+      sx = sy = 0;
+      sw = dw = w;
+      sh = dh = h;
+    } else if (arguments.length == 5) {
+      dx = arguments[1];
+      dy = arguments[2];
+      dw = arguments[3];
+      dh = arguments[4];
+      sx = sy = 0;
+      sw = w;
+      sh = h;
+    } else if (arguments.length == 9) {
+      sx = arguments[1];
+      sy = arguments[2];
+      sw = arguments[3];
+      sh = arguments[4];
+      dx = arguments[5];
+      dy = arguments[6];
+      dw = arguments[7];
+      dh = arguments[8];
+    } else {
+      throw Error('Invalid number of arguments');
+    }
+
+    var d = this.getCoords_(dx, dy);
+
+    var w2 = sw / 2;
+    var h2 = sh / 2;
+
+    var vmlStr = [];
+
+    var W = 10;
+    var H = 10;
+
+    // For some reason that I've now forgotten, using divs didn't work
+    vmlStr.push(' <g_vml_:group',
+                ' coordsize="', Z * W, ',', Z * H, '"',
+                ' coordorigin="0,0"' ,
+                ' style="width:', W, 'px;height:', H, 'px;position:absolute;');
+
+    // If filters are necessary (rotation exists), create them
+    // filters are bog-slow, so only create them if abbsolutely necessary
+    // The following check doesn't account for skews (which don't exist
+    // in the canvas spec (yet) anyway.
+
+    if (this.m_[0][0] != 1 || this.m_[0][1]) {
+      var filter = [];
+
+      // Note the 12/21 reversal
+      filter.push('M11=', this.m_[0][0], ',',
+                  'M12=', this.m_[1][0], ',',
+                  'M21=', this.m_[0][1], ',',
+                  'M22=', this.m_[1][1], ',',
+                  'Dx=', mr(d.x / Z), ',',
+                  'Dy=', mr(d.y / Z), '');
+
+      // Bounding box calculation (need to minimize displayed area so that
+      // filters don't waste time on unused pixels.
+      var max = d;
+      var c2 = this.getCoords_(dx + dw, dy);
+      var c3 = this.getCoords_(dx, dy + dh);
+      var c4 = this.getCoords_(dx + dw, dy + dh);
+
+      max.x = m.max(max.x, c2.x, c3.x, c4.x);
+      max.y = m.max(max.y, c2.y, c3.y, c4.y);
+
+      vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),
+                  'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',
+                  filter.join(''), ", sizingmethod='clip');")
+    } else {
+      vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');
+    }
+
+    vmlStr.push(' ">' ,
+                '<g_vml_:image src="', image.src, '"',
+                ' style="width:', Z * dw, 'px;',
+                ' height:', Z * dh, 'px;"',
+                ' cropleft="', sx / w, '"',
+                ' croptop="', sy / h, '"',
+                ' cropright="', (w - sx - sw) / w, '"',
+                ' cropbottom="', (h - sy - sh) / h, '"',
+                ' />',
+                '</g_vml_:group>');
+
+    this.element_.insertAdjacentHTML('BeforeEnd',
+                                    vmlStr.join(''));
+  };
+
+  contextPrototype.stroke = function(aFill) {
+    var lineStr = [];
+    var lineOpen = false;
+    var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
+    var color = a.color;
+    var opacity = a.alpha * this.globalAlpha;
+
+    var W = 10;
+    var H = 10;
+
+    lineStr.push('<g_vml_:shape',
+                 ' filled="', !!aFill, '"',
+                 ' style="position:absolute;width:', W, 'px;height:', H, 'px;"',
+                 ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
+                 ' stroked="', !aFill, '"',
+                 ' path="');
+
+    var newSeq = false;
+    var min = {x: null, y: null};
+    var max = {x: null, y: null};
+
+    for (var i = 0; i < this.currentPath_.length; i++) {
+      var p = this.currentPath_[i];
+      var c;
+
+      switch (p.type) {
+        case 'moveTo':
+          c = p;
+          lineStr.push(' m ', mr(p.x), ',', mr(p.y));
+          break;
+        case 'lineTo':
+          lineStr.push(' l ', mr(p.x), ',', mr(p.y));
+          break;
+        case 'close':
+          lineStr.push(' x ');
+          p = null;
+          break;
+        case 'bezierCurveTo':
+          lineStr.push(' c ',
+                       mr(p.cp1x), ',', mr(p.cp1y), ',',
+                       mr(p.cp2x), ',', mr(p.cp2y), ',',
+                       mr(p.x), ',', mr(p.y));
+          break;
+        case 'at':
+        case 'wa':
+          lineStr.push(' ', p.type, ' ',
+                       mr(p.x - this.arcScaleX_ * p.radius), ',',
+                       mr(p.y - this.arcScaleY_ * p.radius), ' ',
+                       mr(p.x + this.arcScaleX_ * p.radius), ',',
+                       mr(p.y + this.arcScaleY_ * p.radius), ' ',
+                       mr(p.xStart), ',', mr(p.yStart), ' ',
+                       mr(p.xEnd), ',', mr(p.yEnd));
+          break;
+      }
+
+
+      // TODO: Following is broken for curves due to
+      //       move to proper paths.
+
+      // Figure out dimensions so we can do gradient fills
+      // properly
+      if (p) {
+        if (min.x == null || p.x < min.x) {
+          min.x = p.x;
+        }
+        if (max.x == null || p.x > max.x) {
+          max.x = p.x;
+        }
+        if (min.y == null || p.y < min.y) {
+          min.y = p.y;
+        }
+        if (max.y == null || p.y > max.y) {
+          max.y = p.y;
+        }
+      }
+    }
+    lineStr.push(' ">');
+
+    if (!aFill) {
+      var lineWidth = this.lineScale_ * this.lineWidth;
+
+      // VML cannot correctly render a line if the width is less than 1px.
+      // In that case, we dilute the color to make the line look thinner.
+      if (lineWidth < 1) {
+        opacity *= lineWidth;
+      }
+
+      lineStr.push(
+        '<g_vml_:stroke',
+        ' opacity="', opacity, '"',
+        ' joinstyle="', this.lineJoin, '"',
+        ' miterlimit="', this.miterLimit, '"',
+        ' endcap="', processLineCap(this.lineCap), '"',
+        ' weight="', lineWidth, 'px"',
+        ' color="', color, '" />'
+      );
+    } else if (typeof this.fillStyle == 'object') {
+      var fillStyle = this.fillStyle;
+      var angle = 0;
+      var focus = {x: 0, y: 0};
+
+      // additional offset
+      var shift = 0;
+      // scale factor for offset
+      var expansion = 1;
+
+      if (fillStyle.type_ == 'gradient') {
+        var x0 = fillStyle.x0_ / this.arcScaleX_;
+        var y0 = fillStyle.y0_ / this.arcScaleY_;
+        var x1 = fillStyle.x1_ / this.arcScaleX_;
+        var y1 = fillStyle.y1_ / this.arcScaleY_;
+        var p0 = this.getCoords_(x0, y0);
+        var p1 = this.getCoords_(x1, y1);
+        var dx = p1.x - p0.x;
+        var dy = p1.y - p0.y;
+        angle = Math.atan2(dx, dy) * 180 / Math.PI;
+
+        // The angle should be a non-negative number.
+        if (angle < 0) {
+          angle += 360;
+        }
+
+        // Very small angles produce an unexpected result because they are
+        // converted to a scientific notation string.
+        if (angle < 1e-6) {
+          angle = 0;
+        }
+      } else {
+        var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_);
+        var width  = max.x - min.x;
+        var height = max.y - min.y;
+        focus = {
+          x: (p0.x - min.x) / width,
+          y: (p0.y - min.y) / height
+        };
+
+        width  /= this.arcScaleX_ * Z;
+        height /= this.arcScaleY_ * Z;
+        var dimension = m.max(width, height);
+        shift = 2 * fillStyle.r0_ / dimension;
+        expansion = 2 * fillStyle.r1_ / dimension - shift;
+      }
+
+      // We need to sort the color stops in ascending order by offset,
+      // otherwise IE won't interpret it correctly.
+      var stops = fillStyle.colors_;
+      stops.sort(function(cs1, cs2) {
+        return cs1.offset - cs2.offset;
+      });
+
+      var length = stops.length;
+      var color1 = stops[0].color;
+      var color2 = stops[length - 1].color;
+      var opacity1 = stops[0].alpha * this.globalAlpha;
+      var opacity2 = stops[length - 1].alpha * this.globalAlpha;
+
+      var colors = [];
+      for (var i = 0; i < length; i++) {
+        var stop = stops[i];
+        colors.push(stop.offset * expansion + shift + ' ' + stop.color);
+      }
+
+      // When colors attribute is used, the meanings of opacity and o:opacity2
+      // are reversed.
+      lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',
+                   ' method="none" focus="100%"',
+                   ' color="', color1, '"',
+                   ' color2="', color2, '"',
+                   ' colors="', colors.join(','), '"',
+                   ' opacity="', opacity2, '"',
+                   ' g_o_:opacity2="', opacity1, '"',
+                   ' angle="', angle, '"',
+                   ' focusposition="', focus.x, ',', focus.y, '" />');
+    } else {
+      lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
+                   '" />');
+    }
+
+    lineStr.push('</g_vml_:shape>');
+
+    this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
+  };
+
+  contextPrototype.fill = function() {
+    this.stroke(true);
+  }
+
+  contextPrototype.closePath = function() {
+    this.currentPath_.push({type: 'close'});
+  };
+
+  /**
+   * @private
+   */
+  contextPrototype.getCoords_ = function(aX, aY) {
+    var m = this.m_;
+    return {
+      x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
+      y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
+    }
+  };
+
+  contextPrototype.save = function() {
+    var o = {};
+    copyState(this, o);
+    this.aStack_.push(o);
+    this.mStack_.push(this.m_);
+    this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+  };
+
+  contextPrototype.restore = function() {
+    copyState(this.aStack_.pop(), this);
+    this.m_ = this.mStack_.pop();
+  };
+
+  contextPrototype.translate = function(aX, aY) {
+    var m1 = [
+      [1,  0,  0],
+      [0,  1,  0],
+      [aX, aY, 1]
+    ];
+
+    this.m_ = matrixMultiply(m1, this.m_);
+  };
+
+  contextPrototype.rotate = function(aRot) {
+    var c = mc(aRot);
+    var s = ms(aRot);
+
+    var m1 = [
+      [c,  s, 0],
+      [-s, c, 0],
+      [0,  0, 1]
+    ];
+
+    this.m_ = matrixMultiply(m1, this.m_);
+  };
+
+  contextPrototype.scale = function(aX, aY) {
+    this.arcScaleX_ *= aX;
+    this.arcScaleY_ *= aY;
+    var m1 = [
+      [aX, 0,  0],
+      [0,  aY, 0],
+      [0,  0,  1]
+    ];
+
+    var m = this.m_ = matrixMultiply(m1, this.m_);
+
+    // Get the line scale.
+    // Determinant of this.m_ means how much the area is enlarged by the
+    // transformation. So its square root can be used as a scale factor
+    // for width.
+    var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
+    this.lineScale_ = sqrt(abs(det));
+  };
+
+  /******** STUBS ********/
+  contextPrototype.clip = function() {
+    // TODO: Implement
+  };
+
+  contextPrototype.arcTo = function() {
+    // TODO: Implement
+  };
+
+  contextPrototype.createPattern = function() {
+    return new CanvasPattern_;
+  };
+
+  // Gradient / Pattern Stubs
+  function CanvasGradient_(aType) {
+    this.type_ = aType;
+    this.x0_ = 0;
+    this.y0_ = 0;
+    this.r0_ = 0;
+    this.x1_ = 0;
+    this.y1_ = 0;
+    this.r1_ = 0;
+    this.colors_ = [];
+  }
+
+  CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+    aColor = processStyle(aColor);
+    this.colors_.push({offset: aOffset,
+                       color: aColor.color,
+                       alpha: aColor.alpha});
+  };
+
+  function CanvasPattern_() {}
+
+  // set up externs
+  G_vmlCanvasManager = G_vmlCanvasManager_;
+  CanvasRenderingContext2D = CanvasRenderingContext2D_;
+  CanvasGradient = CanvasGradient_;
+  CanvasPattern = CanvasPattern_;
+
+})();
+
+} // if
diff --git a/live/lib/rgbcolor.js b/live/lib/rgbcolor.js
new file mode 100644 (file)
index 0000000..4316814
--- /dev/null
@@ -0,0 +1,250 @@
+/**\r
+ * A class to parse color values\r
+ *\r
+ * NOTE: modified by danvk. I removed the "getHelpXML" function to reduce the\r
+ * file size.\r
+ *\r
+ * @author Stoyan Stefanov <sstoo@gmail.com>\r
+ * @link   http://www.phpied.com/rgb-color-parser-in-javascript/\r
+ * @license Use it if you like it\r
+ */\r
+function RGBColor(color_string)\r
+{\r
+    this.ok = false;\r
+\r
+    // strip any leading #\r
+    if (color_string.charAt(0) == '#') { // remove # if any\r
+        color_string = color_string.substr(1,6);\r
+    }\r
+\r
+    color_string = color_string.replace(/ /g,'');\r
+    color_string = color_string.toLowerCase();\r
+\r
+    // before getting into regexps, try simple matches\r
+    // and overwrite the input\r
+    var simple_colors = {\r
+        aliceblue: 'f0f8ff',\r
+        antiquewhite: 'faebd7',\r
+        aqua: '00ffff',\r
+        aquamarine: '7fffd4',\r
+        azure: 'f0ffff',\r
+        beige: 'f5f5dc',\r
+        bisque: 'ffe4c4',\r
+        black: '000000',\r
+        blanchedalmond: 'ffebcd',\r
+        blue: '0000ff',\r
+        blueviolet: '8a2be2',\r
+        brown: 'a52a2a',\r
+        burlywood: 'deb887',\r
+        cadetblue: '5f9ea0',\r
+        chartreuse: '7fff00',\r
+        chocolate: 'd2691e',\r
+        coral: 'ff7f50',\r
+        cornflowerblue: '6495ed',\r
+        cornsilk: 'fff8dc',\r
+        crimson: 'dc143c',\r
+        cyan: '00ffff',\r
+        darkblue: '00008b',\r
+        darkcyan: '008b8b',\r
+        darkgoldenrod: 'b8860b',\r
+        darkgray: 'a9a9a9',\r
+        darkgreen: '006400',\r
+        darkkhaki: 'bdb76b',\r
+        darkmagenta: '8b008b',\r
+        darkolivegreen: '556b2f',\r
+        darkorange: 'ff8c00',\r
+        darkorchid: '9932cc',\r
+        darkred: '8b0000',\r
+        darksalmon: 'e9967a',\r
+        darkseagreen: '8fbc8f',\r
+        darkslateblue: '483d8b',\r
+        darkslategray: '2f4f4f',\r
+        darkturquoise: '00ced1',\r
+        darkviolet: '9400d3',\r
+        deeppink: 'ff1493',\r
+        deepskyblue: '00bfff',\r
+        dimgray: '696969',\r
+        dodgerblue: '1e90ff',\r
+        feldspar: 'd19275',\r
+        firebrick: 'b22222',\r
+        floralwhite: 'fffaf0',\r
+        forestgreen: '228b22',\r
+        fuchsia: 'ff00ff',\r
+        gainsboro: 'dcdcdc',\r
+        ghostwhite: 'f8f8ff',\r
+        gold: 'ffd700',\r
+        goldenrod: 'daa520',\r
+        gray: '808080',\r
+        green: '008000',\r
+        greenyellow: 'adff2f',\r
+        honeydew: 'f0fff0',\r
+        hotpink: 'ff69b4',\r
+        indianred : 'cd5c5c',\r
+        indigo : '4b0082',\r
+        ivory: 'fffff0',\r
+        khaki: 'f0e68c',\r
+        lavender: 'e6e6fa',\r
+        lavenderblush: 'fff0f5',\r
+        lawngreen: '7cfc00',\r
+        lemonchiffon: 'fffacd',\r
+        lightblue: 'add8e6',\r
+        lightcoral: 'f08080',\r
+        lightcyan: 'e0ffff',\r
+        lightgoldenrodyellow: 'fafad2',\r
+        lightgrey: 'd3d3d3',\r
+        lightgreen: '90ee90',\r
+        lightpink: 'ffb6c1',\r
+        lightsalmon: 'ffa07a',\r
+        lightseagreen: '20b2aa',\r
+        lightskyblue: '87cefa',\r
+        lightslateblue: '8470ff',\r
+        lightslategray: '778899',\r
+        lightsteelblue: 'b0c4de',\r
+        lightyellow: 'ffffe0',\r
+        lime: '00ff00',\r
+        limegreen: '32cd32',\r
+        linen: 'faf0e6',\r
+        magenta: 'ff00ff',\r
+        maroon: '800000',\r
+        mediumaquamarine: '66cdaa',\r
+        mediumblue: '0000cd',\r
+        mediumorchid: 'ba55d3',\r
+        mediumpurple: '9370d8',\r
+        mediumseagreen: '3cb371',\r
+        mediumslateblue: '7b68ee',\r
+        mediumspringgreen: '00fa9a',\r
+        mediumturquoise: '48d1cc',\r
+        mediumvioletred: 'c71585',\r
+        midnightblue: '191970',\r
+        mintcream: 'f5fffa',\r
+        mistyrose: 'ffe4e1',\r
+        moccasin: 'ffe4b5',\r
+        navajowhite: 'ffdead',\r
+        navy: '000080',\r
+        oldlace: 'fdf5e6',\r
+        olive: '808000',\r
+        olivedrab: '6b8e23',\r
+        orange: 'ffa500',\r
+        orangered: 'ff4500',\r
+        orchid: 'da70d6',\r
+        palegoldenrod: 'eee8aa',\r
+        palegreen: '98fb98',\r
+        paleturquoise: 'afeeee',\r
+        palevioletred: 'd87093',\r
+        papayawhip: 'ffefd5',\r
+        peachpuff: 'ffdab9',\r
+        peru: 'cd853f',\r
+        pink: 'ffc0cb',\r
+        plum: 'dda0dd',\r
+        powderblue: 'b0e0e6',\r
+        purple: '800080',\r
+        red: 'ff0000',\r
+        rosybrown: 'bc8f8f',\r
+        royalblue: '4169e1',\r
+        saddlebrown: '8b4513',\r
+        salmon: 'fa8072',\r
+        sandybrown: 'f4a460',\r
+        seagreen: '2e8b57',\r
+        seashell: 'fff5ee',\r
+        sienna: 'a0522d',\r
+        silver: 'c0c0c0',\r
+        skyblue: '87ceeb',\r
+        slateblue: '6a5acd',\r
+        slategray: '708090',\r
+        snow: 'fffafa',\r
+        springgreen: '00ff7f',\r
+        steelblue: '4682b4',\r
+        tan: 'd2b48c',\r
+        teal: '008080',\r
+        thistle: 'd8bfd8',\r
+        tomato: 'ff6347',\r
+        turquoise: '40e0d0',\r
+        violet: 'ee82ee',\r
+        violetred: 'd02090',\r
+        wheat: 'f5deb3',\r
+        white: 'ffffff',\r
+        whitesmoke: 'f5f5f5',\r
+        yellow: 'ffff00',\r
+        yellowgreen: '9acd32'\r
+    };\r
+    for (var key in simple_colors) {\r
+        if (color_string == key) {\r
+            color_string = simple_colors[key];\r
+        }\r
+    }\r
+    // emd of simple type-in colors\r
+\r
+    // array of color definition objects\r
+    var color_defs = [\r
+        {\r
+            re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,\r
+            example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],\r
+            process: function (bits){\r
+                return [\r
+                    parseInt(bits[1]),\r
+                    parseInt(bits[2]),\r
+                    parseInt(bits[3])\r
+                ];\r
+            }\r
+        },\r
+        {\r
+            re: /^(\w{2})(\w{2})(\w{2})$/,\r
+            example: ['#00ff00', '336699'],\r
+            process: function (bits){\r
+                return [\r
+                    parseInt(bits[1], 16),\r
+                    parseInt(bits[2], 16),\r
+                    parseInt(bits[3], 16)\r
+                ];\r
+            }\r
+        },\r
+        {\r
+            re: /^(\w{1})(\w{1})(\w{1})$/,\r
+            example: ['#fb0', 'f0f'],\r
+            process: function (bits){\r
+                return [\r
+                    parseInt(bits[1] + bits[1], 16),\r
+                    parseInt(bits[2] + bits[2], 16),\r
+                    parseInt(bits[3] + bits[3], 16)\r
+                ];\r
+            }\r
+        }\r
+    ];\r
+\r
+    // search through the definitions to find a match\r
+    for (var i = 0; i < color_defs.length; i++) {\r
+        var re = color_defs[i].re;\r
+        var processor = color_defs[i].process;\r
+        var bits = re.exec(color_string);\r
+        if (bits) {\r
+            channels = processor(bits);\r
+            this.r = channels[0];\r
+            this.g = channels[1];\r
+            this.b = channels[2];\r
+            this.ok = true;\r
+        }\r
+\r
+    }\r
+\r
+    // validate/cleanup values\r
+    this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);\r
+    this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);\r
+    this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);\r
+\r
+    // some getters\r
+    this.toRGB = function () {\r
+        return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';\r
+    }\r
+    this.toHex = function () {\r
+        var r = this.r.toString(16);\r
+        var g = this.g.toString(16);\r
+        var b = this.b.toString(16);\r
+        if (r.length == 1) r = '0' + r;\r
+        if (g.length == 1) g = '0' + g;\r
+        if (b.length == 1) b = '0' + b;\r
+        return '#' + r + g + b;\r
+    }\r
+\r
+\r
+}\r
+\r
diff --git a/live/lib/strftime-min.js b/live/lib/strftime-min.js
new file mode 100644 (file)
index 0000000..8207714
--- /dev/null
@@ -0,0 +1 @@
+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};