fix some range calculation bugs with error bars, gaps with error bars
[dygraphs.git] / dygraph.js
index ea1f904..ef06082 100644 (file)
@@ -155,6 +155,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.wilsonInterval_ = attrs.wilsonInterval || true;
   this.customBars_ = attrs.customBars || false;
 
+  // Clear the div. This ensure that, if multiple dygraphs are passed the same
+  // div, then only one will be drawn.
+  div.innerHTML = "";
+
   // If the div isn't already sized then give it a default size.
   if (div.style.width == '') {
     div.style.width = Dygraph.DEFAULT_WIDTH + "px";
@@ -607,6 +611,8 @@ Dygraph.prototype.mouseMove_ = function(event) {
     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;
 
@@ -614,6 +620,7 @@ Dygraph.prototype.mouseMove_ = function(event) {
     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;
       if (this.attr_("labelsSeparateLines")) {
         replace += "<br/>";
       }
@@ -630,6 +637,7 @@ 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;
       ctx.beginPath();
       ctx.fillStyle = this.colors_[i%clen].toRGBString();
       ctx.arc(canvasx, selPoints[i%clen].canvasy, circleSize, 0, 360, false);
@@ -954,6 +962,46 @@ Dygraph.prototype.addYTicks_ = function(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.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
@@ -963,7 +1011,7 @@ Dygraph.prototype.addYTicks_ = function(minY, maxY) {
  * @private
  */
 Dygraph.prototype.drawGraph_ = function(data) {
-  var maxY = null;
+  var minY = null, maxY = null;
   this.layout_.removeAllDatasets();
   this.setColors_();
 
@@ -985,32 +1033,15 @@ Dygraph.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]);
-          var y = bars ? series[k][1][0] : series[k][1];
-          if (maxY == null || y > maxY) maxY = y;
         }
       }
       series = pruned;
-    } else {
-      if (!this.customBars_) {
-        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;
-          }
-        }
-      } else {
-        // 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];
-          var high = series[j][1][2];
-          if (high > y) y = high;
-          if (maxY == null || y > maxY) {
-            maxY = y;
-          }
-        }
-      }
     }
 
+    [thisMinY, thisMaxY] = this.extremeValues_(series);
+    if (!minY || thisMinY < minY) minY = thisMinY;
+    if (!maxY || thisMaxY > maxY) maxY = thisMaxY;
+
     if (bars) {
       var vals = [];
       for (var j=0; j<series.length; j++)
@@ -1028,9 +1059,20 @@ Dygraph.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.
-    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_();
@@ -1128,6 +1170,10 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
     // 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.attr_("errorBars")){
+      if (rollPeriod == 1) {
+        return originalData;
+      }
+
       for (var i = 0; i < num_init_points; i++) {
         var sum = 0;
         for (var j = 0; j < i + 1; j++)
@@ -1147,13 +1193,21 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
       for (var i = 0; i < num_init_points; i++) {
         var sum = 0;
         var variance = 0;
+        var num_ok = 0;
         for (var j = 0; j < i + 1; j++) {
-          sum += originalData[j][1][0];
+          var y = originalData[j][1][0];
+          if (!y || isNaN(y)) continue;
+          num_ok++;
+          sum += y;
           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]];
+        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]];
+        }
       }
       // Calculate the rolling average for the remaining points
       for (var i = Math.min(rollPeriod - 1, originalData.length - 2);
@@ -1161,13 +1215,21 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
           i++) {
         var sum = 0;
         var variance = 0;
+        var num_ok = 0;
         for (var j = 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);
         }
-        var stddev = Math.sqrt(variance) / rollPeriod;
-        rollingData[i] = [originalData[i][0],
-                          [sum / rollPeriod, sigma * stddev, sigma * stddev]];
+        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]];
+        }
       }
     }
   }