Merge pull request #210 from klausw-g/range-pad-2
authorDan Vanderkam <danvdk@gmail.com>
Sat, 16 Feb 2013 19:50:27 +0000 (11:50 -0800)
committerDan Vanderkam <danvdk@gmail.com>
Sat, 16 Feb 2013 19:50:27 +0000 (11:50 -0800)
Add new options xRangePad and yRangePad

1  2 
dygraph-layout.js
dygraph-options-reference.js
dygraph.js

diff --combined dygraph-layout.js
@@@ -130,7 -130,7 +130,7 @@@ DygraphLayout.prototype.setAnnotations 
    var parse = this.attr_('xValueParser') || function(x) { return x; };
    for (var i = 0; i < ann.length; i++) {
      var a = {};
 -    if (!ann[i].xval && !ann[i].x) {
 +    if (!ann[i].xval && ann[i].x === undefined) {
        this.dygraph_.error("Annotations must have an 'x' property");
        return;
      }
@@@ -168,24 -168,11 +168,11 @@@ DygraphLayout.prototype.evaluate = func
  };
  
  DygraphLayout.prototype._evaluateLimits = function() {
-   this.minxval = this.maxxval = null;
-   if (this.dateWindow_) {
-     this.minxval = this.dateWindow_[0];
-     this.maxxval = this.dateWindow_[1];
-   } else {
-     for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
-       var series = this.datasets[setIdx];
-       if (series.length > 1) {
-         var x1 = series[0][0];
-         if (!this.minxval || x1 < this.minxval) this.minxval = x1;
-         var x2 = series[series.length - 1][0];
-         if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
-       }
-     }
-   }
-   this.xrange = this.maxxval - this.minxval;
-   this.xscale = (this.xrange !== 0 ? 1/this.xrange : 1.0);
+   var xlimits = this.dygraph_.xAxisRange();
+   this.minxval = xlimits[0];
+   this.maxxval = xlimits[1];
+   var xrange = xlimits[1] - xlimits[0];
+   this.xscale = (xrange !== 0 ? 1 / xrange : 1.0);
  
    for (var i = 0; i < this.yAxes_.length; i++) {
      var axis = this.yAxes_[i];
@@@ -269,10 -269,10 +269,10 @@@ Dygraph.OPTIONS_REFERENCE =  // <JSON
    "underlayCallback": {
      "default": "null",
      "labels": ["Callbacks"],
 -    "type": "function(canvas, area, dygraph)",
 +    "type": "function(context, area, dygraph)",
      "parameters": [
 -      [ "canvas" , "the canvas to draw on" ],
 -      [ "area" , "" ],
 +      [ "context" , "the canvas drawing context on which to draw" ],
 +      [ "area" , "An object with {x,y,w,h} properties describing the drawing area." ],
        [ "dygraph" , "the reference graph" ]
      ],
      "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
      "default": "false",
      "labels": ["Data Line display"],
      "type": "boolean",
 -    "description": "When set, display the graph as a step plot instead of a line plot."
 +    "description": "When set, display the graph as a step plot instead of a line plot. This option may either be set for the whole graph or for single series."
    },
    "labelsKMB": {
      "default": "false",
    },
    "avoidMinZero": {
      "default": "false",
-     "labels": ["Axis display"],
+     "labels": ["Deprecated"],
      "type": "boolean",
-     "description": "When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
+     "description": "Deprecated, please use yRangePad instead. When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
    },
    "drawAxesAtZero": {
      "default": "false",
      "type": "boolean",
      "description": "When set, draw the X axis at the Y=0 position and the Y axis at the X=0 position if those positions are inside the graph's visible area. Otherwise, draw the axes at the bottom or left graph edge as usual."
    },
+   "xRangePad": {
+     "default": "0",
+     "labels": ["Axis display"],
+     "type": "float",
+     "description": "Add the specified amount of extra space (in pixels) around the X-axis value range to ensure points at the edges remain visible."
+   },
+   "yRangePad": {
+     "default": "null",
+     "labels": ["Axis display"],
+     "type": "float",
+     "description": "If set, add the specified amount of extra space (in pixels) around the Y-axis value range to ensure points at the edges remain visible. If unset, use the traditional Y padding algorithm."
+   },
    "xAxisLabelFormatter": {
      "default": "",
      "labels": ["Deprecated"],
diff --combined dygraph.js
@@@ -246,6 -246,8 +246,8 @@@ Dygraph.DEFAULT_ATTRS = 
  
    stepPlot: false,
    avoidMinZero: false,
+   xRangePad: 0,
+   yRangePad: null,
    drawAxesAtZero: false,
  
    // Sizes of the various chart labels.
@@@ -657,8 -659,18 +659,18 @@@ Dygraph.prototype.xAxisRange = function
   * data set.
   */
  Dygraph.prototype.xAxisExtremes = function() {
+   var pad = this.attr_('xRangePad') / this.plotter_.area.w;
+   if (this.numRows() == 0) {
+     return [0 - pad, 1 + pad];
+   }
    var left = this.rawData_[0][0];
    var right = this.rawData_[this.rawData_.length - 1][0];
+   if (pad) {
+     // Must keep this in sync with dygraph-layout _evaluateLimits()
+     var range = right - left;
+     left -= range * pad;
+     right += range * pad;
+   }
    return [left, right];
  };
  
@@@ -874,6 -886,7 +886,7 @@@ Dygraph.prototype.toPercentXCoord = fun
   * @return { Integer } The number of columns.
   */
  Dygraph.prototype.numColumns = function() {
+   if (!this.rawData_) return 0;
    return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
  };
  
   * @return { Integer } The number of rows, less any header.
   */
  Dygraph.prototype.numRows = function() {
+   if (!this.rawData_) return 0;
    return this.rawData_.length;
  };
  
  /**
-  * Returns the full range of the x-axis, as determined by the most extreme
-  * values in the data set. Not affected by zooming, visibility, etc.
-  * TODO(danvk): merge w/ xAxisExtremes
-  * @return { Array<Number> } A [low, high] pair
-  * @private
-  */
- Dygraph.prototype.fullXRange_ = function() {
-   if (this.numRows() > 0) {
-     return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]];
-   } else {
-     return [0, 1];
-   }
- };
- /**
   * Returns the value in the given row and column. If the row and column exceed
   * the bounds on the data, returns null. Also returns null if the value is
   * missing.
@@@ -930,8 -929,6 +929,8 @@@ Dygraph.prototype.createInterface_ = fu
    this.graphDiv = document.createElement("div");
    this.graphDiv.style.width = this.width_ + "px";
    this.graphDiv.style.height = this.height_ + "px";
 +  // TODO(danvk): any other styles that are useful to set here?
 +  this.graphDiv.style.textAlign = 'left';  // This is a CSS "reset"
    enclosing.appendChild(this.graphDiv);
  
    // Create the canvas for interactive parts of the chart.
    };
  
    this.mouseOutHandler_ = function(e) {
 -    dygraph.mouseOut_(e);
 +    // The mouse has left the chart if:
 +    // 1. e.target is inside the chart
 +    // 2. e.relatedTarget is outside the chart
 +    var target = e.target || e.fromElement;
 +    var relatedTarget = e.relatedTarget || e.toElement;
 +    if (Dygraph.isElementContainedBy(target, dygraph.graphDiv) &&
 +        !Dygraph.isElementContainedBy(relatedTarget, dygraph.graphDiv)) {
 +      dygraph.mouseOut_(e);
 +    }
    };
  
 +  this.addEvent(window, 'mouseout', this.mouseOutHandler_);
    this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
 -  this.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler_);
  
    // Don't recreate and register the resize handler on subsequent calls.
    // This happens when the graph is resized.
@@@ -1013,9 -1002,9 +1012,9 @@@ Dygraph.prototype.destroy = function() 
    this.registeredEvents_ = [];
  
    // remove mouse event handlers (This may not be necessary anymore)
 -  Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler_);
 +  Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_);
    Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
 -  Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_);
 +  Dygraph.removeEvent(this.mouseEventElement_, 'mouseup', this.mouseUpHandler_);
  
    // remove window handlers
    Dygraph.removeEvent(window,'resize',this.resizeHandler_);
@@@ -1621,13 -1610,9 +1620,13 @@@ Dygraph.prototype.getArea = function() 
   * Returns a two-element array: [X, Y].
   */
  Dygraph.prototype.eventToDomCoords = function(event) {
 -  var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
 -  var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_);
 -  return [canvasx, canvasy];
 +  if (event.offsetX && event.offsetY) {
 +    return [ event.offsetX, event.offsetY ];
 +  } else {
 +    var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
 +    var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_);
 +    return [canvasx, canvasy];
 +  }
  };
  
  /**
@@@ -2096,7 -2081,7 +2095,7 @@@ Dygraph.prototype.addXTicks_ = function
    if (this.dateWindow_) {
      range = [this.dateWindow_[0], this.dateWindow_[1]];
    } else {
-     range = this.fullXRange_();
+     range = this.xAxisExtremes();
    }
  
    var xAxisOptionsView = this.optionsViewForAxis_('x');
@@@ -2554,35 -2539,71 +2553,71 @@@ Dygraph.prototype.computeYAxisRanges_ 
            maxY = Math.max(extremeMaxY, maxY);
          }
        }
-       if (includeZero && minY > 0) minY = 0;
+       // Include zero if requested by the user.
+       if (includeZero && !logscale) {
+         if (minY > 0) minY = 0;
+         if (maxY < 0) maxY = 0;
+       }
  
        // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
        if (minY == Infinity) minY = 0;
        if (maxY == -Infinity) maxY = 1;
  
-       // 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.
-       if (span === 0) { span = maxY; }
+       // special case: if we have no sense of scale, center on the sole value.
+       if (span === 0) {
+         if (maxY !== 0) {
+           span = Math.abs(maxY);
+         } else {
+           // ... and if the sole value is zero, use range 0-1.
+           maxY = 1;
+           span = 1;
+         }
+       }
+       // Add some padding. This supports two Y padding operation modes:
+       //
+       // - backwards compatible (yRangePad not set):
+       //   10% padding for automatic Y ranges, but not for user-supplied
+       //   ranges, and move a close-to-zero edge to zero except if
+       //   avoidMinZero is set, since drawing at the edge results in
+       //   invisible lines. Unfortunately lines drawn at the edge of a
+       //   user-supplied range will still be invisible. If logscale is
+       //   set, add a variable amount of padding at the top but
+       //   none at the bottom.
+       //
+       // - new-style (yRangePad set by the user):
+       //   always add the specified Y padding.
+       //
+       var ypadCompat = true;
+       var ypad = 0.1; // add 10%
+       if (this.attr_('yRangePad') !== null) {
+         ypadCompat = false;
+         // Convert pixel padding to ratio
+         ypad = this.attr_('yRangePad') / this.plotter_.area.h;
+       }
  
        var maxAxisY, minAxisY;
        if (logscale) {
-         maxAxisY = maxY + 0.1 * span;
-         minAxisY = minY;
+         if (ypadCompat) {
+           maxAxisY = maxY + ypad * span;
+           minAxisY = minY;
+         } else {
+           var logpad = Math.exp(Math.log(span) * ypad);
+           maxAxisY = maxY * logpad;
+           minAxisY = minY / logpad;
+         }
        } else {
-         maxAxisY = maxY + 0.1 * span;
-         minAxisY = minY - 0.1 * span;
+         maxAxisY = maxY + ypad * span;
+         minAxisY = minY - ypad * span;
  
-         // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
-         if (!this.attr_("avoidMinZero")) {
+         // Backwards-compatible behavior: Move the span to start or end at zero if it's
+         // close to zero, but not if avoidMinZero is set.
+         if (ypadCompat && !this.attr_("avoidMinZero")) {
            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;
-         }
        }
        axis.extremeRange = [minAxisY, maxAxisY];
      }
        axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
      } else if (axis.valueRange) {
        // This is a user-set value range for this axis.
-       axis.computedValueRange = [
-          isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0],
-          isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1]
-       ];
+       var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
+       var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
+       if (!ypadCompat) {
+         if (axis.logscale) {
+           var logpad = Math.exp(Math.log(span) * ypad);
+           y0 *= logpad;
+           y1 /= logpad;
+         } else {
+           var span = y1 - y0;
+           y0 -= span * ypad;
+           y1 += span * ypad;
+         }
+       }
+       axis.computedValueRange = [y0, y1];
      } else {
        axis.computedValueRange = axis.extremeRange;
      }
@@@ -2676,6 -2707,8 +2721,6 @@@ Dygraph.prototype.extractSeries_ = func
   *                            data
   */
  Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
 -  if (originalData.length < 2)
 -    return originalData;
    rollPeriod = Math.min(rollPeriod, originalData.length);
    var rollingData = [];
    var sigma = this.attr_("sigma");
@@@ -3510,12 -3543,9 +3555,12 @@@ Dygraph.prototype.annotations = functio
  /**
   * Get the list of label names for this graph. The first column is the
   * x-axis, so the data series names start at index 1.
 + *
 + * Returns null when labels have not yet been defined.
   */
  Dygraph.prototype.getLabels = function() {
 -  return this.attr_("labels").slice();
 +  var labels = this.attr_("labels");
 +  return labels ? labels.slice() : null;
  };
  
  /**