actually draw the graph
[dygraphs.git] / dygraph.js
index f282990..8c3bc59 100644 (file)
@@ -91,6 +91,7 @@ Dygraph.DEFAULT_ATTRS = {
   },
   labelsSeparateLines: false,
   labelsKMB: false,
+  labelsKMG2: false,
 
   strokeWidth: 1.0,
 
@@ -105,6 +106,8 @@ Dygraph.DEFAULT_ATTRS = {
   xValueParser: Dygraph.dateParser,
   xTicker: Dygraph.dateTicker,
 
+  delimiter: ',',
+
   sigma: 2.0,
   errorBars: false,
   fractions: false,
@@ -125,7 +128,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
   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 });
+    Dygraph.update(attrs, { 'labels': new_labels });
   }
   this.__init__(div, file, attrs);
 };
@@ -178,10 +181,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // 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);
+  Dygraph.update(this.user_attrs_, attrs);
 
   this.attrs_ = {};
-  MochiKit.Base.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
+  Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
 
   // Make a note of whether labels will be pulled from the CSV file.
   this.labelsFromCSV_ = (this.attr_("labels") == null);
@@ -189,30 +192,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // Create the containing DIV and other interactive elements
   this.createInterface_();
 
-  // Create the PlotKit grapher
-  // TODO(danvk): why does the Layout need its own set of options?
-  this.layoutOptions_ = { 'errorBars': (this.attr_("errorBars") ||
-                                        this.attr_("customBars")),
-                          'xOriginIsZero': false };
-  MochiKit.Base.update(this.layoutOptions_, this.attrs_);
-  MochiKit.Base.update(this.layoutOptions_, this.user_attrs_);
-
-  this.layout_ = new DygraphLayout(this, this.layoutOptions_);
-
-  // TODO(danvk): why does the Renderer need its own set of options?
-  this.renderOptions_ = { colorScheme: this.colors_,
-                          strokeColor: null,
-                          axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
-  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.createDragInterface_();
-
   this.start_();
 };
 
