Merge branch 'master' into per_series
[dygraphs.git] / dygraph.js
index 1383bcb..517f0c6 100644 (file)
@@ -90,8 +90,12 @@ Dygraph.DEFAULT_ATTRS = {
     // 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,
 
@@ -99,6 +103,7 @@ Dygraph.DEFAULT_ATTRS = {
   axisLabelFontSize: 14,
   xAxisLabelWidth: 50,
   yAxisLabelWidth: 50,
+  xAxisLabelFormatter: Dygraph.dateAxisFormatter,
   rightGap: 5,
 
   showRoller: false,
@@ -108,11 +113,20 @@ Dygraph.DEFAULT_ATTRS = {
 
   delimiter: ',',
 
+  logScale: false,
   sigma: 2.0,
   errorBars: false,
   fractions: false,
   wilsonInterval: true,  // only relevant if fractions is true
-  customBars: false
+  customBars: false,
+  fillGraph: false,
+  fillAlpha: 0.15,
+  connectSeparatedPoints: false,
+
+  stackedGraph: false,
+  hideOverlayOnMouseOut: true,
+
+  stepPlot: false
 };
 
 // Various logging levels.
@@ -121,6 +135,9 @@ 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,
@@ -137,8 +154,8 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
  * and interaction <canvas> 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 {Array.<String>} labels Names of the data series
  * @param {Object} attrs Miscellaneous other options
  * @private
  */
@@ -156,25 +173,49 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   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 give it a default size.
+  // 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 = Dygraph.DEFAULT_WIDTH + "px";
+    div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
   }
   if (div.style.height == '') {
-    div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
+    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.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
@@ -186,17 +227,25 @@ Dygraph.prototype.__init__ = function(div, file, 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) {
-  if (typeof(this.user_attrs_[name]) != 'undefined') {
+Dygraph.prototype.attr_ = function(name, series) {
+  if (series &&
+      typeof(this.user_attrs_[series]) != 'undefined' &&
+      typeof(this.user_attrs_[series][name]) != 'undefined') {
+    return this.user_attrs_[series][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];
@@ -242,6 +291,97 @@ 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;
@@ -254,6 +394,13 @@ Dygraph.addEvent = function(el, evt, 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
@@ -269,24 +416,40 @@ Dygraph.prototype.createInterface_ = function() {
   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_ = document.createElement("canvas");
   this.canvas_ = Dygraph.createCanvas();
   this.canvas_.style.position = "absolute";
   this.canvas_.width = this.width_;
   this.canvas_.height = this.height_;
   this.canvas_.style.width = this.width_ + "px";    // for IE
   this.canvas_.style.height = this.height_ + "px";  // for IE
-  this.graphDiv.appendChild(this.canvas_);
 
   // ... 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.hidden_, 'mousemove', function(e) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
     dygraph.mouseMove_(e);
   });
-  Dygraph.addEvent(this.hidden_, 'mouseout', function(e) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
     dygraph.mouseOut_(e);
   });
 
@@ -313,7 +476,35 @@ Dygraph.prototype.createInterface_ = function() {
   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
@@ -323,16 +514,17 @@ Dygraph.prototype.createInterface_ = function() {
  * @private
  */
 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
-  // var h = document.createElement("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
-  this.graphDiv.appendChild(h);
   return h;
 };
 
@@ -384,59 +576,85 @@ Dygraph.prototype.setColors_ = function() {
   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++) {
-      var hue = (1.0*i/(1+num));
-      this.colors_.push( Dygraph.hsvToRGB(hue, sat, val) );
+      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. 
+  // 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_);
 }
 
-// The following functions are from quirksmode.org
+/**
+ * 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 (obj.offsetParent) {
+  if(obj.offsetParent)
+    while(1)
+    {
       curleft += obj.offsetLeft;
+      if(!obj.offsetParent)
+        break;
       obj = obj.offsetParent;
     }
-  }
-  else if (obj.x)
+  else if(obj.x)
     curleft += obj.x;
   return curleft;
 };
-                   
+
 Dygraph.findPosY = function(obj) {
   var curtop = 0;
-  if (obj.offsetParent) {
-    while (obj.offsetParent) {
+  if(obj.offsetParent)
+    while(1)
+    {
       curtop += obj.offsetTop;
+      if(!obj.offsetParent)
+        break;
       obj = obj.offsetParent;
     }
-  }
-  else if (obj.y)
+  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(){
+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 = {
@@ -539,10 +757,10 @@ Dygraph.prototype.createDragInterface_ = function() {
   var px = 0;
   var py = 0;
   var getX = function(e) { return Dygraph.pageX(e) - px };
-  var getY = function(e) { return Dygraph.pageX(e) - py };
+  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.hidden_, 'mousemove', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
     if (isZooming) {
       dragEndX = getX(event);
       dragEndY = getY(event);
@@ -564,13 +782,13 @@ Dygraph.prototype.createDragInterface_ = function() {
   });
 
   // Track the beginning of drag events
-  Dygraph.addEvent(this.hidden_, 'mousedown', function(event) {
+  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) {
+    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];
@@ -598,7 +816,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   });
 
   // Temporarily cancel the dragging event when the mouse leaves the graph
-  Dygraph.addEvent(this.hidden_, 'mouseout', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
     if (isZooming) {
       dragEndX = null;
       dragEndY = null;
@@ -607,7 +825,7 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   // 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.hidden_, 'mouseup', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
     if (isZooming) {
       isZooming = false;
       dragEndX = getX(event);
@@ -616,10 +834,31 @@ Dygraph.prototype.createDragInterface_ = function() {
       var regionHeight = Math.abs(dragEndY - dragStartY);
 
       if (regionWidth < 2 && regionHeight < 2 &&
-          self.attr_('clickCallback') != null &&
-          self.lastx_ != undefined) {
-        // TODO(danvk): pass along more info about the points.
-        self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
+          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) {
@@ -643,7 +882,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   });
 
   // Double-clicking zooms back out
-  Dygraph.addEvent(this.hidden_, 'dblclick', function(event) {
+  Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
     if (self.dateWindow_ == null) return;
     self.dateWindow_ = null;
     self.drawGraph_(self.rawData_);
@@ -694,19 +933,10 @@ Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
  */
 Dygraph.prototype.doZoom_ = function(lowX, highX) {
   // Find the earliest and latest dates contained in this canvasx range.
-  var points = this.layout_.points;
-  var minDate = null;
-  var maxDate = null;
-  // Find the nearest [minDate, maxDate] that contains [lowX, highX]
-  for (var i = 0; i < points.length; i++) {
-    var cx = points[i].canvasx;
-    var x = points[i].xval;
-    if (cx < lowX  && (minDate == null || x > minDate)) minDate = x;
-    if (cx > highX && (maxDate == null || x < maxDate)) maxDate = x;
-  }
-  // Use the extremes if either is missing
-  if (minDate == null) minDate = points[0].xval;
-  if (maxDate == null) maxDate = points[points.length-1].xval;
+  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_);
@@ -723,7 +953,7 @@ Dygraph.prototype.doZoom_ = function(lowX, highX) {
  * @private
  */
 Dygraph.prototype.mouseMove_ = function(event) {
-  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.hidden_);
+  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
   var points = this.layout_.points;
 
   var lastx = -1;
@@ -735,7 +965,7 @@ Dygraph.prototype.mouseMove_ = function(event) {
   var idx = -1;
   for (var i = 0; i < points.length; i++) {
     var dist = Math.abs(points[i].canvasx - canvasx);
-    if (dist > minDist) break;
+    if (dist > minDist) continue;
     minDist = dist;
     idx = i;
   }
@@ -746,16 +976,50 @@ Dygraph.prototype.mouseMove_ = function(event) {
 
   // Extract the points we've selected
   this.selPoints_ = [];
-  for (var i = 0; i < points.length; i++) {
-    if (points[i].xval == lastx) {
-      this.selPoints_.push(points[i]);
+  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")) {
-    this.attr_("highlightCallback")(event, lastx, this.selPoints_);
+    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 circleSize = this.attr_('highlightCircleSize');
   var ctx = this.canvas_.getContext("2d");
@@ -770,31 +1034,36 @@ Dygraph.prototype.mouseMove_ = function(event) {
     var canvasx = this.selPoints_[0].canvasx;
 
     // Set the status message to indicate the selected point(s)
-    var replace = this.attr_('xValueFormatter')(lastx, this) + ":";
+    var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
+    var fmtFunc = this.attr_('yValueFormatter');
     var clen = this.colors_.length;
-    for (var i = 0; i < this.selPoints_.length; i++) {
-      if (!isOK(this.selPoints_[i].canvasy)) continue;
-      if (this.attr_("labelsSeparateLines")) {
-        replace += "<br/>";
+
+    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.colors_[i%clen]);
+        var yval = fmtFunc(point.yval);
+        replace += " <b><font color='" + c.toHex() + "'>"
+                + point.name + "</font></b>:"
+                + yval;
       }
-      var point = this.selPoints_[i];
-      var c = new RGBColor(this.colors_[i%clen]);
-      replace += " <b><font color='" + c.toHex() + "'>"
-              + point.name + "</font></b>:"
-              + this.round_(point.yval, 2);
-    }
-    this.attr_("labelsDiv").innerHTML = replace;
 
-    // Save last x position for callbacks.
-    this.lastx_ = lastx;
+      this.attr_("labelsDiv").innerHTML = replace;
+    }
 
     // Draw colored circles over the center of each selected point
-    ctx.save()
+    ctx.save();
     for (var i = 0; i < this.selPoints_.length; i++) {
-      if (!isOK(this.selPoints_[i%clen].canvasy)) continue;
+      if (!isOK(this.selPoints_[i].canvasy)) continue;
       ctx.beginPath();
-      ctx.fillStyle = this.colors_[i%clen];
-      ctx.arc(canvasx, this.selPoints_[i%clen].canvasy, circleSize,
+      ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
+      ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
               0, 2 * Math.PI, false);
       ctx.fill();
     }
@@ -805,16 +1074,84 @@ Dygraph.prototype.mouseMove_ = function(event) {
 };
 
 /**
+ * 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) {
+        this.selPoints_.push(this.layout_.points[pos+row]);
+      }
+      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;
@@ -826,17 +1163,36 @@ Dygraph.zeropad = function(x) {
  * @return {String} A time of the form "HH:MM:SS"
  * @private
  */
-Dygraph.prototype.hmsString_ = function(date) {
+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 if (d.getMinutes()) {
+  } 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 {
-    return zeropad(d.getHours());
+    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());
+    }
   }
 }
 
@@ -845,7 +1201,6 @@ Dygraph.prototype.hmsString_ = function(date) {
  * @param {Number} date The JavaScript date (ms since epoch)
  * @return {String} A date of the form "YYYY/MM/DD"
  * @private
- * TODO(danvk): why is this part of the prototype?
  */
 Dygraph.dateString_ = function(date, self) {
   var zeropad = Dygraph.zeropad;
@@ -860,7 +1215,7 @@ Dygraph.dateString_ = function(date, self) {
 
   var ret = "";
   var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-  if (frac) ret = " " + self.hmsString_(date);
+  if (frac) ret = " " + Dygraph.hmsString_(date);
 
   return year + "/" + month + "/" + day + ret;
 };
@@ -872,7 +1227,7 @@ Dygraph.dateString_ = function(date, self) {
  * @return {Number} The rounded number
  * @private
  */
-Dygraph.prototype.round_ = function(num, places) {
+Dygraph.round_ = function(num, places) {
   var shift = Math.pow(10, places);
   return Math.round(num * shift)/shift;
 };
@@ -912,30 +1267,40 @@ Dygraph.prototype.addXTicks_ = function() {
 
 // Time granularity enumeration
 Dygraph.SECONDLY = 0;
-Dygraph.TEN_SECONDLY = 1;
-Dygraph.THIRTY_SECONDLY  = 2;
-Dygraph.MINUTELY = 3;
-Dygraph.TEN_MINUTELY = 4;
-Dygraph.THIRTY_MINUTELY = 5;
-Dygraph.HOURLY = 6;
-Dygraph.SIX_HOURLY = 7;
-Dygraph.DAILY = 8;
-Dygraph.WEEKLY = 9;
-Dygraph.MONTHLY = 10;
-Dygraph.QUARTERLY = 11;
-Dygraph.BIANNUAL = 12;
-Dygraph.ANNUAL = 13;
-Dygraph.DECADAL = 14;
-Dygraph.NUM_GRANULARITIES = 15;
+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;
@@ -972,25 +1337,45 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
 //   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"
-    // TODO(danvk): be smarter about making sure this really hits a "nice" time.
-    if (granularity < Dygraph.HOURLY) {
-      start_time = spacing * Math.floor(0.5 + start_time / spacing);
-    }
-    for (var t = start_time; t <= end_time; t += spacing) {
-      var d = new Date(t);
-      var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-      if (frac == 0 || granularity >= Dygraph.DAILY) {
-        // the extra hour covers DST problems.
-        ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) });
+
+    // 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 {
-        ticks.push({ v:t, label: this.hmsString_(t) });
+        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
@@ -1020,7 +1405,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
         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: new Date(t).strftime('%b %y') });
+        ticks.push({ v:t, label: formatter(new Date(t), granularity) });
       }
     }
   }
@@ -1065,6 +1450,7 @@ Dygraph.numericTicks = function(minV, maxV, self) {
   // 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 {
@@ -1083,7 +1469,7 @@ Dygraph.numericTicks = function(minV, maxV, self) {
       scale = base_scale * mults[j];
       low_val = Math.floor(minV / scale) * scale;
       high_val = Math.ceil(maxV / scale) * scale;
-      nTicks = (high_val - low_val) / 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;
@@ -1105,16 +1491,19 @@ Dygraph.numericTicks = function(minV, maxV, self) {
     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 = self.round_(tickV, 2);
+    var 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 = self.round_(tickV / n, 1) + k_labels[j];
+          label = Dygraph.round_(tickV / n, 1) + k_labels[j];
           break;
         }
       }
@@ -1187,58 +1576,122 @@ Dygraph.prototype.extremeValues_ = function(series) {
  * @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 all fields in the dataset
-  for (var i = 1; i < data[0].length; i++) {
+  // 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++) {
-      var date = data[j][0];
-      series[j] = [date, data[j][i]];
+      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 && series[k][0] <= high) {
-          pruned.push(series[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 || thisMinY < minY) minY = thisMinY;
-    if (!maxY || thisMaxY > maxY) maxY = thisMaxY;
+    if (minY === null || thisMinY < minY) minY = thisMinY;
+    if (maxY === null || thisMaxY > maxY) maxY = thisMaxY;
 
     if (bars) {
-      var vals = [];
-      for (var j=0; j<series.length; j++)
-        vals[j] = [series[j][0],
-                   series[j][1][0], series[j][1][1], series[j][1][2]];
-      this.layout_.addDataset(this.attr_("labels")[i], vals);
-    } else {
-      this.layout_.addDataset(this.attr_("labels")[i], series);
+      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;
 
@@ -1252,6 +1705,7 @@ Dygraph.prototype.drawGraph_ = function(data) {
     }
 
     this.addYTicks_(minAxisY, maxAxisY);
+    this.displayedYRange_ = [minAxisY, maxAxisY];
   }
 
   this.addXTicks_();
@@ -1263,6 +1717,10 @@ Dygraph.prototype.drawGraph_ = function(data) {
   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);
+  }
 };
 
 /**
@@ -1411,7 +1869,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
 Dygraph.dateParser = function(dateStr, self) {
   var dateStrSlashed;
   var d;
-  if (dateStr.length == 10 && dateStr.search("-") != -1) {  // e.g. '2009-07-12'
+  if (dateStr.search("-") != -1) {  // e.g. '2009-7-12' or '2009-07-12'
     dateStrSlashed = dateStr.replace("-", "/", "g");
     while (dateStrSlashed.search("-") != -1) {
       dateStrSlashed = dateStrSlashed.replace("-", "/");
@@ -1455,10 +1913,12 @@ Dygraph.prototype.detectTypeFromString_ = function(str) {
     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;
   }
 };
 
@@ -1495,9 +1955,16 @@ Dygraph.prototype.parseCSV_ = function(data) {
     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
@@ -1518,27 +1985,30 @@ Dygraph.prototype.parseCSV_ = function(data) {
       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] = [parseFloat(vals[0]), parseFloat(vals[1])];
+        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] = [parseFloat(inFields[j]),
-                               parseFloat(inFields[j + 1])];
+        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] = [ parseFloat(vals[0]),
-                      parseFloat(vals[1]),
-                      parseFloat(vals[2]) ];
+        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] = parseFloat(inFields[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) {
@@ -1547,6 +2017,12 @@ Dygraph.prototype.parseCSV_ = function(data) {
                  ") " + 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;
 };
 
@@ -1580,18 +2056,20 @@ Dygraph.prototype.parseArray_ = function(data) {
   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");
+        this.error("Row " + (1 + i) + " of data is empty");
         return null;
       }
       if (parsedData[i][0] == null
-          || typeof(parsedData[i][0].getTime) != 'function') {
-        this.error("x value in row " << (1 + i) << " is not a Date");
+          || 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();
@@ -1610,7 +2088,7 @@ Dygraph.prototype.parseArray_ = function(data) {
  * 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. Returned value is in the same format as return value of parseCSV_.
+ * fixed. Fills out rawData_.
  * @param {Array.<Object>} data See above.
  * @private
  */
@@ -1618,57 +2096,111 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   var cols = data.getNumberOfColumns();
   var rows = data.getNumberOfRows();
 
-  // Read column labels
-  var labels = [];
-  for (var i = 0; i < cols; i++) {
-    labels.push(data.getColumnLabel(i));
-    if (i != 0 && this.attr_("errorBars")) i += 1;
-  }
-  this.attrs_.labels = labels;
-  cols = labels.length;
-
   var indepType = data.getColumnType(0);
-  if (indepType == 'date') {
+  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' and 'number' types are supported for column 1 " +
-               "of DataTable input (Got '" + indepType + "')");
+    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]));
+  }
+  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.warning("Ignoring row " + i +
-                   " of DataTable because of undefined or null first column.");
+      this.warn("Ignoring row " + i +
+                " of DataTable because of undefined or null first column.");
       continue;
     }
 
-    if (indepType == 'date') {
+    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 = 1; j < cols; j++) {
-        row.push(data.getValue(i, j));
+      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);
   }
-  return ret;
+
+  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.
@@ -1686,7 +2218,7 @@ Dygraph.update = function (self, o) {
 Dygraph.isArrayLike = function (o) {
   var typ = typeof(o);
   if (
-      (typ != 'object' && !(typ == 'function' && 
+      (typ != 'object' && !(typ == 'function' &&
         typeof(o.item) == 'function')) ||
       o === null ||
       typeof(o.length) != 'number' ||
@@ -1734,7 +2266,7 @@ Dygraph.prototype.start_ = function() {
   } else if (typeof this.file_ == 'object' &&
              typeof this.file_.getColumnRange == 'function') {
     // must be a DataTable from gviz.
-    this.rawData_ = this.parseDataTable_(this.file_);
+    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.
@@ -1778,13 +2310,17 @@ Dygraph.prototype.updateOptions = function(attrs) {
   if (attrs.valueRange) {
     this.valueRange_ = attrs.valueRange;
   }
+
+  // TODO(danvk): validate per-series options.
+
   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'] && attrs['file'] != this.file_) {
+  if (attrs['file']) {
     this.file_ = attrs['file'];
     this.start_();
   } else {
@@ -1804,6 +2340,11 @@ Dygraph.prototype.updateOptions = function(attrs) {
  * @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.");
@@ -1826,6 +2367,8 @@ Dygraph.prototype.resize = function(width, height) {
 
   this.createInterface_();
   this.drawGraph_(this.rawData_);
+
+  this.resize_lock = false;
 };
 
 /**
@@ -1839,6 +2382,80 @@ Dygraph.prototype.adjustRoll = function(length) {
 };
 
 /**
+ * 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_;
+};
+
+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.
  */
@@ -1867,5 +2484,40 @@ Dygraph.GVizChart.prototype.draw = function(data, options) {
   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;