Merge pull request #560 from sslepian/set-visibility-multiple
[dygraphs.git] / dygraph.js
index 49ecc3e..f82285b 100644 (file)
@@ -47,7 +47,6 @@
 if (typeof(DEBUG) === 'undefined') DEBUG=true;
 
 var Dygraph = (function() {
-/*jshint globalstrict: true */
 /*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */
 "use strict";
 
@@ -84,7 +83,7 @@ var Dygraph = function(div, data, opts, opt_fourth_param) {
 };
 
 Dygraph.NAME = "Dygraph";
-Dygraph.VERSION = "1.0.1";
+Dygraph.VERSION = "1.1.0";
 Dygraph.__repr__ = function() {
   return "[" + Dygraph.NAME + " " + Dygraph.VERSION + "]";
 };
@@ -118,10 +117,8 @@ Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ];
  * and maxNumberWidth options.
  * @param {number} x The number to be formatted
  * @param {Dygraph} opts An options view
- * @param {string} name The name of the point's data series
- * @param {Dygraph} g The dygraph object
  */
-Dygraph.numberValueFormatter = function(x, opts, pt, g) {
+Dygraph.numberValueFormatter = function(x, opts) {
   var sigFigs = opts('sigFigs');
 
   if (sigFigs !== null) {
@@ -192,8 +189,8 @@ Dygraph.numberValueFormatter = function(x, opts, pt, g) {
  * variant for use as an axisLabelFormatter.
  * @private
  */
-Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) {
-  return Dygraph.numberValueFormatter(x, opts, g);
+Dygraph.numberAxisLabelFormatter = function(x, granularity, opts) {
+  return Dygraph.numberValueFormatter.call(this, x, opts);
 };
 
 /**
@@ -229,12 +226,12 @@ Dygraph.dateAxisLabelFormatter = function(date, granularity, opts) {
   if (granularity >= Dygraph.DECADAL) {
     return '' + year;
   } else if (granularity >= Dygraph.MONTHLY) {
-    return Dygraph.SHORT_MONTH_NAMES_[month] + ' ' + year;
+    return Dygraph.SHORT_MONTH_NAMES_[month] + ' ' + year;
   } else {
     var frac = hours * 3600 + mins * 60 + secs + 1e-3 * millis;
     if (frac === 0 || granularity >= Dygraph.DAILY) {
-      // e.g. '21Jan' (%d%b)
-      return Dygraph.zeropad(day) + Dygraph.SHORT_MONTH_NAMES_[month];
+      // e.g. '21 Jan' (%d%b)
+      return Dygraph.zeropad(day) + ' ' + Dygraph.SHORT_MONTH_NAMES_[month];
     } else {
       return Dygraph.hmsString_(hours, mins, secs);
     }
@@ -293,8 +290,6 @@ Dygraph.DEFAULT_ATTRS = {
 
   axisTickSize: 3,
   axisLabelFontSize: 14,
-  xAxisLabelWidth: 50,
-  yAxisLabelWidth: 50,
   rightGap: 5,
 
   showRoller: false,
@@ -315,9 +310,7 @@ Dygraph.DEFAULT_ATTRS = {
   stackedGraphNaNFill: 'all',
   hideOverlayOnMouseOut: true,
 
-  // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
-  legend: 'onmouseover',  // the only relevant value at the moment is 'always'.
-
+  legend: 'onmouseover',
   stepPlot: false,
   avoidMinZero: false,
   xRangePad: 0,
@@ -329,15 +322,11 @@ Dygraph.DEFAULT_ATTRS = {
   xLabelHeight: 18,
   yLabelWidth: 18,
 
-  drawXAxis: true,
-  drawYAxis: true,
   axisLineColor: "black",
   axisLineWidth: 0.3,
   gridLineWidth: 0.3,
   axisLabelColor: "black",
   axisLabelWidth: 50,
-  drawYGrid: true,
-  drawXGrid: true,
   gridLineColor: "rgb(128,128,128)",
 
   interactionModel: null,  // will be set to Dygraph.Interaction.defaultModel
@@ -363,7 +352,8 @@ Dygraph.DEFAULT_ATTRS = {
   // per-axis options
   axes: {
     x: {
-      pixelsPerLabel: 60,
+      pixelsPerLabel: 70,
+      axisLabelWidth: 60,
       axisLabelFormatter: Dygraph.dateAxisLabelFormatter,
       valueFormatter: Dygraph.dateValueFormatter,
       drawGrid: true,
@@ -372,6 +362,7 @@ Dygraph.DEFAULT_ATTRS = {
       ticker: null  // will be set in dygraph-tickers.js
     },
     y: {
+      axisLabelWidth: 50,
       pixelsPerLabel: 30,
       valueFormatter: Dygraph.numberValueFormatter,
       axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
@@ -381,10 +372,11 @@ Dygraph.DEFAULT_ATTRS = {
       ticker: null  // will be set in dygraph-tickers.js
     },
     y2: {
+      axisLabelWidth: 50,
       pixelsPerLabel: 30,
       valueFormatter: Dygraph.numberValueFormatter,
       axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
-      drawAxis: false,
+      drawAxis: true,  // only applies when there are two axes of data.
       drawGrid: false,
       independentTicks: false,
       ticker: null  // will be set in dygraph-tickers.js
@@ -427,21 +419,10 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
  * @private
  */
 Dygraph.prototype.__init__ = function(div, file, attrs) {
-  // Hack for IE: if we're using excanvas and the document hasn't finished
-  // loading yet (and hence may not have initialized whatever it needs to
-  // initialize), then keep calling this routine periodically until it has.
-  if (/MSIE/.test(navigator.userAgent) && !window.opera &&
-      typeof(G_vmlCanvasManager) != 'undefined' &&
-      document.readyState != 'complete') {
-    var self = this;
-    setTimeout(function() { self.__init__(div, file, attrs); }, 100);
-    return;
-  }
-
   // Support two-argument constructor
   if (attrs === null || attrs === undefined) { attrs = {}; }
 
-  attrs = Dygraph.mapLegacyOptions_(attrs);
+  attrs = Dygraph.copyUserAttrs_(attrs);
 
   if (typeof(div) == 'string') {
     div = document.getElementById(div);
@@ -452,8 +433,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
     return;
   }
 
-  this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
-
   // Copy the important bits into the object
   // TODO(danvk): most of these should just stay in the attrs_ dictionary.
   this.maindiv_ = div;
@@ -555,6 +534,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
 
     var handlers = pluginInstance.activate(this);
     for (var eventName in handlers) {
+      if (!handlers.hasOwnProperty(eventName)) continue;
       // TODO(danvk): validate eventName.
       pluginDict.events[eventName] = handlers[eventName];
     }
@@ -1079,7 +1059,7 @@ Dygraph.prototype.toPercentXCoord = function(x) {
   var xRange = this.xAxisRange();
   var pct;
   var logscale = this.attributes_.getForAxis("logscale", 'x') ;
-  if (logscale == true) { // logscale can be null so we test for true explicitly.
+  if (logscale === true) {  // logscale can be null so we test for true explicitly.
     var logr0 = Dygraph.log10(xRange[0]);
     var logr1 = Dygraph.log10(xRange[1]);
     pct = (Dygraph.log10(x) - logr0) / (logr1 - logr0);
@@ -1297,18 +1277,7 @@ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
  * @private
  */
 Dygraph.prototype.createMouseEventElement_ = function() {
-  if (this.isUsingExcanvas_) {
-    var elem = document.createElement("div");
-    elem.style.position = 'absolute';
-    elem.style.backgroundColor = 'white';
-    elem.style.filter = 'alpha(opacity=0)';
-    elem.style.width = this.width_ + "px";
-    elem.style.height = this.height_ + "px";
-    this.graphDiv.appendChild(elem);
-    return elem;
-  } else {
-    return this.canvas_;
-  }
+  return this.canvas_;
 };
 
 /**
@@ -1489,6 +1458,26 @@ Dygraph.prototype.createDragInterface_ = function() {
       contextB.dragStartY = Dygraph.dragGetY_(event, contextB);
       contextB.cancelNextDblclick = false;
       contextB.tarp.cover();
+    },
+    destroy: function() {
+      var context = this;
+      if (context.isZooming || context.isPanning) {
+        context.isZooming = false;
+        context.dragStartX = null;
+        context.dragStartY = null;
+      }
+
+      if (context.isPanning) {
+        context.isPanning = false;
+        context.draggingDate = null;
+        context.dateRange = null;
+        for (var i = 0; i < self.axes_.length; i++) {
+          delete self.axes_[i].draggingValue;
+          delete self.axes_[i].dragValueRange;
+        }
+      }
+
+      context.tarp.uncover();
     }
   };
 
@@ -1512,27 +1501,13 @@ Dygraph.prototype.createDragInterface_ = function() {
 
   // If the user releases the mouse button during a drag, but not over the
   // canvas, then it doesn't count as a zooming action.
-  var mouseUpHandler = function(event) {
-    if (context.isZooming || context.isPanning) {
-      context.isZooming = false;
-      context.dragStartX = null;
-      context.dragStartY = null;
-    }
-
-    if (context.isPanning) {
-      context.isPanning = false;
-      context.draggingDate = null;
-      context.dateRange = null;
-      for (var i = 0; i < self.axes_.length; i++) {
-        delete self.axes_[i].draggingValue;
-        delete self.axes_[i].dragValueRange;
-      }
-    }
-
-    context.tarp.uncover();
-  };
+  if (!interactionModel.willDestroyContextMyself) {
+    var mouseUpHandler = function(event) {
+      context.destroy();
+    };
 
-  this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
+    this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
+  }
 };
 
 /**
@@ -1585,10 +1560,6 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
                    this.layout_.getPlotArea().w, Math.abs(endY - startY));
     }
   }
-
-  if (this.isUsingExcanvas_) {
-    this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0];
-  }
 };
 
 /**
@@ -1597,7 +1568,7 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
  */
 Dygraph.prototype.clearZoomRect_ = function() {
   this.currentZoomRectArgs_ = null;
-  this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+  this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
 };
 
 /**
@@ -2048,6 +2019,14 @@ Dygraph.prototype.animateSelection_ = function(direction) {
 
   var thisId = ++this.animateId;
   var that = this;
+  var cleanupIfClearing = function() {
+    // if we haven't reached fadeLevel 0 in the max frame time,
+    // ensure that the clear happens and just go to 0
+    if (that.fadeLevel !== 0 && direction < 0) {
+      that.fadeLevel = 0;
+      that.clearSelection();
+    }
+  };
   Dygraph.repeatAndCleanup(
     function(n) {
       // ignore simultaneous animations
@@ -2060,7 +2039,7 @@ Dygraph.prototype.animateSelection_ = function(direction) {
         that.updateSelection_(that.fadeLevel / totalSteps);
       }
     },
-    steps, millis, function() {});
+    steps, millis, cleanupIfClearing);
 };
 
 /**
@@ -2071,6 +2050,7 @@ Dygraph.prototype.animateSelection_ = function(direction) {
 Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
   /*var defaultPrevented = */
   this.cascadeEvents_('select', {
+    selectedRow: this.lastRow_,
     selectedX: this.lastx_,
     selectedPoints: this.selPoints_
   });
@@ -2115,10 +2095,6 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
                   2 * maxCircleSize + 2, this.height_);
   }
 
-  if (this.isUsingExcanvas_ && this.currentZoomRectArgs_) {
-    Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_);
-  }
-
   if (this.selPoints_.length > 0) {
     // Draw colored circles over the center of each selected point
     var canvasx = this.selPoints_[0].canvasx;
@@ -2150,7 +2126,7 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
  * legend. The selection can be cleared using clearSelection() and queried
  * using getSelection().
  * @param {number} row Row number that should be highlighted (i.e. appear with
- * hover dots on the chart). Set to false to clear any selection.
+ * hover dots on the chart).
  * @param {seriesName} optional series name to highlight that series with the
  * the highlightSeriesOpts setting.
  * @param { locked } optional If true, keep seriesName selected when mousing
@@ -2313,7 +2289,7 @@ Dygraph.prototype.addXTicks_ = function() {
   var xTicks = xAxisOptionsView('ticker')(
       range[0],
       range[1],
-      this.width_,  // TODO(danvk): should be area.width
+      this.plotter_.area.w,  // TODO(danvk): should be area.width
       xAxisOptionsView,
       this);
   // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
@@ -2719,8 +2695,7 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
 
   // TODO(danvk): is this a performance bottleneck when panning?
   // The interaction canvas should already be empty in that situation.
-  this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
-                                          this.canvas_.height);
+  this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_);
 
   if (this.getFunctionOption("drawCallback") !== null) {
     this.getFunctionOption("drawCallback")(this, is_initial_draw);
@@ -2968,7 +2943,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
       var ticker = opts('ticker');
       axis.ticks = ticker(axis.computedValueRange[0],
               axis.computedValueRange[1],
-              this.height_,  // TODO(danvk): should be area.height
+              this.plotter_.area.h,
               opts,
               this);
       // Define the first independent axis as primary axis.
@@ -2999,7 +2974,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
 
       axis.ticks = ticker(axis.computedValueRange[0],
                           axis.computedValueRange[1],
-                          this.height_,  // TODO(danvk): should be area.height
+                          this.plotter_.area.h,
                           opts,
                           this,
                           tick_values);
@@ -3491,9 +3466,9 @@ Dygraph.prototype.start_ = function() {
 Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
   if (typeof(block_redraw) == 'undefined') block_redraw = false;
 
-  // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
+  // copyUserAttrs_ drops the "file" parameter as a convenience to us.
   var file = input_attrs.file;
-  var attrs = Dygraph.mapLegacyOptions_(input_attrs);
+  var attrs = Dygraph.copyUserAttrs_(input_attrs);
 
   // TODO(danvk): this is a mess. Move these options into attr_.
   if ('rollPeriod' in attrs) {
@@ -3542,47 +3517,15 @@ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
 };
 
 /**
- * Returns a copy of the options with deprecated names converted into current
- * names. Also drops the (potentially-large) 'file' attribute. If the caller is
- * interested in that, they should save a copy before calling this.
- * @private
+ * Make a copy of input attributes, removing file as a convenience.
  */
-Dygraph.mapLegacyOptions_ = function(attrs) {
+Dygraph.copyUserAttrs_ = function(attrs) {
   var my_attrs = {};
   for (var k in attrs) {
+    if (!attrs.hasOwnProperty(k)) continue;
     if (k == 'file') continue;
     if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
   }
-
-  var set = function(axis, opt, value) {
-    if (!my_attrs.axes) my_attrs.axes = {};
-    if (!my_attrs.axes[axis]) my_attrs.axes[axis] = {};
-    my_attrs.axes[axis][opt] = value;
-  };
-  var map = function(opt, axis, new_opt) {
-    if (typeof(attrs[opt]) != 'undefined') {
-      console.warn("Option " + opt + " is deprecated. Use the " +
-          new_opt + " option for the " + axis + " axis instead. " +
-          "(e.g. { axes : { " + axis + " : { " + new_opt + " : ... } } } " +
-          "(see http://dygraphs.com/per-axis.html for more information.");
-      set(axis, new_opt, attrs[opt]);
-      delete my_attrs[opt];
-    }
-  };
-
-  // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
-  map('xValueFormatter', 'x', 'valueFormatter');
-  map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
-  map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
-  map('xTicker', 'x', 'ticker');
-  map('yValueFormatter', 'y', 'valueFormatter');
-  map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
-  map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
-  map('yTicker', 'y', 'ticker');
-  map('drawXGrid', 'x', 'drawGrid');
-  map('drawXAxis', 'x', 'drawAxis');
-  map('drawYGrid', 'y', 'drawGrid');
-  map('drawYAxis', 'y', 'drawAxis');
   return my_attrs;
 };
 
@@ -3659,19 +3602,25 @@ Dygraph.prototype.visibility = function() {
 };
 
 /**
- * Changes the visiblity of a series.
+ * Changes the visibility of one or more series.
  *
- * @param {number} num the series index
+ * @param {number|number[]} num the series index or an array of series indices
  * @param {boolean} value true or false, identifying the visibility.
  */
 Dygraph.prototype.setVisibility = function(num, value) {
   var x = this.visibility();
-  if (num < 0 || num >= x.length) {
-    console.warn("invalid series number in setVisibility: " + num);
-  } else {
-    x[num] = value;
-    this.predraw_();
+
+  if (num.constructor !== Array) num = [num];
+
+  for (var i = 0; i < num.length; i++) {
+    if (num[i] < 0 || num[i] >= x.length) {
+      console.warn("invalid series number in setVisibility: " + num[i]);
+    } else {
+      x[num[i]] = value;
+    }
   }
+
+  this.predraw_();
 };
 
 /**
@@ -3734,6 +3683,35 @@ Dygraph.prototype.indexFromSetName = function(name) {
 };
 
 /**
+ * Find the row number corresponding to the given x-value.
+ * Returns null if there is no such x-value in the data.
+ * If there are multiple rows with the same x-value, this will return the
+ * first one.
+ * @param {number} xVal The x-value to look for (e.g. millis since epoch).
+ * @return {?number} The row number, which you can pass to getValue(), or null.
+ */
+Dygraph.prototype.getRowForX = function(xVal) {
+  var low = 0,
+      high = this.numRows() - 1;
+
+  while (low <= high) {
+    var idx = (high + low) >> 1;
+    var x = this.getValue(idx, 0);
+    if (x < xVal) {
+      low = idx + 1;
+    } else if (x > xVal) {
+      high = idx - 1;
+    } else if (low != idx) {  // equal, but there may be an earlier match.
+      high = idx;
+    } else {
+      return idx;
+    }
+  }
+
+  return null;
+};
+
+/**
  * Trigger a callback when the dygraph has drawn itself and is ready to be
  * manipulated. This is primarily useful when dygraphs has to do an XHR for the
  * data (i.e. a URL is passed as the data source) and the chart is drawn