gross merge mostly done; left MERGE markers in a few spots
authorDan Vanderkam <danvk@google.com>
Thu, 21 Oct 2010 23:50:19 +0000 (19:50 -0400)
committerDan Vanderkam <danvk@google.com>
Thu, 21 Oct 2010 23:50:19 +0000 (19:50 -0400)
1  2 
dygraph-canvas.js
dygraph.js

Simple merge
diff --cc dygraph.js
@@@ -172,6 -177,13 +177,7 @@@ Dygraph.prototype.__init__ = function(d
    this.previousVerticalX_ = -1;
    this.fractions_ = attrs.fractions || false;
    this.dateWindow_ = attrs.dateWindow || null;
 -  // valueRange and valueWindow are similar, but not the same. valueRange is a
 -  // locally-stored copy of the attribute. valueWindow starts off the same as
 -  // valueRange but is impacted by zoom or pan effects. valueRange is kept
 -  // around to restore the original value back to valueRange.
 -  this.valueRange_ = attrs.valueRange || null;
 -  this.valueWindow_ = this.valueRange_;
    this.wilsonInterval_ = attrs.wilsonInterval || true;
    this.is_initial_draw_ = true;
    this.annotations_ = [];
@@@ -763,11 -806,24 +792,28 @@@ Dygraph.prototype.createDragInterface_ 
        dragEndY = getY(event);
  
        // Want to have it so that:
-       // 1. draggingDate appears at dragEndX
+       // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
        // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
-       self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
-       self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
+       // 3. draggingValue appears at dragEndY.
+       // 4. valueRange is unaltered.
+       var minDate = draggingDate - (dragEndX / self.width_) * dateRange;
+       var maxDate = minDate + dateRange;
+       self.dateWindow_ = [minDate, maxDate];
++      // MERGE
++=======
+       // y-axis scaling is automatic unless a valueRange is defined or
+       // if the user zooms in on the y-axis. If neither is true, valueWindow_
+       // will be null.
+       if (self.valueWindow_) {
+         var maxValue = draggingValue + (dragEndY / self.height_) * valueRange;
+         var minValue = maxValue - valueRange;
+         self.valueWindow_ = [ minValue, maxValue ];
+       }
++>>>>>>> master
 +
        self.drawGraph_();
      }
    });
@@@ -932,7 -1039,78 +1029,79 @@@ Dygraph.prototype.doZoomXDates_ = funct
    this.dateWindow_ = [minDate, maxDate];
    this.drawGraph_();
    if (this.attr_("zoomCallback")) {
-     this.attr_("zoomCallback")(minDate, maxDate);
+     var yRange = this.yAxisRange();
+     this.attr_("zoomCallback")(minDate, maxDate, yRange[0], yRange[1]);
+   }
+ };
+ /**
+  * Zoom to something containing [lowY, highY]. These are pixel coordinates in
+  * the canvas. The exact zoom window may be slightly larger if there are no
+  * data points near lowY or highY.  Don't confuse this function with
+  * doZoomYValues, which accepts parameters that match the raw data. This
+  * function redraws the graph.
+  * 
+  * @param {Number} lowY The topmost pixel value that should be visible.
+  * @param {Number} highY The lowest pixel value that should be visible.
+  * @private
+  */
+ Dygraph.prototype.doZoomY_ = function(lowY, highY) {
+   // Find the highest and lowest values in pixel range.
+   var r = this.toDataCoords(null, lowY);
+   var maxValue = r[1];
+   r = this.toDataCoords(null, highY);
+   var minValue = r[1];
+   this.doZoomYValues_(minValue, maxValue);
+ };
+ /**
+  * Zoom to something containing [minValue, maxValue] values. Don't confuse this
+  * method with doZoomY which accepts pixel coordinates. This function redraws
+  * the graph.
+  * 
+  * @param {Number} minValue The minimum Value that should be visible.
+  * @param {Number} maxValue The maximum value that should be visible.
+  * @private
+  */
++// MERGE: this doesn't make sense anymore.
+ Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) {
+   this.valueWindow_ = [minValue, maxValue];
+   this.drawGraph_();
+   if (this.attr_("zoomCallback")) {
+     var xRange = this.xAxisRange(); 
+     this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue);
+   }
+ };
+ /**
+  * Reset the zoom to the original view coordinates. This is the same as
+  * double-clicking on the graph.
+  * 
+  * @private
+  */
+ Dygraph.prototype.doUnzoom_ = function() {
+   var dirty = null;
+   if (this.dateWindow_ != null) {
+     dirty = 1;
+     this.dateWindow_ = null;
+   }
+   if (this.valueWindow_ != null) {
+     dirty = 1;
+     this.valueWindow_ = this.valueRange_;
+   }
+   if (dirty) {
+     // Putting the drawing operation before the callback because it resets
+     // yAxisRange.
+     this.drawGraph_();
+     if (this.attr_("zoomCallback")) {
+       var minDate = this.rawData_[0][0];
+       var maxDate = this.rawData_[this.rawData_.length - 1][0];
+       var minValue = this.yAxisRange()[0];
+       var maxValue = this.yAxisRange()[1];
+       this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue);
+     }
    }
  };
  
