Allow width/height to be specified as a % and as an attribute. Handle datetime proper...
[dygraphs.git] / dygraph.js
index 473e55d..1378c13 100644 (file)
@@ -108,11 +108,13 @@ Dygraph.DEFAULT_ATTRS = {
 
   delimiter: ',',
 
+  logScale: false,
   sigma: 2.0,
   errorBars: false,
   fractions: false,
   wilsonInterval: true,  // only relevant if fractions is true
-  customBars: false
+  customBars: false,
+  fillGraph: false
 };
 
 // Various logging levels.
@@ -161,20 +163,30 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // div, then only one will be drawn.
   div.innerHTML = "";
 
-  // If the div isn't already sized then give it a default size.
+  // If the div isn't already sized then inherit from our attrs or
+  // give it a default size.
   if (div.style.width == '') {
-    div.style.width = Dygraph.DEFAULT_WIDTH + "px";
+    div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
   }
   if (div.style.height == '') {
-    div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
+    div.style.height = attrs.height || Dygraph.DEFAULT_HEIGHT + "px";
   }
   this.width_ = parseInt(div.style.width, 10);
   this.height_ = parseInt(div.style.height, 10);
+  // The div might have been specified as percent of the current window size,
+  // convert that to an appropriate number of pixels.
+  if (div.style.width.indexOf("%") == div.style.width.length - 1) {
+    // Minus ten pixels  keeps scrollbars from showing up for a 100% width div.
+    this.width_ = (this.width_ * self.innerWidth / 100) - 10;
+  }
+  if (div.style.height.indexOf("%") == div.style.height.length - 1) {
+    this.height_ = (this.height_ * self.innerHeight / 100) - 10;
+  }
 
   // Dygraphs has many options, some of which interact with one another.
   // To keep track of everything, we maintain two sets of options:
   //
-  //  this.user_attrs_   only options explicitly set by the user. 
+  //  this.user_attrs_   only options explicitly set by the user.
   //  this.attrs_        defaults, options derived from user_attrs_, data.
   //
   // Options are then accessed this.attr_('attr'), which first looks at
@@ -395,7 +407,7 @@ Dygraph.prototype.setColors_ = function() {
     }
   }
 
-  // TODO(danvk): update this w/r/t/ the new options system. 
+  // TODO(danvk): update this w/r/t/ the new options system.
   this.renderOptions_.colorScheme = this.colors_;
   Dygraph.update(this.plotter_.options, this.renderOptions_);
   Dygraph.update(this.layoutOptions_, this.user_attrs_);
@@ -416,7 +428,7 @@ Dygraph.findPosX = function(obj) {
     curleft += obj.x;
   return curleft;
 };
-                   
+
 Dygraph.findPosY = function(obj) {
   var curtop = 0;
   if (obj.offsetParent) {
@@ -526,6 +538,7 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   // Tracks whether the mouse is down right now
   var isZooming = false;
+  var isPanning = false;
   var dragStartX = null;
   var dragStartY = null;
   var dragEndX = null;
@@ -569,7 +582,7 @@ Dygraph.prototype.createDragInterface_ = function() {
     dragStartX = getX(event);
     dragStartY = getY(event);
 
-    if (event.altKey) {
+    if (event.altKey || event.shiftKey) {
       if (!self.dateWindow_) return;  // have to be zoomed in to pan.
       isPanning = true;
       dateRange = self.dateWindow_[1] - self.dateWindow_[0];
@@ -911,30 +924,40 @@ Dygraph.prototype.addXTicks_ = function() {
 
 // Time granularity enumeration
 Dygraph.SECONDLY = 0;
-Dygraph.TEN_SECONDLY = 1;
-Dygraph.THIRTY_SECONDLY  = 2;
-Dygraph.MINUTELY = 3;
-Dygraph.TEN_MINUTELY = 4;
-Dygraph.THIRTY_MINUTELY = 5;
-Dygraph.HOURLY = 6;
-Dygraph.SIX_HOURLY = 7;
-Dygraph.DAILY = 8;
-Dygraph.WEEKLY = 9;
-Dygraph.MONTHLY = 10;
-Dygraph.QUARTERLY = 11;
-Dygraph.BIANNUAL = 12;
-Dygraph.ANNUAL = 13;
-Dygraph.DECADAL = 14;
-Dygraph.NUM_GRANULARITIES = 15;
+Dygraph.TWO_SECONDLY = 1;
+Dygraph.FIVE_SECONDLY = 2;
+Dygraph.TEN_SECONDLY = 3;
+Dygraph.THIRTY_SECONDLY  = 4;
+Dygraph.MINUTELY = 5;
+Dygraph.TWO_MINUTELY = 6;
+Dygraph.FIVE_MINUTELY = 7;
+Dygraph.TEN_MINUTELY = 8;
+Dygraph.THIRTY_MINUTELY = 9;
+Dygraph.HOURLY = 10;
+Dygraph.TWO_HOURLY = 11;
+Dygraph.SIX_HOURLY = 12;
+Dygraph.DAILY = 13;
+Dygraph.WEEKLY = 14;
+Dygraph.MONTHLY = 15;
+Dygraph.QUARTERLY = 16;
+Dygraph.BIANNUAL = 17;
+Dygraph.ANNUAL = 18;
+Dygraph.DECADAL = 19;
+Dygraph.NUM_GRANULARITIES = 20;
 
 Dygraph.SHORT_SPACINGS = [];
 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]    = 1000 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]   = 1000 * 5;
 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]    = 1000 * 10;
 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]        = 1000 * 60;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]    = 1000 * 60 * 2;
+Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]   = 1000 * 60 * 5;
 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]    = 1000 * 60 * 10;
 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]          = 1000 * 3600;
+Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]      = 1000 * 3600 * 2;
 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]      = 1000 * 3600 * 6;
 Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
@@ -976,10 +999,36 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
     // Generate one tick mark for every fixed interval of time.
     var spacing = Dygraph.SHORT_SPACINGS[granularity];
     var format = '%d%b';  // e.g. "1Jan"
-    // TODO(danvk): be smarter about making sure this really hits a "nice" time.
-    if (granularity < Dygraph.HOURLY) {
-      start_time = spacing * Math.floor(0.5 + start_time / spacing);
+
+    // Find a time less than start_time which occurs on a "nice" time boundary
+    // for this granularity.
+    var g = spacing / 1000;
+    var d = new Date(start_time);
+    if (g <= 60) {  // seconds
+      var x = d.getSeconds(); d.setSeconds(x - x % g);
+    } else {
+      d.setSeconds(0);
+      g /= 60;
+      if (g <= 60) {  // minutes
+        var x = d.getMinutes(); d.setMinutes(x - x % g);
+      } else {
+        d.setMinutes(0);
+        g /= 60;
+
+        if (g <= 24) {  // days
+          var x = d.getHours(); d.setHours(x - x % g);
+        } else {
+          d.setHours(0);
+          g /= 24;
+
+          if (g == 7) {  // one week
+            d.setDate(d.getDate() - d.getDay());
+          }
+        }
+      }
     }
+    start_time = d.getTime();
+
     for (var t = start_time; t <= end_time; t += spacing) {
       var d = new Date(t);
       var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
@@ -1064,12 +1113,21 @@ Dygraph.numericTicks = function(minV, maxV, self) {
   // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
   // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
   // The first spacing greater than pixelsPerYLabel is what we use.
-  var mults = [1, 2, 5];
+  // TODO(danvk): version that works on a log scale.
+  if (self.attr_("labelsKMG2")) {
+    var mults = [1, 2, 4, 8];
+  } else {
+    var mults = [1, 2, 5];
+  }
   var scale, low_val, high_val, nTicks;
   // TODO(danvk): make it possible to set this for x- and y-axes independently.
   var pixelsPerTick = self.attr_('pixelsPerYLabel');
   for (var i = -10; i < 50; i++) {
-    var base_scale = Math.pow(10, i);
+    if (self.attr_("labelsKMG2")) {
+      var base_scale = Math.pow(16, i);
+    } else {
+      var base_scale = Math.pow(10, i);
+    }
     for (var j = 0; j < mults.length; j++) {
       scale = base_scale * mults[j];
       low_val = Math.floor(minV / scale) * scale;
@@ -1185,6 +1243,8 @@ Dygraph.prototype.drawGraph_ = function(data) {
 
   // Loop over all fields in the dataset
   for (var i = 1; i < data[0].length; i++) {
+    if (!this.visibility()[i - 1]) continue;
+
     var series = [];
     for (var j = 0; j < data.length; j++) {
       var date = data[j][0];
@@ -1619,7 +1679,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   cols = labels.length;
 
   var indepType = data.getColumnType(0);
-  if (indepType == 'date') {
+  if (indepType == 'date' || 'datetime') {
     this.attrs_.xValueFormatter = Dygraph.dateString_;
     this.attrs_.xValueParser = Dygraph.dateParser;
     this.attrs_.xTicker = Dygraph.dateTicker;
@@ -1628,7 +1688,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     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 " +
+    this.error("only 'date', 'datetime' and 'number' types are supported for column 1 " +
                "of DataTable input (Got '" + indepType + "')");
     return null;
   }
@@ -1636,8 +1696,14 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   var ret = [];
   for (var i = 0; i < rows; i++) {
     var row = [];
-    if (!data.getValue(i, 0)) continue;
-    if (indepType == 'date') {
+    if (typeof(data.getValue(i, 0)) === 'undefined' ||
+        data.getValue(i, 0) === null) {
+      this.warning("Ignoring row " + i +
+                   " of DataTable because of undefined or null first column.");
+      continue;
+    }
+
+    if (indepType == 'date' || indepType == 'datetime') {
       row.push(data.getValue(i, 0).getTime());
     } else {
       row.push(data.getValue(i, 0));
@@ -1671,7 +1737,7 @@ Dygraph.update = function (self, o) {
 Dygraph.isArrayLike = function (o) {
   var typ = typeof(o);
   if (
-      (typ != 'object' && !(typ == 'function' && 
+      (typ != 'object' && !(typ == 'function' &&
         typeof(o.item) == 'function')) ||
       o === null ||
       typeof(o.length) != 'number' ||
@@ -1824,6 +1890,34 @@ Dygraph.prototype.adjustRoll = function(length) {
 };
 
 /**
+ * Returns a boolean array of visibility statuses.
+ */
+Dygraph.prototype.visibility = function() {
+  // Do lazy-initialization, so that this happens after we know the number of
+  // data series.
+  if (!this.attr_("visibility")) {
+    this.attrs_["visibility"] = [];
+  }
+  while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
+    this.attr_("visibility").push(true);
+  }
+  return this.attr_("visibility");
+};
+
+/**
+ * Changes the visiblity of a series.
+ */
+Dygraph.prototype.setVisibility = function(num, value) {
+  var x = this.visibility();
+  if (num < 0 && num >= x.length) {
+    this.warn("invalid series number in setVisibility: " + num);
+  } else {
+    x[num] = value;
+    this.drawGraph_(this.rawData_);
+  }
+};
+
+/**
  * Create a new canvas element. This is more complex than a simple
  * document.createElement("canvas") because of IE and excanvas.
  */