- Don't use invalid values for axis extreme calculation.
[dygraphs.git] / dygraph.js
index 661ebfb..4bd775a 100644 (file)
@@ -198,6 +198,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.is_initial_draw_ = true;
   this.annotations_ = [];
 
+  // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
+  this.zoomed_x_ = false;
+  this.zoomed_y_ = false;
+
   // Clear the div. This ensure that, if multiple dygraphs are passed the same
   // div, then only one will be drawn.
   div.innerHTML = "";
@@ -260,6 +264,28 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.start_();
 };
 
+/**
+ * Returns the zoomed status of the chart for one or both axes.
+ *
+ * Axis is an optional parameter. Can be set to 'x' or 'y'.
+ *
+ * The zoomed status for an axis is set whenever a user zooms using the mouse
+ * or when the dateWindow or valueRange are updated (unless the noZoomFlagChange
+ * option is also specified).
+ */
+Dygraph.prototype.isZoomed = function(axis) {
+  if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
+  if (axis == 'x') return this.zoomed_x_;
+  if (axis == 'y') return this.zoomed_y_;
+  throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
+};
+
+Dygraph.prototype.toString = function() {
+  var maindiv = this.maindiv_;
+  var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
+  return "[Dygraph " + id + "]";
+}
+
 Dygraph.prototype.attr_ = function(name, seriesName) {
   if (seriesName &&
       typeof(this.user_attrs_[seriesName]) != 'undefined' &&
@@ -966,7 +992,8 @@ Dygraph.movePan = function(event, g, context) {
       var maxValue = axis.initialTopValue + unitsDragged;
       var minValue = maxValue - axis.dragValueRange;
       if (axis.logscale) {
-        axis.valueWindow = [ Math.pow(10, minValue), Math.pow(10, maxValue) ];
+        axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
+                             Math.pow(Dygraph.LOG_SCALE, maxValue) ];
       } else {
         axis.valueWindow = [ minValue, maxValue ];
       }
@@ -1317,6 +1344,7 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) {
  */
 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
   this.dateWindow_ = [minDate, maxDate];
+  this.zoomed_x_ = true;
   this.drawGraph_();
   if (this.attr_("zoomCallback")) {
     this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
@@ -1344,9 +1372,11 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) {
     valueRanges.push([low, hi]);
   }
 
+  this.zoomed_y_ = true;
   this.drawGraph_();
   if (this.attr_("zoomCallback")) {
     var xRange = this.xAxisRange();
+    var yRange = this.yAxisRange();
     this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
   }
 };
@@ -1374,6 +1404,8 @@ Dygraph.prototype.doUnzoom_ = function() {
   if (dirty) {
     // Putting the drawing operation before the callback because it resets
     // yAxisRange.
+    this.zoomed_x_ = false;
+    this.zoomed_y_ = false;
     this.drawGraph_();
     if (this.attr_("zoomCallback")) {
       var minDate = this.rawData_[0][0];
@@ -1410,10 +1442,6 @@ Dygraph.prototype.mouseMove_ = function(event) {
     idx = i;
   }
   if (idx >= 0) lastx = points[idx].xval;
-  // Check that you can really highlight the last day's data
-  var last = points[points.length-1];
-  if (last != null && canvasx > last.canvasx)
-    lastx = points[points.length-1].xval;
 
   // Extract the points we've selected
   this.selPoints_ = [];
@@ -1924,6 +1952,7 @@ Dygraph.dateTicker = function(startDate, endDate, self) {
 // This is a list of human-friendly values at which to show tick marks on a log
 // scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
 // ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
+// NOTE: this assumes that Dygraph.LOG_SCALE = 10.
 Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
   var vals = [];
   for (var power = -39; power <= 39; power++) {
@@ -1981,7 +2010,7 @@ Dygraph.binarySearch = function(val, arry, abs, low, high) {
     }
     return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
   }
-}
+};
 
 /**
  * Add ticks when the x axis has numbers on it (instead of dates)
@@ -2181,7 +2210,7 @@ Dygraph.prototype.extremeValues_ = function(series) {
  * number of axes, rolling averages, etc.
  */
 Dygraph.prototype.predraw_ = function() {
-  // TODO(danvk): movabilitye more computations out of drawGraph_ and into here.
+  // TODO(danvk): move more computations out of drawGraph_ and into here.
   this.computeYAxes_();
 
   // Create a new plotter.
@@ -2245,7 +2274,7 @@ Dygraph.prototype.drawGraph_ = function() {
         // On the log scale, points less than zero do not exist.
         // This will create a gap in the chart. Note that this ignores
         // connectSeparatedPoints.
-        if (point < 0) {
+        if (point <= 0) {
           point = null;
         }
         series.push([date, point]);
@@ -2332,18 +2361,22 @@ Dygraph.prototype.drawGraph_ = function() {
     this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
   }
 
-  // TODO(danvk): this method doesn't need to return anything.
-  var out = this.computeYAxisRanges_(extremes);
-  var axes = out[0];
-  var seriesToAxisMap = out[1];
-  this.layout_.updateOptions( { yAxes: axes,
-                                seriesToAxisMap: seriesToAxisMap
-                              } );
-
+  if (datasets.length > 0) {
+    // TODO(danvk): this method doesn't need to return anything.
+    var out = this.computeYAxisRanges_(extremes);
+    var axes = out[0];
+    var seriesToAxisMap = out[1];
+    this.layout_.updateOptions( { yAxes: axes,
+                                  seriesToAxisMap: seriesToAxisMap
+                                } );
+  }
   this.addXTicks_();
 
+  // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously
+  var tmp_zoomed_x = this.zoomed_x_;
   // Tell PlotKit to use this new data and render itself
   this.layout_.updateOptions({dateWindow: this.dateWindow_});
+  this.zoomed_x_ = tmp_zoomed_x;
   this.layout_.evaluateWithError();
   this.plotter_.clear();
   this.plotter_.render();
@@ -2366,6 +2399,15 @@ Dygraph.prototype.drawGraph_ = function() {
  *   indices are into the axes_ array.
  */
 Dygraph.prototype.computeYAxes_ = function() {
+  var valueWindows;
+  if (this.axes_ != undefined) {
+    // Preserve valueWindow settings.
+    valueWindows = [];
+    for (var index = 0; index < this.axes_.length; index++) {
+      valueWindows.push(this.axes_[index].valueWindow);
+    }
+  }
+
   this.axes_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
   this.seriesToAxisMap_ = {};
 
@@ -2442,6 +2484,13 @@ Dygraph.prototype.computeYAxes_ = function() {
     if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
   }
   this.seriesToAxisMap_ = seriesToAxisFiltered;
+
+  if (valueWindows != undefined) {
+    // Restore valueWindow settings.
+    for (var index = 0; index < valueWindows.length; index++) {
+      this.axes_[index].valueWindow = valueWindows[index];
+    }
+  }
 };
 
 /**
@@ -2489,12 +2538,29 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
       var series = seriesForAxis[i];
       var minY = Infinity;  // extremes[series[0]][0];
       var maxY = -Infinity;  // extremes[series[0]][1];
+      var extremeMinY, extremeMaxY;
       for (var j = 0; j < series.length; j++) {
-        minY = Math.min(extremes[series[j]][0], minY);
-        maxY = Math.max(extremes[series[j]][1], maxY);
+        // Only use valid extremes to stop null data series' from corrupting the scale.
+        extremeMinY = extremes[series[j]][0];
+        if (extremeMinY != null) {
+            minY = Math.min(extremeMinY, minY);
+        }
+        extremeMaxY = extremes[series[j]][1];
+        if (extremeMaxY != null) {
+            maxY = Math.max(extremeMaxY, maxY);
+        }
       }
       if (axis.includeZero && minY > 0) minY = 0;
 
+      // Ensure we have a valid scale, otherwise defualt to zero for safety.
+      if (minY == Infinity) {
+        minY = 0;
+      }
+
+      if (maxY == -Infinity) {
+        maxY = 0;
+      }
+
       // Add some padding and round up to an integer to be human-friendly.
       var span = maxY - minY;
       // special case: if we have no sense of scale, use +/-10% of the sole value.
@@ -2732,7 +2798,7 @@ Dygraph.dateParser = function(dateStr, self) {
  */
 Dygraph.prototype.detectTypeFromString_ = function(str) {
   var isDate = false;
-  if (str.indexOf('-') >= 0 ||
+  if (str.indexOf('-') > 0 ||
       str.indexOf('/') >= 0 ||
       isNaN(parseFloat(str))) {
     isDate = true;
@@ -3136,6 +3202,12 @@ Dygraph.prototype.start_ = function() {
  * <li>file: changes the source data for the graph</li>
  * <li>errorBars: changes whether the data contains stddev</li>
  * </ul>
+ *
+ * If the dateWindow or valueRange options are specified, the relevant zoomed_x_
+ * or zoomed_y_ flags are set, unless the noZoomFlagChange option is also
+ * secified. This allows for the chart to be programmatically zoomed without
+ * altering the zoomed flags.
+ *
  * @param {Object} attrs The new properties and values
  */
 Dygraph.prototype.updateOptions = function(attrs) {
@@ -3145,6 +3217,12 @@ Dygraph.prototype.updateOptions = function(attrs) {
   }
   if ('dateWindow' in attrs) {
     this.dateWindow_ = attrs.dateWindow;
+    if (!('noZoomFlagChange' in attrs)) {
+      this.zoomed_x_ = attrs.dateWindow != null;
+    }
+  }
+  if ('valueRange' in attrs && !('noZoomFlagChange' in attrs)) {
+    this.zoomed_y_ = attrs.valueRange != null;
   }
 
   // TODO(danvk): validate per-series options.