@@@ -1584,36 -1760,6 +1753,37 @@@ Dygraph.prototype.extremeValues_ = func
  };
  
  /**
 + * This function is called once when the chart's data is changed or the options
 + * dictionary is updated. It is _not_ called when the user pans or zooms. The
 + * idea is that values derived from the chart's data can be computed here,
 + * rather than every time the chart is drawn. This includes things like the
 + * number of axes, rolling averages, etc.
 + */
 +Dygraph.prototype.predraw_ = function() {
 +  // TODO(danvk): move more computations out of drawGraph_ and into here.
 +  this.computeYAxes_();
 +
 +  // Create a new plotter.
 +  if (this.plotter_) this.plotter_.clear();
 +  this.plotter_ = new DygraphCanvasRenderer(this,
 +                                            this.hidden_, this.layout_,
 +                                            this.renderOptions_);
 +
 +  // The roller sits in the bottom left corner of the chart. We don't know where
 +  // this will be until the options are available, so it's positioned here.
 +  this.roller_ = this.createRollInterface_();
 +
 +  // Same thing applies for the labelsDiv. It's right edge should be flush with
 +  // the right edge of the charting area (which may not be the same as the right
 +  // edge of the div, if we have two y-axes.
 +  this.positionLabelsDiv_();
 +
 +  // If the data or options have changed, then we'd better redraw.
 +  this.drawGraph_();
 +};
 +
 +/**
++=======
   * Update the graph with new data. This method is called when the viewing area
   * has changed. If the underlying data or options have changed, predraw_ will
   * be called before drawGraph_ is called.
@@@ -1653,6 -1796,6 +1823,8 @@@ Dygraph.prototype.drawGraph_ = function
          series.push([date, data[j][i]]);
        }
      }
++
++    // TODO(danvk): move this into predraw_. It's insane to do it here.
      series = this.rollingAverage(series, this.rollPeriod_);
  
      // Prune down to the desired range, if necessary (for zooming)
    this.plotter_.clear();
    this.plotter_.render();
    this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
--                                         this.canvas_.height);
++                                          this.canvas_.height);
  
    if (this.attr_("drawCallback") !== null) {
      this.attr_("drawCallback")(this, is_initial_draw);
  };
  
  /**
-       if (minAxisY < 0 && minY >= 0) minAxisY = 0;
-       if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
 + * Determine properties of the y-axes which are independent of the data
 + * currently being displayed. This includes things like the number of axes and
 + * the style of the axes. It does not include the range of each axis and its
 + * tick marks.
 + * This fills in this.axes_ and this.seriesToAxisMap_.
 + * axes_ = [ { options } ]
 + * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
 + *   indices are into the axes_ array.
 + */
 +Dygraph.prototype.computeYAxes_ = function() {
 +  this.axes_ = [{}];  // always have at least one y-axis.
 +  this.seriesToAxisMap_ = {};
 +
 +  // Get a list of series names.
 +  var labels = this.attr_("labels");
 +  var series = [];
 +  for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
 +
 +  // all options which could be applied per-axis:
 +  var axisOptions = [
 +    'includeZero',
 +    'valueRange',
 +    'labelsKMB',
 +    'labelsKMG2',
 +    'pixelsPerYLabel',
 +    'yAxisLabelWidth',
 +    'axisLabelFontSize',
 +    'axisTickSize'
 +  ];
 +
 +  // Copy global axis options over to the first axis.
 +  for (var i = 0; i < axisOptions.length; i++) {
 +    var k = axisOptions[i];
 +    var v = this.attr_(k);
 +    if (v) this.axes_[0][k] = v;
 +  }
 +
 +  // Go through once and add all the axes.
 +  for (var seriesName in series) {
 +    if (!series.hasOwnProperty(seriesName)) continue;
 +    var axis = this.attr_("axis", seriesName);
 +    if (axis == null) {
 +      this.seriesToAxisMap_[seriesName] = 0;
 +      continue;
 +    }
 +    if (typeof(axis) == 'object') {
 +      // Add a new axis, making a copy of its per-axis options.
 +      var opts = {};
 +      Dygraph.update(opts, this.axes_[0]);
 +      Dygraph.update(opts, { valueRange: null });  // shouldn't inherit this.
 +      Dygraph.update(opts, axis);
 +      this.axes_.push(opts);
 +      this.seriesToAxisMap_[seriesName] = this.axes_.length - 1;
 +    }
 +  }
 +
 +  // Go through one more time and assign series to an axis defined by another
 +  // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
 +  for (var seriesName in series) {
 +    if (!series.hasOwnProperty(seriesName)) continue;
 +    var axis = this.attr_("axis", seriesName);
 +    if (typeof(axis) == 'string') {
 +      if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
 +        this.error("Series " + seriesName + " wants to share a y-axis with " +
 +                   "series " + axis + ", which does not define its own axis.");
 +        return null;
 +      }
 +      var idx = this.seriesToAxisMap_[axis];
 +      this.seriesToAxisMap_[seriesName] = idx;
 +    }
 +  }
 +};
 +
 +/**
 + * Returns the number of y-axes on the chart.
 + * @return {Number} the number of axes.
 + */
 +Dygraph.prototype.numAxes = function() {
 +  var last_axis = 0;
 +  for (var series in this.seriesToAxisMap_) {
 +    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
 +    var idx = this.seriesToAxisMap_[series];
 +    if (idx > last_axis) last_axis = idx;
 +  }
 +  return 1 + last_axis;
 +};
 +
 +/**
 + * Determine the value range and tick marks for each axis.
 + * @param {Object} extremes A mapping from seriesName -> [low, high]
 + * This fills in the valueRange and ticks fields in each entry of this.axes_.
 + */
 +Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
 +  // Build a map from axis number -> [list of series names]
 +  var seriesForAxis = [];
 +  for (var series in this.seriesToAxisMap_) {
 +    if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
 +    var idx = this.seriesToAxisMap_[series];
 +    while (seriesForAxis.length <= idx) seriesForAxis.push([]);
 +    seriesForAxis[idx].push(series);
 +  }
 +
 +  // Compute extreme values, a span and tick marks for each axis.
 +  for (var i = 0; i < this.axes_.length; i++) {
 +    var axis = this.axes_[i];
 +    if (axis.valueRange) {
 +      axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
 +    } else {
 +      // Calcuate the extremes of extremes.
 +      var series = seriesForAxis[i];
 +      var minY = Infinity;  // extremes[series[0]][0];
 +      var maxY = -Infinity;  // extremes[series[0]][1];
 +      for (var j = 0; j < series.length; j++) {
 +        minY = Math.min(extremes[series[j]][0], minY);
 +        maxY = Math.max(extremes[series[j]][1], maxY);
 +      }
 +      if (axis.includeZero && minY > 0) minY = 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.
 +      if (span == 0) { span = maxY; }
 +      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 (!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.computedValueRange = [minAxisY, maxAxisY];
 +    }
 +
 +    // Add ticks. By default, all axes inherit the tick positions of the
 +    // primary axis. However, if an axis is specifically marked as having
 +    // independent ticks, then that is permissible as well.
 +    if (i == 0 || axis.independentTicks) {
 +      axis.ticks =
 +        Dygraph.numericTicks(axis.computedValueRange[0],
 +                             axis.computedValueRange[1],
 +                             this,
 +                             axis);
 +    } else {
 +      var p_axis = this.axes_[0];
 +      var p_ticks = p_axis.ticks;
 +      var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
 +      var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
 +      var tick_values = [];
 +      for (var i = 0; i < p_ticks.length; i++) {
 +        var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
 +        var y_val = axis.computedValueRange[0] + y_frac * scale;
 +        tick_values.push(y_val);
 +      }
 +
 +      axis.ticks =
 +        Dygraph.numericTicks(axis.computedValueRange[0],
 +                             axis.computedValueRange[1],
 +                             this, axis, tick_values);
 +    }
 +  }
 +
 +  return [this.axes_, this.seriesToAxisMap_];
 +};
 + 
 +/**
   * Calculates the rolling average of a data set.
   * If originalData is [label, val], rolls the average of those.
   * If originalData is [label, [, it's interpreted as [value, stddev]
@@@ -2499,12 -2496,16 +2673,12 @@@ Dygraph.prototype.start_ = function() 
   */
  Dygraph.prototype.updateOptions = function(attrs) {
    // TODO(danvk): this is a mess. Rethink this function.
-   if (attrs.rollPeriod) {
+   if ('rollPeriod' in attrs) {
      this.rollPeriod_ = attrs.rollPeriod;
    }
-   if (attrs.dateWindow) {
+   if ('dateWindow' in attrs) {
      this.dateWindow_ = attrs.dateWindow;
    }
 -  if ('valueRange' in attrs) {
 -    this.valueRange_ = attrs.valueRange;
 -    this.valueWindow_ = attrs.valueRange;
 -  }
  
    // TODO(danvk): validate per-series options.
    // Supported: