remove dependence on PlotKit.Base as well
[dygraphs.git] / dygraph.js
index e49c935..df41fdc 100644 (file)
@@ -3,42 +3,41 @@
 
 /**
  * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
 
 /**
  * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
- * string. DateGraph can handle multiple series with or without error bars. The
- * date/value ranges will be automatically set. DateGraph uses the
+ * string. Dygraph can handle multiple series with or without error bars. The
+ * date/value ranges will be automatically set. Dygraph uses the
  * <canvas> 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">
  * &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 DateGraph(document.getElementById("graphdiv"),
-                   "datafile.csv",
-                     ["Series 1", "Series 2"],
-                     { }); // options
+     new Dygraph(document.getElementById("graphdiv"),
+                 "datafile.csv",  // CSV file with headers
+                 { }); // options
    </script>
 
  The CSV file is of the form
 
    </script>
 
  The CSV file is of the form
 
+   Date,SeriesA,SeriesB,SeriesC
    YYYYMMDD,A1,B1,C1
    YYYYMMDD,A2,B2,C2
 
    YYYYMMDD,A1,B1,C1
    YYYYMMDD,A2,B2,C2
 
- If null is passed as the third parameter (series names), then the first line
- of the CSV file is assumed to contain names for each series.
-
  If the 'errorBars' option is set in the constructor, the input should be of
  the form
 
  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:
 
    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.
 
    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/dg/
+ For further documentation and examples, see http://www.danvk.org/dygraphs
 
  */
 
 
  */
 
  * returns this data. The expected format for each line is
  * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
  * YYYYMMDD,val1,stddev1,val2,stddev2,...
  * 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 {Array.<String>} labels Labels for the data series
  * @param {Object} attrs Various other attributes, e.g. errorBars determines
  * whether the input data contains error ranges.
  */
  * @param {Object} attrs Various other attributes, e.g. errorBars determines
  * whether the input data contains error ranges.
  */
-DateGraph = function(div, file, labels, attrs) {
-  if (arguments.length > 0)
-    this.__init__(div, file, labels, attrs);
+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);
+    }
+  }
 };
 
 };
 
-DateGraph.NAME = "DateGraph";
-DateGraph.VERSION = "1.1";
-DateGraph.__repr__ = function() {
+Dygraph.NAME = "Dygraph";
+Dygraph.VERSION = "1.2";
+Dygraph.__repr__ = function() {
   return "[" + this.NAME + " " + this.VERSION + "]";
 };
   return "[" + this.NAME + " " + this.VERSION + "]";
 };
-DateGraph.toString = function() {
+Dygraph.toString = function() {
   return this.__repr__();
 };
 
 // Various default values
   return this.__repr__();
 };
 
 // Various default values
-DateGraph.DEFAULT_ROLL_PERIOD = 1;
-DateGraph.DEFAULT_WIDTH = 480;
-DateGraph.DEFAULT_HEIGHT = 320;
-DateGraph.DEFAULT_STROKE_WIDTH = 1.0;
-DateGraph.AXIS_LINE_WIDTH = 0.3;
+Dygraph.DEFAULT_ROLL_PERIOD = 1;
+Dygraph.DEFAULT_WIDTH = 480;
+Dygraph.DEFAULT_HEIGHT = 320;
+Dygraph.AXIS_LINE_WIDTH = 0.3;
 
 // Default attribute values.
 
 // Default attribute values.
-DateGraph.DEFAULT_ATTRS = {
+Dygraph.DEFAULT_ATTRS = {
   highlightCircleSize: 3,
   pixelsPerXLabel: 60,
   pixelsPerYLabel: 30,
   highlightCircleSize: 3,
   pixelsPerXLabel: 60,
   pixelsPerYLabel: 30,
+
   labelsDivWidth: 250,
   labelsDivStyles: {
     // TODO(danvk): move defaults from createStatusMessage_ here.
   labelsDivWidth: 250,
   labelsDivStyles: {
     // TODO(danvk): move defaults from createStatusMessage_ here.
-  }
+  },
+  labelsSeparateLines: false,
+  labelsKMB: false,
+
+  strokeWidth: 1.0,
+
+  axisTickSize: 3,
+  axisLabelFontSize: 14,
+  xAxisLabelWidth: 50,
+  yAxisLabelWidth: 50,
+  rightGap: 5,
+
+  showRoller: false,
+  xValueFormatter: Dygraph.dateString_,
+  xValueParser: Dygraph.dateParser,
+  xTicker: Dygraph.dateTicker,
+
+  sigma: 2.0,
+  errorBars: false,
+  fractions: false,
+  wilsonInterval: true,  // only relevant if fractions is true
+  customBars: false
+};
 
 
-  // TODO(danvk): default padding
+// Various logging levels.
+Dygraph.DEBUG = 1;
+Dygraph.INFO = 2;
+Dygraph.WARNING = 3;
+Dygraph.ERROR = 3;
+
+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]);
+    MochiKit.Base.update(attrs, { 'labels': new_labels });
+  }
+  this.__init__(div, file, attrs);
 };
 
 /**
 };
 
 /**
- * Initializes the DateGraph. This creates a new DIV and constructs the PlotKit
+ * 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 {String | Function} file Source data
  * and interaction &lt;canvas&gt; inside of it. See the constructor for details
  * on the parameters.
  * @param {String | Function} file Source data
@@ -95,70 +139,75 @@ DateGraph.DEFAULT_ATTRS = {
  * @param {Object} attrs Miscellaneous other options
  * @private
  */
  * @param {Object} attrs Miscellaneous other options
  * @private
  */
-DateGraph.prototype.__init__ = function(div, file, labels, attrs) {
+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;
   // Copy the important bits into the object
   // TODO(danvk): most of these should just stay in the attrs_ dictionary.
   this.maindiv_ = div;
-  this.labels_ = labels;
   this.file_ = file;
   this.file_ = file;
-  this.rollPeriod_ = attrs.rollPeriod || DateGraph.DEFAULT_ROLL_PERIOD;
+  this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
   this.previousVerticalX_ = -1;
   this.previousVerticalX_ = -1;
-  this.width_ = parseInt(div.style.width, 10);
-  this.height_ = parseInt(div.style.height, 10);
-  this.errorBars_ = attrs.errorBars || false;
   this.fractions_ = attrs.fractions || false;
   this.fractions_ = attrs.fractions || false;
-  this.strokeWidth_ = attrs.strokeWidth || DateGraph.DEFAULT_STROKE_WIDTH;
   this.dateWindow_ = attrs.dateWindow || null;
   this.valueRange_ = attrs.valueRange || null;
   this.dateWindow_ = attrs.dateWindow || null;
   this.valueRange_ = attrs.valueRange || null;
-  this.labelsSeparateLines = attrs.labelsSeparateLines || false;
-  this.labelsDiv_ = attrs.labelsDiv || null;
-  this.labelsKMB_ = attrs.labelsKMB || false;
-  this.xValueParser_ = attrs.xValueParser || DateGraph.prototype.dateParser;
-  this.xValueFormatter_ = attrs.xValueFormatter ||
-      DateGraph.prototype.dateString_;
-  this.xTicker_ = attrs.xTicker || DateGraph.prototype.dateTicker;
-  this.sigma_ = attrs.sigma || 2.0;
   this.wilsonInterval_ = attrs.wilsonInterval || true;
   this.wilsonInterval_ = attrs.wilsonInterval || true;
-  this.customBars_ = attrs.customBars || false;
 
 
-  this.attrs_ = {};
-  MochiKit.Base.update(this.attrs_, DateGraph.DEFAULT_ATTRS);
-  MochiKit.Base.update(this.attrs_, attrs);
+  // Clear the div. This ensure that, if multiple dygraphs are passed the same
+  // div, then only one will be drawn.
+  div.innerHTML = "";
 
 
-  if (typeof this.attrs_.pixelsPerXLabel == 'undefined') {
-    this.attrs_.pixelsPerXLabel = 60;
+  // If the div isn't already sized then give it a default size.
+  if (div.style.width == '') {
+    div.style.width = Dygraph.DEFAULT_WIDTH + "px";
+  }
+  if (div.style.height == '') {
+    div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
   }
   }
+  this.width_ = parseInt(div.style.width, 10);
+  this.height_ = parseInt(div.style.height, 10);
 
 
-  // Make a note of whether labels will be pulled from the CSV file.
-  this.labelsFromCSV_ = (this.labels_ == null);
-  if (this.labels_ == null)
-    this.labels_ = [];
+  // 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_ = {};
+  MochiKit.Base.update(this.user_attrs_, attrs);
 
 
-  // Prototype of the callback is "void clickCallback(event, date)"
-  this.clickCallback_ = attrs.clickCallback || null;
+  this.attrs_ = {};
+  MochiKit.Base.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
 
 
-  // Prototype of zoom callback is "void dragCallback(minDate, maxDate)"
-  this.zoomCallback_ = attrs.zoomCallback || null;
+  // Make a note of whether labels will be pulled from the CSV file.
+  this.labelsFromCSV_ = (this.attr_("labels") == null);
 
   // Create the containing DIV and other interactive elements
   this.createInterface_();
 
   // Create the PlotKit grapher
 
   // Create the containing DIV and other interactive elements
   this.createInterface_();
 
   // Create the PlotKit grapher
-  this.layoutOptions_ = { 'errorBars': (this.errorBars_ || this.customBars_),
+  // TODO(danvk): why does the Layout need its own set of options?
+  this.layoutOptions_ = { 'errorBars': (this.attr_("errorBars") ||
+                                        this.attr_("customBars")),
                           'xOriginIsZero': false };
                           'xOriginIsZero': false };
-  MochiKit.Base.update(this.layoutOptions_, attrs);
-  this.setColors_(attrs);
+  MochiKit.Base.update(this.layoutOptions_, this.attrs_);
+  MochiKit.Base.update(this.layoutOptions_, this.user_attrs_);
 
 
-  this.layout_ = new DateGraphLayout(this.layoutOptions_);
+  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,
   this.renderOptions_ = { colorScheme: this.colors_,
                           strokeColor: null,
-                          strokeWidth: this.strokeWidth_,
-                          axisLabelFontSize: 14,
-                          axisLineWidth: DateGraph.AXIS_LINE_WIDTH };
-  MochiKit.Base.update(this.renderOptions_, attrs);
-  this.plotter_ = new DateGraphCanvasRenderer(this.hidden_, this.layout_,
-                                              this.renderOptions_);
+                          axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
+  MochiKit.Base.update(this.renderOptions_, this.attrs_);
+  MochiKit.Base.update(this.renderOptions_, this.user_attrs_);
+  this.plotter_ = new DygraphCanvasRenderer(this,
+                                            this.hidden_, this.layout_,
+                                            this.renderOptions_);
 
   this.createStatusMessage_();
   this.createRollInterface_();
 
   this.createStatusMessage_();
   this.createRollInterface_();
@@ -168,21 +217,60 @@ DateGraph.prototype.__init__ = function(div, file, labels, attrs) {
   this.start_();
 };
 
   this.start_();
 };
 
+Dygraph.prototype.attr_ = function(name) {
+  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
  */
 /**
  * Returns the current rolling period, as set by the user or an option.
  * @return {Number} The number of days in the rolling window
  */
-DateGraph.prototype.rollPeriod = function() {
+Dygraph.prototype.rollPeriod = function() {
   return this.rollPeriod_;
 }
 
 /**
   return this.rollPeriod_;
 }
 
 /**
- * Generates interface elements for the DateGraph: a containing div, a div to
+ * 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.
  * @private
  */
  * display the current point, and a textbox to adjust the rolling average
  * period.
  * @private
  */
-DateGraph.prototype.createInterface_ = function() {
+Dygraph.prototype.createInterface_ = function() {
   // Create the all-enclosing graph div
   var enclosing = this.maindiv_;
 
   // Create the all-enclosing graph div
   var enclosing = this.maindiv_;
 
@@ -192,10 +280,20 @@ DateGraph.prototype.createInterface_ = function() {
   appendChildNodes(enclosing, this.graphDiv);
 
   // Create the canvas to store
   appendChildNodes(enclosing, this.graphDiv);
 
   // Create the canvas to store
+  // We need to subtract out some space for the x- and y-axis labels.
+  // For the x-axis:
+  //   - remove from height: (axisTickSize + height of tick label)
+  //          height of tick label == axisLabelFontSize?
+  //   - remove from width: axisLabelWidth / 2 (maybe on both ends)
+  // For the y-axis:
+  //   - remove axisLabelFontSize from the top
+  //   - remove axisTickSize from the left
+
   var canvas = MochiKit.DOM.CANVAS;
   this.canvas_ = canvas( { style: { 'position': 'absolute' },
                           width: this.width_,
   var canvas = MochiKit.DOM.CANVAS;
   this.canvas_ = canvas( { style: { 'position': 'absolute' },
                           width: this.width_,
-                          height: this.height_});
+                          height: this.height_
+                         });
   appendChildNodes(this.graphDiv, this.canvas_);
 
   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
   appendChildNodes(this.graphDiv, this.canvas_);
 
   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
@@ -205,12 +303,12 @@ DateGraph.prototype.createInterface_ = function() {
 
 /**
  * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
 
 /**
  * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
- * this particular canvas. All DateGraph work is done on this.canvas_.
- * @param {Object} canvas The DateGraph canvas to over which to overlay the plot
+ * 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
  */
  * @return {Object} The newly-created canvas
  * @private
  */
-DateGraph.prototype.createPlotKitCanvas_ = function(canvas) {
+Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
   var h = document.createElement("canvas");
   h.style.position = "absolute";
   h.style.top = canvas.style.top;
   var h = document.createElement("canvas");
   h.style.position = "absolute";
   h.style.top = canvas.style.top;
@@ -226,49 +324,86 @@ DateGraph.prototype.createPlotKitCanvas_ = function(canvas) {
  * 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.
  * 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.
- * @param {Object} attrs Various attributes, e.g. saturation and value
  * @private
  */
  * @private
  */
-DateGraph.prototype.setColors_ = function(attrs) {
-  var num = this.labels_.length;
+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_ = [];
   this.colors_ = [];
-  if (!attrs.colors) {
-    var sat = attrs.colorSaturation || 1.0;
-    var val = attrs.colorValue || 0.5;
+  var colors = this.attr_('colors');
+  if (!colors) {
+    var sat = this.attr_('colorSaturation') || 1.0;
+    var val = this.attr_('colorValue') || 0.5;
     for (var i = 1; i <= num; i++) {
       var hue = (1.0*i/(1+num));
       this.colors_.push( MochiKit.Color.Color.fromHSV(hue, sat, val) );
     }
   } else {
     for (var i = 0; i < num; i++) {
     for (var i = 1; i <= num; i++) {
       var hue = (1.0*i/(1+num));
       this.colors_.push( MochiKit.Color.Color.fromHSV(hue, sat, val) );
     }
   } else {
     for (var i = 0; i < num; i++) {
-      var colorStr = attrs.colors[i % attrs.colors.length];
+      var colorStr = colors[i % colors.length];
       this.colors_.push( MochiKit.Color.Color.fromString(colorStr) );
     }
   }
       this.colors_.push( MochiKit.Color.Color.fromString(colorStr) );
     }
   }
+
+  // TODO(danvk): update this w/r/t/ the new options system. 
+  this.renderOptions_.colorScheme = this.colors_;
+  MochiKit.Base.update(this.plotter_.options, this.renderOptions_);
+  MochiKit.Base.update(this.layoutOptions_, this.user_attrs_);
+  MochiKit.Base.update(this.layoutOptions_, this.attrs_);
 }
 
 }
 
+// The following functions are from quirksmode.org
+// http://www.quirksmode.org/js/findpos.html
+Dygraph.findPosX = function(obj) {
+  var curleft = 0;
+  if (obj.offsetParent) {
+    while (obj.offsetParent) {
+      curleft += obj.offsetLeft;
+      obj = obj.offsetParent;
+    }
+  }
+  else if (obj.x)
+    curleft += obj.x;
+  return curleft;
+};
+                   
+Dygraph.findPosY = function(obj) {
+  var curtop = 0;
+  if (obj.offsetParent) {
+    while (obj.offsetParent) {
+      curtop += obj.offsetTop;
+      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
  */
 /**
  * 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
  */
-DateGraph.prototype.createStatusMessage_ = function(){
-  if (!this.labelsDiv_) {
-    var divWidth = this.attrs_.labelsDivWidth;
+Dygraph.prototype.createStatusMessage_ = function(){
+  if (!this.attr_("labelsDiv")) {
+    var divWidth = this.attr_('labelsDivWidth');
     var messagestyle = { "style": {
       "position": "absolute",
       "fontSize": "14px",
       "zIndex": 10,
       "width": divWidth + "px",
       "top": "0px",
     var messagestyle = { "style": {
       "position": "absolute",
       "fontSize": "14px",
       "zIndex": 10,
       "width": divWidth + "px",
       "top": "0px",
-      "left": this.width_ - divWidth + "px",
+      "left": (this.width_ - divWidth - 2) + "px",
       "background": "white",
       "textAlign": "left",
       "overflow": "hidden"}};
       "background": "white",
       "textAlign": "left",
       "overflow": "hidden"}};
-    MochiKit.Base.update(messagestyle["style"], this.attrs_.labelsDivStyles);
-    this.labelsDiv_ = MochiKit.DOM.DIV(messagestyle);
-    MochiKit.DOM.appendChildNodes(this.graphDiv, this.labelsDiv_);
+    MochiKit.Base.update(messagestyle["style"], this.attr_('labelsDivStyles'));
+    var div = MochiKit.DOM.DIV(messagestyle);
+    MochiKit.DOM.appendChildNodes(this.graphDiv, div);
+    this.attrs_.labelsDiv = div;
   }
 };
 
   }
 };
 
@@ -277,19 +412,15 @@ DateGraph.prototype.createStatusMessage_ = function(){
  * @return {Object} The newly-created text box
  * @private
  */
  * @return {Object} The newly-created text box
  * @private
  */
-DateGraph.prototype.createRollInterface_ = function() {
-  var padding = this.plotter_.options.padding;
-  if (typeof this.attrs_.showRoller == 'undefined') {
-    this.attrs_.showRoller = false;
-  }
-  var display = this.attrs_.showRoller ? "block" : "none";
+Dygraph.prototype.createRollInterface_ = function() {
+  var display = this.attr_('showRoller') ? "block" : "none";
   var textAttr = { "type": "text",
                    "size": "2",
                    "value": this.rollPeriod_,
                    "style": { "position": "absolute",
                               "zIndex": 10,
   var textAttr = { "type": "text",
                    "size": "2",
                    "value": this.rollPeriod_,
                    "style": { "position": "absolute",
                               "zIndex": 10,
-                              "top": (this.height_ - 25 - padding.bottom) + "px",
-                              "left": (padding.left+1) + "px",
+                              "top": (this.plotter_.area.h - 25) + "px",
+                              "left": (this.plotter_.area.x + 1) + "px",
                               "display": display }
                   };
   var roller = MochiKit.DOM.INPUT(textAttr);
                               "display": display }
                   };
   var roller = MochiKit.DOM.INPUT(textAttr);
@@ -305,7 +436,7 @@ DateGraph.prototype.createRollInterface_ = function() {
  * events. Uses MochiKit.Signal to attach all the event handlers.
  * @private
  */
  * events. Uses MochiKit.Signal to attach all the event handlers.
  * @private
  */
-DateGraph.prototype.createDragInterface_ = function() {
+Dygraph.prototype.createDragInterface_ = function() {
   var self = this;
 
   // Tracks whether the mouse is down right now
   var self = this;
 
   // Tracks whether the mouse is down right now
@@ -336,8 +467,8 @@ DateGraph.prototype.createDragInterface_ = function() {
   // Track the beginning of drag events
   connect(this.hidden_, 'onmousedown', function(event) {
     mouseDown = true;
   // Track the beginning of drag events
   connect(this.hidden_, 'onmousedown', function(event) {
     mouseDown = true;
-    px = PlotKit.Base.findPosX(self.canvas_);
-    py = PlotKit.Base.findPosY(self.canvas_);
+    px = Dygraph.findPosX(self.canvas_);
+    py = Dygraph.findPosY(self.canvas_);
     dragStartX = getX(event);
     dragStartY = getY(event);
   });
     dragStartX = getX(event);
     dragStartY = getY(event);
   });
@@ -371,9 +502,10 @@ DateGraph.prototype.createDragInterface_ = function() {
       var regionHeight = Math.abs(dragEndY - dragStartY);
 
       if (regionWidth < 2 && regionHeight < 2 &&
       var regionHeight = Math.abs(dragEndY - dragStartY);
 
       if (regionWidth < 2 && regionHeight < 2 &&
-          self.clickCallback_ != null &&
+          self.attr_('clickCallback') != null &&
           self.lastx_ != undefined) {
           self.lastx_ != undefined) {
-        self.clickCallback_(event, new Date(self.lastx_));
+        // TODO(danvk): pass along more info about the point.
+        self.attr_('clickCallback')(event, new Date(self.lastx_));
       }
 
       if (regionWidth >= 10) {
       }
 
       if (regionWidth >= 10) {
@@ -396,8 +528,8 @@ DateGraph.prototype.createDragInterface_ = function() {
     self.drawGraph_(self.rawData_);
     var minDate = self.rawData_[0][0];
     var maxDate = self.rawData_[self.rawData_.length - 1][0];
     self.drawGraph_(self.rawData_);
     var minDate = self.rawData_[0][0];
     var maxDate = self.rawData_[self.rawData_.length - 1][0];
-    if (self.zoomCallback_) {
-      self.zoomCallback_(minDate, maxDate);
+    if (self.attr_("zoomCallback")) {
+      self.attr_("zoomCallback")(minDate, maxDate);
     }
   });
 };
     }
   });
 };
@@ -414,7 +546,7 @@ DateGraph.prototype.createDragInterface_ = function() {
  * function. Used to avoid excess redrawing
  * @private
  */
  * function. Used to avoid excess redrawing
  * @private
  */
-DateGraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
+Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
   var ctx = this.canvas_.getContext("2d");
 
   // Clean up from the previous rect if necessary
   var ctx = this.canvas_.getContext("2d");
 
   // Clean up from the previous rect if necessary
@@ -439,7 +571,7 @@ DateGraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
  * @param {Number} highX The rightmost pixel value that should be visible.
  * @private
  */
  * @param {Number} highX The rightmost pixel value that should be visible.
  * @private
  */
-DateGraph.prototype.doZoom_ = function(lowX, highX) {
+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;
   // Find the earliest and latest dates contained in this canvasx range.
   var points = this.layout_.points;
   var minDate = null;
@@ -457,8 +589,8 @@ DateGraph.prototype.doZoom_ = function(lowX, highX) {
 
   this.dateWindow_ = [minDate, maxDate];
   this.drawGraph_(this.rawData_);
 
   this.dateWindow_ = [minDate, maxDate];
   this.drawGraph_(this.rawData_);
-  if (this.zoomCallback_) {
-    this.zoomCallback_(minDate, maxDate);
+  if (this.attr_("zoomCallback")) {
+    this.attr_("zoomCallback")(minDate, maxDate);
   }
 };
 
   }
 };
 
@@ -469,8 +601,8 @@ DateGraph.prototype.doZoom_ = function(lowX, highX) {
  * @param {Object} event The mousemove event from the browser.
  * @private
  */
  * @param {Object} event The mousemove event from the browser.
  * @private
  */
-DateGraph.prototype.mouseMove_ = function(event) {
-  var canvasx = event.mouse().page.x - PlotKit.Base.findPosX(this.hidden_);
+Dygraph.prototype.mouseMove_ = function(event) {
+  var canvasx = event.mouse().page.x - Dygraph.findPosX(this.hidden_);
   var points = this.layout_.points;
 
   var lastx = -1;
   var points = this.layout_.points;
 
   var lastx = -1;
@@ -500,21 +632,24 @@ DateGraph.prototype.mouseMove_ = function(event) {
   }
 
   // Clear the previously drawn vertical, if there is one
   }
 
   // Clear the previously drawn vertical, if there is one
-  var circleSize = this.attrs_.highlightCircleSize;
+  var circleSize = this.attr_('highlightCircleSize');
   var ctx = this.canvas_.getContext("2d");
   if (this.previousVerticalX_ >= 0) {
     var px = this.previousVerticalX_;
     ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
   }
 
   var ctx = this.canvas_.getContext("2d");
   if (this.previousVerticalX_ >= 0) {
     var px = this.previousVerticalX_;
     ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
   }
 
+  var isOK = function(x) { return x && !isNaN(x); };
+
   if (selPoints.length > 0) {
     var canvasx = selPoints[0].canvasx;
 
     // Set the status message to indicate the selected point(s)
   if (selPoints.length > 0) {
     var canvasx = selPoints[0].canvasx;
 
     // Set the status message to indicate the selected point(s)
-    var replace = this.xValueFormatter_(lastx) + ":";
+    var replace = this.attr_('xValueFormatter')(lastx, this) + ":";
     var clen = this.colors_.length;
     for (var i = 0; i < selPoints.length; i++) {
     var clen = this.colors_.length;
     for (var i = 0; i < selPoints.length; i++) {
-      if (this.labelsSeparateLines) {
+      if (!isOK(selPoints[i].canvasy)) continue;
+      if (this.attr_("labelsSeparateLines")) {
         replace += "<br/>";
       }
       var point = selPoints[i];
         replace += "<br/>";
       }
       var point = selPoints[i];
@@ -522,7 +657,7 @@ DateGraph.prototype.mouseMove_ = function(event) {
               + point.name + "</font></b>:"
               + this.round_(point.yval, 2);
     }
               + point.name + "</font></b>:"
               + this.round_(point.yval, 2);
     }
-    this.labelsDiv_.innerHTML = replace;
+    this.attr_("labelsDiv").innerHTML = replace;
 
     // Save last x position for callbacks.
     this.lastx_ = lastx;
 
     // Save last x position for callbacks.
     this.lastx_ = lastx;
@@ -530,6 +665,7 @@ DateGraph.prototype.mouseMove_ = function(event) {
     // Draw colored circles over the center of each selected point
     ctx.save()
     for (var i = 0; i < selPoints.length; i++) {
     // Draw colored circles over the center of each selected point
     ctx.save()
     for (var i = 0; i < selPoints.length; i++) {
+      if (!isOK(selPoints[i%clen].canvasy)) continue;
       ctx.beginPath();
       ctx.fillStyle = this.colors_[i%clen].toRGBString();
       ctx.arc(canvasx, selPoints[i%clen].canvasy, circleSize, 0, 360, false);
       ctx.beginPath();
       ctx.fillStyle = this.colors_[i%clen].toRGBString();
       ctx.arc(canvasx, selPoints[i%clen].canvasy, circleSize, 0, 360, false);
@@ -546,14 +682,14 @@ DateGraph.prototype.mouseMove_ = function(event) {
  * @param {Object} event the mouseout event from the browser.
  * @private
  */
  * @param {Object} event the mouseout event from the browser.
  * @private
  */
-DateGraph.prototype.mouseOut_ = function(event) {
+Dygraph.prototype.mouseOut_ = function(event) {
   // Get rid of the overlay data
   var ctx = this.canvas_.getContext("2d");
   ctx.clearRect(0, 0, this.width_, this.height_);
   // Get rid of the overlay data
   var ctx = this.canvas_.getContext("2d");
   ctx.clearRect(0, 0, this.width_, this.height_);
-  this.labelsDiv_.innerHTML = "";
+  this.attr_("labelsDiv").innerHTML = "";
 };
 
 };
 
-DateGraph.zeropad = function(x) {
+Dygraph.zeropad = function(x) {
   if (x < 10) return "0" + x; else return "" + x;
 }
 
   if (x < 10) return "0" + x; else return "" + x;
 }
 
@@ -563,8 +699,8 @@ DateGraph.zeropad = function(x) {
  * @return {String} A time of the form "HH:MM:SS"
  * @private
  */
  * @return {String} A time of the form "HH:MM:SS"
  * @private
  */
-DateGraph.prototype.hmsString_ = function(date) {
-  var zeropad = DateGraph.zeropad;
+Dygraph.prototype.hmsString_ = function(date) {
+  var zeropad = Dygraph.zeropad;
   var d = new Date(date);
   if (d.getSeconds()) {
     return zeropad(d.getHours()) + ":" +
   var d = new Date(date);
   if (d.getSeconds()) {
     return zeropad(d.getHours()) + ":" +
@@ -582,9 +718,10 @@ DateGraph.prototype.hmsString_ = function(date) {
  * @param {Number} date The JavaScript date (ms since epoch)
  * @return {String} A date of the form "YYYY/MM/DD"
  * @private
  * @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?
  */
  */
-DateGraph.prototype.dateString_ = function(date) {
-  var zeropad = DateGraph.zeropad;
+Dygraph.dateString_ = function(date, self) {
+  var zeropad = Dygraph.zeropad;
   var d = new Date(date);
 
   // Get the year:
   var d = new Date(date);
 
   // Get the year:
@@ -596,7 +733,7 @@ DateGraph.prototype.dateString_ = function(date) {
 
   var ret = "";
   var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
 
   var ret = "";
   var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-  if (frac) ret = " " + this.hmsString_(date);
+  if (frac) ret = " " + self.hmsString_(date);
 
   return year + "/" + month + "/" + day + ret;
 };
 
   return year + "/" + month + "/" + day + ret;
 };
@@ -608,7 +745,7 @@ DateGraph.prototype.dateString_ = function(date) {
  * @return {Number} The rounded number
  * @private
  */
  * @return {Number} The rounded number
  * @private
  */
-DateGraph.prototype.round_ = function(num, places) {
+Dygraph.prototype.round_ = function(num, places) {
   var shift = Math.pow(10, places);
   return Math.round(num * shift)/shift;
 };
   var shift = Math.pow(10, places);
   return Math.round(num * shift)/shift;
 };
@@ -618,20 +755,20 @@ DateGraph.prototype.round_ = function(num, places) {
  * @param {String} data Raw CSV data to be plotted
  * @private
  */
  * @param {String} data Raw CSV data to be plotted
  * @private
  */
-DateGraph.prototype.loadedEvent_ = function(data) {
+Dygraph.prototype.loadedEvent_ = function(data) {
   this.rawData_ = this.parseCSV_(data);
   this.drawGraph_(this.rawData_);
 };
 
   this.rawData_ = this.parseCSV_(data);
   this.drawGraph_(this.rawData_);
 };
 
-DateGraph.prototype.months =  ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
-                               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
-DateGraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
+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
  */
 
 /**
  * Add ticks on the x-axis representing years, months, quarters, weeks, or days
  * @private
  */
-DateGraph.prototype.addXTicks_ = function() {
+Dygraph.prototype.addXTicks_ = function() {
   // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
   var startDate, endDate;
   if (this.dateWindow_) {
   // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
   var startDate, endDate;
   if (this.dateWindow_) {
@@ -642,47 +779,57 @@ DateGraph.prototype.addXTicks_ = function() {
     endDate   = this.rawData_[this.rawData_.length - 1][0];
   }
 
     endDate   = this.rawData_[this.rawData_.length - 1][0];
   }
 
-  var xTicks = this.xTicker_(startDate, endDate);
+  var xTicks = this.attr_('xTicker')(startDate, endDate, this);
   this.layout_.updateOptions({xTicks: xTicks});
 };
 
 // Time granularity enumeration
   this.layout_.updateOptions({xTicks: xTicks});
 };
 
 // Time granularity enumeration
-DateGraph.SECONDLY = 0;
-DateGraph.MINUTELY = 1;
-DateGraph.HOURLY = 2;
-DateGraph.DAILY = 3;
-DateGraph.WEEKLY = 4;
-DateGraph.MONTHLY = 5;
-DateGraph.QUARTERLY = 6;
-DateGraph.BIANNUAL = 7;
-DateGraph.ANNUAL = 8;
-DateGraph.DECADAL = 9;
-DateGraph.NUM_GRANULARITIES = 10;
-
-DateGraph.SHORT_SPACINGS = [];
-DateGraph.SHORT_SPACINGS[DateGraph.SECONDLY] = 1000 * 1;
-DateGraph.SHORT_SPACINGS[DateGraph.MINUTELY] = 1000 * 60;
-DateGraph.SHORT_SPACINGS[DateGraph.HOURLY]   = 1000 * 3600;
-DateGraph.SHORT_SPACINGS[DateGraph.DAILY]    = 1000 * 86400;
-DateGraph.SHORT_SPACINGS[DateGraph.WEEKLY]   = 1000 * 604800;
+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.SHORT_SPACINGS = [];
+Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
+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.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.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.
 //
 
 // NumXTicks()
 //
 //   If we used this time granularity, how many ticks would there be?
 //   This is only an approximation, but it's generally good enough.
 //
-DateGraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
-  if (granularity < DateGraph.MONTHLY) {
+Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
+  if (granularity < Dygraph.MONTHLY) {
     // Generate one tick mark for every fixed interval of time.
     // Generate one tick mark for every fixed interval of time.
-    var spacing = DateGraph.SHORT_SPACINGS[granularity];
+    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;
     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 == DateGraph.QUARTERLY) num_months = 3;
-    if (granularity == DateGraph.BIANNUAL) num_months = 2;
-    if (granularity == DateGraph.ANNUAL) num_months = 1;
-    if (granularity == DateGraph.DECADAL) { num_months = 1; year_mod = 10; }
+    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;
 
     var msInYear = 365.2524 * 24 * 3600 * 1000;
     var num_years = 1.0 * (end_time - start_time) / msInYear;
@@ -697,16 +844,20 @@ DateGraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
 //
 //   Returns an array containing {v: millis, label: label} dictionaries.
 //
 //
 //   Returns an array containing {v: millis, label: label} dictionaries.
 //
-DateGraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
+Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
   var ticks = [];
   var ticks = [];
-  if (granularity < DateGraph.MONTHLY) {
+  if (granularity < Dygraph.MONTHLY) {
     // Generate one tick mark for every fixed interval of time.
     // Generate one tick mark for every fixed interval of time.
-    var spacing = DateGraph.SHORT_SPACINGS[granularity];
+    var spacing = Dygraph.SHORT_SPACINGS[granularity];
     var format = '%d%b';  // e.g. "1 Jan"
     var format = '%d%b';  // e.g. "1 Jan"
+    // 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();
     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 >= DateGraph.DAILY) {
+      if (frac == 0 || granularity >= Dygraph.DAILY) {
         // the extra hour covers DST problems.
         ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) });
       } else {
         // the extra hour covers DST problems.
         ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) });
       } else {
@@ -720,23 +871,22 @@ DateGraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
     var months;
     var year_mod = 1;  // e.g. to only print one point every 10 years.
 
     var months;
     var year_mod = 1;  // e.g. to only print one point every 10 years.
 
-    // TODO(danvk): use CachingRoundTime where appropriate to get boundaries.
-    if (granularity == DateGraph.MONTHLY) {
+    if (granularity == Dygraph.MONTHLY) {
       months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
       months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
-    } else if (granularity == DateGraph.QUARTERLY) {
+    } else if (granularity == Dygraph.QUARTERLY) {
       months = [ 0, 3, 6, 9 ];
       months = [ 0, 3, 6, 9 ];
-    } else if (granularity == DateGraph.BIANNUAL) {
+    } else if (granularity == Dygraph.BIANNUAL) {
       months = [ 0, 6 ];
       months = [ 0, 6 ];
-    } else if (granularity == DateGraph.ANNUAL) {
+    } else if (granularity == Dygraph.ANNUAL) {
       months = [ 0 ];
       months = [ 0 ];
-    } else if (granularity == DateGraph.DECADAL) {
+    } 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();
       months = [ 0 ];
       year_mod = 10;
     }
 
     var start_year = new Date(start_time).getFullYear();
     var end_year   = new Date(end_time).getFullYear();
-    var zeropad = DateGraph.zeropad;
+    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++) {
     for (var i = start_year; i <= end_year; i++) {
       if (i % year_mod != 0) continue;
       for (var j = 0; j < months.length; j++) {
@@ -759,18 +909,18 @@ DateGraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
  * @return {Array.<Object>} Array of {label, value} tuples.
  * @public
  */
  * @return {Array.<Object>} Array of {label, value} tuples.
  * @public
  */
-DateGraph.prototype.dateTicker = function(startDate, endDate) {
+Dygraph.dateTicker = function(startDate, endDate, self) {
   var chosen = -1;
   var chosen = -1;
-  for (var i = 0; i < DateGraph.NUM_GRANULARITIES; i++) {
-    var num_ticks = this.NumXTicks(startDate, endDate, i);
-    if (this.width_ / num_ticks >= this.attrs_.pixelsPerXLabel) {
+  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) {
       chosen = i;
       break;
     }
   }
 
   if (chosen >= 0) {
-    return this.GetXAxis(startDate, endDate, chosen);
+    return self.GetXAxis(startDate, endDate, chosen);
   } else {
     // TODO(danvk): signal error.
   }
   } else {
     // TODO(danvk): signal error.
   }
@@ -783,44 +933,42 @@ DateGraph.prototype.dateTicker = function(startDate, endDate) {
  * @return {Array.<Object>} Array of {label, value} tuples.
  * @public
  */
  * @return {Array.<Object>} Array of {label, value} tuples.
  * @public
  */
-DateGraph.prototype.numericTicks = function(minV, maxV) {
+Dygraph.numericTicks = function(minV, maxV, self) {
   // Basic idea:
   // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
   // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
   // 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 this.attrs_.pixelsPerYLabel is what we use.
+  // The first spacing greater than pixelsPerYLabel is what we use.
   var mults = [1, 2, 5];
   var scale, low_val, high_val, nTicks;
   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++) {
     var base_scale = Math.pow(10, i);
     for (var j = 0; j < mults.length; j++) {
       scale = base_scale * mults[j];
   for (var i = -10; i < 50; i++) {
     var base_scale = Math.pow(10, i);
     for (var j = 0; j < mults.length; j++) {
       scale = base_scale * mults[j];
-      console.log("i/j/scale: " + i + "/" + j + "/" + scale);
       low_val = Math.floor(minV / scale) * scale;
       high_val = Math.ceil(maxV / scale) * scale;
       nTicks = (high_val - low_val) / scale;
       low_val = Math.floor(minV / scale) * scale;
       high_val = Math.ceil(maxV / scale) * scale;
       nTicks = (high_val - low_val) / scale;
-      var spacing = this.height_ / nTicks;
+      var spacing = self.height_ / nTicks;
       // wish I could break out of both loops at once...
       // wish I could break out of both loops at once...
-      if (spacing > this.attrs_.pixelsPerYLabel) break;
+      if (spacing > pixelsPerTick) break;
     }
     }
-    if (spacing > this.attrs_.pixelsPerYLabel) break;
+    if (spacing > pixelsPerTick) break;
   }
   }
-  console.log("scale: " + scale);
-  console.log("low_val: " + low_val);
-  console.log("high_val: " + high_val);
 
   // Construct labels for the ticks
   var ticks = [];
   for (var i = 0; i < nTicks; i++) {
     var tickV = low_val + i * scale;
 
   // Construct labels for the ticks
   var ticks = [];
   for (var i = 0; i < nTicks; i++) {
     var tickV = low_val + i * scale;
-    var label = this.round_(tickV, 2);
-    if (this.labelsKMB_) {
+    var label = self.round_(tickV, 2);
+    if (self.attr_("labelsKMB")) {
       var k = 1000;
       if (tickV >= k*k*k) {
       var k = 1000;
       if (tickV >= k*k*k) {
-        label = this.round_(tickV/(k*k*k), 1) + "B";
+        label = self.round_(tickV/(k*k*k), 1) + "B";
       } else if (tickV >= k*k) {
       } else if (tickV >= k*k) {
-        label = this.round_(tickV/(k*k), 1) + "M";
+        label = self.round_(tickV/(k*k), 1) + "M";
       } else if (tickV >= k) {
       } else if (tickV >= k) {
-        label = this.round_(tickV/k, 1) + "K";
+        label = self.round_(tickV/k, 1) + "K";
       }
     }
     ticks.push( {label: label, v: tickV} );
       }
     }
     ticks.push( {label: label, v: tickV} );
@@ -834,13 +982,54 @@ DateGraph.prototype.numericTicks = function(minV, maxV) {
  * @param {Number} maxY The maximum Y value in the data set
  * @private
  */
  * @param {Number} maxY The maximum Y value in the data set
  * @private
  */
-DateGraph.prototype.addYTicks_ = function(minY, maxY) {
+Dygraph.prototype.addYTicks_ = function(minY, maxY) {
   // Set the number of ticks so that the labels are human-friendly.
   // Set the number of ticks so that the labels are human-friendly.
-  var ticks = this.numericTicks(minY, maxY);
+  // TODO(danvk): make this an attribute as well.
+  var ticks = Dygraph.numericTicks(minY, maxY, this);
   this.layout_.updateOptions( { yAxis: [minY, maxY],
                                 yTicks: ticks } );
 };
 
   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) 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
 /**
  * Update the graph with new data. Data is in the format
  * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
@@ -849,9 +1038,12 @@ DateGraph.prototype.addYTicks_ = function(minY, maxY) {
  * @param {Array.<Object>} data The data (see above)
  * @private
  */
  * @param {Array.<Object>} data The data (see above)
  * @private
  */
-DateGraph.prototype.drawGraph_ = function(data) {
-  var maxY = null;
+Dygraph.prototype.drawGraph_ = function(data) {
+  var minY = null, maxY = null;
   this.layout_.removeAllDatasets();
   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++) {
     var series = [];
   // Loop over all fields in the dataset
   for (var i = 1; i < data[0].length; i++) {
     var series = [];
@@ -862,7 +1054,7 @@ DateGraph.prototype.drawGraph_ = function(data) {
     series = this.rollingAverage(series, this.rollPeriod_);
 
     // Prune down to the desired range, if necessary (for zooming)
     series = this.rollingAverage(series, this.rollPeriod_);
 
     // Prune down to the desired range, if necessary (for zooming)
-    var bars = this.errorBars_ || this.customBars_;
+    var bars = this.attr_("errorBars") || this.attr_("customBars");
     if (this.dateWindow_) {
       var low = this.dateWindow_[0];
       var high= this.dateWindow_[1];
     if (this.dateWindow_) {
       var low = this.dateWindow_[0];
       var high= this.dateWindow_[1];
@@ -870,28 +1062,25 @@ DateGraph.prototype.drawGraph_ = function(data) {
       for (var k = 0; k < series.length; k++) {
         if (series[k][0] >= low && series[k][0] <= high) {
           pruned.push(series[k]);
       for (var k = 0; k < series.length; k++) {
         if (series[k][0] >= low && series[k][0] <= high) {
           pruned.push(series[k]);
-          var y = bars ? series[k][1][0] : series[k][1];
-          if (maxY == null || y > maxY) maxY = y;
         }
       }
       series = pruned;
         }
       }
       series = pruned;
-    } else {
-      for (var j = 0; j < series.length; j++) {
-        var y = bars ? series[j][1][0] : series[j][1];
-        if (maxY == null || y > maxY) {
-          maxY = bars ? y + series[j][1][1] : y;
-        }
-      }
     }
 
     }
 
+    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 (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]];
     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.labels_[i - 1], vals);
+      this.layout_.addDataset(this.attr_("labels")[i], vals);
     } else {
     } else {
-      this.layout_.addDataset(this.labels_[i - 1], series);
+      this.layout_.addDataset(this.attr_("labels")[i], series);
     }
   }
 
     }
   }
 
@@ -901,9 +1090,20 @@ DateGraph.prototype.drawGraph_ = function(data) {
     this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
   } else {
     // Add some padding and round up to an integer to be human-friendly.
     this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
   } else {
     // Add some padding and round up to an integer to be human-friendly.
-    maxY *= 1.1;
-    if (maxY <= 0.0) maxY = 1.0;
-    this.addYTicks_(0, maxY);
+    var span = maxY - minY;
+    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 (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.addXTicks_();
   }
 
   this.addXTicks_();
@@ -927,12 +1127,12 @@ DateGraph.prototype.drawGraph_ = function(data) {
  * @param {Array} originalData The data in the appropriate format (see above)
  * @param {Number} rollPeriod The number of days over which to average the data
  */
  * @param {Array} originalData The data in the appropriate format (see above)
  * @param {Number} rollPeriod The number of days over which to average the data
  */
-DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
+Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
   if (originalData.length < 2)
     return originalData;
   var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
   var rollingData = [];
   if (originalData.length < 2)
     return originalData;
   var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
   var rollingData = [];
-  var sigma = this.sigma_;
+  var sigma = this.attr_("sigma");
 
   if (this.fractions_) {
     var num = 0;
 
   if (this.fractions_) {
     var num = 0;
@@ -948,7 +1148,7 @@ DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
 
       var date = originalData[i][0];
       var value = den ? num / den : 0.0;
 
       var date = originalData[i][0];
       var value = den ? num / den : 0.0;
-      if (this.errorBars_) {
+      if (this.attr_("errorBars")) {
         if (this.wilsonInterval_) {
           // For more details on this confidence interval, see:
           // http://en.wikipedia.org/wiki/Binomial_confidence_interval
         if (this.wilsonInterval_) {
           // For more details on this confidence interval, see:
           // http://en.wikipedia.org/wiki/Binomial_confidence_interval
@@ -971,7 +1171,7 @@ DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
         rollingData[i] = [date, mult * value];
       }
     }
         rollingData[i] = [date, mult * value];
       }
     }
-  } else if (this.customBars_) {
+  } else if (this.attr_("customBars")) {
     var low = 0;
     var mid = 0;
     var high = 0;
     var low = 0;
     var mid = 0;
     var high = 0;
@@ -1000,47 +1200,46 @@ DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
     // 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);
     // 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.errorBars_){
-      for (var i = 0; i < num_init_points; i++) {
-        var sum = 0;
-        for (var j = 0; j < i + 1; j++)
-          sum += originalData[j][1];
-        rollingData[i] = [originalData[i][0], sum / (i + 1)];
+    if (!this.attr_("errorBars")){
+      if (rollPeriod == 1) {
+        return originalData;
       }
       }
-      // Calculate the rolling average for the remaining points
-      for (var i = Math.min(rollPeriod - 1, originalData.length - 2);
-          i < originalData.length;
-          i++) {
+
+      for (var i = 0; i < originalData.length; i++) {
         var sum = 0;
         var sum = 0;
-        for (var j = i - rollPeriod + 1; j < i + 1; j++)
+        var num_ok = 0;
+        for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
+          var y = originalData[j][1];
+          if (!y || isNaN(y)) continue;
+          num_ok++;
           sum += originalData[j][1];
           sum += originalData[j][1];
-        rollingData[i] = [originalData[i][0], sum / rollPeriod];
+        }
+        if (num_ok) {
+          rollingData[i] = [originalData[i][0], sum / num_ok];
+        } else {
+          rollingData[i] = [originalData[i][0], null];
+        }
       }
       }
+
     } else {
     } else {
-      for (var i = 0; i < num_init_points; i++) {
+      for (var i = 0; i < originalData.length; i++) {
         var sum = 0;
         var variance = 0;
         var sum = 0;
         var variance = 0;
-        for (var j = 0; j < i + 1; j++) {
+        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 || isNaN(y)) continue;
+          num_ok++;
           sum += originalData[j][1][0];
           variance += Math.pow(originalData[j][1][1], 2);
         }
           sum += originalData[j][1][0];
           variance += Math.pow(originalData[j][1][1], 2);
         }
-        var stddev = Math.sqrt(variance)/(i+1);
-        rollingData[i] = [originalData[i][0],
-                          [sum/(i+1), sigma * stddev, sigma * stddev]];
-      }
-      // Calculate the rolling average for the remaining points
-      for (var i = Math.min(rollPeriod - 1, originalData.length - 2);
-          i < originalData.length;
-          i++) {
-        var sum = 0;
-        var variance = 0;
-        for (var j = i - rollPeriod + 1; j < i + 1; j++) {
-          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]];
         }
         }
-        var stddev = Math.sqrt(variance) / rollPeriod;
-        rollingData[i] = [originalData[i][0],
-                          [sum / rollPeriod, sigma * stddev, sigma * stddev]];
       }
     }
   }
       }
     }
   }
@@ -1050,27 +1249,63 @@ DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
 
 /**
  * Parses a date, returning the number of milliseconds since epoch. This can be
 
 /**
  * Parses a date, returning the number of milliseconds since epoch. This can be
- * passed in as an xValueParser in the DateGraph constructor.
+ * 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
  */
  * @param {String} A date in YYYYMMDD format.
  * @return {Number} Milliseconds since epoch.
  * @public
  */
-DateGraph.prototype.dateParser = function(dateStr) {
+Dygraph.dateParser = function(dateStr, self) {
   var dateStrSlashed;
   var dateStrSlashed;
+  var d;
   if (dateStr.length == 10 && dateStr.search("-") != -1) {  // e.g. '2009-07-12'
     dateStrSlashed = dateStr.replace("-", "/", "g");
     while (dateStrSlashed.search("-") != -1) {
       dateStrSlashed = dateStrSlashed.replace("-", "/");
     }
   if (dateStr.length == 10 && dateStr.search("-") != -1) {  // e.g. '2009-07-12'
     dateStrSlashed = dateStr.replace("-", "/", "g");
     while (dateStrSlashed.search("-") != -1) {
       dateStrSlashed = dateStrSlashed.replace("-", "/");
     }
-    return Date.parse(dateStrSlashed);
+    d = Date.parse(dateStrSlashed);
   } else if (dateStr.length == 8) {  // e.g. '20090712'
   } 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);
     dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
                        + "/" + dateStr.substr(6,2);
-    return Date.parse(dateStrSlashed);
+    d = Date.parse(dateStrSlashed);
   } else {
     // Any format that Date.parse will accept, e.g. "2009/07/12" or
     // "2009/07/12 12:34:56"
   } else {
     // Any format that Date.parse will accept, e.g. "2009/07/12" or
     // "2009/07/12 12:34:56"
-    return Date.parse(dateStr);
+    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;
+  } else {
+    this.attrs_.xValueFormatter = function(x) { return x; };
+    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
+    this.attrs_.xTicker = Dygraph.numericTicks;
   }
 };
 
   }
 };
 
@@ -1078,35 +1313,44 @@ DateGraph.prototype.dateParser = function(dateStr) {
  * 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.
  * 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 this.errorBars_ is set, then interpret the fields as:
+ * if the errorBars attribute is set, then interpret the fields as:
  * date, series1, stddev1, series2, stddev2, ...
  * @param {Array.<Object>} data See above.
  * @private
  * 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 ]
  */
  */
-DateGraph.prototype.parseCSV_ = function(data) {
+Dygraph.prototype.parseCSV_ = function(data) {
   var ret = [];
   var lines = data.split("\n");
   var ret = [];
   var lines = data.split("\n");
-  var start = this.labelsFromCSV_ ? 1 : 0;
+  var start = 0;
   if (this.labelsFromCSV_) {
   if (this.labelsFromCSV_) {
-    var labels = lines[0].split(",");
-    labels.shift();  // a "date" parameter is assumed.
-    this.labels_ = labels;
-    // regenerate automatic colors.
-    this.setColors_(this.attrs_);
-    this.renderOptions_.colorScheme = this.colors_;
-    MochiKit.Base.update(this.plotter_.options, this.renderOptions_);
-    MochiKit.Base.update(this.layoutOptions_, this.attrs_);
+    start = 1;
+    this.attrs_.labels = lines[0].split(",");
   }
 
   }
 
+  var xParser;
+  var defaultParserSet = false;  // attempt to auto-detect x value type
+  var expectedCols = this.attr_("labels").length;
   for (var i = start; i < lines.length; i++) {
     var line = lines[i];
     if (line.length == 0) continue;  // skip blank lines
     var inFields = line.split(',');
   for (var i = start; i < lines.length; i++) {
     var line = lines[i];
     if (line.length == 0) continue;  // skip blank lines
     var inFields = line.split(',');
-    if (inFields.length < 2)
-      continue;
+    if (inFields.length < 2) continue;
 
     var fields = [];
 
     var fields = [];
-    fields[0] = this.xValueParser_(inFields[0]);
+    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_) {
 
     // If fractions are expected, parse the numbers as "A/B"
     if (this.fractions_) {
@@ -1115,12 +1359,12 @@ DateGraph.prototype.parseCSV_ = function(data) {
         var vals = inFields[j].split("/");
         fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
       }
         var vals = inFields[j].split("/");
         fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
       }
-    } else if (this.errorBars_) {
+    } 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])];
       // 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])];
-    } else if (this.customBars_) {
+    } 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(";");
       // Bars are a low;center;high tuple
       for (var j = 1; j < inFields.length; j++) {
         var vals = inFields[j].split(";");
@@ -1130,15 +1374,77 @@ DateGraph.prototype.parseCSV_ = function(data) {
       }
     } else {
       // Values are just numbers
       }
     } else {
       // Values are just numbers
-      for (var j = 1; j < inFields.length; j++)
+      for (var j = 1; j < inFields.length; j++) {
         fields[j] = parseFloat(inFields[j]);
         fields[j] = parseFloat(inFields[j]);
+      }
     }
     ret.push(fields);
     }
     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);
+    }
   }
   return ret;
 };
 
 /**
   }
   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 (MochiKit.Base.isDateLike(data[0][0])) {
+    // Some intelligent defaults for a date x-axis.
+    this.attrs_.xValueFormatter = Dygraph.dateString_;
+    this.attrs_.xTicker = Dygraph.dateTicker;
+
+    // Assume they're all dates.
+    var parsedData = MochiKit.Base.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') {
+        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
  * 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
@@ -1147,7 +1453,7 @@ DateGraph.prototype.parseCSV_ = function(data) {
  * @param {Array.<Object>} data See above.
  * @private
  */
  * @param {Array.<Object>} data See above.
  * @private
  */
-DateGraph.prototype.parseDataTable_ = function(data) {
+Dygraph.prototype.parseDataTable_ = function(data) {
   var cols = data.getNumberOfColumns();
   var rows = data.getNumberOfRows();
 
   var cols = data.getNumberOfColumns();
   var rows = data.getNumberOfRows();
 
@@ -1156,25 +1462,27 @@ DateGraph.prototype.parseDataTable_ = function(data) {
   for (var i = 0; i < cols; i++) {
     labels.push(data.getColumnLabel(i));
   }
   for (var i = 0; i < cols; i++) {
     labels.push(data.getColumnLabel(i));
   }
-  labels.shift();  // the x-axis parameter is assumed and unnamed.
-  this.labels_ = labels;
-  // regenerate automatic colors.
-  this.setColors_(this.attrs_);
-  this.renderOptions_.colorScheme = this.colors_;
-  MochiKit.Base.update(this.plotter_.options, this.renderOptions_);
-  MochiKit.Base.update(this.layoutOptions_, this.attrs_);
+  this.attrs_.labels = labels;
 
   var indepType = data.getColumnType(0);
 
   var indepType = data.getColumnType(0);
-  if (indepType != 'date' && indepType != 'number') {
-    // TODO(danvk): standardize error reporting.
-    alert("only 'date' and 'number' types are supported for column 1" +
-          "of DataTable input (Got '" + indepType + "')");
+  if (indepType == 'date') {
+    this.attrs_.xValueFormatter = Dygraph.dateString_;
+    this.attrs_.xValueParser = Dygraph.dateParser;
+    this.attrs_.xTicker = Dygraph.dateTicker;
+  } else if (indepType == 'number') {
+    this.attrs_.xValueFormatter = function(x) { return x; };
+    this.attrs_.xValueParser = function(x) { return parseFloat(x); };
+    this.attrs_.xTicker = Dygraph.numericTicks;
+  } else {
+    this.error("only 'date' and 'number' types are supported for column 1 " +
+               "of DataTable input (Got '" + indepType + "')");
     return null;
   }
 
   var ret = [];
   for (var i = 0; i < rows; i++) {
     var row = [];
     return null;
   }
 
   var ret = [];
   for (var i = 0; i < rows; i++) {
     var row = [];
+    if (!data.getValue(i, 0)) continue;
     if (indepType == 'date') {
       row.push(data.getValue(i, 0).getTime());
     } else {
     if (indepType == 'date') {
       row.push(data.getValue(i, 0).getTime());
     } else {
@@ -1193,28 +1501,38 @@ DateGraph.prototype.parseDataTable_ = function(data) {
  * file, do an XMLHttpRequest to get it.
  * @private
  */
  * file, do an XMLHttpRequest to get it.
  * @private
  */
-DateGraph.prototype.start_ = function() {
+Dygraph.prototype.start_ = function() {
   if (typeof this.file_ == 'function') {
   if (typeof this.file_ == 'function') {
-    // Stubbed out to allow this to run off a filesystem
+    // CSV string. Pretend we got it via XHR.
     this.loadedEvent_(this.file_());
     this.loadedEvent_(this.file_());
+  } else if (MochiKit.Base.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.rawData_ = this.parseDataTable_(this.file_);
     this.drawGraph_(this.rawData_);
   } else if (typeof this.file_ == 'object' &&
              typeof this.file_.getColumnRange == 'function') {
     // must be a DataTable from gviz.
     this.rawData_ = this.parseDataTable_(this.file_);
     this.drawGraph_(this.rawData_);
-  } else {
-    var req = new XMLHttpRequest();
-    var caller = this;
-    req.onreadystatechange = function () {
-      if (req.readyState == 4) {
-        if (req.status == 200) {
-          caller.loadedEvent_(req.responseText);
+  } 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);
+      req.open("GET", this.file_, true);
+      req.send(null);
+    }
+  } else {
+    this.error("Unknown data format: " + (typeof this.file_));
   }
 };
 
   }
 };
 
@@ -1226,16 +1544,8 @@ DateGraph.prototype.start_ = function() {
  * </ul>
  * @param {Object} attrs The new properties and values
  */
  * </ul>
  * @param {Object} attrs The new properties and values
  */
-DateGraph.prototype.updateOptions = function(attrs) {
-  if (attrs.errorBars) {
-    this.errorBars_ = attrs.errorBars;
-  }
-  if (attrs.customBars) {
-    this.customBars_ = attrs.customBars;
-  }
-  if (attrs.strokeWidth) {
-    this.strokeWidth_ = attrs.strokeWidth;
-  }
+Dygraph.prototype.updateOptions = function(attrs) {
+  // TODO(danvk): this is a mess. Rethink this function.
   if (attrs.rollPeriod) {
     this.rollPeriod_ = attrs.rollPeriod;
   }
   if (attrs.rollPeriod) {
     this.rollPeriod_ = attrs.rollPeriod;
   }
@@ -1245,12 +1555,12 @@ DateGraph.prototype.updateOptions = function(attrs) {
   if (attrs.valueRange) {
     this.valueRange_ = attrs.valueRange;
   }
   if (attrs.valueRange) {
     this.valueRange_ = attrs.valueRange;
   }
-  MochiKit.Base.update(this.attrs_, attrs);
-  if (typeof(attrs.labels) != 'undefined') {
-    this.labels_ = attrs.labels;
-    this.labelsFromCSV_ = (attrs.labels == null);
-  }
-  this.layout_.updateOptions({ 'errorBars': this.errorBars_ });
+  MochiKit.Base.update(this.user_attrs_, 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_) {
     this.file_ = attrs['file'];
     this.start_();
   if (attrs['file'] && attrs['file'] != this.file_) {
     this.file_ = attrs['file'];
     this.start_();
@@ -1264,21 +1574,24 @@ DateGraph.prototype.updateOptions = function(attrs) {
  * reflect the new averaging period.
  * @param {Number} length Number of days over which to average the data.
  */
  * reflect the new averaging period.
  * @param {Number} length Number of days over which to average the data.
  */
-DateGraph.prototype.adjustRoll = function(length) {
+Dygraph.prototype.adjustRoll = function(length) {
   this.rollPeriod_ = length;
   this.drawGraph_(this.rawData_);
 };
 
 
 /**
   this.rollPeriod_ = length;
   this.drawGraph_(this.rawData_);
 };
 
 
 /**
- * A wrapper around DateGraph that implements the gviz API.
+ * A wrapper around Dygraph that implements the gviz API.
  * @param {Object} container The DOM object the visualization should live in.
  */
  * @param {Object} container The DOM object the visualization should live in.
  */
-DateGraph.GVizChart = function(container) {
+Dygraph.GVizChart = function(container) {
   this.container = container;
 }
 
   this.container = container;
 }
 
-DateGraph.GVizChart.prototype.draw = function(data, options) {
+Dygraph.GVizChart.prototype.draw = function(data, options) {
   this.container.innerHTML = '';
   this.container.innerHTML = '';
-  this.date_graph = new DateGraph(this.container, data, null, options || {});
+  this.date_graph = new Dygraph(this.container, data, options);
 }
 }
+
+// Older pages may still use this name.
+DateGraph = Dygraph;