Add a thin wrapper that directly supports the gviz API. This resolves issue 1.
[dygraphs.git] / dygraph.js
index 6369f11..09a6684 100644 (file)
@@ -144,7 +144,8 @@ DateGraph.prototype.__init__ = function(div, file, labels, attrs) {
   this.createRollInterface_();
   this.createDragInterface_();
 
-  MochiKit.DOM.addLoadEvent(this.start_());
+  // connect(window, 'onload', this, function(e) { this.start_(); });
+  this.start_();
 };
 
 /**
@@ -257,13 +258,18 @@ DateGraph.prototype.createStatusMessage_ = function(){
  */
 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";
   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" }
+                              "left": (padding.left+1) + "px",
+                              "display": display }
                   };
   var roller = MochiKit.DOM.INPUT(textAttr);
   var pa = this.graphDiv;
@@ -290,8 +296,8 @@ DateGraph.prototype.createDragInterface_ = function() {
   var prevEndX = null;
 
   // Utility function to convert page-wide coordinates to canvas coords
-  var px = PlotKit.Base.findPosX(this.canvas_);
-  var py = PlotKit.Base.findPosY(this.canvas_);
+  var px = 0;
+  var py = 0;
   var getX = function(e) { return e.mouse().page.x - px };
   var getY = function(e) { return e.mouse().page.y - py };
 
@@ -309,6 +315,8 @@ DateGraph.prototype.createDragInterface_ = function() {
   // 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_);
     dragStartX = getX(event);
     dragStartY = getY(event);
   });
@@ -367,7 +375,9 @@ DateGraph.prototype.createDragInterface_ = function() {
     self.drawGraph_(self.rawData_);
     var minDate = self.rawData_[0][0];
     var maxDate = self.rawData_[self.rawData_.length - 1][0];
-    self.zoomCallback_(minDate, maxDate);
+    if (self.zoomCallback_) {
+      self.zoomCallback_(minDate, maxDate);
+    }
   });
 };
 
@@ -426,7 +436,9 @@ DateGraph.prototype.doZoom_ = function(lowX, highX) {
 
   this.dateWindow_ = [minDate, maxDate];
   this.drawGraph_(this.rawData_);
-  this.zoomCallback_(minDate, maxDate);
+  if (this.zoomCallback_) {
+    this.zoomCallback_(minDate, maxDate);
+  }
 };
 
 /**
@@ -521,24 +533,51 @@ DateGraph.prototype.mouseOut_ = function(event) {
 };
 
 /**
+ * Return a string version of the hours, minutes and seconds portion of a date.
+ * @param {Number} date The JavaScript date (ms since epoch)
+ * @return {String} A time of the form "HH:MM:SS"
+ * @private
+ */
+DateGraph.prototype.hmsString_ = function(date) {
+  var zeropad = function(x) {
+    if (x < 10) return "0" + x; else return "" + x;
+  };
+  var d = new Date(date);
+  if (d.getSeconds()) {
+    return zeropad(d.getHours()) + ":" +
+           zeropad(d.getMinutes()) + ":" +
+           zeropad(d.getSeconds());
+  } else if (d.getMinutes()) {
+    return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
+  } else {
+    return zeropad(d.getHours());
+  }
+}
+
+/**
  * Convert a JS date (millis since epoch) to YYYY/MM/DD
  * @param {Number} date The JavaScript date (ms since epoch)
  * @return {String} A date of the form "YYYY/MM/DD"
  * @private
  */
 DateGraph.prototype.dateString_ = function(date) {
+  var zeropad = function(x) {
+    if (x < 10) return "0" + x; else return "" + x;
+  };
   var d = new Date(date);
 
   // Get the year:
   var year = "" + d.getFullYear();
   // Get a 0 padded month string
-  var month = "" + (d.getMonth() + 1);  //months are 0-offset, sigh
-  if (month.length < 2)  month = "0" + month;
+  var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
   // Get a 0 padded day string
-  var day = "" + d.getDate();
-  if (day.length < 2)  day = "0" + day;
+  var day = zeropad(d.getDate());
 
-  return year + "/" + month + "/" + day;
+  var ret = "";
+  var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
+  if (frac) ret = " " + this.hmsString_(date);
+
+  return year + "/" + month + "/" + day + ret;
 };
 
 /**
@@ -619,10 +658,15 @@ DateGraph.prototype.dateTicker = function(startDate, endDate) {
     for (var week = startDate - 14; week < endDate + 14; week += 7) {
       scale.push(week * ONE_DAY);
     }
-  } else {                        // daily
+  } else if (dateSpan > 1) {      // daily
     for (var day = startDate - 14; day < endDate + 14; day += 1) {
       scale.push(day * ONE_DAY);
     }
+  } else {                        // hourly
+    for (var hour = Math.floor(startDate - 1) * 24;
+         hour < (endDate + 1) * 24; hour += 1) {
+      scale.push(hour * 60*60*1000);
+    }
   }
 
   var xTicks = [];
@@ -639,11 +683,18 @@ DateGraph.prototype.dateTicker = function(startDate, endDate) {
     }
   } else {
     for (var i = 0; i < scale.length; i++) {
-      var date = new Date(scale[i]);
-      var year = date.getFullYear().toString();
-      var label = this.months[date.getMonth()] + date.getDate();
-      label += "'" + year.substr(year.length - 2, 2);
-      xTicks.push( {label: label, v: date} );
+      // TODO(danvk): this is _gross_. Unify all this with dateString_.
+      var d = new Date(scale[i]);
+      var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
+      var label;
+      if (frac == 0) {
+        var year = d.getFullYear().toString();
+        var label = this.months[d.getMonth()] + d.getDate();
+        label += "'" + year.substr(year.length - 2, 2);
+      } else {
+        label = this.hmsString_(d);
+      }
+      xTicks.push( {label: label, v: d} );
     }
   }
   return xTicks;
@@ -841,12 +892,29 @@ DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
       }
     }
   } else if (this.customBars_) {
-    // just ignore the rolling for now.
-    // TODO(danvk): do something reasonable.
+    var low = 0;
+    var mid = 0;
+    var high = 0;
+    var count = 0;
     for (var i = 0; i < originalData.length; i++) {
       var data = originalData[i][1];
       var y = data[1];
       rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
+
+      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;
+      }
+      rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
+                                              1.0 * (mid - low) / count,
+                                              1.0 * (high - mid) / count ]];
     }
   } else {
     // Calculate the rolling average for the first rollPeriod - 1 points where
@@ -909,15 +977,21 @@ DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
  */
 DateGraph.prototype.dateParser = function(dateStr) {
   var dateStrSlashed;
-  if (dateStr.search("-") != -1) {
+  if (dateStr.length == 10 && dateStr.search("-") != -1) {  // e.g. '2009-07-12'
     dateStrSlashed = dateStr.replace("-", "/", "g");
-  } else if (dateStr.search("/") != -1) {
-    return Date.parse(dateStr);
-  } else {
+    while (dateStrSlashed.search("-") != -1) {
+      dateStrSlashed = dateStrSlashed.replace("-", "/");
+    }
+    return Date.parse(dateStrSlashed);
+  } else if (dateStr.length == 8) {  // e.g. '20090712'
     dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
                        + "/" + dateStr.substr(6,2);
+    return Date.parse(dateStrSlashed);
+  } else {
+    // Any format that Date.parse will accept, e.g. "2009/07/12" or
+    // "2009/07/12 12:34:56"
+    return Date.parse(dateStr);
   }
-  return Date.parse(dateStrSlashed);
 };
 
 /**
@@ -985,6 +1059,50 @@ DateGraph.prototype.parseCSV_ = function(data) {
 };
 
 /**
+ * Parses a DataTable object from gviz.
+ * The data is expected to have a first column that is either a date or a
+ * number. All subsequent columns must be numbers. If there is a clear mismatch
+ * between this.xValueParser_ and the type of the first column, it will be
+ * fixed. Returned value is in the same format as return value of parseCSV_.
+ * @param {Array.<Object>} data See above.
+ * @private
+ */
+DateGraph.prototype.parseDataTable_ = function(data) {
+  var cols = data.getNumberOfColumns();
+  var rows = data.getNumberOfRows();
+
+  // Read column labels
+  var labels = [];
+  for (var i = 0; i < cols; i++) {
+    labels.push(data.getColumnLabel(i));
+  }
+  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_);
+
+  // Assume column 1 is a date type for now.
+  if (data.getColumnType(0) != 'date') {
+    alert("only date type is support for column 1 of DataTable input.");
+    return null;
+  }
+
+  var ret = [];
+  for (var i = 0; i < rows; i++) {
+    var row = [];
+    row.push(data.getValue(i, 0).getTime());
+    for (var j = 1; j < cols; j++) {
+      row.push(data.getValue(i, j));
+    }
+    ret.push(row);
+  }
+  return ret;
+}
+
+/**
  * Get the CSV data. If it's in a function, call that function. If it's in a
  * file, do an XMLHttpRequest to get it.
  * @private
@@ -993,6 +1111,11 @@ DateGraph.prototype.start_ = function() {
   if (typeof this.file_ == 'function') {
     // Stubbed out to allow this to run off a filesystem
     this.loadedEvent_(this.file_());
+  } 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;
@@ -1061,3 +1184,17 @@ DateGraph.prototype.adjustRoll = function(length) {
   this.rollPeriod_ = length;
   this.drawGraph_(this.rawData_);
 };
+
+
+/**
+ * A wrapper around DateGraph that implements the gviz API.
+ * @param {Object} container The DOM object the visualization should live in.
+ */
+DateGraph.GVizChart = function(container) {
+  this.container = container;
+}
+
+DateGraph.GVizChart.prototype.draw = function(data, options) {
+  this.container.innerHTML = '';
+  this.date_graph = new DateGraph(this.container, data, null, options || {});
+}