function getAutoRange(gd, ax) {
    var i, j;
    var newRange = [];

    var fullLayout = gd._fullLayout;
    var getPadMin = makePadFn(fullLayout, ax, 0);
    var getPadMax = makePadFn(fullLayout, ax, 1);
    var extremes = concatExtremes(gd, ax);
    var minArray = extremes.min;
    var maxArray = extremes.max;

    if(minArray.length === 0 || maxArray.length === 0) {
        return Lib.simpleMap(ax.range, ax.r2l);
    }

    var minmin = minArray[0].val;
    var maxmax = maxArray[0].val;

    for(i = 1; i < minArray.length; i++) {
        if(minmin !== maxmax) break;
        minmin = Math.min(minmin, minArray[i].val);
    }
    for(i = 1; i < maxArray.length; i++) {
        if(minmin !== maxmax) break;
        maxmax = Math.max(maxmax, maxArray[i].val);
    }

    var autorange = ax.autorange;
    var axReverse =
        autorange === 'reversed' ||
        autorange === 'min reversed' ||
        autorange === 'max reversed';

    if(!axReverse && ax.range) {
        var rng = Lib.simpleMap(ax.range, ax.r2l);
        axReverse = rng[1] < rng[0];
    }

    // one-time setting to easily reverse the axis
    // when plotting from code
    if(ax.autorange === 'reversed') {
        ax.autorange = true;
    }

    var rangeMode = ax.rangemode;
    var toZero = rangeMode === 'tozero';
    var nonNegative = rangeMode === 'nonnegative';
    var axLen = ax._length;
    // don't allow padding to reduce the data to < 10% of the length
    var minSpan = axLen / 10;

    var mbest = 0;
    var minpt, maxpt, minbest, maxbest, dp, dv;

    for(i = 0; i < minArray.length; i++) {
        minpt = minArray[i];
        for(j = 0; j < maxArray.length; j++) {
            maxpt = maxArray[j];
            dv = maxpt.val - minpt.val - calcBreaksLength(ax, minpt.val, maxpt.val);
            if(dv > 0) {
                dp = axLen - getPadMin(minpt) - getPadMax(maxpt);
                if(dp > minSpan) {
                    if(dv / dp > mbest) {
                        minbest = minpt;
                        maxbest = maxpt;
                        mbest = dv / dp;
                    }
                } else if(dv / axLen > mbest) {
                    // in case of padding longer than the axis
                    // at least include the unpadded data values.
                    minbest = {val: minpt.val, nopad: 1};
                    maxbest = {val: maxpt.val, nopad: 1};
                    mbest = dv / axLen;
                }
            }
        }
    }

    function maximumPad(prev, pt) {
        return Math.max(prev, getPadMax(pt));
    }

    if(minmin === maxmax) {
        var lower = minmin - 1;
        var upper = minmin + 1;
        if(toZero) {
            if(minmin === 0) {
                // The only value we have on this axis is 0, and we want to
                // autorange so zero is one end.
                // In principle this could be [0, 1] or [-1, 0] but usually
                // 'tozero' pins 0 to the low end, so follow that.
                newRange = [0, 1];
            } else {
                var maxPad = (minmin > 0 ? maxArray : minArray).reduce(maximumPad, 0);
                // we're pushing a single value away from the edge due to its
                // padding, with the other end clamped at zero
                // 0.5 means don't push it farther than the center.
                var rangeEnd = minmin / (1 - Math.min(0.5, maxPad / axLen));
                newRange = minmin > 0 ? [0, rangeEnd] : [rangeEnd, 0];
            }
        } else if(nonNegative) {
            newRange = [Math.max(0, lower), Math.max(1, upper)];
        } else {
            newRange = [lower, upper];
        }
    } else {
        if(toZero) {
            if(minbest.val >= 0) {
                minbest = {val: 0, nopad: 1};
            }
            if(maxbest.val <= 0) {
                maxbest = {val: 0, nopad: 1};
            }
        } else if(nonNegative) {
            if(minbest.val - mbest * getPadMin(minbest) < 0) {
                minbest = {val: 0, nopad: 1};
            }
            if(maxbest.val <= 0) {
                maxbest = {val: 1, nopad: 1};
            }
        }

        // in case it changed again...
        mbest = (maxbest.val - minbest.val - calcBreaksLength(ax, minpt.val, maxpt.val)) /
            (axLen - getPadMin(minbest) - getPadMax(maxbest));

        newRange = [
            minbest.val - mbest * getPadMin(minbest),
            maxbest.val + mbest * getPadMax(maxbest)
        ];
    }

    newRange = applyAutorangeOptions(newRange, ax);

    if(ax.limitRange) ax.limitRange();

    // maintain reversal
    if(axReverse) newRange.reverse();

    return Lib.simpleMap(newRange, ax.l2r || Number);
}
getAutoRange

Collects all _extremes values corresponding to a given axis
and computes its auto range.

Note that getAutoRange uses return values from findExtremes.

@param {object} gd:
  graph div object with filled-in fullData and fullLayout, in particular
  with filled-in '_extremes' containers:
  {
     val: calcdata value,
     pad: extra pixels beyond this value,
     extrapad: bool, does this point want 5% extra padding
  }
@param {object} ax:
  full axis object, in particular with filled-in '_traceIndices'
  and '_annIndices' / '_shapeIndices' if applicable
@return {array}
  an array of [min, max]. These are calcdata for log and category axes
  and data for linear and date axes.

TODO: we want to change log to data as well, but it's hard to do this
maintaining backward compatibility. category will always have to use calcdata
though, because otherwise values between categories (or outside all categories)
would be impossible.