Merge https://github.com/danvk/dygraphs into logscale
authorRobert Konigsberg <konigsberg@google.com>
Fri, 28 Jan 2011 21:37:48 +0000 (16:37 -0500)
committerRobert Konigsberg <konigsberg@google.com>
Fri, 28 Jan 2011 21:37:48 +0000 (16:37 -0500)
Conflicts:
dygraph.js

1  2 
dygraph.js

diff --combined dygraph.js
@@@ -24,6 -24,7 +24,6 @@@
  
   If the 'errorBars' option is set in the constructor, the input should be of
   the form
 -
     Date,SeriesA,SeriesB,...
     YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
     YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
@@@ -78,11 -79,6 +78,11 @@@ Dygraph.DEFAULT_WIDTH = 480
  Dygraph.DEFAULT_HEIGHT = 320;
  Dygraph.AXIS_LINE_WIDTH = 0.3;
  
 +Dygraph.LOG_SCALE = 10;
 +Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
 +Dygraph.log10 = function(x) {
 +  return Math.log(x) / Dygraph.LN_TEN;
 +}
  
  // Default attribute values.
  Dygraph.DEFAULT_ATTRS = {
  
    delimiter: ',',
  
 -  logScale: false,
    sigma: 2.0,
    errorBars: false,
    fractions: false,
@@@ -359,153 -356,46 +359,153 @@@ Dygraph.prototype.yAxisRanges = functio
   * If specified, do this conversion for the coordinate system of a particular
   * axis. Uses the first axis by default.
   * Returns a two-element array: [X, Y]
 + *
 + * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
 + * instead of toDomCoords(null, y, axis).
   */
  Dygraph.prototype.toDomCoords = function(x, y, axis) {
 -  var ret = [null, null];
 +  return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
 +};
 +
 +/**
 + * Convert from data x coordinates to canvas/div X coordinate.
 + * If specified, do this conversion for the coordinate system of a particular
 + * axis.
 + * Returns a single value or null if x is null.
 + */
 +Dygraph.prototype.toDomXCoord = function(x) {
 +  if (x == null) {
 +    return null;
 +  };
 +
    var area = this.plotter_.area;
 -  if (x !== null) {
 -    var xRange = this.xAxisRange();
 -    ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
 -  }
 +  var xRange = this.xAxisRange();
 +  return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
 +}
  
 -  if (y !== null) {
 -    var yRange = this.yAxisRange(axis);
 -    ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
 -  }
 +/**
 + * Convert from data x coordinates to canvas/div Y coordinate and optional
 + * axis. Uses the first axis by default.
 + *
 + * returns a single value or null if y is null.
 + */
 +Dygraph.prototype.toDomYCoord = function(y, axis) {
 +  var pct = this.toPercentYCoord(y, axis);
  
 -  return ret;
 -};
 +  if (pct == null) {
 +    return null;
 +  }
 +  var area = this.plotter_.area;
 +  return area.y + pct * area.h;
 +}
  
  /**
   * Convert from canvas/div coords to data coordinates.
   * If specified, do this conversion for the coordinate system of a particular
   * axis. Uses the first axis by default.
 - * Returns a two-element array: [X, Y]
 + * Returns a two-element array: [X, Y].
 + *
 + * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
 + * instead of toDataCoords(null, y, axis).
   */
  Dygraph.prototype.toDataCoords = function(x, y, axis) {
 -  var ret = [null, null];
 -  var area = this.plotter_.area;
 -  if (x !== null) {
 -    var xRange = this.xAxisRange();
 -    ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
 +  return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
 +};
 +
 +/**
 + * Convert from canvas/div x coordinate to data coordinate.
 + *
 + * If x is null, this returns null.
 + */
 +Dygraph.prototype.toDataXCoord = function(x) {
 +  if (x == null) {
 +    return null;
    }
  
 -  if (y !== null) {
 -    var yRange = this.yAxisRange(axis);
 -    ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
 +  var area = this.plotter_.area;
 +  var xRange = this.xAxisRange();
 +  return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
 +};
 +
 +/**
 + * Convert from canvas/div y coord to value.
 + *
 + * If y is null, this returns null.
 + * if axis is null, this uses the first axis.
 + */
 +Dygraph.prototype.toDataYCoord = function(y, axis) {
 +  if (y == null) {
 +    return null;
    }
  
 -  return ret;
 +  var area = this.plotter_.area;
 +  var yRange = this.yAxisRange(axis);
 +
 +  if (!axis.logscale) {
 +    return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
 +  } else {
 +    // Computing the inverse of toDomCoord.
 +    var pct = (y - area.y) / area.h
 +
 +    // Computing the inverse of toPercentYCoord. The function was arrived at with
 +    // the following steps:
 +    //
 +    // Original calcuation:
 +    // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
 +    //
 +    // Move denominator to both sides:
 +    // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
 +    //
 +    // subtract logr1, and take the negative value.
 +    // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
 +    //
 +    // Swap both sides of the equation, and we can compute the log of the
 +    // return value. Which means we just need to use that as the exponent in
 +    // e^exponent.
 +    // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
 +
 +    var logr1 = Dygraph.log10(yRange[1]);
 +    var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
 +    var value = Math.pow(Dygraph.LOG_SCALE, exponent);
 +    return value;
 +  }
  };
  
  /**
 + * Converts a y for an axis to a percentage from the top to the
 + * bottom of the div.
 + *
 + * If the coordinate represents a value visible on the canvas, then
 + * the value will be between 0 and 1, where 0 is the top of the canvas.
 + * However, this method will return values outside the range, as
 + * values can fall outside the canvas.
 + *
 + * If y is null, this returns null.
 + * if axis is null, this uses the first axis.
 + */
 +Dygraph.prototype.toPercentYCoord = function(y, axis) {
 +  if (y == null) {
 +    return null;
 +  }
 +  if (typeof(axis) == "undefined") axis = 0;
 +
 +  var area = this.plotter_.area;
 +  var yRange = this.yAxisRange(axis);
 +
 +  var pct;
 +  if (!this.axes_[axis].logscale) {
 +    // yrange[1] - y is unit distance from the bottom.
 +    // yrange[1] - yrange[0] is the scale of the range.
 +    // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
 +    pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
 +  } else {
 +    var logr1 = Dygraph.log10(yRange[1]);
 +    pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
 +  }
 +  return pct;
 +}
 +
 +/**
   * Returns the number of columns (including the independent variable).
   */
  Dygraph.prototype.numColumns = function() {
@@@ -911,6 -801,8 +911,8 @@@ Dygraph.startPan = function(event, g, c
    context.isPanning = true;
    var xRange = g.xAxisRange();
    context.dateRange = xRange[1] - xRange[0];
+   context.initialLeftmostDate = xRange[0];
+   context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
  
    // Record the range of each y-axis at the start of the drag.
    // If any axis has a valueRange or valueWindow, then we want a 2D pan.
    for (var i = 0; i < g.axes_.length; i++) {
      var axis = g.axes_[i];
      var yRange = g.yAxisRange(i);
+     // TODO(konigsberg): These values should be in |context|.
      axis.dragValueRange = yRange[1] - yRange[0];
-     axis.draggingValue = g.toDataYCoord(context.dragStartY, i);
+     axis.initialTopValue = yRange[1];
+     axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
      if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
    }
-   // TODO(konigsberg): Switch from all this math to toDataCoords?
-   // Seems to work for the dragging value.
-   context.draggingDate = (context.dragStartX / g.width_) * context.dateRange + xRange[0];
  };
  
  // Called in response to an interaction model operation that
@@@ -939,30 -829,18 +939,18 @@@ Dygraph.movePan = function(event, g, co
    context.dragEndX = g.dragGetX_(event, context);
    context.dragEndY = g.dragGetY_(event, context);
  
-   // TODO(danvk): update this comment
-   // Want to have it so that:
-   // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
-   // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
-   // 3. draggingValue appears at dragEndY.
-   // 4. valueRange is unaltered.
-   var minDate = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange;
+   var minDate = context.initialLeftmostDate -
+     (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
    var maxDate = minDate + context.dateRange;
    g.dateWindow_ = [minDate, maxDate];
  
    // y-axis scaling is automatic unless this is a full 2D pan.
    if (context.is2DPan) {
      // Adjust each axis appropriately.
-     // NOTE(konigsberg): I don't think this computation for y_frac is correct.
-     // I think it doesn't take into account the display of the x axis.
-     // See, when I tested this with console.log(y_frac), and move the mouse
-     // cursor to the botom, the largest y_frac was 0.94, and not 1.0. That
-     // could also explain why panning tends to start with a small jumpy shift.
-     var y_frac = context.dragEndY / g.height_;
      for (var i = 0; i < g.axes_.length; i++) {
        var axis = g.axes_[i];
-       var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
+       var maxValue = axis.initialTopValue +
+         (context.dragEndY - context.dragStartY) * axis.unitsPerPixel;
        var minValue = maxValue - axis.dragValueRange;
        axis.valueWindow = [ minValue, maxValue ];
      }
  // panning behavior.
  //
  Dygraph.endPan = function(event, g, context) {
+   // TODO(konigsberg): Clear the context data from the axis.
+   // TODO(konigsberg): mouseup should just delete the
+   // context object, and mousedown should create a new one.
    context.isPanning = false;
    context.is2DPan = false;
-   context.draggingDate = null;
+   context.initialLeftmostDate = null;
    context.dateRange = null;
    context.valueRange = null;
  }
@@@ -1157,12 -1038,12 +1148,12 @@@ Dygraph.prototype.createDragInterface_ 
      prevEndY: null,
      prevDragDirection: null,
  
-     // TODO(danvk): update this comment
-     // draggingDate and draggingValue represent the [date,value] point on the
-     // graph at which the mouse was pressed. As the mouse moves while panning,
-     // the viewport must pan so that the mouse position points to
-     // [draggingDate, draggingValue]
-     draggingDate: null,
+     // The value on the left side of the graph when a pan operation starts.
+     initialLeftmostDate: null,
+     // The number of units each pixel spans. (This won't be valid for log
+     // scales)
+     xUnitsPerPixel: null,
  
      // TODO(danvk): update this comment
      // The range in second/value units that the viewport encompasses during a
@@@ -1293,8 -1174,10 +1284,8 @@@ Dygraph.prototype.drawZoomRect_ = funct
  Dygraph.prototype.doZoomX_ = function(lowX, highX) {
    // Find the earliest and latest dates contained in this canvasx range.
    // Convert the call to date ranges of the raw data.
 -  var r = this.toDataCoords(lowX, null);
 -  var minDate = r[0];
 -  r = this.toDataCoords(highX, null);
 -  var maxDate = r[0];
 +  var minDate = this.toDataXCoord(lowX);
 +  var maxDate = this.toDataXCoord(highX);
    this.doZoomXDates_(minDate, maxDate);
  };
  
@@@ -1330,10 -1213,10 +1321,10 @@@ Dygraph.prototype.doZoomY_ = function(l
    // coordinates increase as you go up the screen.
    var valueRanges = [];
    for (var i = 0; i < this.axes_.length; i++) {
 -    var hi = this.toDataCoords(null, lowY, i);
 -    var low = this.toDataCoords(null, highY, i);
 -    this.axes_[i].valueWindow = [low[1], hi[1]];
 -    valueRanges.push([low[1], hi[1]]);
 +    var hi = this.toDataYCoord(lowY, i);
 +    var low = this.toDataYCoord(highY, i);
 +    this.axes_[i].valueWindow = [low, hi];
 +    valueRanges.push([low, hi]);
    }
  
    this.drawGraph_();
@@@ -1913,71 -1796,10 +1904,71 @@@ Dygraph.dateTicker = function(startDate
    }
  };
  
 +Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
 +  var vals = [];
 +  for (var power = -39; power <= 39; power++) {
 +    var range = Math.pow(10, power);
 +    for (var mult = 1; mult <= 9; mult++) {
 +      var val = range * mult;
 +      vals.push(val);
 +    }
 +  }
 +  return vals;
 +}();
 +
 +// val is the value to search for
 +// arry is the value over which to search
 +// if abs > 0, find the lowest entry greater than val
 +// if abs < 0, find the highest entry less than val
 +// if abs == 0, find the entry that equals val.
 +// Currently does not work when val is outside the range of arry's values.
 +Dygraph.binarySearch = function(val, arry, abs, low, high) {
 +  if (low == null || high == null) {
 +    low = 0;
 +    high = arry.length - 1;
 +  }
 +  if (low > high) {
 +    return -1;
 +  }
 +  if (abs == null) {
 +    abs = 0;
 +  }
 +  var validIndex = function(idx) {
 +    return idx >= 0 && idx < arry.length;
 +  }
 +  var mid = parseInt((low + high) / 2);
 +  var element = arry[mid];
 +  if (element == val) {
 +    return mid;
 +  }
 +  if (element > val) {
 +    if (abs > 0) {
 +      // Accept if element > val, but also if prior element < val.
 +      var idx = mid - 1;
 +      if (validIndex(idx) && arry[idx] < val) {
 +        return mid;
 +      }
 +    }
 +    return Dygraph.binarySearch(val, arry, abs, low, mid - 1);    
 +  }
 +  if (element < val) {
 +    if (abs < 0) {
 +      // Accept if element < val, but also if prior element > val.
 +      var idx = mid + 1;
 +      if (validIndex(idx) && arry[idx] > val) {
 +        return mid;
 +      }
 +    }
 +    return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
 +  }
 +}
 +
  /**
   * Add ticks when the x axis has numbers on it (instead of dates)
 - * @param {Number} startDate Start of the date window (millis since epoch)
 - * @param {Number} endDate End of the date window (millis since epoch)
 + * TODO(konigsberg): Update comment.
 + *
 + * @param {Number} minV minimum value
 + * @param {Number} maxV maximum value
   * @param self
   * @param {function} attribute accessor function.
   * @return {Array.<Object>} Array of {label, value} tuples.
@@@ -1995,70 -1817,43 +1986,70 @@@ Dygraph.numericTicks = function(minV, m
        ticks.push({v: vals[i]});
      }
    } else {
 -    // Basic idea:
 -    // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
 -    // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
 -    // The first spacing greater than pixelsPerYLabel is what we use.
 -    // TODO(danvk): version that works on a log scale.
 -    if (attr("labelsKMG2")) {
 -      var mults = [1, 2, 4, 8];
 -    } else {
 -      var mults = [1, 2, 5];
 +    if (axis_props && attr("logscale")) {
 +      var pixelsPerTick = attr('pixelsPerYLabel');
 +      // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
 +      var nTicks  = Math.floor(self.height_ / pixelsPerTick);
 +      var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
 +      var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
 +      if (minIdx == -1) {
 +        minIdx = 0;
 +      }
 +      if (maxIdx == -1) {
 +        maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
 +      }
 +      console.log(minIdx, maxIdx);
 +      // Count the number of tick values would appear, if we can get at least
 +      // nTicks / 4 accept them.
 +      if (maxIdx - minIdx >= nTicks / 4) {
 +        for (var idx = maxIdx; idx >= minIdx; idx--) {
 +          var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
 +          ticks.push({ v: tickValue });
 +        }
 +        // Since we went in backwards order.
 +        ticks.reverse();
 +      }
      }
 -    var scale, low_val, high_val, nTicks;
 -    // TODO(danvk): make it possible to set this for x- and y-axes independently.
 -    var pixelsPerTick = attr('pixelsPerYLabel');
 -    for (var i = -10; i < 50; i++) {
 +    // ticks.length won't be 0 if the log scale function finds values to insert.
 +    if (ticks.length == 0) {
 +      // Basic idea:
 +      // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
 +      // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
 +      // The first spacing greater than pixelsPerYLabel is what we use.
 +      // TODO(danvk): version that works on a log scale.
        if (attr("labelsKMG2")) {
 -        var base_scale = Math.pow(16, i);
 +        var mults = [1, 2, 4, 8];
        } else {
 -        var base_scale = Math.pow(10, i);
 +        var mults = [1, 2, 5];
        }
 -      for (var j = 0; j < mults.length; j++) {
 -        scale = base_scale * mults[j];
 -        low_val = Math.floor(minV / scale) * scale;
 -        high_val = Math.ceil(maxV / scale) * scale;
 -        nTicks = Math.abs(high_val - low_val) / scale;
 -        var spacing = self.height_ / nTicks;
 -        // wish I could break out of both loops at once...
 +      var scale, low_val, high_val, nTicks;
 +      // TODO(danvk): make it possible to set this for x- and y-axes independently.
 +      var pixelsPerTick = attr('pixelsPerYLabel');
 +      for (var i = -10; i < 50; i++) {
 +        if (attr("labelsKMG2")) {
 +          var base_scale = Math.pow(16, i);
 +        } else {
 +          var base_scale = Math.pow(10, i);
 +        }
 +        for (var j = 0; j < mults.length; j++) {
 +          scale = base_scale * mults[j];
 +          low_val = Math.floor(minV / scale) * scale;
 +          high_val = Math.ceil(maxV / scale) * scale;
 +          nTicks = Math.abs(high_val - low_val) / scale;
 +          var spacing = self.height_ / nTicks;
 +          // wish I could break out of both loops at once...
 +          if (spacing > pixelsPerTick) break;
 +        }
          if (spacing > pixelsPerTick) break;
        }
 -      if (spacing > pixelsPerTick) break;
 -    }
  
 -    // Construct the set of ticks.
 -    // Allow reverse y-axis if it's explicitly requested.
 -    if (low_val > high_val) scale *= -1;
 -    for (var i = 0; i < nTicks; i++) {
 -      var tickV = low_val + i * scale;
 -      ticks.push( {v: tickV} );
 +      // Construct the set of ticks.
 +      // Allow reverse y-axis if it's explicitly requested.
 +      if (low_val > high_val) scale *= -1;
 +      for (var i = 0; i < nTicks; i++) {
 +        var tickV = low_val + i * scale;
 +        ticks.push( {v: tickV} );
 +      }
      }
    }
  
    }
    var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter'); 
  
 +  // Add labels to the ticks.
    for (var i = 0; i < ticks.length; i++) {
 -    var tickV = ticks[i].v;
 -    var absTickV = Math.abs(tickV);
 -    var label;
 -    if (formatter != undefined) {
 -      label = formatter(tickV);
 -    } else {
 -      label = Dygraph.round_(tickV, 2);
 -    }
 -    if (k_labels.length) {
 -      // Round up to an appropriate unit.
 -      var n = k*k*k*k;
 -      for (var j = 3; j >= 0; j--, n /= k) {
 -        if (absTickV >= n) {
 -          label = Dygraph.round_(tickV / n, 1) + k_labels[j];
 -          break;
 +    if (ticks[i].label == null) {
 +      var tickV = ticks[i].v;
 +      var absTickV = Math.abs(tickV);
 +      var label;
 +      if (formatter != undefined) {
 +        label = formatter(tickV);
 +      } else {
 +        label = Dygraph.round_(tickV, 2);
 +      }
 +      if (k_labels.length) {
 +        // Round up to an appropriate unit.
 +        var n = k*k*k*k;
 +        for (var j = 3; j >= 0; j--, n /= k) {
 +          if (absTickV >= n) {
 +            label = Dygraph.round_(tickV / n, 1) + k_labels[j];
 +            break;
 +          }
          }
        }
 +      ticks[i].label = label;
      }
 -    ticks[i].label = label;
    }
    return ticks;
  };
@@@ -2151,7 -1943,7 +2142,7 @@@ Dygraph.prototype.extremeValues_ = func
   * number of axes, rolling averages, etc.
   */
  Dygraph.prototype.predraw_ = function() {
 -  // TODO(danvk): move more computations out of drawGraph_ and into here.
 +  // TODO(danvk): movabilitye more computations out of drawGraph_ and into here.
    this.computeYAxes_();
  
    // Create a new plotter.
  };
  
  /**
 -=======
   * 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.
@@@ -2205,24 -1998,12 +2196,24 @@@ Dygraph.prototype.drawGraph_ = function
  
      var seriesName = this.attr_("labels")[i];
      var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
 +    var logScale = this.attr_('logscale', i);
  
      var series = [];
      for (var j = 0; j < data.length; j++) {
 -      if (data[j][i] != null || !connectSeparatedPoints) {
 -        var date = data[j][0];
 -        series.push([date, data[j][i]]);
 +      var date = data[j][0];
 +      var point = data[j][i];
 +      if (logScale) {
 +        // 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) {
 +          point = null;
 +        }
 +        series.push([date, point]);
 +      } else {
 +        if (point != null || !connectSeparatedPoints) {
 +          series.push([date, point]);
 +        }
        }
      }
  
@@@ -2353,8 -2134,7 +2344,8 @@@ Dygraph.prototype.computeYAxes_ = funct
      'pixelsPerYLabel',
      'yAxisLabelWidth',
      'axisLabelFontSize',
 -    'axisTickSize'
 +    'axisTickSize',
 +    'logscale'
    ];
  
    // Copy global axis options over to the first axis.
@@@ -2466,26 -2246,18 +2457,26 @@@ Dygraph.prototype.computeYAxisRanges_ 
        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;
 -      }
 +      var maxAxisY;
 +      var minAxisY;
 +      if (axis.logscale) {
 +        var maxAxisY = maxY + 0.1 * span;
 +        var minAxisY = minY;
 +      } else {
 +        var maxAxisY = maxY + 0.1 * span;
 +        var minAxisY = minY - 0.1 * span;
  
 -      if (this.attr_("includeZero")) {
 -        if (maxY < 0) maxAxisY = 0;
 -        if (minY > 0) minAxisY = 0;
 +        // 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];