@@ -278,7 +257,7 @@ Dygraph.addEvent = function(el, evt, fn) {
 /**
  * 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.
+ * period. Also creates the Renderer/Layout elements.
  * @private
  */
 Dygraph.prototype.createInterface_ = function() {
@@ -291,10 +270,13 @@ Dygraph.prototype.createInterface_ = function() {
   enclosing.appendChild(this.graphDiv);
 
   // Create the canvas for interactive parts of the chart.
-  this.canvas_ = document.createElement("canvas");
+  // 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.
@@ -307,6 +289,30 @@ Dygraph.prototype.createInterface_ = function() {
   Dygraph.addEvent(this.hidden_, 'mouseout', function(e) {
     dygraph.mouseOut_(e);
   });
+
+  // Create the grapher
+  // TODO(danvk): why does the Layout need its own set of options?
+  this.layoutOptions_ = { 'xOriginIsZero': false };
+  Dygraph.update(this.layoutOptions_, this.attrs_);
+  Dygraph.update(this.layoutOptions_, this.user_attrs_);
+  Dygraph.update(this.layoutOptions_, {
+    'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
+
+  this.layout_ = new DygraphLayout(this, this.layoutOptions_);
+
+  // TODO(danvk): why does the Renderer need its own set of options?
+  this.renderOptions_ = { colorScheme: this.colors_,
+                          strokeColor: null,
+                          axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
+  Dygraph.update(this.renderOptions_, this.attrs_);
+  Dygraph.update(this.renderOptions_, this.user_attrs_);
+  this.plotter_ = new DygraphCanvasRenderer(this,
+                                            this.hidden_, this.layout_,
+                                            this.renderOptions_);
+
+  this.createStatusMessage_();
+  this.createRollInterface_();
+  this.createDragInterface_();
 }
 
 /**
@@ -317,16 +323,51 @@ Dygraph.prototype.createInterface_ = function() {
  * @private
  */
 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
-  var h = document.createElement("canvas");
+  // var h = document.createElement("canvas");
+  var h = Dygraph.createCanvas();
   h.style.position = "absolute";
   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;
 };
 
+// Taken from MochiKit.Color
+Dygraph.hsvToRGB = function (hue, saturation, value) {
+  var red;
+  var green;
+  var blue;
+  if (saturation === 0) {
+    red = value;
+    green = value;
+    blue = value;
+  } else {
+    var i = Math.floor(hue * 6);
+    var f = (hue * 6) - i;
+    var p = value * (1 - saturation);
+    var q = value * (1 - (saturation * f));
+    var t = value * (1 - (saturation * (1 - f)));
+    switch (i) {
+      case 1: red = q; green = value; blue = p; break;
+      case 2: red = p; green = value; blue = t; break;
+      case 3: red = p; green = q; blue = value; break;
+      case 4: red = t; green = p; blue = value; break;
+      case 5: red = value; green = p; blue = q; break;
+      case 6: // fall through
+      case 0: red = value; green = t; blue = p; break;
+    }
+  }
+  red = Math.floor(255 * red + 0.5);
+  green = Math.floor(255 * green + 0.5);
+  blue = Math.floor(255 * blue + 0.5);
+  return 'rgb(' + red + ',' + green + ',' + blue + ')';
+};
+
+
 /**
  * Generate a set of distinct colors for the data series. This is done with a
  * color wheel. Saturation/Value are customizable, and the hue is
@@ -345,20 +386,20 @@ Dygraph.prototype.setColors_ = function() {
     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) );
+      this.colors_.push( Dygraph.hsvToRGB(hue, sat, val) );
     }
   } else {
     for (var i = 0; i < num; i++) {
       var colorStr = colors[i % colors.length];
-      this.colors_.push( MochiKit.Color.Color.fromString(colorStr) );
+      this.colors_.push(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_);
+  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
@@ -408,10 +449,12 @@ Dygraph.prototype.createStatusMessage_ = function(){
       "background": "white",
       "textAlign": "left",
       "overflow": "hidden"};
-    MochiKit.Base.update(messagestyle, this.attr_('labelsDivStyles'));
+    Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
     var div = document.createElement("div");
     for (var name in messagestyle) {
-      div.style[name] = messagestyle[name];
+      if (messagestyle.hasOwnProperty(name)) {
+        div.style[name] = messagestyle[name];
+      }
     }
     this.graphDiv.appendChild(div);
     this.attrs_.labelsDiv = div;
@@ -436,7 +479,9 @@ Dygraph.prototype.createRollInterface_ = function() {
   roller.size = "2";
   roller.value = this.rollPeriod_;
   for (var name in textAttr) {
-    roller.style[name] = textAttr[name];
+    if (textAttr.hasOwnProperty(name)) {
+      roller.style[name] = textAttr[name];
+    }
   }
 
   var pa = this.graphDiv;
@@ -473,7 +518,7 @@ Dygraph.pageY = function(e) {
 
 /**
  * Set up all the mouse handlers needed to capture dragging behavior for zoom
- * events. Uses MochiKit.Signal to attach all the event handlers.
+ * events.
  * @private
  */
 Dygraph.prototype.createDragInterface_ = function() {
@@ -544,8 +589,8 @@ Dygraph.prototype.createDragInterface_ = function() {
       if (regionWidth < 2 && regionHeight < 2 &&
           self.attr_('clickCallback') != null &&
           self.lastx_ != undefined) {
-        // TODO(danvk): pass along more info about the point.
-        self.attr_('clickCallback')(event, new Date(self.lastx_));
+        // TODO(danvk): pass along more info about the points.
+        self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
       }
 
       if (regionWidth >= 10) {
@@ -564,6 +609,7 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   // Double-clicking zooms back out
   Dygraph.addEvent(this.hidden_, 'dblclick', function(event) {
+    if (self.dateWindow_ == null) return;
     self.dateWindow_ = null;
     self.drawGraph_(self.rawData_);
     var minDate = self.rawData_[0][0];
@@ -664,13 +710,17 @@ Dygraph.prototype.mouseMove_ = function(event) {
     lastx = points[points.length-1].xval;
 
   // Extract the points we've selected
-  var selPoints = [];
+  this.selPoints_ = [];
   for (var i = 0; i < points.length; i++) {
     if (points[i].xval == lastx) {
-      selPoints.push(points[i]);
+      this.selPoints_.push(points[i]);
     }
   }
 
+  if (this.attr_("highlightCallback")) {
+    this.attr_("highlightCallback")(event, lastx, this.selPoints_);
+  }
+
   // Clear the previously drawn vertical, if there is one
   var circleSize = this.attr_('highlightCircleSize');
   var ctx = this.canvas_.getContext("2d");
@@ -681,19 +731,20 @@ Dygraph.prototype.mouseMove_ = function(event) {
 
   var isOK = function(x) { return x && !isNaN(x); };
 
-  if (selPoints.length > 0) {
-    var canvasx = selPoints[0].canvasx;
+  if (this.selPoints_.length > 0) {
+    var canvasx = this.selPoints_[0].canvasx;
 
     // Set the status message to indicate the selected point(s)
     var replace = this.attr_('xValueFormatter')(lastx, this) + ":";
     var clen = this.colors_.length;
-    for (var i = 0; i < selPoints.length; i++) {
-      if (!isOK(selPoints[i].canvasy)) continue;
+    for (var i = 0; i < this.selPoints_.length; i++) {
+      if (!isOK(this.selPoints_[i].canvasy)) continue;
       if (this.attr_("labelsSeparateLines")) {
         replace += "<br/>";
       }
-      var point = selPoints[i];
-      replace += " <b><font color='" + this.colors_[i%clen].toHexString() + "'>"
+      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);
     }
@@ -704,11 +755,12 @@ Dygraph.prototype.mouseMove_ = function(event) {
 
     // 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;
+    for (var i = 0; i < this.selPoints_.length; i++) {
+      if (!isOK(this.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.fillStyle = this.colors_[i%clen];
+      ctx.arc(canvasx, this.selPoints_[i%clen].canvasy, circleSize,
+              0, 2 * Math.PI, false);
       ctx.fill();
     }
     ctx.restore();
@@ -849,7 +901,7 @@ 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.SIX_HOURLY]      = 1000 * 3600 * 6;
 Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
 
@@ -998,17 +1050,30 @@ Dygraph.numericTicks = function(minV, maxV, self) {
 
   // Construct labels for the ticks
   var ticks = [];
+  var k;
+  var k_labels = [];
+  if (self.attr_("labelsKMB")) {
+    k = 1000;
+    k_labels = [ "K", "M", "B", "T" ];
+  }
+  if (self.attr_("labelsKMG2")) {
+    if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+    k = 1024;
+    k_labels = [ "k", "M", "G", "T" ];
+  }
+
   for (var i = 0; i < nTicks; i++) {
     var tickV = low_val + i * scale;
+    var absTickV = Math.abs(tickV);
     var label = self.round_(tickV, 2);
-    if (self.attr_("labelsKMB")) {
-      var k = 1000;
-      if (tickV >= k*k*k) {
-        label = self.round_(tickV/(k*k*k), 1) + "B";
-      } else if (tickV >= k*k) {
-        label = self.round_(tickV/(k*k), 1) + "M";
-      } else if (tickV >= k) {
-        label = self.round_(tickV/k, 1) + "K";
+    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];
+          break;
+        }
       }
     }
     ticks.push( {label: label, v: tickV} );
@@ -1221,16 +1286,20 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
       var y = data[1];
       rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
 
-      low += data[0];
-      mid += y;
-      high += data[2];
-      count += 1;
+      if (y != null && !isNaN(y)) {
+        low += data[0];
+        mid += y;
+        high += data[2];
+        count += 1;
+      }
       if (i - rollPeriod >= 0) {
         var prev = originalData[i - rollPeriod];
-        low -= prev[1][0];
-        mid -= prev[1][1];
-        high -= prev[1][2];
-        count -= 1;
+        if (prev[1][1] != null && !isNaN(prev[1][1])) {
+          low -= prev[1][0];
+          mid -= prev[1][1];
+          high -= prev[1][2];
+          count -= 1;
+        }
       }
       rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
                                               1.0 * (mid - low) / count,
@@ -1250,7 +1319,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
         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;
+          if (y == null || isNaN(y)) continue;
           num_ok++;
           sum += originalData[j][1];
         }
@@ -1268,7 +1337,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
         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;
+          if (y == null || isNaN(y)) continue;
           num_ok++;
           sum += originalData[j][1][0];
           variance += Math.pow(originalData[j][1][1], 2);
@@ -1369,10 +1438,17 @@ Dygraph.prototype.detectTypeFromString_ = function(str) {
 Dygraph.prototype.parseCSV_ = function(data) {
   var ret = [];
   var lines = data.split("\n");
+
+  // Use the default delimiter or fall back to a tab if that makes sense.
+  var delim = this.attr_('delimiter');
+  if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
+    delim = '\t';
+  }
+
   var start = 0;
   if (this.labelsFromCSV_) {
     start = 1;
-    this.attrs_.labels = lines[0].split(",");
+    this.attrs_.labels = lines[0].split(delim);
   }
 
   var xParser;
@@ -1381,7 +1457,8 @@ Dygraph.prototype.parseCSV_ = function(data) {
   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 (line[0] == '#') continue;    // skip comment lines
+    var inFields = line.split(delim);
     if (inFields.length < 2) continue;
 
     var fields = [];
@@ -1456,13 +1533,13 @@ Dygraph.prototype.parseArray_ = function(data) {
     }
   }
 
-  if (MochiKit.Base.isDateLike(data[0][0])) {
+  if (Dygraph.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);
+    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");
@@ -1501,8 +1578,10 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   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') {
@@ -1528,14 +1607,68 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     } else {
       row.push(data.getValue(i, 0));
     }
-    for (var j = 1; j < cols; j++) {
-      row.push(data.getValue(i, j));
+    if (!this.attr_("errorBars")) {
+      for (var j = 1; j < cols; j++) {
+        row.push(data.getValue(i, j));
+      }
+    } else {
+      for (var j = 0; j < cols - 1; j++) {
+        row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
+      }
     }
     ret.push(row);
   }
   return ret;
 }
 
+// These functions are all based on MochiKit.
+Dygraph.update = function (self, o) {
+  if (typeof(o) != 'undefined' && o !== null) {
+    for (var k in o) {
+      if (o.hasOwnProperty(k)) {
+        self[k] = o[k];
+      }
+    }
+  }
+  return self;
+};
+
+Dygraph.isArrayLike = function (o) {
+  var typ = typeof(o);
+  if (
+      (typ != 'object' && !(typ == 'function' && 
+        typeof(o.item) == 'function')) ||
+      o === null ||
+      typeof(o.length) != 'number' ||
+      o.nodeType === 3
+     ) {
+    return false;
+  }
+  return true;
+};
+
+Dygraph.isDateLike = function (o) {
+  if (typeof(o) != "object" || o === null ||
+      typeof(o.getTime) != 'function') {
+    return false;
+  }
+  return true;
+};
+
+Dygraph.clone = function(o) {
+  // TODO(danvk): figure out how MochiKit's version works
+  var r = [];
+  for (var i = 0; i < o.length; i++) {
+    if (Dygraph.isArrayLike(o[i])) {
+      r.push(Dygraph.clone(o[i]));
+    } else {
+      r.push(o[i]);
+    }
+  }
+  return r;
+};
+
+
 /**
  * Get the CSV data. If it's in a function, call that function. If it's in a
  * file, do an XMLHttpRequest to get it.
@@ -1545,7 +1678,7 @@ Dygraph.prototype.start_ = function() {
   if (typeof this.file_ == 'function') {
     // CSV string. Pretend we got it via XHR.
     this.loadedEvent_(this.file_());
-  } else if (MochiKit.Base.isArrayLike(this.file_)) {
+  } else if (Dygraph.isArrayLike(this.file_)) {
     this.rawData_ = this.parseArray_(this.file_);
     this.drawGraph_(this.rawData_);
   } else if (typeof this.file_ == 'object' &&
@@ -1595,7 +1728,7 @@ Dygraph.prototype.updateOptions = function(attrs) {
   if (attrs.valueRange) {
     this.valueRange_ = attrs.valueRange;
   }
-  MochiKit.Base.update(this.user_attrs_, attrs);
+  Dygraph.update(this.user_attrs_, attrs);
 
   this.labelsFromCSV_ = (this.attr_("labels") == null);
 
@@ -1610,6 +1743,39 @@ Dygraph.prototype.updateOptions = function(attrs) {
 };
 
 /**
+ * Resizes the dygraph. If no parameters are specified, resizes to fill the
+ * containing div (which has presumably changed size since the dygraph was
+ * instantiated. If the width/height are specified, the div will be resized.
+ *
+ * This is far more efficient than destroying and re-instantiating a
+ * Dygraph, since it doesn't have to reparse the underlying data.
+ *
+ * @param {Number} width Width (in pixels)
+ * @param {Number} height Height (in pixels)
+ */
+Dygraph.prototype.resize = function(width, height) {
+  if ((width === null) != (height === null)) {
+    this.warn("Dygraph.resize() should be called with zero parameters or " +
+              "two non-NULL parameters. Pretending it was zero.");
+    width = height = null;
+  }
+
+  this.maindiv_.innerHTML = "";
+  if (width) {
+    this.maindiv_.style.width = width + "px";
+    this.maindiv_.style.height = height + "px";
+    this.width_ = width;
+    this.height_ = height;
+  } else {
+    this.width_ = this.maindiv_.offsetWidth;
+    this.height_ = this.maindiv_.offsetHeight;
+  }
+
+  this.createInterface_();
+  this.drawGraph_(this.rawData_);
+};
+
+/**
  * Adjusts the number of days in the rolling average. Updates the graph to
  * reflect the new averaging period.
  * @param {Number} length Number of days over which to average the data.
@@ -1619,6 +1785,21 @@ Dygraph.prototype.adjustRoll = function(length) {
   this.drawGraph_(this.rawData_);
 };
 
+/**
+ * Create a new canvas element. This is more complex than a simple
+ * document.createElement("canvas") because of IE and excanvas.
+ */
+Dygraph.createCanvas = function() {
+  var canvas = document.createElement("canvas");
+
+  isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if (isIE) {
+    canvas = G_vmlCanvasManager.initElement(canvas);
+  }
+
+  return canvas;
+};
+
 
 /**
  * A wrapper around Dygraph that implements the gviz API.