diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index e87752bb1e7..65b753e7077 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -34,63 +34,54 @@ function annAutorange(gd) { Lib.filterVisible(fullLayout.annotations).forEach(function(ann) { var xa = Axes.getFromId(gd, ann.xref); var ya = Axes.getFromId(gd, ann.yref); - var headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; - var startHeadSize = 3 * ann.startarrowsize * ann.arrowwidth || 0; - var headPlus, headMinus, startHeadPlus, startHeadMinus; - - if(xa) { - headPlus = headSize + ann.xshift; - headMinus = headSize - ann.xshift; - startHeadPlus = startHeadSize + ann.xshift; - startHeadMinus = startHeadSize - ann.xshift; + ann._extremes = {}; + if(xa) calcAxisExpansion(ann, xa); + if(ya) calcAxisExpansion(ann, ya); + }); +} - if(ann.axref === ann.xref) { - // expand for the arrowhead (padded by arrowhead) - Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: headPlus, - ppadminus: headMinus - }); - // again for the textbox (padded by textbox) - Axes.expand(xa, [xa.r2c(ann.ax)], { - ppadplus: Math.max(ann._xpadplus, startHeadPlus), - ppadminus: Math.max(ann._xpadminus, startHeadMinus) - }); - } - else { - startHeadPlus = ann.ax ? startHeadPlus + ann.ax : startHeadPlus; - startHeadMinus = ann.ax ? startHeadMinus - ann.ax : startHeadMinus; - Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: Math.max(ann._xpadplus, headPlus, startHeadPlus), - ppadminus: Math.max(ann._xpadminus, headMinus, startHeadMinus) - }); - } - } +function calcAxisExpansion(ann, ax) { + var axId = ax._id; + var letter = axId.charAt(0); + var pos = ann[letter]; + var apos = ann['a' + letter]; + var ref = ann[letter + 'ref']; + var aref = ann['a' + letter + 'ref']; + var padplus = ann['_' + letter + 'padplus']; + var padminus = ann['_' + letter + 'padminus']; + var shift = {x: 1, y: -1}[letter] * ann[letter + 'shift']; + var headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; + var headPlus = headSize + shift; + var headMinus = headSize - shift; + var startHeadSize = 3 * ann.startarrowsize * ann.arrowwidth || 0; + var startHeadPlus = startHeadSize + shift; + var startHeadMinus = startHeadSize - shift; + var extremes; - if(ya) { - headPlus = headSize - ann.yshift; - headMinus = headSize + ann.yshift; - startHeadPlus = startHeadSize - ann.yshift; - startHeadMinus = startHeadSize + ann.yshift; + if(aref === ref) { + // expand for the arrowhead (padded by arrowhead) + var extremeArrowHead = Axes.findExtremes(ax, [ax.r2c(pos)], { + ppadplus: headPlus, + ppadminus: headMinus + }); + // again for the textbox (padded by textbox) + var extremeText = Axes.findExtremes(ax, [ax.r2c(apos)], { + ppadplus: Math.max(padplus, startHeadPlus), + ppadminus: Math.max(padminus, startHeadMinus) + }); + extremes = { + min: [extremeArrowHead.min[0], extremeText.min[0]], + max: [extremeArrowHead.max[0], extremeText.max[0]] + }; + } else { + startHeadPlus = apos ? startHeadPlus + apos : startHeadPlus; + startHeadMinus = apos ? startHeadMinus - apos : startHeadMinus; + extremes = Axes.findExtremes(ax, [ax.r2c(pos)], { + ppadplus: Math.max(padplus, headPlus, startHeadPlus), + ppadminus: Math.max(padminus, headMinus, startHeadMinus) + }); + } - if(ann.ayref === ann.yref) { - Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: headPlus, - ppadminus: headMinus - }); - Axes.expand(ya, [ya.r2c(ann.ay)], { - ppadplus: Math.max(ann._ypadplus, startHeadPlus), - ppadminus: Math.max(ann._ypadminus, startHeadMinus) - }); - } - else { - startHeadPlus = ann.ay ? startHeadPlus + ann.ay : startHeadPlus; - startHeadMinus = ann.ay ? startHeadMinus - ann.ay : startHeadMinus; - Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: Math.max(ann._ypadplus, headPlus, startHeadPlus), - ppadminus: Math.max(ann._ypadminus, headMinus, startHeadMinus) - }); - } - } - }); + ann._extremes[axId] = extremes; } diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js index 2ca55c44bae..b04657c2995 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -48,6 +48,11 @@ function handleAnnotationDefaults(annIn, annOut, fullLayout) { // xref, yref var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper'); + if(axRef !== 'paper') { + var ax = Axes.getFromId(gdMock, axRef); + ax._annIndices.push(annOut._index); + } + // x, y Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5); diff --git a/src/components/errorbars/calc.js b/src/components/errorbars/calc.js index 124211f913b..5340cdc6b27 100644 --- a/src/components/errorbars/calc.js +++ b/src/components/errorbars/calc.js @@ -21,16 +21,15 @@ module.exports = function calc(gd) { var calcdata = gd.calcdata; for(var i = 0; i < calcdata.length; i++) { - var calcTrace = calcdata[i], - trace = calcTrace[0].trace; - - if(!Registry.traceIs(trace, 'errorBarsOK')) continue; - - var xa = Axes.getFromId(gd, trace.xaxis), - ya = Axes.getFromId(gd, trace.yaxis); - - calcOneAxis(calcTrace, trace, xa, 'x'); - calcOneAxis(calcTrace, trace, ya, 'y'); + var calcTrace = calcdata[i]; + var trace = calcTrace[0].trace; + + if(trace.visible === true && Registry.traceIs(trace, 'errorBarsOK')) { + var xa = Axes.getFromId(gd, trace.xaxis); + var ya = Axes.getFromId(gd, trace.yaxis); + calcOneAxis(calcTrace, trace, xa, 'x'); + calcOneAxis(calcTrace, trace, ya, 'y'); + } } }; @@ -57,5 +56,8 @@ function calcOneAxis(calcTrace, trace, axis, coord) { } } - Axes.expand(axis, vals, {padded: true}); + var extremes = Axes.findExtremes(axis, vals, {padded: true}); + var axId = axis._id; + trace._extremes[axId].min = trace._extremes[axId].min.concat(extremes.min); + trace._extremes[axId].max = trace._extremes[axId].max.concat(extremes.max); } diff --git a/src/components/rangeslider/calc_autorange.js b/src/components/rangeslider/calc_autorange.js index c722836a453..8681877621b 100644 --- a/src/components/rangeslider/calc_autorange.js +++ b/src/components/rangeslider/calc_autorange.js @@ -21,15 +21,12 @@ module.exports = function calcAutorange(gd) { // this step in subsequent draw calls. for(var i = 0; i < axes.length; i++) { - var ax = axes[i], - opts = ax[constants.name]; + var ax = axes[i]; + var opts = ax[constants.name]; - // Don't try calling getAutoRange if _min and _max are filled in. - // This happens on updates where the calc step is skipped. - - if(opts && opts.visible && opts.autorange && ax._min.length && ax._max.length) { + if(opts && opts.visible && opts.autorange) { opts._input.autorange = true; - opts._input.range = opts.range = getAutoRange(ax); + opts._input.range = opts.range = getAutoRange(gd, ax); } } }; diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 967cbcc4992..a05a6bc11c2 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -24,6 +24,7 @@ module.exports = function calcAutorange(gd) { for(var i = 0; i < shapeList.length; i++) { var shape = shapeList[i]; + shape._extremes = {}; var ax, bounds; @@ -33,8 +34,9 @@ module.exports = function calcAutorange(gd) { ax = Axes.getFromId(gd, shape.xref); bounds = shapeBounds(ax, vx0, vx1, shape.path, constants.paramIsX); - - if(bounds) Axes.expand(ax, bounds, calcXPaddingOptions(shape)); + if(bounds) { + shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcXPaddingOptions(shape)); + } } if(shape.yref !== 'paper') { @@ -43,7 +45,9 @@ module.exports = function calcAutorange(gd) { ax = Axes.getFromId(gd, shape.yref); bounds = shapeBounds(ax, vy0, vy1, shape.path, constants.paramIsY); - if(bounds) Axes.expand(ax, bounds, calcYPaddingOptions(shape)); + if(bounds) { + shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcYPaddingOptions(shape)); + } } } }; diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index bc2934e0fee..fff7f9d612b 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -61,6 +61,7 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { if(axRef !== 'paper') { ax = Axes.getFromId(gdMock, axRef); + ax._shapeIndices.push(shapeOut._index); r2pos = helpers.rangeToShapePosition(ax); pos2r = helpers.shapePositionToRange(ax); } diff --git a/src/lib/index.js b/src/lib/index.js index 6e0a3e24a61..9df929ce3e4 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -150,6 +150,21 @@ lib.isIndex = function(v, len) { lib.noop = require('./noop'); lib.identity = require('./identity'); +/** + * create an array of length 'cnt' filled with 'v' at all indices + * + * @param {any} v + * @param {number} cnt + * @return {array} + */ +lib.repeat = function(v, cnt) { + var out = new Array(cnt); + for(var i = 0; i < cnt; i++) { + out[i] = v; + } + return out; +}; + /** * swap x and y of the same attribute in container cont * specify attr with a ? in place of x/y diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index d45aa6683d9..dcbcec47112 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -576,7 +576,7 @@ exports.doAutoRangeAndConstraints = function(gd) { for(var i = 0; i < axList.length; i++) { var ax = axList[i]; cleanAxisConstraints(gd, ax); - doAutoRange(ax); + doAutoRange(gd, ax); } enforceAxisConstraints(gd); diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 63017327ee0..eaab8f37b5d 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var isNumeric = require('fast-isnumeric'); @@ -18,51 +17,69 @@ module.exports = { getAutoRange: getAutoRange, makePadFn: makePadFn, doAutoRange: doAutoRange, - expand: expand + findExtremes: findExtremes, + concatExtremes: concatExtremes }; -// Find the autorange for this axis -// -// assumes ax._min and ax._max have already been set by calling axes.expand -// using calcdata from all traces. These are arrays of objects: -// { -// val: calcdata value, -// pad: extra pixels beyond this value, -// extrapad: bool, does this point want 5% extra padding -// } -// -// Returns 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. -function getAutoRange(ax) { +/** + * 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. + */ +function getAutoRange(gd, ax) { + var i, j; var newRange = []; - var minmin = ax._min[0].val; - var maxmax = ax._max[0].val; - var mbest = 0; - var axReverse = false; var getPad = makePadFn(ax); + var extremes = concatExtremes(gd, ax); + var minArray = extremes.min; + var maxArray = extremes.max; - var i, j, minpt, maxpt, minbest, maxbest, dp, dv; + 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 < ax._min.length; i++) { + for(i = 1; i < minArray.length; i++) { if(minmin !== maxmax) break; - minmin = Math.min(minmin, ax._min[i].val); + minmin = Math.min(minmin, minArray[i].val); } - for(i = 1; i < ax._max.length; i++) { + for(i = 1; i < maxArray.length; i++) { if(minmin !== maxmax) break; - maxmax = Math.max(maxmax, ax._max[i].val); + maxmax = Math.max(maxmax, maxArray[i].val); } + var axReverse = false; + if(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') { @@ -70,10 +87,13 @@ function getAutoRange(ax) { ax.autorange = true; } - for(i = 0; i < ax._min.length; i++) { - minpt = ax._min[i]; - for(j = 0; j < ax._max.length; j++) { - maxpt = ax._max[j]; + 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; dp = ax._length - getPad(minpt) - getPad(maxpt); if(dv > 0 && dp > 0 && dv / dp > mbest) { @@ -89,11 +109,9 @@ function getAutoRange(ax) { var upper = minmin + 1; if(ax.rangemode === 'tozero') { newRange = minmin < 0 ? [lower, 0] : [0, upper]; - } - else if(ax.rangemode === 'nonnegative') { + } else if(ax.rangemode === 'nonnegative') { newRange = [Math.max(0, lower), Math.max(0, upper)]; - } - else { + } else { newRange = [lower, upper]; } } @@ -133,11 +151,9 @@ function getAutoRange(ax) { if(ax.rangemode === 'tozero') { if(newRange[0] < 0) { newRange = [newRange[0], 0]; - } - else if(newRange[0] > 0) { + } else if(newRange[0] > 0) { newRange = [0, newRange[0]]; - } - else { + } else { newRange = [0, 1]; } } @@ -173,15 +189,45 @@ function makePadFn(ax) { return function getPad(pt) { return pt.pad + (pt.extrapad ? extrappad : 0); }; } -function doAutoRange(ax) { +function concatExtremes(gd, ax) { + var axId = ax._id; + var fullData = gd._fullData; + var fullLayout = gd._fullLayout; + var minArray = []; + var maxArray = []; + var i, j, d; + + function _concat(cont, indices) { + for(i = 0; i < indices.length; i++) { + var item = cont[indices[i]]; + var extremes = (item._extremes || {})[axId]; + if(item.visible === true && extremes) { + for(j = 0; j < extremes.min.length; j++) { + d = extremes.min[j]; + collapseMinArray(minArray, d.val, d.pad, {extrapad: d.extrapad}); + } + for(j = 0; j < extremes.max.length; j++) { + d = extremes.max[j]; + collapseMaxArray(maxArray, d.val, d.pad, {extrapad: d.extrapad}); + } + } + } + } + + _concat(fullData, ax._traceIndices); + _concat(fullLayout.annotations || [], ax._annIndices || []); + _concat(fullLayout.shapes || [], ax._shapeIndices || []); + + return {min: minArray, max: maxArray}; +} + +function doAutoRange(gd, ax) { if(!ax._length) ax.setScale(); - // TODO do we really need this? - var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length); var axIn; - if(ax.autorange && hasDeps) { - ax.range = getAutoRange(ax); + if(ax.autorange) { + ax.range = getAutoRange(gd, ax); ax._r = ax.range.slice(); ax._rl = Lib.simpleMap(ax._r, ax.r2l); @@ -198,11 +244,7 @@ function doAutoRange(ax) { var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; if(axeRangeOpts) { if(axeRangeOpts.rangemode === 'auto') { - if(hasDeps) { - axeRangeOpts.range = getAutoRange(ax); - } else { - axeRangeOpts.range = ax._rangeInitial ? ax._rangeInitial.slice() : ax.range.slice(); - } + axeRangeOpts.range = getAutoRange(gd, ax); } } axIn = ax._anchorAxis._input; @@ -210,18 +252,27 @@ function doAutoRange(ax) { } } -/* - * expand: if autoranging, include new data in the outer limits for this axis. - * Note that `expand` is called during `calc`, when we don't yet know the axis +/** + * findExtremes + * + * Find min/max extremes of an array of coordinates on a given axis. + * + * Note that findExtremes is called during `calc`, when we don't yet know the axis * length; all the inputs should be based solely on the trace data, nothing * about the axis layout. + * * Note that `ppad` and `vpad` as well as their asymmetric variants refer to * the before and after padding of the passed `data` array, not to the whole axis. * - * @param {object} ax: the axis being expanded. The result will be more entries - * in ax._min and ax._max if necessary to include the new data - * @param {array} data: an array of numbers (ie already run through ax.d2c) - * @param {object} options: available keys are: + * @param {object} ax: full axis object + * relies on + * - ax.type + * - ax._m (just its sign) + * - ax.d2l + * @param {array} data: + * array of numbers (i.e. already run though ax.d2c) + * @param {object} options: + * available keys are: * vpad: (number or number array) pad values (data value +-vpad) * ppad: (number or number array) pad pixels (pixel location +-ppad) * ppadplus, ppadminus, vpadplus, vpadminus: @@ -230,21 +281,28 @@ function doAutoRange(ax) { * (unless one end is overridden by tozero) * tozero: (boolean) make sure to include zero if axis is linear, * and make it a tight bound if possible + * + * @return {object} + * - min {array of objects} + * - max {array of objects} + * each object item has fields: + * - val {number} + * - pad {number} + * - extrappad {number} */ -function expand(ax, data, options) { - if(!ax._min) ax._min = []; - if(!ax._max) ax._max = []; +function findExtremes(ax, data, options) { if(!options) options = {}; if(!ax._m) ax.setScale(); + var minArray = []; + var maxArray = []; + var len = data.length; var extrapad = options.padded || false; var tozero = options.tozero && (ax.type === 'linear' || ax.type === '-'); - var isLog = (ax.type === 'log'); - - var i, j, k, v, di, dmin, dmax, ppadiplus, ppadiminus, includeThis, vmin, vmax; - + var isLog = ax.type === 'log'; var hasArrayOption = false; + var i, v, di, dmin, dmax, ppadiplus, ppadiminus, vmin, vmax; function makePadAccessor(item) { if(Array.isArray(item)) { @@ -277,8 +335,7 @@ function expand(ax, data, options) { if(v < vmin && v > 0) vmin = v; if(v > vmax && v < FP_SAFE) vmax = v; } - } - else { + } else { for(i = 0; i < len; i++) { v = data[i]; if(v < vmin && v > -FP_SAFE) vmin = v; @@ -290,6 +347,8 @@ function expand(ax, data, options) { len = 2; } + var collapseOpts = {tozero: tozero, extrapad: extrapad}; + function addItem(i) { di = data[i]; if(!isNumeric(di)) return; @@ -310,49 +369,11 @@ function expand(ax, data, options) { dmin = Math.min(0, dmin); dmax = Math.max(0, dmax); } - - for(k = 0; k < 2; k++) { - var newVal = k ? dmax : dmin; - if(goodNumber(newVal)) { - var extremes = k ? ax._max : ax._min; - var newPad = k ? ppadiplus : ppadiminus; - var atLeastAsExtreme = k ? greaterOrEqual : lessOrEqual; - - includeThis = true; - /* - * Take items v from ax._min/_max and compare them to the presently active point: - * - Since we don't yet know the relationship between pixels and values - * (that's what we're trying to figure out!) AND we don't yet know how - * many pixels `extrapad` represents (it's going to be 5% of the length, - * but we don't want to have to redo _min and _max just because length changed) - * two point must satisfy three criteria simultaneously for one to supersede the other: - * - at least as extreme a `val` - * - at least as big a `pad` - * - an unpadded point cannot supersede a padded point, but any other combination can - * - * - If the item supersedes the new point, set includethis false - * - If the new pt supersedes the item, delete it from ax._min/_max - */ - for(j = 0; j < extremes.length && includeThis; j++) { - v = extremes[j]; - if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) { - includeThis = false; - break; - } - else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) { - extremes.splice(j, 1); - j--; - } - } - if(includeThis) { - var clipAtZero = (tozero && newVal === 0); - extremes.push({ - val: newVal, - pad: clipAtZero ? 0 : newPad, - extrapad: clipAtZero ? false : extrapad - }); - } - } + if(goodNumber(dmin)) { + collapseMinArray(minArray, dmin, ppadiminus, collapseOpts); + } + if(goodNumber(dmax)) { + collapseMaxArray(maxArray, dmax, ppadiplus, collapseOpts); } } @@ -362,6 +383,78 @@ function expand(ax, data, options) { var iMax = Math.min(6, len); for(i = 0; i < iMax; i++) addItem(i); for(i = len - 1; i >= iMax; i--) addItem(i); + + return {min: minArray, max: maxArray}; +} + +function collapseMinArray(array, newVal, newPad, opts) { + collapseArray(array, newVal, newPad, opts, lessOrEqual); +} + +function collapseMaxArray(array, newVal, newPad, opts) { + collapseArray(array, newVal, newPad, opts, greaterOrEqual); +} + +/** + * collapseArray + * + * Takes items from 'array' and compares them to 'newVal', 'newPad'. + * + * @param {array} array: + * current set of min or max extremes + * @param {number} newVal: + * new value to compare against + * @param {number} newPad: + * pad value associated with 'newVal' + * @param {object} opts: + * - tozero {boolean} + * - extrapad {number} + * @param {function} atLeastAsExtreme: + * comparison function, use + * - lessOrEqual for min 'array' and + * - greaterOrEqual for max 'array' + * + * In practice, 'array' is either + * - 'extremes[ax._id].min' or + * - 'extremes[ax._id].max + * found in traces and layout items that affect autorange. + * + * Since we don't yet know the relationship between pixels and values + * (that's what we're trying to figure out!) AND we don't yet know how + * many pixels `extrapad` represents (it's going to be 5% of the length, + * but we don't want to have to redo calc just because length changed) + * two point must satisfy three criteria simultaneously for one to supersede the other: + * - at least as extreme a `val` + * - at least as big a `pad` + * - an unpadded point cannot supersede a padded point, but any other combination can + * + * Then: + * - If the item supersedes the new point, set includeThis false + * - If the new pt supersedes the item, delete it from 'array' + */ +function collapseArray(array, newVal, newPad, opts, atLeastAsExtreme) { + var tozero = opts.tozero; + var extrapad = opts.extrapad; + var includeThis = true; + + for(var j = 0; j < array.length && includeThis; j++) { + var v = array[j]; + if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) { + includeThis = false; + break; + } else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) { + array.splice(j, 1); + j--; + } + } + if(includeThis) { + var clipAtZero = (tozero && newVal === 0); + array.push({ + val: newVal, + pad: clipAtZero ? 0 : newPad, + extrapad: clipAtZero ? false : extrapad + }); + } } // In order to stop overflow errors, don't consider points diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 38d56e912ca..97b06b9c64a 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -48,8 +48,8 @@ axes.getFromId = axisIds.getFromId; axes.getFromTrace = axisIds.getFromTrace; var autorange = require('./autorange'); -axes.expand = autorange.expand; axes.getAutoRange = autorange.getAutoRange; +axes.findExtremes = autorange.findExtremes; /* * find the list of possible axes to reference with an xref or yref attribute diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index f88bb397c10..70859a2ad0b 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -12,6 +12,7 @@ var id2name = require('./axis_ids').id2name; var scaleZoom = require('./scale_zoom'); var makePadFn = require('./autorange').makePadFn; +var concatExtremes = require('./autorange').concatExtremes; var ALMOST_EQUAL = require('../../constants/numerical').ALMOST_EQUAL; @@ -112,7 +113,7 @@ exports.enforce = function enforceAxisConstraints(gd) { factor *= rangeShrunk; } - if(ax.autorange && ax._min.length && ax._max.length) { + if(ax.autorange) { /* * range & factor may need to change because range was * calculated for the larger scaling, so some pixel @@ -140,18 +141,21 @@ exports.enforce = function enforceAxisConstraints(gd) { updateDomain(ax, factor); ax.setScale(); var m = Math.abs(ax._m); + var extremes = concatExtremes(gd, ax); + var minArray = extremes.min; + var maxArray = extremes.max; var newVal; var k; - for(k = 0; k < ax._min.length; k++) { - newVal = ax._min[k].val - getPad(ax._min[k]) / m; + for(k = 0; k < minArray.length; k++) { + newVal = minArray[k].val - getPad(minArray[k]) / m; if(newVal > outerMin && newVal < rangeMin) { rangeMin = newVal; } } - for(k = 0; k < ax._max.length; k++) { - newVal = ax._max[k].val + getPad(ax._max[k]) / m; + for(k = 0; k < maxArray.length; k++) { + newVal = maxArray[k].val + getPad(maxArray[k]) / m; if(newVal < outerMax && newVal > rangeMax) { rangeMax = newVal; } diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 0ccbc57a4c4..c1619c91fd3 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -216,7 +216,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback var className = (_module.layerName || name + 'layer'); var plotMethod = _module.plot; - // plot all traces of this type on this subplot at once + // plot all visible traces of this type on this subplot at once cdModuleAndOthers = getModuleCalcData(cdSubplot, plotMethod); cdModule = cdModuleAndOthers[0]; // don't need to search the found traces again - in fact we need to NOT diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index f17c71f7f19..6c2f824781e 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -9,7 +9,6 @@ 'use strict'; -var Registry = require('../../registry'); var Lib = require('../../lib'); var Color = require('../../components/color'); var Template = require('../../plot_api/plot_template'); @@ -20,31 +19,57 @@ var handleTypeDefaults = require('./type_defaults'); var handleAxisDefaults = require('./axis_defaults'); var handleConstraintDefaults = require('./constraint_defaults'); var handlePositionDefaults = require('./position_defaults'); + var axisIds = require('./axis_ids'); +var id2name = axisIds.id2name; +var name2id = axisIds.name2id; +var Registry = require('../../registry'); +var traceIs = Registry.traceIs; +var getComponentMethod = Registry.getComponentMethod; + +function appendList(cont, k, item) { + if(Array.isArray(cont[k])) cont[k].push(item); + else cont[k] = [item]; +} module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { + var ax2traces = {}; var xaCheater = {}; var xaNonCheater = {}; var outerTicks = {}; var noGrids = {}; - var i; + var i, j; // look for axes in the data for(i = 0; i < fullData.length; i++) { var trace = fullData[i]; - - if(!Registry.traceIs(trace, 'cartesian') && !Registry.traceIs(trace, 'gl2d')) { - continue; + if(!traceIs(trace, 'cartesian') && !traceIs(trace, 'gl2d')) continue; + + var xaName; + if(trace.xaxis) { + xaName = id2name(trace.xaxis); + appendList(ax2traces, xaName, trace); + } else if(trace.xaxes) { + for(j = 0; j < trace.xaxes.length; j++) { + appendList(ax2traces, id2name(trace.xaxes[j]), trace); + } } - var xaName = axisIds.id2name(trace.xaxis); - var yaName = axisIds.id2name(trace.yaxis); + var yaName; + if(trace.yaxis) { + yaName = id2name(trace.yaxis); + appendList(ax2traces, yaName, trace); + } else if(trace.yaxes) { + for(j = 0; j < trace.yaxes.length; j++) { + appendList(ax2traces, id2name(trace.yaxes[j]), trace); + } + } // Two things trigger axis visibility: // 1. is not carpet // 2. carpet that's not cheater - if(!Registry.traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) { + if(!traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) { if(xaName) xaNonCheater[xaName] = 1; } @@ -57,22 +82,22 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } // check for default formatting tweaks - if(Registry.traceIs(trace, '2dMap')) { - outerTicks[xaName] = true; - outerTicks[yaName] = true; + if(traceIs(trace, '2dMap')) { + outerTicks[xaName] = 1; + outerTicks[yaName] = 1; } - if(Registry.traceIs(trace, 'oriented')) { + if(traceIs(trace, 'oriented')) { var positionAxis = trace.orientation === 'h' ? yaName : xaName; - noGrids[positionAxis] = true; + noGrids[positionAxis] = 1; } } var subplots = layoutOut._subplots; var xIds = subplots.xaxis; var yIds = subplots.yaxis; - var xNames = Lib.simpleMap(xIds, axisIds.id2name); - var yNames = Lib.simpleMap(yIds, axisIds.id2name); + var xNames = Lib.simpleMap(xIds, id2name); + var yNames = Lib.simpleMap(yIds, id2name); var axNames = xNames.concat(yNames); // plot_bgcolor only makes sense if there's a (2D) plot! @@ -108,7 +133,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var axName2 = list[j]; if(axName2 !== axName && !(layoutIn[axName2] || {}).overlaying) { - out.push(axisIds.name2id(axName2)); + out.push(name2id(axName2)); } } @@ -127,7 +152,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutIn = layoutIn[axName]; axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis'); - handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, fullData, axName); + var traces = ax2traces[axName] || []; + axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; }); + axLayoutOut._annIndices = []; + axLayoutOut._shapeIndices = []; + + handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, traces, axName); var overlayableAxes = getOverlayableAxes(axLetter, axName); @@ -136,7 +166,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { font: layoutOut.font, outerTicks: outerTicks[axName], showGrid: !noGrids[axName], - data: fullData, + data: traces, bgColor: bgColor, calendar: layoutOut.calendar, automargin: true, @@ -173,8 +203,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } // quick second pass for range slider and selector defaults - var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults'); - var rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults'); + var rangeSliderDefaults = getComponentMethod('rangeslider', 'handleDefaults'); + var rangeSelectorDefaults = getComponentMethod('rangeselector', 'handleDefaults'); for(i = 0; i < xNames.length; i++) { axName = xNames[i]; @@ -201,7 +231,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutIn = layoutIn[axName]; axLayoutOut = layoutOut[axName]; - var anchoredAxis = layoutOut[axisIds.id2name(axLayoutOut.anchor)]; + var anchoredAxis = layoutOut[id2name(axLayoutOut.anchor)]; var fixedRangeDflt = ( anchoredAxis && diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index d3471547328..5c3a6295dd1 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -52,8 +52,7 @@ function fromLog(v) { * Creates/updates these conversion functions, and a few more utilities * like cleanRange, and makeCalcdata * - * also clears the autorange bounds ._min and ._max - * and the autotick constraints ._minDtick, ._forceTick0 + * also clears the autotick constraints ._minDtick, ._forceTick0 */ module.exports = function setConvert(ax, fullLayout) { fullLayout = fullLayout || {}; @@ -460,15 +459,6 @@ module.exports = function setConvert(ax, fullLayout) { }; ax.clearCalc = function() { - // for autoranging: arrays of objects: - // { - // val: axis value, - // pad: pixel padding, - // extrapad: boolean, should this val get 5% additional padding - // } - ax._min = []; - ax._max = []; - // initialize the category list, if there is one, so we start over // to be filled in later by ax.d2c ax._categories = (ax._initialCategories || []).slice(); diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js index 7e5e7cc0ac6..5a9414054b6 100644 --- a/src/plots/cartesian/type_defaults.js +++ b/src/plots/cartesian/type_defaults.js @@ -108,7 +108,7 @@ function getFirstNonEmptyTrace(data, id, axLetter) { if(trace.type === 'splom' && trace._length > 0 && - trace['_' + axLetter + 'axes'][id] + (trace['_' + axLetter + 'axes'] || {})[id] ) { return trace; } diff --git a/src/plots/get_data.js b/src/plots/get_data.js index 8d6bceb5b20..de6710a5159 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -69,6 +69,7 @@ exports.getModuleCalcData = function(calcdata, arg1) { for(var i = 0; i < calcdata.length; i++) { var cd = calcdata[i]; var trace = cd[0].trace; + // N.B. 'legendonly' traces do not make it past here if(trace.visible !== true) continue; // group calcdata trace not by 'module' (as the name of this function diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 9bc5db60cf2..56e12929508 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -441,7 +441,7 @@ proto.plot = function(fullData, calcData, fullLayout) { ax = this[AXES[i]]; ax._length = options.viewBox[i + 2] - options.viewBox[i]; - doAutoRange(ax); + doAutoRange(this.graphDiv, ax); ax.setScale(); } diff --git a/src/plots/plots.js b/src/plots/plots.js index 15fddfc844c..90dcfaeea44 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -277,6 +277,9 @@ var extraFormatKeys = [ * gd._fullLayout._modules * is a list of all the trace modules required to draw the plot. * + * gd._fullLayout._visibleModules + * subset of _modules, a list of modules corresponding to visible:true traces. + * * gd._fullLayout._basePlotModules * is a list of all the plot modules required to draw the plot. * @@ -378,6 +381,7 @@ plots.supplyDefaults = function(gd, opts) { // clear the lists of trace and baseplot modules, and subplots newFullLayout._modules = []; + newFullLayout._visibleModules = []; newFullLayout._basePlotModules = []; var subplots = newFullLayout._subplots = emptySubplotLists(); @@ -420,7 +424,7 @@ plots.supplyDefaults = function(gd, opts) { newFullLayout._has = plots._hasPlotType.bind(newFullLayout); // special cases that introduce interactions between traces - var _modules = newFullLayout._modules; + var _modules = newFullLayout._visibleModules; for(i = 0; i < _modules.length; i++) { var _module = _modules[i]; if(_module.cleanData) _module.cleanData(newFullData); @@ -696,7 +700,7 @@ plots._hasPlotType = function(category) { if(basePlotModules[i].name === category) return true; } - // check trace modules + // check trace modules (including non-visible:true) var modules = this._modules || []; for(i = 0; i < modules.length; i++) { var name = modules[i].name; @@ -898,6 +902,7 @@ plots.clearExpandedTraceDefaultColors = function(trace) { plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var modules = fullLayout._modules; + var visibleModules = fullLayout._visibleModules; var basePlotModules = fullLayout._basePlotModules; var cnt = 0; var colorCnt = 0; @@ -912,9 +917,9 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var _module = fullTrace._module; if(!_module) return; - if(fullTrace.visible === true) Lib.pushUnique(modules, _module); + Lib.pushUnique(modules, _module); + if(fullTrace.visible === true) Lib.pushUnique(visibleModules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); - cnt++; // TODO: do we really want color not to increment for explicitly invisible traces? @@ -1475,7 +1480,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } // trace module layout defaults - var modules = layoutOut._modules; + var modules = layoutOut._visibleModules; for(i = 0; i < modules.length; i++) { _module = modules[i]; @@ -1579,7 +1584,7 @@ plots.purge = function(gd) { }; plots.style = function(gd) { - var _modules = gd._fullLayout._modules; + var _modules = gd._fullLayout._visibleModules; var styleModules = []; var i; @@ -2450,10 +2455,13 @@ plots.doCalcdata = function(gd, traces) { } } - // find array attributes in trace for(i = 0; i < fullData.length; i++) { trace = fullData[i]; + trace._arrayAttrs = PlotSchema.findArrayAttributes(trace); + + // keep track of trace extremes (for autorange) in here + trace._extremes = {}; } // add polar axes to axis list @@ -2567,7 +2575,7 @@ function clearAxesCalc(axList) { plots.doSetPositions = function(gd) { var fullLayout = gd._fullLayout; var subplots = fullLayout._subplots.cartesian; - var modules = fullLayout._modules; + var modules = fullLayout._visibleModules; var methods = []; var i, j; diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index 5c103448f72..850301deef2 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -55,6 +55,7 @@ function handleDefaults(contIn, contOut, coerce, opts) { // propagate the template. var axOut = contOut[axName] = {}; axOut._id = axOut._name = axName; + axOut._traceIndices = subplotData.map(function(t) { return t._expandedIndex; }); var dataAttr = constants.axisName2dataArray[axName]; var axType = handleAxisTypeDefaults(axIn, axOut, coerceAxis, subplotData, dataAttr); @@ -80,11 +81,11 @@ function handleDefaults(contIn, contOut, coerce, opts) { // Furthermore, angular axes don't have a set range. // // Mocked domains and ranges are set by the polar subplot instances, - // but Axes.expand uses the sign of _m to determine which padding value + // but Axes.findExtremes uses the sign of _m to determine which padding value // to use. // - // By setting, _m to 1 here, we make Axes.expand think that range[1] > range[0], - // and vice-versa for `autorange: 'reversed'` below. + // By setting, _m to 1 here, we make Axes.findExtremes think that + // range[1] > range[0], and vice-versa for `autorange: 'reversed'` below. axOut._m = 1; switch(axName) { diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 3a91414d45b..e300807ed1c 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -302,7 +302,7 @@ proto.doAutoRange = function(fullLayout, polarLayout) { var ax = this.radialAxis; setScale(ax, radialLayout, fullLayout); - doAutoRange(ax); + doAutoRange(this.gd, ax); radialLayout.range = ax.range.slice(); radialLayout._input.range = ax.range.slice(); @@ -1207,12 +1207,6 @@ proto.fillViewInitialKey = function(key, val) { function setScale(ax, axLayout, fullLayout) { Axes.setConvert(ax, fullLayout); - - // _min and _max are filled in during Axes.expand - // and cleared during Axes.setConvert - ax._min = axLayout._min; - ax._max = axLayout._max; - ax.setScale(); } diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 423682eff04..10fc320af06 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -6,12 +6,8 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; -var isNumeric = require('fast-isnumeric'); -var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; - var Axes = require('../../plots/cartesian/axes'); var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleCalc = require('../../components/colorscale/calc'); @@ -27,24 +23,14 @@ module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), ya = Axes.getFromId(gd, trace.yaxis || 'y'), orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), - sa, pos, size, i, scalendar; + pos, size, i; if(orientation === 'h') { - sa = xa; size = xa.makeCalcdata(trace, 'x'); pos = ya.makeCalcdata(trace, 'y'); - - // not sure if it really makes sense to have dates for bar size data... - // ideally if we want to make gantt charts or something we'd treat - // the actual size (trace.x or y) as time delta but base as absolute - // time. But included here for completeness. - scalendar = trace.xcalendar; - } - else { - sa = ya; + } else { size = ya.makeCalcdata(trace, 'y'); pos = xa.makeCalcdata(trace, 'x'); - scalendar = trace.ycalendar; } // create the "calculated data" to plot @@ -60,33 +46,6 @@ module.exports = function calc(gd, trace) { } } - // set base - var base = trace.base, - b; - - if(isArrayOrTypedArray(base)) { - for(i = 0; i < Math.min(base.length, cd.length); i++) { - b = sa.d2c(base[i], 0, scalendar); - if(isNumeric(b)) { - cd[i].b = +b; - cd[i].hasB = 1; - } - else cd[i].b = 0; - } - for(; i < cd.length; i++) { - cd[i].b = 0; - } - } - else { - b = sa.d2c(base, 0, scalendar); - var hasBase = isNumeric(b); - b = hasBase ? b : 0; - for(i = 0; i < cd.length; i++) { - cd[i].b = b; - if(hasBase) cd[i].hasB = 1; - } - } - // auto-z and autocolorscale if applicable if(hasColorscale(trace, 'marker')) { colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index b9a2fdf0746..1021a400a16 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -65,6 +65,8 @@ function setGroupPositions(gd, pa, sa, calcTraces) { included, i, calcTrace, fullTrace; + initBase(gd, pa, sa, calcTraces); + if(overlay) { setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces); } @@ -110,6 +112,45 @@ function setGroupPositions(gd, pa, sa, calcTraces) { collectExtents(calcTraces, pa); } +function initBase(gd, pa, sa, calcTraces) { + var i, j; + + for(i = 0; i < calcTraces.length; i++) { + var cd = calcTraces[i]; + var trace = cd[0].trace; + var base = trace.base; + var b; + + // not sure if it really makes sense to have dates for bar size data... + // ideally if we want to make gantt charts or something we'd treat + // the actual size (trace.x or y) as time delta but base as absolute + // time. But included here for completeness. + var scalendar = trace.orientation === 'h' ? trace.xcalendar : trace.ycalendar; + + if(isArrayOrTypedArray(base)) { + for(j = 0; j < Math.min(base.length, cd.length); j++) { + b = sa.d2c(base[j], 0, scalendar); + if(isNumeric(b)) { + cd[j].b = +b; + cd[j].hasB = 1; + } + else cd[j].b = 0; + } + for(; j < cd.length; j++) { + cd[j].b = 0; + } + } else { + b = sa.d2c(base, 0, scalendar); + var hasBase = isNumeric(b); + b = hasBase ? b : 0; + for(j = 0; j < cd.length; j++) { + cd[j].b = b; + if(hasBase) cd[j].hasB = 1; + } + } + } +} + function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) { var barnorm = gd._fullLayout.barnorm, @@ -455,7 +496,8 @@ function updatePositionAxis(gd, pa, sieve, allowMinDtick) { } } - Axes.expand(pa, [pMin, pMax], {padded: false}); + var extremes = Axes.findExtremes(pa, [pMin, pMax], {padded: false}); + putExtremes(calcTraces, pa, extremes); } function expandRange(range, newValue) { @@ -489,7 +531,8 @@ function setBaseAndTop(gd, sa, sieve) { } } - Axes.expand(sa, sRange, {tozero: true, padded: true}); + var extremes = Axes.findExtremes(sa, sRange, {tozero: true, padded: true}); + putExtremes(traces, sa, extremes); } @@ -527,7 +570,10 @@ function stackBars(gd, sa, sieve) { } // if barnorm is set, let normalizeBars update the axis range - if(!barnorm) Axes.expand(sa, sRange, {tozero: true, padded: true}); + if(!barnorm) { + var extremes = Axes.findExtremes(sa, sRange, {tozero: true, padded: true}); + putExtremes(traces, sa, extremes); + } } @@ -592,7 +638,8 @@ function normalizeBars(gd, sa, sieve) { } // update range of size axis - Axes.expand(sa, sRange, {tozero: true, padded: padded}); + var extremes = Axes.findExtremes(sa, sRange, {tozero: true, padded: padded}); + putExtremes(traces, sa, extremes); } @@ -600,6 +647,12 @@ function getAxisLetter(ax) { return ax._id.charAt(0); } +function putExtremes(cd, ax, extremes) { + for(var i = 0; i < cd.length; i++) { + cd[i][0].trace._extremes[ax._id] = extremes; + } +} + // find the full position span of bars at each position // for use by hover, to ensure labels move in if bars are // narrower than the space they're in. diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index d9f6aeb7e5b..095048f19a7 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -122,7 +122,8 @@ module.exports = function calc(gd, trace) { } calcSelection(cd, trace); - Axes.expand(valAxis, val, {padded: true}); + var extremes = Axes.findExtremes(valAxis, val, {padded: true}); + trace._extremes[valAxis._id] = extremes; if(cd.length > 0) { cd[0].t = { diff --git a/src/traces/box/set_positions.js b/src/traces/box/set_positions.js index ca460fd8297..c10f511ce83 100644 --- a/src/traces/box/set_positions.js +++ b/src/traces/box/set_positions.js @@ -86,12 +86,6 @@ function setPositionOffset(traceType, gd, boxList, posAxis, pad) { // check for forced minimum dtick Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true); - // set the width of all boxes - for(i = 0; i < boxList.length; i++) { - calcTrace = calcdata[boxList[i]]; - calcTrace[0].t.dPos = dPos; - } - var gap = fullLayout[traceType + 'gap']; var groupgap = fullLayout[traceType + 'groupgap']; var padfactor = (1 - gap) * (1 - groupgap) * dPos / fullLayout[numKey]; @@ -99,10 +93,19 @@ function setPositionOffset(traceType, gd, boxList, posAxis, pad) { // autoscale the x axis - including space for points if they're off the side // TODO: this will overdo it if the outermost boxes don't have // their points as far out as the other boxes - Axes.expand(posAxis, boxdv.vals, { + var extremes = Axes.findExtremes(posAxis, boxdv.vals, { vpadminus: dPos + pad[0] * padfactor, vpadplus: dPos + pad[1] * padfactor }); + + for(i = 0; i < boxList.length; i++) { + calcTrace = calcdata[boxList[i]]; + // set the width of all boxes + calcTrace[0].t.dPos = dPos; + // link extremes to all boxes + calcTrace[0].trace._extremes[posAxis._id] = extremes; + } + } module.exports = { diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index f9b0e65b648..3a69c021e40 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -82,8 +82,8 @@ module.exports = function calc(gd, trace) { xrange = [xc - dx * grow, xc + dx * grow]; yrange = [yc - dy * grow, yc + dy * grow]; - Axes.expand(xa, xrange, {padded: true}); - Axes.expand(ya, yrange, {padded: true}); + trace._extremes[xa._id] = Axes.findExtremes(xa, xrange, {padded: true}); + trace._extremes[ya._id] = Axes.findExtremes(ya, yrange, {padded: true}); // Enumerate the gridlines, both major and minor, and store them on the trace // object: diff --git a/src/traces/contourgl/convert.js b/src/traces/contourgl/convert.js index 417b6c096d1..32632c5ee11 100644 --- a/src/traces/contourgl/convert.js +++ b/src/traces/contourgl/convert.js @@ -125,9 +125,10 @@ proto.update = function(fullTrace, calcTrace) { this.contour.update(this.contourOptions); this.heatmap.update(this.heatmapOptions); - // expand axes - Axes.expand(this.scene.xaxis, calcPt.x); - Axes.expand(this.scene.yaxis, calcPt.y); + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x); + fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y); }; proto.dispose = function() { diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index a1f0e7bb6c1..37477db0315 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -124,8 +124,8 @@ module.exports = function calc(gd, trace) { // handled in gl2d convert step if(!isGL2D) { - Axes.expand(xa, xArray); - Axes.expand(ya, yArray); + trace._extremes[xa._id] = Axes.findExtremes(xa, xArray); + trace._extremes[ya._id] = Axes.findExtremes(ya, yArray); } var cd0 = { diff --git a/src/traces/heatmapgl/convert.js b/src/traces/heatmapgl/convert.js index bb66bb1410f..8fc9980fcfb 100644 --- a/src/traces/heatmapgl/convert.js +++ b/src/traces/heatmapgl/convert.js @@ -95,8 +95,10 @@ proto.update = function(fullTrace, calcTrace) { this.heatmap.update(this.options); - Axes.expand(this.scene.xaxis, calcPt.x); - Axes.expand(this.scene.yaxis, calcPt.y); + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x); + fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y); }; proto.dispose = function() { diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js index 42de0e1a086..f7553444bfa 100644 --- a/src/traces/ohlc/calc.js +++ b/src/traces/ohlc/calc.js @@ -25,8 +25,7 @@ function calc(gd, trace) { var cd = calcCommon(gd, trace, x, ya, ptFunc); - Axes.expand(xa, x, {vpad: minDiff / 2}); - + trace._extremes[xa._id] = Axes.findExtremes(xa, x, {vpad: minDiff / 2}); if(cd.length) { Lib.extendFlat(cd[0].t, { wHover: minDiff / 2, @@ -93,7 +92,7 @@ function calcCommon(gd, trace, x, ya, ptFunc) { } } - Axes.expand(ya, l.concat(h), {padded: true}); + trace._extremes[ya._id] = Axes.findExtremes(ya, l.concat(h), {padded: true}); if(cd.length) { cd[0].t = { diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 721b964f5a5..9d7af610ab6 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -11,7 +11,7 @@ var createPointCloudRenderer = require('gl-pointcloud2d'); var str2RGBArray = require('../../lib/str2rgbarray'); -var expandAxis = require('../../plots/cartesian/autorange').expand; +var findExtremes = require('../../plots/cartesian/autorange').findExtremes; var getTraceColor = require('../scatter/get_trace_color'); function Pointcloud(scene, uid) { @@ -195,14 +195,11 @@ proto.updateFast = function(options) { this.pointcloud.update(this.pointcloudOptions); // add item for autorange routine - this.expandAxesFast(bounds, markerSizeMax / 2); // avoid axis reexpand just because of the adaptive point size -}; - -proto.expandAxesFast = function(bounds, markerSize) { - var pad = markerSize || 0.5; - - expandAxis(this.scene.xaxis, [bounds[0], bounds[2]], {ppad: pad}); - expandAxis(this.scene.yaxis, [bounds[1], bounds[3]], {ppad: pad}); + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + var pad = markerSizeMax / 2 || 0.5; + options._extremes[xa._id] = findExtremes(xa, [bounds[0], bounds[2]], {ppad: pad}); + options._extremes[ya._id] = findExtremes(ya, [bounds[1], bounds[3]], {ppad: pad}); }; proto.dispose = function() { diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index 97c3f187bdb..37a9068c8e9 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -97,8 +97,8 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) { } // N.B. asymmetric splom traces call this with blank {} xa or ya - if(xa._id) Axes.expand(xa, x, xOptions); - if(ya._id) Axes.expand(ya, y, yOptions); + if(xa._id) trace._extremes[xa._id] = Axes.findExtremes(xa, x, xOptions); + if(ya._id) trace._extremes[ya._id] = Axes.findExtremes(ya, y, yOptions); } function calcMarkerSize(trace, serieslen) { diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index dd0b3366c14..f0ffbf8220f 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -15,7 +15,6 @@ var rgba = require('color-normalize'); var Registry = require('../../registry'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); -var Axes = require('../../plots/cartesian/axes'); var AxisIDs = require('../../plots/cartesian/axis_ids'); var formatColor = require('../../lib/gl_format_color').formatColor; @@ -511,11 +510,10 @@ function convertErrorBarPositions(gd, trace, positions, x, y) { } } - Axes.expand(ax, [minShoe, maxHat], {padded: true}); - out[axLetter] = { positions: positions, - errors: errors + errors: errors, + _bnds: [minShoe, maxHat] }; } } diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index ab398fa97a9..e4cf251a984 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -19,6 +19,7 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var prepareRegl = require('../../lib/prepare_regl'); var AxisIDs = require('../../plots/cartesian/axis_ids'); +var findExtremes = require('../../plots/cartesian/autorange').findExtremes; var Color = require('../../components/color'); var subTypes = require('../scatter/subtypes'); @@ -85,11 +86,9 @@ function calc(gd, trace) { var opts = sceneOptions(gd, subplot, trace, positions, x, y); var scene = sceneUpdate(gd, subplot); - // Re-use SVG scatter axis expansion routine except - // for graph with very large number of points where it - // performs poorly. - // In big data case, fake Axes.expand outputs with data bounds, - // and an average size for array marker.size inputs. + // Reuse SVG scatter axis expansion routine. + // For graphs with very large number of points and array marker.size, + // use average marker size instead to speed things up. var ppad; if(count < TOO_MANY_POINTS) { ppad = calcMarkerSize(trace, count); @@ -97,6 +96,8 @@ function calc(gd, trace) { ppad = 2 * (opts.marker.sizeAvg || Math.max(opts.marker.size, 3)); } calcAxisExpansion(gd, trace, xa, ya, x, y, ppad); + if(opts.errorX) expandForErrorBars(trace, xa, opts.errorX); + if(opts.errorY) expandForErrorBars(trace, ya, opts.errorY); // set flags to create scene renderers if(opts.fill && !scene.fill2d) scene.fill2d = true; @@ -122,20 +123,27 @@ function calc(gd, trace) { scene.textOptions.push(opts.text); scene.textSelectedOptions.push(opts.textSel); scene.textUnselectedOptions.push(opts.textUnsel); - scene.count++; // stash scene ref stash._scene = scene; - stash.index = scene.count - 1; + stash.index = scene.count; stash.x = x; stash.y = y; stash.positions = positions; stash.count = count; + scene.count++; + gd.firstscatter = false; return [{x: false, y: false, t: stash, trace: trace}]; } +function expandForErrorBars(trace, ax, opts) { + var extremes = trace._extremes[ax._id]; + var errExt = findExtremes(ax, opts._bnds, {padded: true}); + extremes.min = extremes.min.concat(errExt.min); + extremes.max = extremes.max.concat(errExt.max); +} // create scene options function sceneOptions(gd, subplot, trace, positions, x, y) { @@ -230,19 +238,16 @@ function sceneUpdate(gd, subplot) { // apply new option to all regl components (used on drag) scene.update = function update(opt) { - var i; - var opts = new Array(scene.count); - for(i = 0; i < scene.count; i++) { - opts[i] = opt; - } + var opts = Lib.repeat(opt, scene.count); + if(scene.fill2d) scene.fill2d.update(opts); if(scene.scatter2d) scene.scatter2d.update(opts); if(scene.line2d) scene.line2d.update(opts); if(scene.error2d) scene.error2d.update(opts.concat(opts)); if(scene.select2d) scene.select2d.update(opts); if(scene.glText) { - for(i = 0; i < scene.count; i++) { - scene.glText[i].update(opts[i]); + for(var i = 0; i < scene.count; i++) { + scene.glText[i].update(opt); } } @@ -290,18 +295,7 @@ function sceneUpdate(gd, subplot) { }; scene.clear = function clear() { - var fullLayout = gd._fullLayout; - var vpSize = fullLayout._size; - var width = fullLayout.width; - var height = fullLayout.height; - var xaxis = subplot.xaxis; - var yaxis = subplot.yaxis; - var vp = [ - vpSize.l + xaxis.domain[0] * vpSize.w, - vpSize.b + yaxis.domain[0] * vpSize.h, - (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, - (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h - ]; + var vp = getViewport(gd._fullLayout, subplot.xaxis, subplot.yaxis); if(scene.select2d) { clearViewport(scene.select2d, vp); @@ -352,6 +346,18 @@ function sceneUpdate(gd, subplot) { return scene; } +function getViewport(fullLayout, xaxis, yaxis) { + var gs = fullLayout._size; + var width = fullLayout.width; + var height = fullLayout.height; + return [ + gs.l + xaxis.domain[0] * gs.w, + gs.b + yaxis.domain[0] * gs.h, + (width - gs.r) - (1 - xaxis.domain[1]) * gs.w, + (height - gs.t) - (1 - yaxis.domain[1]) * gs.h + ]; +} + function clearViewport(comp, vp) { var gl = comp.regl._gl; gl.enable(gl.SCISSOR_TEST); @@ -363,19 +369,15 @@ function clearViewport(comp, vp) { function plot(gd, subplot, cdata) { if(!cdata.length) return; - var i; - var fullLayout = gd._fullLayout; - var scene = cdata[0][0].t._scene; - var dragmode = fullLayout.dragmode; + var scene = subplot._scene; + var xaxis = subplot.xaxis; + var yaxis = subplot.yaxis; + var i, j; // we may have more subplots than initialized data due to Axes.getSubplots method if(!scene) return; - var vpSize = fullLayout._size; - var width = fullLayout.width; - var height = fullLayout.height; - var success = prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); if(!success) { scene.init(); @@ -516,38 +518,20 @@ function plot(gd, subplot, cdata) { } } - var selectMode = dragmode === 'lasso' || dragmode === 'select'; + // form batch arrays, and check for selected points scene.selectBatch = null; scene.unselectBatch = null; + var dragmode = fullLayout.dragmode; + var selectMode = dragmode === 'lasso' || dragmode === 'select'; - // provide viewport and range - var vpRange = cdata.map(function(cdscatter) { - if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return; - var cd = cdscatter[0]; - var trace = cd.trace; - var stash = cd.t; - var id = stash.index; + for(i = 0; i < cdata.length; i++) { + var cd0 = cdata[i][0]; + var trace = cd0.trace; + var stash = cd0.t; + var index = stash.index; var x = stash.x; var y = stash.y; - var xaxis = subplot.xaxis || AxisIDs.getFromId(gd, trace.xaxis || 'x'); - var yaxis = subplot.yaxis || AxisIDs.getFromId(gd, trace.yaxis || 'y'); - var i; - - var range = [ - (xaxis._rl || xaxis.range)[0], - (yaxis._rl || yaxis.range)[0], - (xaxis._rl || xaxis.range)[1], - (yaxis._rl || yaxis.range)[1] - ]; - - var viewport = [ - vpSize.l + xaxis.domain[0] * vpSize.w, - vpSize.b + yaxis.domain[0] * vpSize.h, - (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, - (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h - ]; - if(trace.selectedpoints || selectMode) { if(!selectMode) selectMode = true; @@ -558,37 +542,35 @@ function plot(gd, subplot, cdata) { // regenerate scene batch, if traces number changed during selection if(trace.selectedpoints) { - var selPts = scene.selectBatch[id] = Lib.selIndices2selPoints(trace); + var selPts = scene.selectBatch[index] = Lib.selIndices2selPoints(trace); var selDict = {}; - for(i = 0; i < selPts.length; i++) { - selDict[selPts[i]] = 1; + for(j = 0; j < selPts.length; j++) { + selDict[selPts[j]] = 1; } var unselPts = []; - for(i = 0; i < stash.count; i++) { - if(!selDict[i]) unselPts.push(i); + for(j = 0; j < stash.count; j++) { + if(!selDict[j]) unselPts.push(j); } - scene.unselectBatch[id] = unselPts; + scene.unselectBatch[index] = unselPts; } // precalculate px coords since we are not going to pan during select - var xpx = new Array(stash.count); - var ypx = new Array(stash.count); - for(i = 0; i < stash.count; i++) { - xpx[i] = xaxis.c2p(x[i]); - ypx[i] = yaxis.c2p(y[i]); + // TODO, could do better here e.g. + // - spin that in a webworker + // - compute selection from polygons in data coordinates + // (maybe just for linear axes) + var xpx = stash.xpx = new Array(stash.count); + var ypx = stash.ypx = new Array(stash.count); + for(j = 0; j < stash.count; j++) { + xpx[j] = xaxis.c2p(x[j]); + ypx[j] = yaxis.c2p(y[j]); } - stash.xpx = xpx; - stash.ypx = ypx; - } - else { + } else { stash.xpx = stash.ypx = null; } + } - return trace.visible ? - {viewport: viewport, range: range} : - null; - }); if(selectMode) { // create select2d @@ -618,6 +600,19 @@ function plot(gd, subplot, cdata) { } } + // provide viewport and range + var vpRange0 = { + viewport: getViewport(fullLayout, xaxis, yaxis), + // TODO do we need those fallbacks? + range: [ + (xaxis._rl || xaxis.range)[0], + (yaxis._rl || yaxis.range)[0], + (xaxis._rl || xaxis.range)[1], + (yaxis._rl || yaxis.range)[1] + ] + }; + var vpRange = Lib.repeat(vpRange0, scene.count); + // upload viewport/range data to GPU if(scene.fill2d) { scene.fill2d.update(vpRange); @@ -635,14 +630,10 @@ function plot(gd, subplot, cdata) { scene.select2d.update(vpRange); } if(scene.glText) { - scene.glText.forEach(function(text, i) { - text.update(vpRange[i]); - }); + scene.glText.forEach(function(text) { text.update(vpRange0); }); } scene.draw(); - - return; } diff --git a/src/traces/scatterpolar/calc.js b/src/traces/scatterpolar/calc.js index 7d9662324d0..f2f10dd7f2d 100644 --- a/src/traces/scatterpolar/calc.js +++ b/src/traces/scatterpolar/calc.js @@ -48,7 +48,7 @@ module.exports = function calc(gd, trace) { } var ppad = calcMarkerSize(trace, len); - Axes.expand(radialAxis, rArray, {ppad: ppad}); + trace._extremes.x = Axes.findExtremes(radialAxis, rArray, {ppad: ppad}); calcColorscale(trace); arraysToCalcdata(cd, trace); diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index 963f9d98657..b5a16ed6937 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -36,13 +36,7 @@ function calc(container, trace) { stash.r = rArray; stash.theta = thetaArray; - Axes.expand(radialAxis, rArray, {tozero: true}); - - if(angularAxis.type !== 'linear') { - angularAxis.autorange = true; - Axes.expand(angularAxis, thetaArray); - delete angularAxis.autorange; - } + trace._extremes.x = Axes.findExtremes(radialAxis, rArray, {tozero: true}); return [{x: false, y: false, t: stash, trace: trace}]; } diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 6905ef6b247..c5613e5844f 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -66,11 +66,9 @@ function calc(gd, trace) { var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {}; var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {}; - // Re-use SVG scatter axis expansion routine except - // for graph with very large number of points where it - // performs poorly. - // In big data case, fake Axes.expand outputs with data bounds, - // and an average size for array marker.size inputs. + // Reuse SVG scatter axis expansion routine. + // For graphs with very large number of points and array marker.size, + // use average marker size instead to speed things up. var ppad; if(hasTooManyPoints) { ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3)); diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js index e4852c02b8d..1994f4235c3 100644 --- a/src/traces/violin/calc.js +++ b/src/traces/violin/calc.js @@ -35,6 +35,9 @@ module.exports = function calc(gd, trace) { }; } + var spanMin = Infinity; + var spanMax = -Infinity; + for(var i = 0; i < cd.length; i++) { var cdi = cd[i]; var vals = cdi.pts.map(helpers.extractVal); @@ -62,10 +65,15 @@ module.exports = function calc(gd, trace) { cdi.density[k] = {v: v, t: t}; } - Axes.expand(valAxis, span, {padded: true}); groupStats.maxCount = Math.max(groupStats.maxCount, vals.length); + + spanMin = Math.min(spanMin, span[0]); + spanMax = Math.max(spanMax, span[1]); } + var extremes = Axes.findExtremes(valAxis, [spanMin, spanMax], {padded: true}); + trace._extremes[valAxis._id] = extremes; + cd[0].t.labels.kde = Lib._(gd, 'kde:'); return cd; diff --git a/test/image/baselines/polar_transforms.png b/test/image/baselines/polar_transforms.png new file mode 100644 index 00000000000..8cebb23abcd Binary files /dev/null and b/test/image/baselines/polar_transforms.png differ diff --git a/test/image/mocks/polar_transforms.json b/test/image/mocks/polar_transforms.json new file mode 100644 index 00000000000..efef707de06 --- /dev/null +++ b/test/image/mocks/polar_transforms.json @@ -0,0 +1,79 @@ +{ + "data": [{ + "type": "scatterpolar", + "mode": "lines+markers", + "r": [1, -1, -2, 0, 1, 3, 3], + "theta": [2, 1, 0, 1, 3, 4, 3], + "thetaunit": "radians", + "transforms": [{ + "type": "groupby", + "groups": ["a", "a", "b", "a", "b", "b", "a"], + "styles": [ + {"target": "a", "value": {"marker": {"color": "orange"}}}, + {"target": "b", "value": {"marker": {"color": "blue"}}} + ] + }, { + "type": "filter", + "target": "r", + "operation": ">=", + "value": 0, + "preservegaps": true + }], + "name": "Groupby+filter" + }, + { + "type": "scatterpolar", + "r": [1, 2, 3, 4, -3], + "theta": [1.1, 2.2, 3.3, 4.4, 5.5], + "thetaunit": "radians", + "marker": { + "size": [0.3, 0.2, 0.1, 0.4, 0.5], + "sizeref": 0.01, + "color": [2, 4, 6, 10, 8], + "opacity": [0.9, 0.6, 0.2, 0.8, 1.0], + "line": { + "color": [2.2, 3.3, 4.4, 5.5, 1.1] + } + }, + "transforms": [{ + "type": "aggregate", + "groups": ["a", "b", "a", "a", "a"], + "aggregations": [ + {"target": "r", "func": "sum"}, + {"target": "theta", "func": "avg"}, + {"target": "marker.size", "func": "min"}, + {"target": "marker.color", "func": "max"}, + {"target": "marker.line.color", "func": "last"}, + {"target": "marker.line.width", "func": "count"} + ] + }], + "name": "Aggregate" + }, + { + "type": "scatterpolar", + "r": [1, 2, 3, 4, 5, 6], + "theta": [1, 4, 2, 6, 5, 3], + "thetaunit": "radians", + "transforms": [{ + "type": "sort", + "target": [1, 6, 2, 5, 3, 4] + }], + "name": "Sort" + }, + { + "type": "scatterpolar", + "r":[4, 5, 6, 4, 5, 6], + "theta": [1, 1, 1, 2, 2, 2], + "thetaunit": "radians", + "marker": {"color": [1, 2, 3, -1, -2, -3], "size": 12}, + "mode": "lines+markers", + "transforms": [ + {"type": "groupby", "groups": [1, 1, 1, 2, 2, 2]} + ] + }], + "layout": { + "width": 600, + "height": 400, + "title": "Transforms on polar subplot" + } +} diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 1039facbd04..a4331307cfe 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -27,7 +27,13 @@ describe('Test annotations', function() { layoutOut._has = Plots._hasPlotType.bind(layoutOut); layoutOut._subplots = {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']}; ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(axName) { - if(!layoutOut[axName]) layoutOut[axName] = {type: 'linear', range: [0, 1]}; + if(!layoutOut[axName]) { + layoutOut[axName] = { + type: 'linear', + range: [0, 1], + _annIndices: [] + }; + } Axes.setConvert(layoutOut[axName]); }); @@ -90,7 +96,11 @@ describe('Test annotations', function() { }; var layoutOut = { - xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] } + xaxis: { + type: 'date', + range: ['2000-01-01', '2016-01-01'], + _annIndices: [] + } }; _supply(layoutIn, layoutOut); @@ -128,10 +138,10 @@ describe('Test annotations', function() { }; var layoutOut = { - xaxis: {type: 'linear', range: [0, 1]}, - yaxis: {type: 'date', range: ['2000-01-01', '2018-01-01']}, - xaxis2: {type: 'log', range: [1, 2]}, - yaxis2: {type: 'category', range: [0, 1]} + xaxis: {type: 'linear', range: [0, 1], _annIndices: []}, + yaxis: {type: 'date', range: ['2000-01-01', '2018-01-01'], _annIndices: []}, + xaxis2: {type: 'log', range: [1, 2], _annIndices: []}, + yaxis2: {type: 'category', range: [0, 1], _annIndices: []} }; _supply(layoutIn, layoutOut); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index fcb899b839c..2538771fc4e 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1421,246 +1421,218 @@ describe('Test axes', function() { describe('getAutoRange', function() { var getAutoRange = Axes.getAutoRange; - var ax; + var gd, ax; - it('returns reasonable range without explicit rangemode or autorange', function() { - ax = { - _min: [ - // add in an extrapad to verify that it gets used on _min - // with a _length of 100, extrapad increases pad by 5 - {val: 1, pad: 15, extrapad: true}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], + function mockGd(min, max) { + return { + _fullData: [{ + type: 'scatter', + visible: true, + xaxis: 'x', + _extremes: { + x: {min: min, max: max} + } + }], + _fullLayout: {} + }; + } + + function mockAx() { + return { + _id: 'x', type: 'linear', - _length: 100 + _length: 100, + _traceIndices: [0] }; + } - expect(getAutoRange(ax)).toEqual([-0.5, 7]); + it('returns reasonable range without explicit rangemode or autorange', function() { + gd = mockGd([ + // add in an extrapad to verify that it gets used on _min + // with a _length of 100, extrapad increases pad by 5 + {val: 1, pad: 15, extrapad: true}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + + expect(getAutoRange(gd, ax)).toEqual([-0.5, 7]); }); it('reverses axes', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - autorange: 'reversed', - rangemode: 'normal', - _length: 100 - }; + gd = mockGd([ + {val: 1, pad: 20}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + ax.autorange = 'reversed'; + ax.rangemode = 'normarl'; - expect(getAutoRange(ax)).toEqual([7, -0.5]); + expect(getAutoRange(gd, ax)).toEqual([7, -0.5]); }); it('expands empty range', function() { - ax = { - _min: [ - {val: 2, pad: 0} - ], - _max: [ - {val: 2, pad: 0} - ], - type: 'linear', - rangemode: 'normal', - _length: 100 - }; + gd = mockGd([ + {val: 2, pad: 0} + ], [ + {val: 2, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'normal'; - expect(getAutoRange(ax)).toEqual([1, 3]); + expect(getAutoRange(gd, ax)).toEqual([1, 3]); }); it('returns a lower bound of 0 on rangemode tozero with positive points', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: 1, pad: 20}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([0, 7]); + expect(getAutoRange(gd, ax)).toEqual([0, 7]); }); it('returns an upper bound of 0 on rangemode tozero with negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: -5, pad: 20}, - {val: -4, pad: 0}, - {val: -6, pad: 10}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: -5, pad: 20}, + {val: -4, pad: 0}, + {val: -6, pad: 10}, + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([-12.5, 0]); + expect(getAutoRange(gd, ax)).toEqual([-12.5, 0]); }); it('returns a positive and negative range on rangemode tozero with positive and negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([-15, 10]); + expect(getAutoRange(gd, ax)).toEqual([-15, 10]); }); it('reverses range after applying rangemode tozero', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - // add in an extrapad to verify that it gets used on _max - {val: 6, pad: 15, extrapad: true}, - {val: 7, pad: 0}, - {val: 5, pad: 10}, - ], - type: 'linear', - autorange: 'reversed', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: 1, pad: 20}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + // add in an extrapad to verify that it gets used on _max + {val: 6, pad: 15, extrapad: true}, + {val: 7, pad: 0}, + {val: 5, pad: 10} + ]); + ax = mockAx(); + ax.autorange = 'reversed'; + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([7.5, 0]); + expect(getAutoRange(gd, ax)).toEqual([7.5, 0]); }); it('expands empty positive range to something including 0 with rangemode tozero', function() { - ax = { - _min: [ - {val: 5, pad: 0} - ], - _max: [ - {val: 5, pad: 0} - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: 5, pad: 0} + ], [ + {val: 5, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([0, 6]); + expect(getAutoRange(gd, ax)).toEqual([0, 6]); }); it('expands empty negative range to something including 0 with rangemode tozero', function() { - ax = { - _min: [ - {val: -5, pad: 0} - ], - _max: [ - {val: -5, pad: 0} - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: -5, pad: 0} + ], [ + {val: -5, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([-6, 0]); + expect(getAutoRange(gd, ax)).toEqual([-6, 0]); }); it('never returns a negative range when rangemode nonnegative is set with positive and negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: 6, pad: 20}, - {val: 7, pad: 0}, - {val: 5, pad: 10}, - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: 6, pad: 20}, + {val: 7, pad: 0}, + {val: 5, pad: 10} + ]); + ax = mockAx(); + ax.rangemode = 'nonnegative'; - expect(getAutoRange(ax)).toEqual([0, 7.5]); + expect(getAutoRange(gd, ax)).toEqual([0, 7.5]); }); it('never returns a negative range when rangemode nonnegative is set with only negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: -5, pad: 20}, - {val: -4, pad: 0}, - {val: -6, pad: 10}, - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: -5, pad: 20}, + {val: -4, pad: 0}, + {val: -6, pad: 10} + ]); + ax = mockAx(); + ax.rangemode = 'nonnegative'; - expect(getAutoRange(ax)).toEqual([0, 1]); + expect(getAutoRange(gd, ax)).toEqual([0, 1]); }); it('expands empty range to something nonnegative with rangemode nonnegative', function() { - ax = { - _min: [ - {val: -5, pad: 0} - ], - _max: [ - {val: -5, pad: 0} - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; + gd = mockGd([ + {val: -5, pad: 0} + ], [ + {val: -5, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'nonnegative'; - expect(getAutoRange(ax)).toEqual([0, 1]); + expect(getAutoRange(gd, ax)).toEqual([0, 1]); }); }); - describe('expand', function() { - var expand = Axes.expand; - var ax, data, options; + describe('findExtremes', function() { + var findExtremes = Axes.findExtremes; + var ax, data, options, out; - // Axes.expand modifies ax, so this provides a simple - // way of getting a new clean copy each time. function getDefaultAx() { return { - autorange: true, c2l: Number, type: 'linear', _m: 1 @@ -1671,55 +1643,42 @@ describe('Test axes', function() { ax = getDefaultAx(); data = [1, 4, 7, 2]; - expand(ax, data); - - expect(ax._min).toEqual([{val: 1, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 7, pad: 0, extrapad: false}]); + out = findExtremes(ax, data); + expect(out.min).toEqual([{val: 1, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 7, pad: 0, extrapad: false}]); }); it('calls ax.setScale if necessary', function() { - ax = { - autorange: true, - c2l: Number, - type: 'linear', - setScale: function() {} - }; + ax = getDefaultAx(); + delete ax._m; + ax.setScale = function() {}; spyOn(ax, 'setScale'); - expand(ax, [1]); - + findExtremes(ax, [1]); expect(ax.setScale).toHaveBeenCalled(); }); it('handles symmetric pads as numbers', function() { ax = getDefaultAx(); data = [1, 4, 2, 7]; - options = { - vpad: 2, - ppad: 10 - }; - - expand(ax, data, options); + options = {vpad: 2, ppad: 10}; - expect(ax._min).toEqual([{val: -1, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 9, pad: 10, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -1, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 9, pad: 10, extrapad: false}]); }); it('handles symmetric pads as number arrays', function() { ax = getDefaultAx(); data = [1, 4, 2, 7]; - options = { - vpad: [1, 10, 6, 3], - ppad: [0, 15, 20, 10] - }; - - expand(ax, data, options); + options = {vpad: [1, 10, 6, 3], ppad: [0, 15, 20, 10]}; - expect(ax._min).toEqual([ + out = findExtremes(ax, data, options); + expect(out.min).toEqual([ {val: -6, pad: 15, extrapad: false}, {val: -4, pad: 20, extrapad: false} ]); - expect(ax._max).toEqual([ + expect(out.max).toEqual([ {val: 14, pad: 15, extrapad: false}, {val: 8, pad: 20, extrapad: false} ]); @@ -1735,10 +1694,9 @@ describe('Test axes', function() { ppadplus: 20 }; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -4, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 11, pad: 20, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -4, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 11, pad: 20, extrapad: false}]); }); it('handles separate pads as number arrays', function() { @@ -1751,13 +1709,12 @@ describe('Test axes', function() { ppadplus: [0, 0, 40, 20] }; - expand(ax, data, options); - - expect(ax._min).toEqual([ + out = findExtremes(ax, data, options); + expect(out.min).toEqual([ {val: 1, pad: 30, extrapad: false}, {val: -3, pad: 10, extrapad: false} ]); - expect(ax._max).toEqual([ + expect(out.max).toEqual([ {val: 9, pad: 0, extrapad: false}, {val: 3, pad: 40, extrapad: false}, {val: 8, pad: 20, extrapad: false} @@ -1776,70 +1733,49 @@ describe('Test axes', function() { ppadplus: 40 }; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -1, pad: 20, extrapad: false}]); - expect(ax._max).toEqual([{val: 9, pad: 40, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -1, pad: 20, extrapad: false}]); + expect(out.max).toEqual([{val: 9, pad: 40, extrapad: false}]); }); it('adds 5% padding if specified by flag', function() { ax = getDefaultAx(); data = [1, 5]; - options = { - vpad: 1, - ppad: 10, - padded: true - }; + options = {vpad: 1, ppad: 10, padded: true}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: 0, pad: 10, extrapad: true}]); - expect(ax._max).toEqual([{val: 6, pad: 10, extrapad: true}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: 0, pad: 10, extrapad: true}]); + expect(out.max).toEqual([{val: 6, pad: 10, extrapad: true}]); }); it('has lower bound zero with all positive data if tozero is sset', function() { ax = getDefaultAx(); data = [2, 5]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; - - expand(ax, data, options); + options = {vpad: 1, ppad: 10, tozero: true}; - expect(ax._min).toEqual([{val: 0, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 6, pad: 10, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: 0, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 6, pad: 10, extrapad: false}]); }); it('has upper bound zero with all negative data if tozero is set', function() { ax = getDefaultAx(); data = [-7, -4]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + options = {vpad: 1, ppad: 10, tozero: true}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -8, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 0, pad: 0, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -8, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 0, pad: 0, extrapad: false}]); }); it('sets neither bound to zero with positive and negative data if tozero is set', function() { ax = getDefaultAx(); data = [-7, 4]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; - - expand(ax, data, options); + options = {vpad: 1, ppad: 10, tozero: true}; - expect(ax._min).toEqual([{val: -8, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 5, pad: 10, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -8, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 5, pad: 10, extrapad: false}]); }); it('overrides padded with tozero', function() { @@ -1852,27 +1788,25 @@ describe('Test axes', function() { padded: true }; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: 0, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 6, pad: 10, extrapad: true}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: 0, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 6, pad: 10, extrapad: true}]); }); it('should fail if no data is given', function() { ax = getDefaultAx(); - expect(function() { expand(ax); }).toThrow(); + expect(function() { findExtremes(ax); }).toThrow(); }); it('should return even if `autorange` is false', function() { ax = getDefaultAx(); - data = [2, 5]; - ax.autorange = false; ax.rangeslider = { autorange: false }; + data = [2, 5]; - expand(ax, data, {}); - expect(ax._min).toEqual([{val: 2, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 5, pad: 0, extrapad: false}]); + out = findExtremes(ax, data, {}); + expect(out.min).toEqual([{val: 2, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 5, pad: 0, extrapad: false}]); }); }); diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index a1c242da8c4..10ff65139d5 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -9,7 +9,7 @@ var Axes = require('@src/plots/cartesian/axes'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var checkTicks = require('../assets/custom_assertions').checkTicks; var supplyAllDefaults = require('../assets/supply_defaults'); @@ -652,8 +652,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)'); }); it('should expand size axis (overlay case)', function() { @@ -679,8 +679,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-11.11, 11.11], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-11.11, 11.11], undefined, '(ya.range)'); }); it('should expand size axis (relative case)', function() { @@ -702,8 +702,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-4.44, 4.44], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-4.44, 4.44], undefined, '(ya.range)'); }); it('should expand size axis (barnorm case)', function() { @@ -725,8 +725,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-1.11, 1.11], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-1.11, 1.11], undefined, '(ya.range)'); }); it('should include explicit base in size axis range', function() { @@ -739,7 +739,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-2.5, 7.5]); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-2.5, 7.5]); }); }); @@ -753,7 +753,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toEqual(['2016-12-31', '2017-01-20']); + expect(Axes.getAutoRange(gd, ya)).toEqual(['2016-12-31', '2017-01-20']); }); }); @@ -767,7 +767,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-0.572, 10.873], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-0.572, 10.873], undefined, '(ya.range)'); }); it('works with log axes (stacked bars)', function() { @@ -780,7 +780,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-0.582, 11.059], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-0.582, 11.059], undefined, '(ya.range)'); }); it('works with log axes (normalized bars)', function() { @@ -795,7 +795,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([1.496, 2.027], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([1.496, 2.027], undefined, '(ya.range)'); }); }); @@ -897,7 +897,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -930,7 +930,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -961,7 +961,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -995,7 +995,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1144,7 +1144,7 @@ describe('A bar plot', function() { assertTextIsInsidePath(text20, path20); // inside assertTextIsInsidePath(text30, path30); // inside }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1225,7 +1225,7 @@ describe('A bar plot', function() { assertTextFont(textNodes[1], expected.outsidetextfont, 1); assertTextFont(textNodes[2], expected.insidetextfont, 2); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1291,7 +1291,7 @@ describe('A bar plot', function() { checkBarsMatch(['bottom', 'width'], 'final'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1328,7 +1328,87 @@ describe('A bar plot', function() { .then(function() { _assertNumberOfBarTextNodes(3); }) - .catch(fail) + .catch(failTest) + .then(done); + }); +}); + +describe('bar visibility toggling:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _assert(msg, xrng, yrng, calls) { + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); + expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); + + var setPositions = gd._fullData[0]._module.setPositions; + expect(setPositions).toHaveBeenCalledTimes(calls); + setPositions.calls.reset(); + } + + it('should update axis range according to visible edits (group case)', function(done) { + Plotly.plot(gd, [ + {type: 'bar', x: [1, 2, 3], y: [1, 2, 1]}, + {type: 'bar', x: [1, 2, 3], y: [-1, -2, -1]} + ]) + .then(function() { + spyOn(gd._fullData[0]._module, 'setPositions').and.callThrough(); + + _assert('base', [0.5, 3.5], [-2.222, 2.222], 0); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + _assert('visible [true,false]', [0.5, 3.5], [0, 2.105], 1); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + _assert('both invisible', [0.5, 3.5], [0, 2.105], 0); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + _assert('visible [false,true]', [0.5, 3.5], [-2.105, 0], 1); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('back to both visible', [0.5, 3.5], [-2.222, 2.222], 1); + }) + .catch(failTest) + .then(done); + }); + + it('should update axis range according to visible edits (stack case)', function(done) { + Plotly.plot(gd, [ + {type: 'bar', x: [1, 2, 3], y: [1, 2, 1]}, + {type: 'bar', x: [1, 2, 3], y: [2, 3, 2]} + ], {barmode: 'stack'}) + .then(function() { + spyOn(gd._fullData[0]._module, 'setPositions').and.callThrough(); + + _assert('base', [0.5, 3.5], [0, 5.263], 0); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + _assert('visible [true,false]', [0.5, 3.5], [0, 2.105], 1); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + _assert('both invisible', [0.5, 3.5], [0, 2.105], 0); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + _assert('visible [false,true]', [0.5, 3.5], [0, 3.157], 1); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('back to both visible', [0.5, 3.5], [0, 5.263], 1); + }) + .catch(failTest) .then(done); }); }); @@ -1384,7 +1464,7 @@ describe('bar hover', function() { var mock = Lib.extendDeep({}, require('@mocks/11.json')); Plotly.plot(gd, mock.data, mock.layout) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1410,7 +1490,7 @@ describe('bar hover', function() { var mock = Lib.extendDeep({}, require('@mocks/bar_attrs_group_norm.json')); Plotly.plot(gd, mock.data, mock.layout) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1476,7 +1556,7 @@ describe('bar hover', function() { var out = _hover(gd, -0.25, 0.5, 'closest'); expect(out.text).toEqual('apple', 'hover text'); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1526,7 +1606,7 @@ describe('bar hover', function() { expect(out).toBe(false, hoverSpec); }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1564,7 +1644,7 @@ describe('bar hover', function() { expect(out.style).toEqual([1, 'red', 200, 1]); assertPos(out.pos, [222, 280, 168, 168]); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1594,7 +1674,7 @@ describe('bar hover', function() { out = _hover(gd, 10, 2, 'closest'); assertPos(out.pos, [145, 155, 15, 15]); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1699,7 +1779,7 @@ describe('bar hover', function() { [true, 3] ); }) - .catch(fail) + .catch(failTest) .then(done); }); }); diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index 3cb326a940f..f225c12e232 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -197,7 +197,7 @@ describe('restyle', function() { return Plotly.restyle(gd, {visible: 'legendonly'}, 1); }) .then(function() { - expect(!!gd._fullLayout._plots.x2y2._scene).toBe(false); + expect(!!gd._fullLayout._plots.x2y2._scene).toBe(true); return Plotly.restyle(gd, {visible: true}, 1); }) .then(function() { diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 1ae5858c3d6..4e34458328f 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -186,6 +186,7 @@ describe('contour calc', function() { supplyAllDefaults(gd); var fullTrace = gd._fullData[0]; + fullTrace._extremes = {}; var out = Contour.calc(gd, fullTrace)[0]; out.trace = fullTrace; diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index 94516d5b7b7..827d283c94d 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -358,27 +358,38 @@ describe('@gl Test gl2d plots', function() { var _mock = Lib.extendDeep({}, mock); _mock.data[0].line.width = 5; + function assertDrawCall(msg, exp) { + var draw = gd._fullLayout._plots.xy._scene.scatter2d.draw; + expect(draw).toHaveBeenCalledTimes(exp, msg); + draw.calls.reset(); + } + Plotly.plot(gd, _mock) .then(delay(30)) .then(function() { + spyOn(gd._fullLayout._plots.xy._scene.scatter2d, 'draw'); return Plotly.restyle(gd, 'visible', 'legendonly'); }) .then(function() { - expect(gd.querySelector('.gl-canvas-context')).toBe(null); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); + assertDrawCall('legendonly', 0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); + assertDrawCall('back to visible', 1); return Plotly.restyle(gd, 'visible', false); }) .then(function() { - expect(gd.querySelector('.gl-canvas-context')).toBe(null); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); + assertDrawCall('visible false', 0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { + assertDrawCall('back up', 1); expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); }) .catch(failTest) diff --git a/test/jasmine/tests/gl2d_pointcloud_test.js b/test/jasmine/tests/gl2d_pointcloud_test.js index b43b7c02827..66f61898f29 100644 --- a/test/jasmine/tests/gl2d_pointcloud_test.js +++ b/test/jasmine/tests/gl2d_pointcloud_test.js @@ -161,43 +161,27 @@ describe('@gl pointcloud traces', function() { var mock = plotData; var scene2d; - var xBaselineMins = [{val: 0, pad: 50, extrapad: false}]; - var xBaselineMaxes = [{val: 9, pad: 50, extrapad: false}]; - - var yBaselineMins = [{val: 0, pad: 50, extrapad: false}]; - var yBaselineMaxes = [{val: 9, pad: 50, extrapad: false}]; - Plotly.plot(gd, mock) .then(function() { scene2d = gd._fullLayout._plots.xy._scene2d; - expect(scene2d.traces[gd._fullData[0].uid].type).toBe('pointcloud'); - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - return Plotly.relayout(gd, 'xaxis.range', [3, 6]); }).then(function() { - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - + expect(scene2d.xaxis.range).toEqual([3, 6]); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); return Plotly.relayout(gd, 'xaxis.autorange', true); }).then(function() { - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); return Plotly.relayout(gd, 'yaxis.range', [8, 20]); }).then(function() { - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toEqual([8, 20]); return Plotly.relayout(gd, 'yaxis.autorange', true); }).then(function() { - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/gl2d_scatterplot_contour_test.js b/test/jasmine/tests/gl2d_scatterplot_contour_test.js index 96ad17c4562..853969b14d1 100644 --- a/test/jasmine/tests/gl2d_scatterplot_contour_test.js +++ b/test/jasmine/tests/gl2d_scatterplot_contour_test.js @@ -222,8 +222,6 @@ describe('@gl contourgl plots', function() { scene2d = gd._fullLayout._plots.xy._scene2d; expect(scene2d.traces[mock.data[0].uid].type).toEqual('contourgl'); - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0, extrapad: false}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0, extrapad: false}]); expect(scene2d.xaxis.range).toEqual([-1, 1]); return Plotly.relayout(gd, 'xaxis.range', [0, -10]); @@ -237,8 +235,6 @@ describe('@gl contourgl plots', function() { return Plotly.restyle(gd, 'type', 'heatmapgl'); }).then(function() { expect(scene2d.traces[mock.data[0].uid].type).toEqual('heatmapgl'); - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0, extrapad: false}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0, extrapad: false}]); expect(scene2d.xaxis.range).toEqual([1, -1]); return Plotly.relayout(gd, 'xaxis.range', [0, -10]); diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index c65a7987043..692a6513d88 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -301,6 +301,8 @@ describe('heatmap calc', function() { var fullTrace = gd._fullData[0]; var fullLayout = gd._fullLayout; + fullTrace._extremes = {}; + var out = Heatmap.calc(gd, fullTrace)[0]; out._xcategories = fullLayout.xaxis._categories; out._ycategories = fullLayout.yaxis._categories; diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 7ea0fa9cad9..dffed81956d 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -879,15 +879,16 @@ describe('end-to-end scatter tests', function() { .then(done); }); - it('should update axis range accordingly on marker.size edits', function(done) { - function _assert(msg, xrng, yrng) { - var fullLayout = gd._fullLayout; - expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); - expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); - } + function assertAxisRanges(msg, xrng, yrng) { + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); + expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); + } + var schema = Plotly.PlotSchema.get(); + + it('should update axis range accordingly on marker.size edits', function(done) { // edit types are important to this test - var schema = Plotly.PlotSchema.get(); expect(schema.traces.scatter.attributes.marker.size.editType) .toBe('calc', 'marker.size editType'); expect(schema.layout.layoutAttributes.xaxis.autorange.editType) @@ -895,29 +896,82 @@ describe('end-to-end scatter tests', function() { Plotly.plot(gd, [{ y: [1, 2, 1] }]) .then(function() { - _assert('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); + assertAxisRanges('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); return Plotly.relayout(gd, { 'xaxis.range': [0, 2], 'yaxis.range': [0, 2] }); }) .then(function() { - _assert('set rng / base marker.size', [0, 2], [0, 2]); + assertAxisRanges('set rng / base marker.size', [0, 2], [0, 2]); return Plotly.restyle(gd, 'marker.size', 50); }) .then(function() { - _assert('set rng / big marker.size', [0, 2], [0, 2]); + assertAxisRanges('set rng / big marker.size', [0, 2], [0, 2]); return Plotly.relayout(gd, { 'xaxis.autorange': true, 'yaxis.autorange': true }); }) .then(function() { - _assert('auto rng / big marker.size', [-0.28, 2.28], [0.75, 2.25]); + assertAxisRanges('auto rng / big marker.size', [-0.28, 2.28], [0.75, 2.25]); return Plotly.restyle(gd, 'marker.size', null); }) .then(function() { - _assert('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); + assertAxisRanges('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); + }) + .catch(failTest) + .then(done); + }); + + it('should update axis range according to visible edits', function(done) { + Plotly.plot(gd, [ + {x: [1, 2, 3], y: [1, 2, 1]}, + {x: [4, 5, 6], y: [-1, -2, -1]} + ]) + .then(function() { + assertAxisRanges('both visible', [0.676, 6.323], [-2.29, 2.29]); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + assertAxisRanges('visible [true,false]', [0.87, 3.128], [0.926, 2.07]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + assertAxisRanges('both invisible', [0.87, 3.128], [0.926, 2.07]); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + assertAxisRanges('visible [false,true]', [3.871, 6.128], [-2.07, -0.926]); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + assertAxisRanges('back to both visible', [0.676, 6.323], [-2.29, 2.29]); + }) + .catch(failTest) + .then(done); + }); + + it('should be able to start from visible:false', function(done) { + function _assert(msg, cnt) { + var layer = d3.select(gd).select('g.scatterlayer'); + expect(layer.selectAll('.point').size()).toBe(cnt, msg + '- scatter pts cnt'); + } + + Plotly.plot(gd, [{ + visible: false, + y: [1, 2, 1] + }]) + .then(function() { + _assert('visible:false', 0); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('visible:true', 3); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert('back to visible:false', 0); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index f73a628de14..807efd92f65 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -86,10 +86,10 @@ describe('Test shapes defaults:', function() { it('should provide the right defaults on all axis types', function() { var fullLayout = { - xaxis: {type: 'linear', range: [0, 20]}, - yaxis: {type: 'log', range: [1, 5]}, - xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']}, - yaxis2: {type: 'category', range: [-0.5, 7.5]}, + xaxis: {type: 'linear', range: [0, 20], _shapeIndices: []}, + yaxis: {type: 'log', range: [1, 5], _shapeIndices: []}, + xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09'], _shapeIndices: []}, + yaxis2: {type: 'category', range: [-0.5, 7.5], _shapeIndices: []}, _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']} }; diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index e894ab7fe88..981c4402c54 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -579,6 +579,35 @@ describe('@gl Test splom interactions:', function() { .catch(failTest) .then(done); }); + + it('should toggle trace correctly', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); + + function _assert(msg, exp) { + for(var i = 0; i < 3; i++) { + expect(Boolean(gd.calcdata[i][0].t._scene)) + .toBe(Boolean(exp[i]), msg + ' - trace ' + i); + } + } + + Plotly.plot(gd, fig).then(function() { + _assert('base', [1, 1, 1]); + return Plotly.restyle(gd, 'visible', 'legendonly', [0, 2]); + }) + .then(function() { + _assert('0-2 legendonly', [0, 1, 0]); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert('all gone', [0, 0, 0]); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('all back', [1, 1, 1]); + }) + .catch(failTest) + .then(done); + }); }); describe('@gl Test splom hover:', function() { diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index 2c838336973..a1de229e472 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -15,6 +15,7 @@ function supplyDataDefaults(dataIn, dataOut) { return Plots.supplyDataDefaults(dataIn, dataOut, {}, { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], + _visibleModules: [], _basePlotModules: [], _traceUids: dataIn.map(function() { return Lib.randstr(); }) }); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 5337fb0b388..f834135c0eb 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -16,6 +16,7 @@ var assertStyle = customAssertions.assertStyle; var mockFullLayout = { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], + _visibleModules: [], _basePlotModules: [], _has: function() {}, _dfltTitle: {x: 'xxx', y: 'yyy'},