diff --git a/src/components/errorbars/calc.js b/src/components/errorbars/calc.js
index 5340cdc6b27..56742248010 100644
--- a/src/components/errorbars/calc.js
+++ b/src/components/errorbars/calc.js
@@ -43,12 +43,29 @@ function calcOneAxis(calcTrace, trace, axis, coord) {
var computeError = makeComputeError(opts);
for(var i = 0; i < calcTrace.length; i++) {
- var calcPt = calcTrace[i],
- calcCoord = calcPt[coord];
+ var calcPt = calcTrace[i];
+
+ var iIn = calcPt.i;
+
+ // for types that don't include `i` in each calcdata point
+ if(iIn === undefined) iIn = i;
+
+ // for stacked area inserted points
+ // TODO: errorbars have been tested cursorily with stacked area,
+ // but not thoroughly. It's not even really clear what you want to do:
+ // Should it just be calculated based on that trace's size data?
+ // Should you add errors from below in quadrature?
+ // And what about normalization, where in principle the errors shrink
+ // again when you get up to the top end?
+ // One option would be to forbid errorbars with stacking until we
+ // decide how to handle these questions.
+ else if(iIn === null) continue;
+
+ var calcCoord = calcPt[coord];
if(!isNumeric(axis.c2l(calcCoord))) continue;
- var errors = computeError(calcCoord, i);
+ var errors = computeError(calcCoord, iIn);
if(isNumeric(errors[0]) && isNumeric(errors[1])) {
var shoe = calcPt[coord + 's'] = calcCoord - errors[0],
hat = calcPt[coord + 'h'] = calcCoord + errors[1];
diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index ff8175c69c5..4c187ae1fa9 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -135,9 +135,9 @@ exports.loneHover = function loneHover(hoverItem, opts) {
index: 0
};
- var container3 = d3.select(opts.container),
- outerContainer3 = opts.outerContainer ?
- d3.select(opts.outerContainer) : container3;
+ var container3 = d3.select(opts.container);
+ var outerContainer3 = opts.outerContainer ?
+ d3.select(opts.outerContainer) : container3;
var fullOpts = {
hovermode: 'closest',
@@ -216,37 +216,26 @@ function _hover(gd, evt, subplot, noHoverEvent) {
var spikedistance = fullLayout.spikedistance === -1 ? Infinity : fullLayout.spikedistance;
// hoverData: the set of candidate points we've found to highlight
- var hoverData = [],
-
- // searchData: the data to search in. Mostly this is just a copy of
- // gd.calcdata, filtered to the subplot and overlays we're on
- // but if a point array is supplied it will be a mapping
- // of indicated curves
- searchData = [],
-
- // [x|y]valArray: the axis values of the hover event
- // mapped onto each of the currently selected overlaid subplots
- xvalArray,
- yvalArray,
-
- // used in loops
- itemnum,
- curvenum,
- cd,
- trace,
- subplotId,
- subploti,
- mode,
- xval,
- yval,
- pointData,
- closedataPreviousLength,
-
- // spikePoints: the set of candidate points we've found to draw spikes to
- spikePoints = {
- hLinePoint: null,
- vLinePoint: null
- };
+ var hoverData = [];
+
+ // searchData: the data to search in. Mostly this is just a copy of
+ // gd.calcdata, filtered to the subplot and overlays we're on
+ // but if a point array is supplied it will be a mapping
+ // of indicated curves
+ var searchData = [];
+
+ // [x|y]valArray: the axis values of the hover event
+ // mapped onto each of the currently selected overlaid subplots
+ var xvalArray, yvalArray;
+
+ var itemnum, curvenum, cd, trace, subplotId, subploti, mode,
+ xval, yval, pointData, closedataPreviousLength;
+
+ // spikePoints: the set of candidate points we've found to draw spikes to
+ var spikePoints = {
+ hLinePoint: null,
+ vLinePoint: null
+ };
// Figure out what we're hovering on:
// mouse location or user-supplied data
@@ -273,8 +262,8 @@ function _hover(gd, evt, subplot, noHoverEvent) {
// [x|y]px: the pixels (from top left) of the mouse location
// on the currently selected plot area
// add pointerX|Y property for drawing the spikes in spikesnap 'cursor' situation
- var hasUserCalledHover = !evt.target,
- xpx, ypx;
+ var hasUserCalledHover = !evt.target;
+ var xpx, ypx;
if(hasUserCalledHover) {
if('xpx' in evt) xpx = evt.xpx;
@@ -576,8 +565,8 @@ function _hover(gd, evt, subplot, noHoverEvent) {
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
// lastly, emit custom hover/unhover events
- var oldhoverdata = gd._hoverdata,
- newhoverdata = [];
+ var oldhoverdata = gd._hoverdata;
+ var newhoverdata = [];
// pull out just the data that's useful to
// other people and send it to the event
@@ -677,8 +666,8 @@ function createHoverText(hoverData, opts, gd) {
// all hover traces hoverinfo must contain the hovermode
// to have common labels
if(showCommonLabel) {
- var i, traceHoverinfo;
var allHaveZ = true;
+ var i, traceHoverinfo;
for(i = 0; i < hoverData.length; i++) {
if(allHaveZ && hoverData[i].zLabel === undefined) allHaveZ = false;
@@ -802,9 +791,9 @@ function createHoverText(hoverData, opts, gd) {
// then put the text in, position the pointer to the data,
// and figure out sizes
hoverLabels.each(function(d) {
- var g = d3.select(this).attr('transform', ''),
- name = '',
- text = '';
+ var g = d3.select(this).attr('transform', '');
+ var name = '';
+ var text = '';
// combine possible non-opaque trace color with bgColor
var baseColor = Color.opacity(d.color) ? d.color : Color.defaultLine;
@@ -872,8 +861,8 @@ function createHoverText(hoverData, opts, gd) {
.call(svgTextUtils.positionText, 0, 0)
.call(svgTextUtils.convertToTspans, gd);
- var tx2 = g.select('text.name'),
- tx2width = 0;
+ var tx2 = g.select('text.name');
+ var tx2width = 0;
// secondary label for non-empty 'name'
if(name && name !== text) {
@@ -897,14 +886,13 @@ function createHoverText(hoverData, opts, gd) {
fill: traceColor,
stroke: contrastColor
});
- var tbb = tx.node().getBoundingClientRect(),
- htx = d.xa._offset + (d.x0 + d.x1) / 2,
- hty = d.ya._offset + (d.y0 + d.y1) / 2,
- dx = Math.abs(d.x1 - d.x0),
- dy = Math.abs(d.y1 - d.y0),
- txTotalWidth = tbb.width + HOVERARROWSIZE + HOVERTEXTPAD + tx2width,
- anchorStartOK,
- anchorEndOK;
+ var tbb = tx.node().getBoundingClientRect();
+ var htx = d.xa._offset + (d.x0 + d.x1) / 2;
+ var hty = d.ya._offset + (d.y0 + d.y1) / 2;
+ var dx = Math.abs(d.x1 - d.x0);
+ var dy = Math.abs(d.y1 - d.y0);
+ var txTotalWidth = tbb.width + HOVERARROWSIZE + HOVERTEXTPAD + tx2width;
+ var anchorStartOK, anchorEndOK;
d.ty0 = outerTop - tbb.top;
d.bx = tbb.width + 2 * HOVERTEXTPAD;
@@ -961,33 +949,41 @@ function createHoverText(hoverData, opts, gd) {
// the other, though it hardly matters - there's just too much
// information then.
function hoverAvoidOverlaps(hoverData, ax, fullLayout) {
- var nummoves = 0,
-
- // make groups of touching points
- pointgroups = hoverData
- .map(function(d, i) {
- var axis = d[ax];
- return [{
- i: i,
- dp: 0,
- pos: d.pos,
- posref: d.posref,
- size: d.by * (axis._id.charAt(0) === 'x' ? YFACTOR : 1) / 2,
- pmin: 0,
- pmax: (axis._id.charAt(0) === 'x' ? fullLayout.width : fullLayout.height)
- }];
- })
- .sort(function(a, b) { return a[0].posref - b[0].posref; }),
- donepositioning,
- topOverlap,
- bottomOverlap,
- i, j,
- pti,
- sumdp;
+ var nummoves = 0;
+
+ var axSign = 1;
+
+ // make groups of touching points
+ var pointgroups = hoverData.map(function(d, i) {
+ var axis = d[ax];
+ var axIsX = axis._id.charAt(0) === 'x';
+ var rng = axis.range;
+ if(!i && rng && ((rng[0] > rng[1]) !== axIsX)) axSign = -1;
+ return [{
+ i: i,
+ traceIndex: d.trace.index,
+ dp: 0,
+ pos: d.pos,
+ posref: d.posref,
+ size: d.by * (axIsX ? YFACTOR : 1) / 2,
+ pmin: 0,
+ pmax: (axIsX ? fullLayout.width : fullLayout.height)
+ }];
+ })
+ .sort(function(a, b) {
+ return (a[0].posref - b[0].posref) ||
+ // for equal positions, sort trace indices increasing or decreasing
+ // depending on whether the axis is reversed or not... so stacked
+ // traces will generally keep their order even if one trace adds
+ // nothing to the stack.
+ (axSign * (b[0].traceIndex - a[0].traceIndex));
+ });
+
+ var donepositioning, topOverlap, bottomOverlap, i, j, pti, sumdp;
function constrainGroup(grp) {
- var minPt = grp[0],
- maxPt = grp[grp.length - 1];
+ var minPt = grp[0];
+ var maxPt = grp[grp.length - 1];
// overlap with the top - positive vals are overlaps
topOverlap = minPt.pmin - minPt.pos - minPt.dp + minPt.size;
@@ -1071,13 +1067,13 @@ function hoverAvoidOverlaps(hoverData, ax, fullLayout) {
i = 0;
while(i < pointgroups.length - 1) {
// the higher (g0) and lower (g1) point group
- var g0 = pointgroups[i],
- g1 = pointgroups[i + 1],
+ var g0 = pointgroups[i];
+ var g1 = pointgroups[i + 1];
- // the lowest point in the higher group (p0)
- // the highest point in the lower group (p1)
- p0 = g0[g0.length - 1],
- p1 = g1[0];
+ // the lowest point in the higher group (p0)
+ // the highest point in the lower group (p1)
+ var p0 = g0[g0.length - 1];
+ var p1 = g1[0];
topOverlap = p0.pos + p0.dp + p0.size - p1.pos - p1.dp + p1.size;
// Only group points that lie on the same axes
@@ -1107,8 +1103,8 @@ function hoverAvoidOverlaps(hoverData, ax, fullLayout) {
for(i = pointgroups.length - 1; i >= 0; i--) {
var grp = pointgroups[i];
for(j = grp.length - 1; j >= 0; j--) {
- var pt = grp[j],
- hoverPt = hoverData[pt.i];
+ var pt = grp[j];
+ var hoverPt = hoverData[pt.i];
hoverPt.offset = pt.dp;
hoverPt.del = pt.del;
}
@@ -1124,13 +1120,15 @@ function alignHoverText(hoverLabels, rotateLabels) {
g.remove();
return;
}
- var horzSign = d.anchor === 'end' ? -1 : 1,
- tx = g.select('text.nums'),
- alignShift = {start: 1, end: -1, middle: 0}[d.anchor],
- txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD),
- tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD),
- offsetX = 0,
- offsetY = d.offset;
+
+ var horzSign = d.anchor === 'end' ? -1 : 1;
+ var tx = g.select('text.nums');
+ var alignShift = {start: 1, end: -1, middle: 0}[d.anchor];
+ var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
+ var tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD);
+ var offsetX = 0;
+ var offsetY = d.offset;
+
if(d.anchor === 'middle') {
txx -= d.tx2width / 2;
tx2x += d.txwidth / 2 + HOVERTEXTPAD;
@@ -1266,12 +1264,11 @@ function createSpikelines(closestPoints, opts) {
var container = opts.container;
var fullLayout = opts.fullLayout;
var evt = opts.event;
- var xa,
- ya;
-
var showY = !!closestPoints.hLinePoint;
var showX = !!closestPoints.vLinePoint;
+ var xa, ya;
+
// Remove old spikeline items
container.selectAll('.spikeline').remove();
@@ -1281,9 +1278,9 @@ function createSpikelines(closestPoints, opts) {
// Horizontal line (to y-axis)
if(showY) {
- var hLinePoint = closestPoints.hLinePoint,
- hLinePointX,
- hLinePointY;
+ var hLinePoint = closestPoints.hLinePoint;
+ var hLinePointX, hLinePointY;
+
xa = hLinePoint && hLinePoint.xa;
ya = hLinePoint && hLinePoint.ya;
var ySnap = ya.spikesnap;
@@ -1297,13 +1294,12 @@ function createSpikelines(closestPoints, opts) {
}
var dfltHLineColor = tinycolor.readability(hLinePoint.color, contrastColor) < 1.5 ?
Color.contrast(contrastColor) : hLinePoint.color;
- var yMode = ya.spikemode,
- yThickness = ya.spikethickness,
- yColor = ya.spikecolor || dfltHLineColor,
- yBB = ya._boundingBox,
- xEdge = ((yBB.left + yBB.right) / 2) < hLinePointX ? yBB.right : yBB.left,
- xBase,
- xEndSpike;
+ var yMode = ya.spikemode;
+ var yThickness = ya.spikethickness;
+ var yColor = ya.spikecolor || dfltHLineColor;
+ var yBB = ya._boundingBox;
+ var xEdge = ((yBB.left + yBB.right) / 2) < hLinePointX ? yBB.right : yBB.left;
+ var xBase, xEndSpike;
if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) {
if(yMode.indexOf('toaxis') !== -1) {
@@ -1318,12 +1314,12 @@ function createSpikelines(closestPoints, opts) {
// Foreground horizontal line (to y-axis)
container.insert('line', ':first-child')
.attr({
- 'x1': xBase,
- 'x2': xEndSpike,
- 'y1': hLinePointY,
- 'y2': hLinePointY,
+ x1: xBase,
+ x2: xEndSpike,
+ y1: hLinePointY,
+ y2: hLinePointY,
'stroke-width': yThickness,
- 'stroke': yColor,
+ stroke: yColor,
'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness)
})
.classed('spikeline', true)
@@ -1332,12 +1328,12 @@ function createSpikelines(closestPoints, opts) {
// Background horizontal Line (to y-axis)
container.insert('line', ':first-child')
.attr({
- 'x1': xBase,
- 'x2': xEndSpike,
- 'y1': hLinePointY,
- 'y2': hLinePointY,
+ x1: xBase,
+ x2: xEndSpike,
+ y1: hLinePointY,
+ y2: hLinePointY,
'stroke-width': yThickness + 2,
- 'stroke': contrastColor
+ stroke: contrastColor
})
.classed('spikeline', true)
.classed('crisp', true);
@@ -1346,19 +1342,18 @@ function createSpikelines(closestPoints, opts) {
if(yMode.indexOf('marker') !== -1) {
container.insert('circle', ':first-child')
.attr({
- 'cx': xEdge + (ya.side !== 'right' ? yThickness : -yThickness),
- 'cy': hLinePointY,
- 'r': yThickness,
- 'fill': yColor
+ cx: xEdge + (ya.side !== 'right' ? yThickness : -yThickness),
+ cy: hLinePointY,
+ r: yThickness,
+ fill: yColor
})
.classed('spikeline', true);
}
}
if(showX) {
- var vLinePoint = closestPoints.vLinePoint,
- vLinePointX,
- vLinePointY;
+ var vLinePoint = closestPoints.vLinePoint;
+ var vLinePointX, vLinePointY;
xa = vLinePoint && vLinePoint.xa;
ya = vLinePoint && vLinePoint.ya;
@@ -1372,14 +1367,13 @@ function createSpikelines(closestPoints, opts) {
vLinePointY = ya._offset + vLinePoint.y;
}
var dfltVLineColor = tinycolor.readability(vLinePoint.color, contrastColor) < 1.5 ?
- Color.contrast(contrastColor) : vLinePoint.color;
- var xMode = xa.spikemode,
- xThickness = xa.spikethickness,
- xColor = xa.spikecolor || dfltVLineColor,
- xBB = xa._boundingBox,
- yEdge = ((xBB.top + xBB.bottom) / 2) < vLinePointY ? xBB.bottom : xBB.top,
- yBase,
- yEndSpike;
+ Color.contrast(contrastColor) : vLinePoint.color;
+ var xMode = xa.spikemode;
+ var xThickness = xa.spikethickness;
+ var xColor = xa.spikecolor || dfltVLineColor;
+ var xBB = xa._boundingBox;
+ var yEdge = ((xBB.top + xBB.bottom) / 2) < vLinePointY ? xBB.bottom : xBB.top;
+ var yBase, yEndSpike;
if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) {
if(xMode.indexOf('toaxis') !== -1) {
@@ -1394,12 +1388,12 @@ function createSpikelines(closestPoints, opts) {
// Foreground vertical line (to x-axis)
container.insert('line', ':first-child')
.attr({
- 'x1': vLinePointX,
- 'x2': vLinePointX,
- 'y1': yBase,
- 'y2': yEndSpike,
+ x1: vLinePointX,
+ x2: vLinePointX,
+ y1: yBase,
+ y2: yEndSpike,
'stroke-width': xThickness,
- 'stroke': xColor,
+ stroke: xColor,
'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness)
})
.classed('spikeline', true)
@@ -1408,12 +1402,12 @@ function createSpikelines(closestPoints, opts) {
// Background vertical line (to x-axis)
container.insert('line', ':first-child')
.attr({
- 'x1': vLinePointX,
- 'x2': vLinePointX,
- 'y1': yBase,
- 'y2': yEndSpike,
+ x1: vLinePointX,
+ x2: vLinePointX,
+ y1: yBase,
+ y2: yEndSpike,
'stroke-width': xThickness + 2,
- 'stroke': contrastColor
+ stroke: contrastColor
})
.classed('spikeline', true)
.classed('crisp', true);
@@ -1423,10 +1417,10 @@ function createSpikelines(closestPoints, opts) {
if(xMode.indexOf('marker') !== -1) {
container.insert('circle', ':first-child')
.attr({
- 'cx': vLinePointX,
- 'cy': yEdge - (xa.side !== 'top' ? xThickness : -xThickness),
- 'r': xThickness,
- 'fill': xColor
+ cx: vLinePointX,
+ cy: yEdge - (xa.side !== 'top' ? xThickness : -xThickness),
+ r: xThickness,
+ fill: xColor
})
.classed('spikeline', true);
}
@@ -1438,8 +1432,8 @@ function hoverChanged(gd, evt, oldhoverdata) {
if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true;
for(var i = oldhoverdata.length - 1; i >= 0; i--) {
- var oldPt = oldhoverdata[i],
- newPt = gd._hoverdata[i];
+ var oldPt = oldhoverdata[i];
+ var newPt = gd._hoverdata[i];
if(oldPt.curveNumber !== newPt.curveNumber ||
String(oldPt.pointNumber) !== String(newPt.pointNumber)) {
return true;
diff --git a/src/constants/numerical.js b/src/constants/numerical.js
index c13765d95bb..77ea5abf465 100644
--- a/src/constants/numerical.js
+++ b/src/constants/numerical.js
@@ -49,6 +49,12 @@ module.exports = {
*/
ALMOST_EQUAL: 1 - 1e-6,
+ /*
+ * If we're asked to clip a non-positive log value, how far off-screen
+ * do we put it?
+ */
+ LOG_CLIP: 10,
+
/*
* not a number, but for displaying numbers: the "minus sign" symbol is
* wider than the regular ascii dash "-"
diff --git a/src/lib/index.js b/src/lib/index.js
index 2c2177ae2b0..26ee71bf1db 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -65,6 +65,7 @@ lib.sorterAsc = searchModule.sorterAsc;
lib.sorterDes = searchModule.sorterDes;
lib.distinctVals = searchModule.distinctVals;
lib.roundUp = searchModule.roundUp;
+lib.sort = searchModule.sort;
lib.findIndexOfMin = searchModule.findIndexOfMin;
var statsModule = require('./stats');
diff --git a/src/lib/search.js b/src/lib/search.js
index 056d707e54a..8fcc5ed9bb1 100644
--- a/src/lib/search.js
+++ b/src/lib/search.js
@@ -115,6 +115,48 @@ exports.roundUp = function(val, arrayIn, reverse) {
return arrayIn[low];
};
+/**
+ * Tweak to Array.sort(sortFn) that improves performance for pre-sorted arrays
+ *
+ * Motivation: sometimes we need to sort arrays but the input is likely to
+ * already be sorted. Browsers don't seem to pick up on pre-sorted arrays,
+ * and in fact Chrome is actually *slower* sorting pre-sorted arrays than purely
+ * random arrays. FF is at least faster if the array is pre-sorted, but still
+ * not as fast as it could be.
+ * Here's how this plays out sorting a length-1e6 array:
+ *
+ * Calls to Sort FN | Chrome bare | FF bare | Chrome tweak | FF tweak
+ * | v68.0 Mac | v61.0 Mac| |
+ * ------------------+---------------+-----------+----------------+------------
+ * ordered | 30.4e6 | 10.1e6 | 1e6 | 1e6
+ * reversed | 29.4e6 | 9.9e6 | 1e6 + reverse | 1e6 + reverse
+ * random | ~21e6 | ~18.7e6 | ~21e6 | ~18.7e6
+ *
+ * So this is a substantial win for pre-sorted (ordered or exactly reversed)
+ * arrays. Including this wrapper on an unsorted array adds a penalty that will
+ * in general be only a few calls to the sort function. The only case this
+ * penalty will be significant is if the array is mostly sorted but there are
+ * a few unsorted items near the end, but the penalty is still at most N calls
+ * out of (for N=1e6) ~20N total calls
+ *
+ * @param {Array} array: the array, to be sorted in place
+ * @param {function} sortFn: As in Array.sort, function(a, b) that puts
+ * item a before item b if the return is negative, a after b if positive,
+ * and no change if zero.
+ * @return {Array}: the original array, sorted in place.
+ */
+exports.sort = function(array, sortFn) {
+ var notOrdered = 0;
+ var notReversed = 0;
+ for(var i = 1; i < array.length; i++) {
+ var pairOrder = sortFn(array[i], array[i - 1]);
+ if(pairOrder < 0) notOrdered = 1;
+ else if(pairOrder > 0) notReversed = 1;
+ if(notOrdered && notReversed) return array.sort(sortFn);
+ }
+ return notReversed ? array : array.reverse();
+};
+
/**
* find index in array 'arr' that minimizes 'fn'
*
diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js
index eaab8f37b5d..3343fb4a899 100644
--- a/src/plots/cartesian/autorange.js
+++ b/src/plots/cartesian/autorange.js
@@ -87,6 +87,13 @@ function getAutoRange(gd, ax) {
ax.autorange = true;
}
+ var rangeMode = ax.rangemode;
+ var toZero = rangeMode === 'tozero';
+ var nonNegative = rangeMode === 'nonnegative';
+ var axLen = ax._length;
+ // don't allow padding to reduce the data to < 10% of the length
+ var minSpan = axLen / 10;
+
var mbest = 0;
var minpt, maxpt, minbest, maxbest, dp, dv;
@@ -95,76 +102,83 @@ function getAutoRange(gd, ax) {
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) {
- minbest = minpt;
- maxbest = maxpt;
- mbest = dv / dp;
+ if(dv > 0) {
+ dp = axLen - getPad(minpt) - getPad(maxpt);
+ if(dp > minSpan) {
+ if(dv / dp > mbest) {
+ minbest = minpt;
+ maxbest = maxpt;
+ mbest = dv / dp;
+ }
+ }
+ else if(dv / axLen > mbest) {
+ // in case of padding longer than the axis
+ // at least include the unpadded data values.
+ minbest = {val: minpt.val, pad: 0};
+ maxbest = {val: maxpt.val, pad: 0};
+ mbest = dv / axLen;
+ }
}
}
}
+ function getMaxPad(prev, pt) {
+ return Math.max(prev, getPad(pt));
+ }
+
if(minmin === maxmax) {
var lower = minmin - 1;
var upper = minmin + 1;
- if(ax.rangemode === 'tozero') {
- newRange = minmin < 0 ? [lower, 0] : [0, upper];
- } else if(ax.rangemode === 'nonnegative') {
- newRange = [Math.max(0, lower), Math.max(0, upper)];
+ if(toZero) {
+ if(minmin === 0) {
+ // The only value we have on this axis is 0, and we want to
+ // autorange so zero is one end.
+ // In principle this could be [0, 1] or [-1, 0] but usually
+ // 'tozero' pins 0 to the low end, so follow that.
+ newRange = [0, 1];
+ }
+ else {
+ var maxPad = (minmin > 0 ? maxArray : minArray).reduce(getMaxPad, 0);
+ // we're pushing a single value away from the edge due to its
+ // padding, with the other end clamped at zero
+ // 0.5 means don't push it farther than the center.
+ var rangeEnd = minmin / (1 - Math.min(0.5, maxPad / axLen));
+ newRange = minmin > 0 ? [0, rangeEnd] : [rangeEnd, 0];
+ }
+ } else if(nonNegative) {
+ newRange = [Math.max(0, lower), Math.max(1, upper)];
} else {
newRange = [lower, upper];
}
}
- else if(mbest) {
- if(ax.type === 'linear' || ax.type === '-') {
- if(ax.rangemode === 'tozero') {
- if(minbest.val >= 0) {
- minbest = {val: 0, pad: 0};
- }
- if(maxbest.val <= 0) {
- maxbest = {val: 0, pad: 0};
- }
+ else {
+ if(toZero) {
+ if(minbest.val >= 0) {
+ minbest = {val: 0, pad: 0};
}
- else if(ax.rangemode === 'nonnegative') {
- if(minbest.val - mbest * getPad(minbest) < 0) {
- minbest = {val: 0, pad: 0};
- }
- if(maxbest.val < 0) {
- maxbest = {val: 1, pad: 0};
- }
+ if(maxbest.val <= 0) {
+ maxbest = {val: 0, pad: 0};
+ }
+ }
+ else if(nonNegative) {
+ if(minbest.val - mbest * getPad(minbest) < 0) {
+ minbest = {val: 0, pad: 0};
+ }
+ if(maxbest.val <= 0) {
+ maxbest = {val: 1, pad: 0};
}
-
- // in case it changed again...
- mbest = (maxbest.val - minbest.val) /
- (ax._length - getPad(minbest) - getPad(maxbest));
-
}
+ // in case it changed again...
+ mbest = (maxbest.val - minbest.val) /
+ (axLen - getPad(minbest) - getPad(maxbest));
+
newRange = [
minbest.val - mbest * getPad(minbest),
maxbest.val + mbest * getPad(maxbest)
];
}
- // don't let axis have zero size, while still respecting tozero and nonnegative
- if(newRange[0] === newRange[1]) {
- if(ax.rangemode === 'tozero') {
- if(newRange[0] < 0) {
- newRange = [newRange[0], 0];
- } else if(newRange[0] > 0) {
- newRange = [0, newRange[0]];
- } else {
- newRange = [0, 1];
- }
- }
- else {
- newRange = [newRange[0] - 1, newRange[0] + 1];
- if(ax.rangemode === 'nonnegative') {
- newRange[0] = Math.max(0, newRange[0]);
- }
- }
- }
-
// maintain reversal
if(axReverse) newRange.reverse();
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 69141a0b784..421fb0e0633 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -20,6 +20,8 @@ var Titles = require('../../components/titles');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
+var axAttrs = require('./layout_attributes');
+
var constants = require('../../constants/numerical');
var ONEAVGYEAR = constants.ONEAVGYEAR;
var ONEAVGMONTH = constants.ONEAVGMONTH;
@@ -2411,11 +2413,12 @@ function swapAxisGroup(gd, xIds, yIds) {
for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i]));
for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i]));
- var allAxKeys = Object.keys(xFullAxes[0]),
- noSwapAttrs = [
- 'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle'
- ],
- numericTypes = ['linear', 'log'];
+ var allAxKeys = Object.keys(axAttrs);
+
+ var noSwapAttrs = [
+ 'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle', 'editType'
+ ];
+ var numericTypes = ['linear', 'log'];
for(i = 0; i < allAxKeys.length; i++) {
var keyi = allAxKeys[i],
diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js
index 06de3b99378..f3691bf8003 100644
--- a/src/plots/cartesian/axis_defaults.js
+++ b/src/plots/cartesian/axis_defaults.js
@@ -48,7 +48,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
setConvert(containerOut, layoutOut);
var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range));
- if(autoRange) coerce('rangemode');
+ if(autoRange && (axType === 'linear' || axType === '-')) coerce('rangemode');
coerce('range');
containerOut.cleanRange();
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index 580f040990a..3d81797b7e9 100644
--- a/src/plots/cartesian/layout_attributes.js
+++ b/src/plots/cartesian/layout_attributes.js
@@ -99,7 +99,8 @@ module.exports = {
'If *tozero*`, the range extends to 0,',
'regardless of the input data',
'If *nonnegative*, the range is non-negative,',
- 'regardless of the input data.'
+ 'regardless of the input data.',
+ 'Applies only to linear axes.'
].join(' ')
},
range: {
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index b239ffb2e58..e057aba7b20 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -21,6 +21,7 @@ var ensureNumber = Lib.ensureNumber;
var numConstants = require('../../constants/numerical');
var FP_SAFE = numConstants.FP_SAFE;
var BADNUM = numConstants.BADNUM;
+var LOG_CLIP = numConstants.LOG_CLIP;
var constants = require('./constants');
var axisIds = require('./axis_ids');
@@ -59,20 +60,15 @@ module.exports = function setConvert(ax, fullLayout) {
var axLetter = (ax._id || 'x').charAt(0);
- // clipMult: how many axis lengths past the edge do we render?
- // for panning, 1-2 would suffice, but for zooming more is nice.
- // also, clipping can affect the direction of lines off the edge...
- var clipMult = 10;
-
function toLog(v, clip) {
if(v > 0) return Math.log(v) / Math.LN10;
else if(v <= 0 && clip && ax.range && ax.range.length === 2) {
- // clip NaN (ie past negative infinity) to clipMult axis
+ // clip NaN (ie past negative infinity) to LOG_CLIP axis
// length past the negative edge
var r0 = ax.range[0],
r1 = ax.range[1];
- return 0.5 * (r0 + r1 - 3 * clipMult * Math.abs(r0 - r1));
+ return 0.5 * (r0 + r1 - 2 * LOG_CLIP * Math.abs(r0 - r1));
}
else return BADNUM;
diff --git a/src/plots/plots.js b/src/plots/plots.js
index c0a2bb36e84..147f83a3914 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -391,6 +391,11 @@ plots.supplyDefaults = function(gd, opts) {
// initialize splom grid defaults
newFullLayout._splomGridDflt = {};
+ // for stacked area traces to share config across traces
+ newFullLayout._scatterStackOpts = {};
+ // for the first scatter trace on each subplot (so it knows tonext->tozero)
+ newFullLayout._firstScatter = {};
+
// for traces to request a default rangeslider on their x axes
// eg set `_requestRangeslider.x2 = true` for xaxis2
newFullLayout._requestRangeslider = {};
@@ -938,8 +943,6 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
fullTrace.uid = fullLayout._traceUids[i];
plots.supplyTraceDefaults(trace, fullTrace, colorCnt, fullLayout, i);
- fullTrace.uid = fullLayout._traceUids[i];
-
fullTrace.index = i;
fullTrace._input = trace;
fullTrace._expandedIndex = cnt;
@@ -1559,7 +1562,6 @@ plots.purge = function(gd) {
// (and to have a record of them...)
delete gd._promises;
delete gd._redrawTimer;
- delete gd.firstscatter;
delete gd._hmlumcount;
delete gd._hmpixcount;
delete gd._transitionData;
@@ -2424,8 +2426,6 @@ plots.doCalcdata = function(gd, traces) {
gd.calcdata = calcdata;
// extra helper variables
- // firstscatter: fill-to-next on the first trace goes to zero
- gd.firstscatter = true;
// how many box/violins plots do we have (in case they're grouped)
fullLayout._numBoxes = 0;
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index 42fcbad3a4d..f8bb7a9103b 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -91,7 +91,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
case 'radialaxis':
var autoRange = coerceAxis('autorange', !axOut.isValidRange(axIn.range));
axIn.autorange = autoRange;
- if(autoRange) coerceAxis('rangemode');
+ if(autoRange && (axType === 'linear' || axType === '-')) coerceAxis('rangemode');
if(autoRange === 'reversed') axOut._m = -1;
coerceAxis('range');
diff --git a/src/traces/bar/layout_attributes.js b/src/traces/bar/layout_attributes.js
index aa84ce21d55..a78a2107ca7 100644
--- a/src/traces/bar/layout_attributes.js
+++ b/src/traces/bar/layout_attributes.js
@@ -36,9 +36,9 @@ module.exports = {
editType: 'calc',
description: [
'Sets the normalization for bar traces on the graph.',
- 'With *fraction*, the value of each bar is divide by the sum of the',
- 'values at the location coordinate.',
- 'With *percent*, the results form *fraction* are presented in percents.'
+ 'With *fraction*, the value of each bar is divided by the sum of all',
+ 'values at that location coordinate.',
+ '*percent* is the same but multiplied by 100 to show percentages.'
].join(' ')
},
bargap: {
diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js
index d8a17c934e8..6c5c2edf7f3 100644
--- a/src/traces/scatter/attributes.js
+++ b/src/traces/scatter/attributes.js
@@ -72,6 +72,74 @@ module.exports = {
'See `y0` for more info.'
].join(' ')
},
+
+ stackgroup: {
+ valType: 'string',
+ role: 'info',
+ dflt: '',
+ editType: 'calc',
+ description: [
+ 'Set several scatter traces (on the same subplot) to the same',
+ 'stackgroup in order to add their y values (or their x values if',
+ '`orientation` is *h*). If blank or omitted this trace will not be',
+ 'stacked. Stacking also turns `fill` on by default, using *tonexty*',
+ '(*tonextx*) if `orientation` is *h* (*v*) and sets the default',
+ '`mode` to *lines* irrespective of point count.',
+ 'You can only stack on a numeric (linear or log) axis.'
+ ].join(' ')
+ },
+ orientation: {
+ valType: 'enumerated',
+ role: 'info',
+ values: ['v', 'h'],
+ editType: 'calc',
+ description: [
+ 'Only relevant when `stackgroup` is used, and only the first',
+ '`orientation` found in the `stackgroup` will be used - including',
+ 'if `visible` is *legendonly* but not if it is `false`. Sets the',
+ 'stacking direction. With *v* (*h*), the y (x) values of subsequent',
+ 'traces are added. Also affects the default value of `fill`.'
+ ].join(' ')
+ },
+ groupnorm: {
+ valType: 'enumerated',
+ values: ['', 'fraction', 'percent'],
+ dflt: '',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Only relevant when `stackgroup` is used, and only the first',
+ '`groupnorm` found in the `stackgroup` will be used - including',
+ 'if `visible` is *legendonly* but not if it is `false`.',
+ 'Sets the normalization for the sum of this `stackgroup`.',
+ 'With *fraction*, the value of each trace at each location is',
+ 'divided by the sum of all trace values at that location.',
+ '*percent* is the same but multiplied by 100 to show percentages.',
+ 'If there are multiple subplots, or multiple `stackgroup`s on one',
+ 'subplot, each will be normalized within its own set.'
+ ].join(' ')
+ },
+ stackgaps: {
+ valType: 'enumerated',
+ values: ['infer zero', 'interpolate'],
+ dflt: 'infer zero',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Only relevant when `stackgroup` is used, and only the first',
+ '`stackgaps` found in the `stackgroup` will be used - including',
+ 'if `visible` is *legendonly* but not if it is `false`.',
+ 'Determines how we handle locations at which other traces in this',
+ 'group have data but this one does not.',
+ 'With *infer zero* we insert a zero at these locations.',
+ 'With *interpolate* we linearly interpolate between existing',
+ 'values, and extrapolate a constant beyond the existing values.'
+ // TODO - implement interrupt mode
+ // '*interrupt* omits this trace from the stack at this location by',
+ // 'dropping abruptly, midway between the existing and missing locations.'
+ ].join(' ')
+ },
+
text: {
valType: 'string',
role: 'info',
@@ -114,7 +182,8 @@ module.exports = {
'If the provided `mode` includes *text* then the `text` elements',
'appear at the coordinates. Otherwise, the `text` elements',
'appear on hover.',
- 'If there are less than ' + constants.PTS_LINESONLY + ' points,',
+ 'If there are less than ' + constants.PTS_LINESONLY + ' points',
+ 'and the trace is not stacked',
'then the default is *lines+markers*. Otherwise, *lines*.'
].join(' ')
},
@@ -212,11 +281,12 @@ module.exports = {
fill: {
valType: 'enumerated',
values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'],
- dflt: 'none',
role: 'style',
editType: 'calc',
description: [
'Sets the area to fill with a solid color.',
+ 'Defaults to *none* unless this trace is stacked, then it gets',
+ '*tonexty* (*tonextx*) if `orientation` is *v* (*h*)',
'Use with `fillcolor` if not *none*.',
'*tozerox* and *tozeroy* fill to x=0 and y=0 respectively.',
'*tonextx* and *tonexty* fill between the endpoints of this',
diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js
index 37a9068c8e9..2ff8728073c 100644
--- a/src/traces/scatter/calc.js
+++ b/src/traces/scatter/calc.js
@@ -9,7 +9,7 @@
'use strict';
var isNumeric = require('fast-isnumeric');
-var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray;
+var Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
var BADNUM = require('../../constants/numerical').BADNUM;
@@ -20,23 +20,69 @@ var arraysToCalcdata = require('./arrays_to_calcdata');
var calcSelection = require('./calc_selection');
function calc(gd, trace) {
+ var fullLayout = gd._fullLayout;
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
var x = xa.makeCalcdata(trace, 'x');
var y = ya.makeCalcdata(trace, 'y');
var serieslen = trace._length;
var cd = new Array(serieslen);
+ var ids = trace.ids;
+ var stackGroupOpts = getStackOpts(trace, fullLayout, xa, ya);
+ var interpolateGaps = false;
+ var isV, i, j, k, interpolate, vali;
- var ppad = calcMarkerSize(trace, serieslen);
- calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
+ setFirstScatter(fullLayout, trace);
- for(var i = 0; i < serieslen; i++) {
- cd[i] = (isNumeric(x[i]) && isNumeric(y[i])) ?
- {x: x[i], y: y[i]} :
- {x: BADNUM, y: BADNUM};
+ var xAttr = 'x';
+ var yAttr = 'y';
+ var posAttr;
+ if(stackGroupOpts) {
+ stackGroupOpts.traceIndices.push(trace.index);
+ isV = stackGroupOpts.orientation === 'v';
+ // size, like we use for bar
+ if(isV) {
+ yAttr = 's';
+ posAttr = 'x';
+ }
+ else {
+ xAttr = 's';
+ posAttr = 'y';
+ }
+ interpolate = stackGroupOpts.stackgaps === 'interpolate';
+ }
+ else {
+ var ppad = calcMarkerSize(trace, serieslen);
+ calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
+ }
+
+ for(i = 0; i < serieslen; i++) {
+ var cdi = cd[i] = {};
+ var xValid = isNumeric(x[i]);
+ var yValid = isNumeric(y[i]);
+ if(xValid && yValid) {
+ cdi[xAttr] = x[i];
+ cdi[yAttr] = y[i];
+ }
+ // if we're stacking we need to hold on to all valid positions
+ // even with invalid sizes
+ else if(stackGroupOpts && (isV ? xValid : yValid)) {
+ cdi[posAttr] = isV ? x[i] : y[i];
+ cdi.gap = true;
+ if(interpolate) {
+ cdi.s = BADNUM;
+ interpolateGaps = true;
+ }
+ else {
+ cdi.s = 0;
+ }
+ }
+ else {
+ cdi[xAttr] = cdi[yAttr] = BADNUM;
+ }
- if(trace.ids) {
- cd[i].id = String(trace.ids[i]);
+ if(ids) {
+ cdi.id = String(ids[i]);
}
}
@@ -44,12 +90,72 @@ function calc(gd, trace) {
calcColorscale(trace);
calcSelection(cd, trace);
- gd.firstscatter = false;
+ if(stackGroupOpts) {
+ // remove bad positions and sort
+ // note that original indices get added to cd in arraysToCalcdata
+ i = 0;
+ while(i < cd.length) {
+ if(cd[i][posAttr] === BADNUM) {
+ cd.splice(i, 1);
+ }
+ else i++;
+ }
+
+ Lib.sort(cd, function(a, b) {
+ return (a[posAttr] - b[posAttr]) || (a.i - b.i);
+ });
+
+ if(interpolateGaps) {
+ // first fill the beginning with constant from the first point
+ i = 0;
+ while(i < cd.length - 1 && cd[i].gap) {
+ i++;
+ }
+ vali = cd[i].s;
+ if(!vali) vali = cd[i].s = 0; // in case of no data AT ALL in this trace - use 0
+ for(j = 0; j < i; j++) {
+ cd[j].s = vali;
+ }
+ // then fill the end with constant from the last point
+ k = cd.length - 1;
+ while(k > i && cd[k].gap) {
+ k--;
+ }
+ vali = cd[k].s;
+ for(j = cd.length - 1; j > k; j--) {
+ cd[j].s = vali;
+ }
+ // now interpolate internal gaps linearly
+ while(i < k) {
+ i++;
+ if(cd[i].gap) {
+ j = i + 1;
+ while(cd[j].gap) {
+ j++;
+ }
+ var pos0 = cd[i - 1][posAttr];
+ var size0 = cd[i - 1].s;
+ var m = (cd[j].s - size0) / (cd[j][posAttr] - pos0);
+ while(i < j) {
+ cd[i].s = size0 + (cd[i][posAttr] - pos0) * m;
+ i++;
+ }
+ }
+ }
+ }
+ }
+
return cd;
}
function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
var serieslen = trace._length;
+ var fullLayout = gd._fullLayout;
+ var xId = xa._id;
+ var yId = ya._id;
+ var firstScatter = fullLayout._firstScatter[xId + yId + trace.type] === trace.uid;
+ var stackOrientation = (getStackOpts(trace, fullLayout, xa, ya) || {}).orientation;
+ var fill = trace.fill;
// cancel minimum tick spacings (only applies to bars and boxes)
xa._minDtick = 0;
@@ -66,17 +172,20 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
// TODO: text size
+ var openEnded = serieslen < 2 || (x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]);
+
// include zero (tight) and extremes (padded) if fill to zero
// (unless the shape is closed, then it's just filling the shape regardless)
- if(((trace.fill === 'tozerox') ||
- ((trace.fill === 'tonextx') && gd.firstscatter)) &&
- ((x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]))) {
+ if(openEnded && (
+ (fill === 'tozerox') ||
+ ((fill === 'tonextx') && (firstScatter || stackOrientation === 'h'))
+ )) {
xOptions.tozero = true;
}
// if no error bars, markers or text, or fill to y=0 remove x padding
else if(!(trace.error_y || {}).visible && (
- ['tonexty', 'tozeroy'].indexOf(trace.fill) !== -1 ||
+ (fill === 'tonexty' || fill === 'tozeroy') ||
(!subTypes.hasMarkers(trace) && !subTypes.hasText(trace))
)) {
xOptions.padded = false;
@@ -86,19 +195,21 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
// now check for y - rather different logic, though still mostly padded both ends
// include zero (tight) and extremes (padded) if fill to zero
// (unless the shape is closed, then it's just filling the shape regardless)
- if(((trace.fill === 'tozeroy') || ((trace.fill === 'tonexty') && gd.firstscatter)) &&
- ((x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]))) {
+ if(openEnded && (
+ (fill === 'tozeroy') ||
+ ((fill === 'tonexty') && (firstScatter || stackOrientation === 'v'))
+ )) {
yOptions.tozero = true;
}
// tight y: any x fill
- else if(['tonextx', 'tozerox'].indexOf(trace.fill) !== -1) {
+ else if(fill === 'tonextx' || fill === 'tozerox') {
yOptions.padded = false;
}
// N.B. asymmetric splom traces call this with blank {} xa or ya
- if(xa._id) trace._extremes[xa._id] = Axes.findExtremes(xa, x, xOptions);
- if(ya._id) trace._extremes[ya._id] = Axes.findExtremes(ya, y, yOptions);
+ if(xId) trace._extremes[xId] = Axes.findExtremes(xa, x, xOptions);
+ if(yId) trace._extremes[yId] = Axes.findExtremes(ya, y, yOptions);
}
function calcMarkerSize(trace, serieslen) {
@@ -120,7 +231,7 @@ function calcMarkerSize(trace, serieslen) {
};
}
- if(isArrayOrTypedArray(marker.size)) {
+ if(Lib.isArrayOrTypedArray(marker.size)) {
// I tried auto-type but category and dates dont make much sense.
var ax = {type: 'linear'};
Axes.setConvert(ax);
@@ -138,8 +249,34 @@ function calcMarkerSize(trace, serieslen) {
}
}
+/**
+ * mark the first scatter trace for each subplot
+ * note that scatter and scattergl each get their own first trace
+ * note also that I'm doing this during calc rather than supplyDefaults
+ * so I don't need to worry about transforms, but if we ever do
+ * per-trace calc this will get confused.
+ */
+function setFirstScatter(fullLayout, trace) {
+ var subplotAndType = trace.xaxis + trace.yaxis + trace.type;
+ var firstScatter = fullLayout._firstScatter;
+ if(!firstScatter[subplotAndType]) firstScatter[subplotAndType] = trace.uid;
+}
+
+function getStackOpts(trace, fullLayout, xa, ya) {
+ var stackGroup = trace.stackgroup;
+ if(!stackGroup) return;
+ var stackOpts = fullLayout._scatterStackOpts[xa._id + ya._id][stackGroup];
+ var stackAx = stackOpts.orientation === 'v' ? ya : xa;
+ // Allow stacking only on numeric axes
+ // calc is a little late to be figuring this out, but during supplyDefaults
+ // we don't know the axis type yet
+ if(stackAx.type === 'linear' || stackAx.type === 'log') return stackOpts;
+}
+
module.exports = {
calc: calc,
calcMarkerSize: calcMarkerSize,
- calcAxisExpansion: calcAxisExpansion
+ calcAxisExpansion: calcAxisExpansion,
+ setFirstScatter: setFirstScatter,
+ getStackOpts: getStackOpts
};
diff --git a/src/traces/scatter/cross_trace_calc.js b/src/traces/scatter/cross_trace_calc.js
new file mode 100644
index 00000000000..64db8eaad7d
--- /dev/null
+++ b/src/traces/scatter/cross_trace_calc.js
@@ -0,0 +1,181 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+var calc = require('./calc');
+
+/*
+ * Scatter stacking & normalization calculations
+ * runs per subplot, and can handle multiple stacking groups
+ */
+
+module.exports = function crossTraceCalc(gd, plotinfo) {
+ var xa = plotinfo.xaxis;
+ var ya = plotinfo.yaxis;
+ var subplot = xa._id + ya._id;
+
+ var subplotStackOpts = gd._fullLayout._scatterStackOpts[subplot];
+ if(!subplotStackOpts) return;
+
+ var calcTraces = gd.calcdata;
+
+ var i, j, k, i2, cd, cd0, posj, sumj, norm;
+ var groupOpts, interpolate, groupnorm, posAttr, valAttr;
+ var hasAnyBlanks;
+
+ function insertBlank(calcTrace, index, position, traceIndex) {
+ hasAnyBlanks[traceIndex] = true;
+ var newEntry = {
+ i: null,
+ gap: true,
+ s: 0
+ };
+ newEntry[posAttr] = position;
+ calcTrace.splice(index, 0, newEntry);
+ // Even if we're not interpolating, if one trace has multiple
+ // values at the same position and this trace only has one value there,
+ // we just duplicate that one value rather than insert a zero.
+ // We also make it look like a real point - because it's ambiguous which
+ // one really is the real one!
+ if(index && position === calcTrace[index - 1][posAttr]) {
+ var prevEntry = calcTrace[index - 1];
+ newEntry.s = prevEntry.s;
+ // TODO is it going to cause any problems to have multiple
+ // calcdata points with the same index?
+ newEntry.i = prevEntry.i;
+ newEntry.gap = prevEntry.gap;
+ }
+ else if(interpolate) {
+ newEntry.s = getInterp(calcTrace, index, position);
+ }
+ if(!index) {
+ // t and trace need to stay on the first cd entry
+ cd[0].t = cd[1].t;
+ cd[0].trace = cd[1].trace;
+ delete cd[1].t;
+ delete cd[1].trace;
+ }
+ }
+
+ function getInterp(calcTrace, index, position) {
+ var pt0 = calcTrace[index - 1];
+ var pt1 = calcTrace[index + 1];
+ if(!pt1) return pt0.s;
+ if(!pt0) return pt1.s;
+ return pt0.s + (pt1.s - pt0.s) * (position - pt0[posAttr]) / (pt1[posAttr] - pt0[posAttr]);
+ }
+
+ for(var stackGroup in subplotStackOpts) {
+ groupOpts = subplotStackOpts[stackGroup];
+ var indices = groupOpts.traceIndices;
+
+ // can get here with no indices if the stack axis is non-numeric
+ if(!indices.length) continue;
+
+ interpolate = groupOpts.stackgaps === 'interpolate';
+ groupnorm = groupOpts.groupnorm;
+ if(groupOpts.orientation === 'v') {
+ posAttr = 'x';
+ valAttr = 'y';
+ }
+ else {
+ posAttr = 'y';
+ valAttr = 'x';
+ }
+ hasAnyBlanks = new Array(indices.length);
+ for(i = 0; i < hasAnyBlanks.length; i++) {
+ hasAnyBlanks[i] = false;
+ }
+
+ // Collect the complete set of all positions across ALL traces.
+ // Start with the first trace, then interleave items from later traces
+ // as needed.
+ // Fill in mising items as we go.
+ cd0 = calcTraces[indices[0]];
+ var allPositions = new Array(cd0.length);
+ for(i = 0; i < cd0.length; i++) {
+ allPositions[i] = cd0[i][posAttr];
+ }
+
+ for(i = 1; i < indices.length; i++) {
+ cd = calcTraces[indices[i]];
+
+ for(j = k = 0; j < cd.length; j++) {
+ posj = cd[j][posAttr];
+ for(; posj > allPositions[k] && k < allPositions.length; k++) {
+ // the current trace is missing a position from some previous trace(s)
+ insertBlank(cd, j, allPositions[k], i);
+ j++;
+ }
+ if(posj !== allPositions[k]) {
+ // previous trace(s) are missing a position from the current trace
+ for(i2 = 0; i2 < i; i2++) {
+ insertBlank(calcTraces[indices[i2]], k, posj, i2);
+ }
+ allPositions.splice(k, 0, posj);
+ }
+ k++;
+ }
+ for(; k < allPositions.length; k++) {
+ insertBlank(cd, j, allPositions[k], i);
+ j++;
+ }
+ }
+
+ var serieslen = allPositions.length;
+
+ // stack (and normalize)!
+ for(j = 0; j < cd0.length; j++) {
+ sumj = cd0[j][valAttr] = cd0[j].s;
+ for(i = 1; i < indices.length; i++) {
+ cd = calcTraces[indices[i]];
+ cd[0].trace._rawLength = cd[0].trace._length;
+ cd[0].trace._length = serieslen;
+ sumj += cd[j].s;
+ cd[j][valAttr] = sumj;
+ }
+
+ if(groupnorm) {
+ norm = ((groupnorm === 'fraction') ? sumj : (sumj / 100)) || 1;
+ for(i = 0; i < indices.length; i++) {
+ var cdj = calcTraces[indices[i]][j];
+ cdj[valAttr] /= norm;
+ cdj.sNorm = cdj.s / norm;
+ }
+ }
+ }
+
+ // autorange
+ for(i = 0; i < indices.length; i++) {
+ cd = calcTraces[indices[i]];
+ var trace = cd[0].trace;
+ var ppad = calc.calcMarkerSize(trace, trace._rawLength);
+ var arrayPad = Array.isArray(ppad);
+ if((ppad && hasAnyBlanks[i]) || arrayPad) {
+ var ppadRaw = ppad;
+ ppad = new Array(serieslen);
+ for(j = 0; j < serieslen; j++) {
+ ppad[j] = cd[j].gap ? 0 : (arrayPad ? ppadRaw[cd[j].i] : ppadRaw);
+ }
+ }
+ var x = new Array(serieslen);
+ var y = new Array(serieslen);
+ for(j = 0; j < serieslen; j++) {
+ x[j] = cd[j].x;
+ y[j] = cd[j].y;
+ }
+ calc.calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
+
+ // while we're here (in a loop over all traces in the stack)
+ // record the orientation, so hover can find it easily
+ cd[0].t.orientation = groupOpts.orientation;
+ }
+ }
+};
diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js
index ba9fa7c5c2c..5668c9ffd9d 100644
--- a/src/traces/scatter/defaults.js
+++ b/src/traces/scatter/defaults.js
@@ -15,6 +15,7 @@ var attributes = require('./attributes');
var constants = require('./constants');
var subTypes = require('./subtypes');
var handleXYDefaults = require('./xy_defaults');
+var handleStackDefaults = require('./stack_defaults');
var handleMarkerDefaults = require('./marker_defaults');
var handleLineDefaults = require('./line_defaults');
var handleLineShapeDefaults = require('./line_shape_defaults');
@@ -26,14 +27,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- var len = handleXYDefaults(traceIn, traceOut, layout, coerce),
- // TODO: default mode by orphan points...
- defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines';
- if(!len) {
- traceOut.visible = false;
- return;
- }
+ var len = handleXYDefaults(traceIn, traceOut, layout, coerce);
+ if(!len) traceOut.visible = false;
+
+ if(!traceOut.visible) return;
+
+ var stackGroupOpts = handleStackDefaults(traceIn, traceOut, layout, coerce);
+ var defaultMode = !stackGroupOpts && (len < constants.PTS_LINESONLY) ?
+ 'lines+markers' : 'lines';
coerce('text');
coerce('hovertext');
coerce('mode', defaultMode);
@@ -61,7 +63,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
dfltHoverOn.push('points');
}
- coerce('fill');
+ // It's possible for this default to be changed by a later trace.
+ // We handle that case in some hacky code inside handleStackDefaults.
+ coerce('fill', stackGroupOpts ? stackGroupOpts.fillDflt : 'none');
if(traceOut.fill !== 'none') {
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce);
diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js
index f85a1a77094..6efe4ca9053 100644
--- a/src/traces/scatter/hover.js
+++ b/src/traces/scatter/hover.js
@@ -68,16 +68,30 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
var yc = ya.c2p(di.y, true);
var rad = di.mrc || 1;
+ // now we're done using the whole `calcdata` array, replace the
+ // index with the original index (in case of inserted point from
+ // stacked area)
+ pointData.index = di.i;
+
+ var orientation = cd[0].t.orientation;
+ // TODO: for scatter and bar, option to show (sub)totals and
+ // raw data? Currently stacked and/or normalized bars just show
+ // the normalized individual sizes, so that's what I'm doing here
+ // for now.
+ var sizeVal = orientation && (di.sNorm || di.s);
+ var xLabelVal = (orientation === 'h') ? sizeVal : di.x;
+ var yLabelVal = (orientation === 'v') ? sizeVal : di.y;
+
Lib.extendFlat(pointData, {
color: getTraceColor(trace, di),
x0: xc - rad,
x1: xc + rad,
- xLabelVal: di.x,
+ xLabelVal: xLabelVal,
y0: yc - rad,
y1: yc + rad,
- yLabelVal: di.y,
+ yLabelVal: yLabelVal,
spikeDistance: dxy(di)
});
diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js
index 020bc06a511..133b54ae32e 100644
--- a/src/traces/scatter/index.js
+++ b/src/traces/scatter/index.js
@@ -21,6 +21,7 @@ Scatter.attributes = require('./attributes');
Scatter.supplyDefaults = require('./defaults');
Scatter.cleanData = require('./clean_data');
Scatter.calc = require('./calc').calc;
+Scatter.crossTraceCalc = require('./cross_trace_calc');
Scatter.arraysToCalcdata = require('./arrays_to_calcdata');
Scatter.plot = require('./plot');
Scatter.colorbar = require('./marker_colorbar');
@@ -33,7 +34,10 @@ Scatter.animatable = true;
Scatter.moduleType = 'trace';
Scatter.name = 'scatter';
Scatter.basePlotModule = require('../../plots/cartesian');
-Scatter.categories = ['cartesian', 'svg', 'symbols', 'errorBarsOK', 'showLegend', 'scatter-like', 'zoomScale'];
+Scatter.categories = [
+ 'cartesian', 'svg', 'symbols', 'errorBarsOK', 'showLegend', 'scatter-like',
+ 'zoomScale'
+];
Scatter.meta = {
description: [
'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.',
diff --git a/src/traces/scatter/line_points.js b/src/traces/scatter/line_points.js
index 8e498e46466..bf18df449a8 100644
--- a/src/traces/scatter/line_points.js
+++ b/src/traces/scatter/line_points.js
@@ -9,7 +9,11 @@
'use strict';
-var BADNUM = require('../../constants/numerical').BADNUM;
+var numConstants = require('../../constants/numerical');
+var BADNUM = numConstants.BADNUM;
+var LOG_CLIP = numConstants.LOG_CLIP;
+var LOG_CLIP_PLUS = LOG_CLIP + 0.5;
+var LOG_CLIP_MINUS = LOG_CLIP - 0.5;
var Lib = require('../../lib');
var segmentsIntersect = Lib.segmentsIntersect;
var constrain = Lib.constrain;
@@ -19,6 +23,10 @@ var constants = require('./constants');
module.exports = function linePoints(d, opts) {
var xa = opts.xaxis;
var ya = opts.yaxis;
+ var xLog = xa.type === 'log';
+ var yLog = ya.type === 'log';
+ var xLen = xa._length;
+ var yLen = ya._length;
var connectGaps = opts.connectGaps;
var baseTolerance = opts.baseTolerance;
var shape = opts.shape;
@@ -59,7 +67,25 @@ module.exports = function linePoints(d, opts) {
if(!di) return false;
var x = xa.c2p(di.x);
var y = ya.c2p(di.y);
- if(x === BADNUM || y === BADNUM) return false;
+
+ // if non-positive log values, set them VERY far off-screen
+ // so the line looks essentially straight from the previous point.
+ if(x === BADNUM) {
+ if(xLog) x = xa.c2p(di.x, true);
+ if(x === BADNUM) return false;
+ // If BOTH were bad log values, make the line follow a constant
+ // exponent rather than a constant slope
+ if(yLog && y === BADNUM) {
+ x *= Math.abs(xa._m * yLen * (xa._m > 0 ? LOG_CLIP_PLUS : LOG_CLIP_MINUS) /
+ (ya._m * xLen * (ya._m > 0 ? LOG_CLIP_PLUS : LOG_CLIP_MINUS)));
+ }
+ x *= 1000;
+ }
+ if(y === BADNUM) {
+ if(yLog) y = ya.c2p(di.y, true);
+ if(y === BADNUM) return false;
+ y *= 1000;
+ }
return [x, y];
}
@@ -79,8 +105,8 @@ module.exports = function linePoints(d, opts) {
var latestXFrac, latestYFrac;
// if we're off-screen, increase tolerance over baseTolerance
function getTolerance(pt, nextPt) {
- var xFrac = pt[0] / xa._length;
- var yFrac = pt[1] / ya._length;
+ var xFrac = pt[0] / xLen;
+ var yFrac = pt[1] / yLen;
var offScreenFraction = Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1);
if(offScreenFraction && (latestXFrac !== undefined) &&
crossesViewport(xFrac, yFrac, latestXFrac, latestYFrac)
@@ -88,7 +114,7 @@ module.exports = function linePoints(d, opts) {
offScreenFraction = 0;
}
if(offScreenFraction && nextPt &&
- crossesViewport(xFrac, yFrac, nextPt[0] / xa._length, nextPt[1] / ya._length)
+ crossesViewport(xFrac, yFrac, nextPt[0] / xLen, nextPt[1] / yLen)
) {
offScreenFraction = 0;
}
@@ -114,10 +140,10 @@ module.exports = function linePoints(d, opts) {
// if both are outside there will be 0 or 2 intersections
// (or 1 if it's right at a corner - we'll treat that like 0)
// returns an array of intersection pts
- var xEdge0 = -xa._length * maxScreensAway;
- var xEdge1 = xa._length * (1 + maxScreensAway);
- var yEdge0 = -ya._length * maxScreensAway;
- var yEdge1 = ya._length * (1 + maxScreensAway);
+ var xEdge0 = -xLen * maxScreensAway;
+ var xEdge1 = xLen * (1 + maxScreensAway);
+ var yEdge0 = -yLen * maxScreensAway;
+ var yEdge1 = yLen * (1 + maxScreensAway);
var edges = [
[xEdge0, yEdge0, xEdge1, yEdge0],
[xEdge1, yEdge0, xEdge1, yEdge1],
@@ -261,8 +287,8 @@ module.exports = function linePoints(d, opts) {
}
function addPt(pt) {
- latestXFrac = pt[0] / xa._length;
- latestYFrac = pt[1] / ya._length;
+ latestXFrac = pt[0] / xLen;
+ latestYFrac = pt[1] / yLen;
// Are we more than maxScreensAway off-screen any direction?
// if so, clip to this box, but in such a way that on-screen
// drawing is unchanged
diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js
index b0e14b2964a..56980cdcf22 100644
--- a/src/traces/scatter/plot.js
+++ b/src/traces/scatter/plot.js
@@ -386,9 +386,17 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
function visFilter(d) {
+ return d.filter(function(v) { return !v.gap && v.vis; });
+ }
+
+ function visFilterWithGaps(d) {
return d.filter(function(v) { return v.vis; });
}
+ function gapFilter(d) {
+ return d.filter(function(v) { return !v.gap; });
+ }
+
function keyFunc(d) {
return d.id;
}
@@ -416,12 +424,24 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
var markerFilter = hideFilter;
var textFilter = hideFilter;
- if(showMarkers) {
- markerFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity;
- }
+ if(showMarkers || showText) {
+ var showFilter = Lib.identity;
+ // if we're stacking, "infer zero" gap mode gets markers in the
+ // gap points - because we've inferred a zero there - but other
+ // modes (currently "interpolate", later "interrupt" hopefully)
+ // we don't draw generated markers
+ var stackGroup = trace.stackgroup;
+ var isInferZero = stackGroup && (
+ gd._fullLayout._scatterStackOpts[xa._id + ya._id][stackGroup].stackgaps === 'infer zero');
+ if(trace.marker.maxdisplayed || trace._needsCull) {
+ showFilter = isInferZero ? visFilterWithGaps : visFilter;
+ }
+ else if(stackGroup && !isInferZero) {
+ showFilter = gapFilter;
+ }
- if(showText) {
- textFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity;
+ if(showMarkers) markerFilter = showFilter;
+ if(showText) textFilter = showFilter;
}
// marker points
diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js
index 5d10050b494..e980a6d7400 100644
--- a/src/traces/scatter/select.js
+++ b/src/traces/scatter/select.js
@@ -36,9 +36,9 @@ module.exports = function selectPoints(searchInfo, polygon) {
x = xa.c2p(di.x);
y = ya.c2p(di.y);
- if(polygon.contains([x, y])) {
+ if((di.i !== null) && polygon.contains([x, y])) {
selection.push({
- pointNumber: i,
+ pointNumber: di.i,
x: xa.c2d(di.x),
y: ya.c2d(di.y)
});
diff --git a/src/traces/scatter/stack_defaults.js b/src/traces/scatter/stack_defaults.js
new file mode 100644
index 00000000000..26cf2178afc
--- /dev/null
+++ b/src/traces/scatter/stack_defaults.js
@@ -0,0 +1,104 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var perStackAttrs = ['orientation', 'groupnorm', 'stackgaps'];
+
+module.exports = function handleStackDefaults(traceIn, traceOut, layout, coerce) {
+ var stackOpts = layout._scatterStackOpts;
+
+ var stackGroup = coerce('stackgroup');
+ if(stackGroup) {
+ // use independent stacking options per subplot
+ var subplot = traceOut.xaxis + traceOut.yaxis;
+ var subplotStackOpts = stackOpts[subplot];
+ if(!subplotStackOpts) subplotStackOpts = stackOpts[subplot] = {};
+
+ var groupOpts = subplotStackOpts[stackGroup];
+ var firstTrace = false;
+ if(groupOpts) {
+ groupOpts.traces.push(traceOut);
+ }
+ else {
+ groupOpts = subplotStackOpts[stackGroup] = {
+ // keep track of trace indices for use during stacking calculations
+ // this will be filled in during `calc` and used during `crossTraceCalc`
+ // so it's OK if we don't recreate it during a non-calc edit
+ traceIndices: [],
+ // Hold on to the whole set of prior traces
+ // First one is most important, so we can clear defaults
+ // there if we find explicit values only in later traces.
+ // We're only going to *use* the values stored in groupOpts,
+ // but for the editor and validate we want things self-consistent
+ // The full set of traces is used only to fix `fill` default if
+ // we find `orientation: 'h'` beyond the first trace
+ traces: [traceOut]
+ };
+ firstTrace = true;
+ }
+ // TODO: how is this going to work with groupby transforms?
+ // in principle it should be OK I guess, as long as explicit group styles
+ // don't override explicit base-trace styles?
+
+ var dflts = {
+ orientation: (traceOut.x && !traceOut.y) ? 'h' : 'v'
+ };
+
+ for(var i = 0; i < perStackAttrs.length; i++) {
+ var attr = perStackAttrs[i];
+ var attrFound = attr + 'Found';
+ if(!groupOpts[attrFound]) {
+ var traceHasAttr = traceIn[attr] !== undefined;
+ var isOrientation = attr === 'orientation';
+ if(traceHasAttr || firstTrace) {
+ groupOpts[attr] = coerce(attr, dflts[attr]);
+
+ if(isOrientation) {
+ groupOpts.fillDflt = groupOpts[attr] === 'h' ?
+ 'tonextx' : 'tonexty';
+ }
+
+ if(traceHasAttr) {
+ // Note: this will show a value here even if it's invalid
+ // in which case it will revert to default.
+ groupOpts[attrFound] = true;
+
+ // Note: only one trace in the stack will get a _fullData
+ // entry for a given stack-wide attribute. If no traces
+ // (or the first trace) specify that attribute, the
+ // first trace will get it. If the first trace does NOT
+ // specify it but some later trace does, then it gets
+ // removed from the first trace and only included in the
+ // one that specified it. This is mostly important for
+ // editors (that want to see the full values to know
+ // what settings are available) and Plotly.react diffing.
+ // Editors may want to use fullLayout._scatterStackOpts
+ // directly and make these settings available from all
+ // traces in the stack... then set the new value into
+ // the first trace, and clear all later traces.
+ if(!firstTrace) {
+ delete groupOpts.traces[0][attr];
+
+ // orientation can affect default fill of previous traces
+ if(isOrientation) {
+ for(var j = 0; j < groupOpts.traces.length - 1; j++) {
+ var trace2 = groupOpts.traces[j];
+ if(trace2._input.fill !== trace2.fill) {
+ trace2.fill = groupOpts.fillDflt;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return groupOpts;
+ }
+};
diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js
index 6b6564d6565..60ca8852fee 100644
--- a/src/traces/scattercarpet/attributes.js
+++ b/src/traces/scattercarpet/attributes.js
@@ -74,6 +74,7 @@ module.exports = {
connectgaps: scatterAttrs.connectgaps,
fill: extendFlat({}, scatterAttrs.fill, {
values: ['none', 'toself', 'tonext'],
+ dflt: 'none',
description: [
'Sets the area to fill with a solid color.',
'Use with `fillcolor` if not *none*.',
diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js
index 3d8798115bd..385fb765742 100644
--- a/src/traces/scattergl/attributes.js
+++ b/src/traces/scattergl/attributes.js
@@ -75,7 +75,7 @@ var attrs = module.exports = overrideAll({
})
}),
connectgaps: scatterAttrs.connectgaps,
- fill: scatterAttrs.fill,
+ fill: extendFlat({}, scatterAttrs.fill, {dflt: 'none'}),
fillcolor: scatterAttrs.fillcolor,
// no hoveron
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index befc5e468f5..0bb6dabe015 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -23,8 +23,10 @@ var findExtremes = require('../../plots/cartesian/autorange').findExtremes;
var Color = require('../../components/color');
var subTypes = require('../scatter/subtypes');
-var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
-var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
+var scatterCalc = require('../scatter/calc');
+var calcMarkerSize = scatterCalc.calcMarkerSize;
+var calcAxisExpansion = scatterCalc.calcAxisExpansion;
+var setFirstScatter = scatterCalc.setFirstScatter;
var calcColorscales = require('../scatter/colorscale_calc');
var linkTraces = require('../scatter/link_traces');
var getTraceColor = require('../scatter/get_trace_color');
@@ -89,6 +91,7 @@ function calc(gd, trace) {
// 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.
+ setFirstScatter(fullLayout, trace);
var ppad;
if(count < TOO_MANY_POINTS) {
ppad = calcMarkerSize(trace, count);
@@ -134,7 +137,6 @@ function calc(gd, trace) {
scene.count++;
- gd.firstscatter = false;
return [{x: false, y: false, t: stash, trace: trace}];
}
diff --git a/src/traces/scatterpolar/attributes.js b/src/traces/scatterpolar/attributes.js
index e05c8f31da3..fa04df7f593 100644
--- a/src/traces/scatterpolar/attributes.js
+++ b/src/traces/scatterpolar/attributes.js
@@ -106,6 +106,7 @@ module.exports = {
fill: extendFlat({}, scatterAttrs.fill, {
values: ['none', 'toself', 'tonext'],
+ dflt: 'none',
description: [
'Sets the area to fill with a solid color.',
'Use with `fillcolor` if not *none*.',
diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js
index e41377fd74e..1dee1c0d70d 100644
--- a/src/traces/scatterternary/attributes.js
+++ b/src/traces/scatterternary/attributes.js
@@ -103,6 +103,7 @@ module.exports = {
cliponaxis: scatterAttrs.cliponaxis,
fill: extendFlat({}, scatterAttrs.fill, {
values: ['none', 'toself', 'tonext'],
+ dflt: 'none',
description: [
'Sets the area to fill with a solid color.',
'Use with `fillcolor` if not *none*.',
diff --git a/test/image/baselines/autorange-tozero-rangemode.png b/test/image/baselines/autorange-tozero-rangemode.png
index 8f52a059a31..c7792350c31 100644
Binary files a/test/image/baselines/autorange-tozero-rangemode.png and b/test/image/baselines/autorange-tozero-rangemode.png differ
diff --git a/test/image/baselines/axes_range_type.png b/test/image/baselines/axes_range_type.png
index de4d196f356..857b9eec629 100644
Binary files a/test/image/baselines/axes_range_type.png and b/test/image/baselines/axes_range_type.png differ
diff --git a/test/image/baselines/contour_log.png b/test/image/baselines/contour_log.png
index e5cbbae7e17..e5cb2e20147 100644
Binary files a/test/image/baselines/contour_log.png and b/test/image/baselines/contour_log.png differ
diff --git a/test/image/baselines/log_lines_fills.png b/test/image/baselines/log_lines_fills.png
new file mode 100644
index 00000000000..9aa6c95be5b
Binary files /dev/null and b/test/image/baselines/log_lines_fills.png differ
diff --git a/test/image/baselines/stacked_area.png b/test/image/baselines/stacked_area.png
new file mode 100644
index 00000000000..2f508049fbc
Binary files /dev/null and b/test/image/baselines/stacked_area.png differ
diff --git a/test/image/baselines/stacked_area_duplicates.png b/test/image/baselines/stacked_area_duplicates.png
new file mode 100644
index 00000000000..3b35f75ba3c
Binary files /dev/null and b/test/image/baselines/stacked_area_duplicates.png differ
diff --git a/test/image/baselines/stacked_area_horz.png b/test/image/baselines/stacked_area_horz.png
new file mode 100644
index 00000000000..3c18529b36d
Binary files /dev/null and b/test/image/baselines/stacked_area_horz.png differ
diff --git a/test/image/baselines/stacked_area_log.png b/test/image/baselines/stacked_area_log.png
new file mode 100644
index 00000000000..737318747aa
Binary files /dev/null and b/test/image/baselines/stacked_area_log.png differ
diff --git a/test/image/mocks/log_lines_fills.json b/test/image/mocks/log_lines_fills.json
new file mode 100644
index 00000000000..b42a1b41d10
--- /dev/null
+++ b/test/image/mocks/log_lines_fills.json
@@ -0,0 +1,33 @@
+{
+ "data": [{
+ "x": [-1, 1.5, 2, 0, 1, 3, 4, 5, 6, 1, -1, -1, 3, 3, 0, 7, 7, -1, -1],
+ "y": [-1, 1.5, 1, -1, -1, 1, 1, 0, 2, 2, 0, 2, 3, 4, 5, 6, -2, -2, -1],
+ "mode": "markers+lines", "fill": "toself"
+ }, {
+ "x": [-1, 1.5, 2, 0, 1, 3, 4, 5, 6, 1, -1, -1, 3, 3, 0, 7, 7, -1, -1],
+ "y": [-1, 1.5, 1, -1, -1, 1, 1, 0, 2, 2, 0, 2, 3, 4, 5, 6, -2, -2, -1],
+ "mode": "markers+lines", "fill": "toself", "xaxis": "x2"
+ }, {
+ "x": [-1, 1.5, 2, 0, 1, 3, 4, 5, 6, 1, -1, -1, 3, 3, 0, 7, 7, -1, -1],
+ "y": [-1, 1.5, 1, -1, -1, 1, 1, 0, 2, 2, 0, 2, 3, 4, 5, 6, -2, -2, -1],
+ "mode": "markers+lines", "fill": "toself", "yaxis": "y2"
+ }, {
+ "x": [-1, 1.5, 2, 0, 1, 3, 4, 5, 6, 1, -1, -1, 3, 3, 0, 7, 7, -1, -1],
+ "y": [-1, 1.5, 1, -1, -1, 1, 1, 0, 2, 2, 0, 2, 3, 4, 5, 6, -2, -2, -1],
+ "mode": "markers+lines", "fill": "toself", "xaxis": "x2", "yaxis": "y2"
+ }, {
+ "x": [0.01, 0.1], "y": [0.01, 0.1],
+ "mode": "markers", "xaxis": "x2", "yaxis": "y2"
+ }],
+ "layout": {
+ "width": 800,
+ "height": 600,
+ "xaxis": {"title": "linear"},
+ "xaxis2": {"title": "log", "type": "log"},
+ "yaxis": {"title": "linear"},
+ "yaxis2": {"title": "log", "type": "log"},
+ "grid": {"columns": 2, "rows": 2},
+ "showlegend": false,
+ "title": "Lines to invalid log values
4 copies of the same self-filled trace, on all combinations of log & linear axes
Purple points should lie exactly on an off-to-infinity log-log line to verify that its slope is 1"
+ }
+}
diff --git a/test/image/mocks/scatter_fill_corner_cases.json b/test/image/mocks/scatter_fill_corner_cases.json
index febf2052b23..1240ce8240f 100644
--- a/test/image/mocks/scatter_fill_corner_cases.json
+++ b/test/image/mocks/scatter_fill_corner_cases.json
@@ -75,7 +75,6 @@
{
"x": [1.5],
"y": [1.25],
- "fill": "tonexty",
"showlegend": false,
"yaxis": "y2"
},
@@ -111,7 +110,6 @@
{
"x": [1.5],
"y": [1.25],
- "fill": "tonexty",
"line": {"shape": "spline"},
"xaxis": "x2",
"showlegend": false,
diff --git a/test/image/mocks/stacked_area.json b/test/image/mocks/stacked_area.json
new file mode 100644
index 00000000000..0c2aac3076b
--- /dev/null
+++ b/test/image/mocks/stacked_area.json
@@ -0,0 +1,83 @@
+{
+ "data": [
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2",
+ "stackgaps": "interpolate"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "b", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers",
+ "groupnorm": "fraction"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "b", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "b", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers",
+ "stackgaps": "interpolate", "groupnorm": "fraction"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5",
+ "groupnorm": "percent"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6",
+ "groupnorm": "percent"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6",
+ "stackgaps": "interpolate"
+ }
+ ],
+ "layout": {
+ "width": 800,
+ "height": 800,
+ "xaxis": {"title": "stackgaps: infer zero"},
+ "xaxis2": {"title": "stackgaps: interpolate"},
+ "yaxis": {"title": "groupnorm: -"},
+ "yaxis3": {"title": "groupnorm: fraction
mode: lines+markers"},
+ "yaxis5": {"title": "groupnorm: percent"},
+ "legend": {"traceorder": "reversed"},
+ "grid": {"columns": 2, "rows": 3, "pattern": "independent", "roworder": "bottom to top"}
+ }
+}
diff --git a/test/image/mocks/stacked_area_duplicates.json b/test/image/mocks/stacked_area_duplicates.json
new file mode 100644
index 00000000000..1f013ba231f
--- /dev/null
+++ b/test/image/mocks/stacked_area_duplicates.json
@@ -0,0 +1,42 @@
+{
+ "data": [
+ {
+ "x": [1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 6, 8],
+ "y": [1, 3, 2, 5, 4, 5, 4, 3, 2, 4, 6, 3, 5, 4, 3],
+ "stackgroup": "a", "mode": "lines+markers"
+ }, {
+ "x": [1, 2, 2, 2, 2, 3, 5, 5, 5, 5, 6, 6, 6, 6, 8],
+ "y": [4, 4, 4, 4, 6, 5, 6, 5, 7, 6, 5, 6, 7, 8, 7],
+ "stackgroup": "a", "mode": "lines+markers"
+ }, {
+ "x": [2, 2, 2, 2, 3, 4, 4, 4, 4, 5, 7, 7, 7, 7, 8],
+ "y": [5, 5, 4, 5, 6, 7, 6, 5, 4, 5, 4, 5, 6, 3, 4],
+ "stackgroup": "a", "mode": "lines+markers"
+ },
+
+ {
+ "x": [1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 6, 8],
+ "y": [1, 3, 2, 5, 4, 5, 4, 3, 2, 4, 6, 3, 5, 4, 3],
+ "stackgroup": "a", "mode": "lines+markers",
+ "xaxis": "x2", "yaxis": "y2", "stackgaps": "interpolate"
+ }, {
+ "x": [1, 2, 2, 2, 2, 3, 5, 5, 5, 5, 6, 6, 6, 6, 8],
+ "y": [4, 4, 4, 4, 6, 5, 6, 5, 7, 6, 5, 6, 7, 8, 7],
+ "stackgroup": "a", "mode": "lines+markers",
+ "xaxis": "x2", "yaxis": "y2", "stackgaps": "interpolate"
+ }, {
+ "x": [2, 2, 2, 2, 3, 4, 4, 4, 4, 5, 7, 7, 7, 7, 8],
+ "y": [5, 5, 4, 5, 6, 7, 6, 5, 4, 5, 4, 5, 6, 3, 4],
+ "stackgroup": "a", "mode": "lines+markers",
+ "xaxis": "x2", "yaxis": "y2", "stackgaps": "interpolate"
+ }
+ ],
+ "layout": {
+ "width": 500,
+ "height": 500,
+ "title": "Duplicate positions",
+ "xaxis": {"title": "infer zero"},
+ "xaxis2": {"title": "interpolate"},
+ "grid": {"columns": 1, "rows": 2, "pattern": "independent"}
+ }
+}
diff --git a/test/image/mocks/stacked_area_horz.json b/test/image/mocks/stacked_area_horz.json
new file mode 100644
index 00000000000..4d0342996c8
--- /dev/null
+++ b/test/image/mocks/stacked_area_horz.json
@@ -0,0 +1,83 @@
+{
+ "data": [
+ {
+ "y": [1, 3, 5, 7], "x": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "orientation": "h", "name": "bottom", "legendgroup": "b"
+ }, {
+ "y": [2, 5, 6], "x": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m"
+ }, {
+ "y": [3, 4 ,5], "x": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t"
+ },
+
+ {
+ "y": [1, 3, 5, 7], "x": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2",
+ "stackgaps": "interpolate"
+ }, {
+ "y": [2, 5, 6], "x": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "orientation": "h", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2"
+ }, {
+ "y": [3, 4 ,5], "x": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2"
+ },
+
+ {
+ "y": [1, 3, 5, 7], "x": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "b", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers",
+ "groupnorm": "fraction"
+ }, {
+ "y": [2, 5, 6], "x": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "b", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers"
+ }, {
+ "y": [3, 4 ,5], "x": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "b", "orientation": "h", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers"
+ },
+
+ {
+ "y": [1, 3, 5, 7], "x": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "orientation": "h", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers"
+ }, {
+ "y": [2, 5, 6], "x": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers",
+ "stackgaps": "interpolate", "groupnorm": "fraction"
+ }, {
+ "y": [3, 4 ,5], "x": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers"
+ },
+
+ {
+ "y": [1, 3, 5, 7], "x": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5"
+ }, {
+ "y": [2, 5, 6], "x": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "orientation": "h", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5"
+ }, {
+ "y": [3, 4 ,5], "x": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5",
+ "groupnorm": "percent"
+ },
+
+ {
+ "y": [1, 3, 5, 7], "x": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6",
+ "groupnorm": "percent"
+ }, {
+ "y": [2, 5, 6], "x": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6"
+ }, {
+ "y": [3, 4 ,5], "x": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "orientation": "h", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6",
+ "stackgaps": "interpolate"
+ }
+ ],
+ "layout": {
+ "width": 800,
+ "height": 800,
+ "xaxis": {"title": "stackgaps: infer zero"},
+ "xaxis2": {"title": "stackgaps: interpolate"},
+ "yaxis": {"title": "groupnorm: -"},
+ "yaxis3": {"title": "groupnorm: fraction
mode: lines+markers"},
+ "yaxis5": {"title": "groupnorm: percent"},
+ "legend": {"traceorder": "reversed"},
+ "grid": {"columns": 2, "rows": 3, "pattern": "independent", "roworder": "bottom to top"}
+ }
+}
diff --git a/test/image/mocks/stacked_area_log.json b/test/image/mocks/stacked_area_log.json
new file mode 100644
index 00000000000..ec0965a01d3
--- /dev/null
+++ b/test/image/mocks/stacked_area_log.json
@@ -0,0 +1,86 @@
+{
+ "data": [
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2",
+ "stackgaps": "interpolate"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x2", "yaxis": "y2"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "b", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers",
+ "groupnorm": "fraction"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "b", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "b", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x3", "yaxis": "y3", "mode": "lines+markers"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers",
+ "stackgaps": "interpolate", "groupnorm": "fraction"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x4", "yaxis": "y4", "mode": "lines+markers"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x5", "yaxis": "y5",
+ "groupnorm": "percent"
+ },
+
+ {
+ "x": [1, 3, 5, 7], "y": [2, 6, 4, 5], "line": {"color": "red"}, "stackgroup": "a", "name": "bottom", "legendgroup": "b",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6",
+ "groupnorm": "percent"
+ }, {
+ "x": [2, 5, 6], "y": [1, 3, 4], "line": {"color": "green"}, "stackgroup": "a", "name": "middle", "legendgroup": "m",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6"
+ }, {
+ "x": [3, 4 ,5], "y": [1, 2, 1], "line": {"color": "blue"}, "stackgroup": "a", "name": "top", "legendgroup": "t",
+ "showlegend": false, "xaxis": "x6", "yaxis": "y6",
+ "stackgaps": "interpolate"
+ }
+ ],
+ "layout": {
+ "width": 800,
+ "height": 800,
+ "xaxis": {"title": "stackgaps: infer zero"},
+ "xaxis2": {"title": "stackgaps: interpolate"},
+ "yaxis": {"title": "groupnorm: -", "type": "log"},
+ "yaxis2": {"type": "log"},
+ "yaxis3": {"title": "groupnorm: fraction
mode: lines+markers", "type": "log"},
+ "yaxis4": {"type": "log"},
+ "yaxis5": {"title": "groupnorm: percent", "type": "log"},
+ "yaxis6": {"type": "log"},
+ "legend": {"traceorder": "reversed"},
+ "grid": {"columns": 2, "rows": 3, "pattern": "independent", "roworder": "bottom to top"}
+ }
+}
diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js
index e288128f303..aab46b67f5e 100644
--- a/test/jasmine/assets/custom_assertions.js
+++ b/test/jasmine/assets/custom_assertions.js
@@ -122,6 +122,7 @@ exports.assertHoverLabelContent = function(expectation, msg) {
expect(ptCnt)
.toBe(expectation.name.length, ptMsg + ' # of visible labels');
+ var bboxes = [];
d3.selectAll(ptSelector).each(function(_, i) {
assertLabelContent(
d3.select(this).select('text.nums'),
@@ -133,7 +134,20 @@ exports.assertHoverLabelContent = function(expectation, msg) {
expectation.name[i],
ptMsg + ' (name ' + i + ')'
);
+ bboxes.push({bbox: this.getBoundingClientRect(), index: i});
});
+ if(expectation.vOrder) {
+ bboxes.sort(function(a, b) {
+ return (a.bbox.top + a.bbox.bottom - b.bbox.top - b.bbox.bottom) / 2;
+ });
+ expect(bboxes.map(function(d) { return d.index; })).toEqual(expectation.vOrder);
+ }
+ if(expectation.hOrder) {
+ bboxes.sort(function(a, b) {
+ return (b.bbox.left + b.bbox.right - a.bbox.left - a.bbox.right) / 2;
+ });
+ expect(bboxes.map(function(d) { return d.index; })).toEqual(expectation.hOrder);
+ }
} else {
if(expectation.nums) {
fail(ptMsg + ': expecting *nums* labels, did not find any.');
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index a5d91c7718e..bfc15effcc0 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -403,6 +403,24 @@ describe('Test axes', function() {
});
});
+ it('only allows rangemode with linear axes', function() {
+ layoutIn = {
+ xaxis: {type: 'log', rangemode: 'tozero'},
+ yaxis: {type: 'date', rangemode: 'tozero'},
+ xaxis2: {type: 'category', rangemode: 'tozero'},
+ yaxis2: {type: 'linear', rangemode: 'tozero'}
+ };
+ layoutOut._subplots.cartesian.push('x2y2');
+ layoutOut._subplots.yaxis.push('x2', 'y2');
+
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+
+ expect(layoutOut.xaxis.rangemode).toBeUndefined();
+ expect(layoutOut.yaxis.rangemode).toBeUndefined();
+ expect(layoutOut.xaxis2.rangemode).toBeUndefined();
+ expect(layoutOut.yaxis2.rangemode).toBe('tozero');
+ });
+
it('finds scaling groups and calculates relative scales', function() {
layoutIn = {
// first group: linked in series, scales compound
@@ -1558,7 +1576,7 @@ describe('Test axes', function() {
expect(getAutoRange(gd, ax)).toEqual([7.5, 0]);
});
- it('expands empty positive range to something including 0 with rangemode tozero', function() {
+ it('expands empty positive range to include 0 with rangemode tozero', function() {
gd = mockGd([
{val: 5, pad: 0}
], [
@@ -1567,7 +1585,7 @@ describe('Test axes', function() {
ax = mockAx();
ax.rangemode = 'tozero';
- expect(getAutoRange(gd, ax)).toEqual([0, 6]);
+ expect(getAutoRange(gd, ax)).toEqual([0, 5]);
});
it('expands empty negative range to something including 0 with rangemode tozero', function() {
@@ -1579,7 +1597,63 @@ describe('Test axes', function() {
ax = mockAx();
ax.rangemode = 'tozero';
- expect(getAutoRange(gd, ax)).toEqual([-6, 0]);
+ expect(getAutoRange(gd, ax)).toEqual([-5, 0]);
+ });
+
+ it('pads an empty range, but not past center, with rangemode tozero', function() {
+ gd = mockGd([
+ {val: 5, pad: 50} // this min pad gets ignored
+ ], [
+ {val: 5, pad: 20}
+ ]);
+ ax = mockAx();
+ ax.rangemode = 'tozero';
+
+ expect(getAutoRange(gd, ax)).toBeCloseToArray([0, 6.25], 0.01);
+
+ gd = mockGd([
+ {val: -5, pad: 80}
+ ], [
+ {val: -5, pad: 0}
+ ]);
+ ax = mockAx();
+ ax.rangemode = 'tozero';
+
+ expect(getAutoRange(gd, ax)).toBeCloseToArray([-10, 0], 0.01);
+ });
+
+ it('shows the data even if it cannot show the padding', function() {
+ gd = mockGd([
+ {val: 0, pad: 44}
+ ], [
+ {val: 1, pad: 44}
+ ]);
+ ax = mockAx();
+
+ // this one is *just* on the allowed side of padding
+ // ie data span is just over 10% of the axis
+ expect(getAutoRange(gd, ax)).toBeCloseToArray([-3.67, 4.67]);
+
+ gd = mockGd([
+ {val: 0, pad: 46}
+ ], [
+ {val: 1, pad: 46}
+ ]);
+ ax = mockAx();
+
+ // this one the padded data span would be too small, so we delete
+ // the padding
+ expect(getAutoRange(gd, ax)).toEqual([0, 1]);
+
+ gd = mockGd([
+ {val: 0, pad: 400}
+ ], [
+ {val: 1, pad: 0}
+ ]);
+ ax = mockAx();
+
+ // this one the padding is simply impossible to accept!
+ expect(getAutoRange(gd, ax)).toEqual([0, 1]);
});
it('never returns a negative range when rangemode nonnegative is set with positive and negative points', function() {
@@ -1614,17 +1688,43 @@ describe('Test axes', function() {
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
});
- it('expands empty range to something nonnegative with rangemode nonnegative', function() {
+ it('never returns a negative range when rangemode nonnegative is set with only nonpositive points', function() {
gd = mockGd([
- {val: -5, pad: 0}
+ {val: -10, pad: 20},
+ {val: -8, pad: 0},
+ {val: -9, pad: 10}
], [
- {val: -5, pad: 0}
+ {val: -5, pad: 20},
+ {val: 0, pad: 0},
+ {val: -6, pad: 10}
]);
ax = mockAx();
ax.rangemode = 'nonnegative';
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
});
+
+ it('expands empty range to something nonnegative with rangemode nonnegative', function() {
+ [
+ [-5, [0, 1]],
+ [0, [0, 1]],
+ [0.5, [0, 1.5]],
+ [1, [0, 2]],
+ [5, [4, 6]]
+ ].forEach(function(testCase) {
+ var val = testCase[0];
+ var expected = testCase[1];
+ gd = mockGd([
+ {val: val, pad: 0}
+ ], [
+ {val: val, pad: 0}
+ ]);
+ ax = mockAx();
+ ax.rangemode = 'nonnegative';
+
+ expect(getAutoRange(gd, ax)).toEqual(expected, val);
+ });
+ });
});
describe('findExtremes', function() {
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index 7dc0edf0feb..aa0981c5ac3 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -520,7 +520,92 @@ describe('hover info', function() {
Lib.clearThrottle();
}
- describe('\'hover info for x/y/z traces', function() {
+ describe('hover label order for stacked traces with zeros', function() {
+ var gd;
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ it('puts the top trace on top', function(done) {
+ Plotly.plot(gd, [
+ {y: [1, 2, 3], type: 'bar', name: 'a'},
+ {y: [2, 0, 1], type: 'bar', name: 'b'},
+ {y: [1, 0, 1], type: 'bar', name: 'c'},
+ {y: [2, 1, 0], type: 'bar', name: 'd'}
+ ], {
+ width: 500,
+ height: 400,
+ margin: {l: 0, t: 0, r: 0, b: 0},
+ barmode: 'stack'
+ })
+ .then(function() {
+ _hover(gd, 250, 250);
+ assertHoverLabelContent({
+ nums: ['2', '0', '0', '1'],
+ name: ['a', 'b', 'c', 'd'],
+ // a, b, c are all in the same place but keep their order
+ // d is included mostly as a sanity check
+ vOrder: [3, 2, 1, 0],
+ axis: '1'
+ });
+
+ // reverse the axis, labels should reverse
+ return Plotly.relayout(gd, 'yaxis.range', gd.layout.yaxis.range.slice().reverse());
+ })
+ .then(function() {
+ _hover(gd, 250, 250);
+ assertHoverLabelContent({
+ nums: ['2', '0', '0', '1'],
+ name: ['a', 'b', 'c', 'd'],
+ vOrder: [0, 1, 2, 3],
+ axis: '1'
+ });
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('puts the right trace on the right', function(done) {
+ Plotly.plot(gd, [
+ {x: [1, 2, 3], type: 'bar', name: 'a', orientation: 'h'},
+ {x: [2, 0, 1], type: 'bar', name: 'b', orientation: 'h'},
+ {x: [1, 0, 1], type: 'bar', name: 'c', orientation: 'h'},
+ {x: [2, 1, 0], type: 'bar', name: 'd', orientation: 'h'}
+ ], {
+ width: 500,
+ height: 400,
+ margin: {l: 0, t: 0, r: 0, b: 0},
+ barmode: 'stack'
+ })
+ .then(function() {
+ _hover(gd, 250, 250);
+ assertHoverLabelContent({
+ nums: ['2', '0', '0', '1'],
+ name: ['a', 'b', 'c', 'd'],
+ // a, b, c are all in the same place but keep their order
+ // d is included mostly as a sanity check
+ hOrder: [3, 2, 1, 0],
+ axis: '1'
+ });
+
+ // reverse the axis, labels should reverse
+ return Plotly.relayout(gd, 'xaxis.range', gd.layout.xaxis.range.slice().reverse());
+ })
+ .then(function() {
+ _hover(gd, 250, 250);
+ assertHoverLabelContent({
+ nums: ['2', '0', '0', '1'],
+ name: ['a', 'b', 'c', 'd'],
+ hOrder: [0, 1, 2, 3],
+ axis: '1'
+ });
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+
+ describe('hover info for x/y/z traces', function() {
var gd;
beforeEach(function() {
gd = createGraphDiv();
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index dffebe8ae9a..e4ed92e3a3c 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -2189,6 +2189,164 @@ describe('Test lib.js:', function() {
});
});
+ describe('sort', function() {
+ var callCount;
+ beforeEach(function() {
+ callCount = 0;
+ });
+
+ function sortCounter(a, b) {
+ callCount++;
+ return a - b;
+ }
+
+ function sortCounterReversed(a, b) {
+ callCount++;
+ return b - a;
+ }
+
+ function ascending(n) {
+ var out = new Array(n);
+ for(var i = 0; i < n; i++) {
+ out[i] = i;
+ }
+ assertAscending(out);
+ return out;
+ }
+
+ function descending(n) {
+ var out = new Array(n);
+ for(var i = 0; i < n; i++) {
+ out[i] = n - 1 - i;
+ }
+ assertDescending(out);
+ return out;
+ }
+
+ function rand(n) {
+ Lib.seedPseudoRandom();
+ var out = new Array(n);
+ for(var i = 0; i < n; i++) {
+ out[i] = Lib.pseudoRandom();
+ }
+ return out;
+ }
+
+ function assertAscending(array) {
+ for(var i = 1; i < array.length; i++) {
+ if(array[i] < array[i - 1]) {
+ // we already know this expect will fail,
+ // just want to format the message nicely and then
+ // quit so we don't get a million messages
+ expect(array[i]).not.toBeLessThan(array[i - 1]);
+ break;
+ }
+ }
+ }
+
+ function assertDescending(array) {
+ for(var i = 1; i < array.length; i++) {
+ if(array[i] < array[i - 1]) {
+ expect(array[i]).not.toBeGreaterThan(array[i - 1]);
+ break;
+ }
+ }
+ }
+
+ function _sort(array, sortFn) {
+ var arrayOut = Lib.sort(array, sortFn);
+ expect(arrayOut).toBe(array);
+ return array;
+ }
+
+ it('sorts ascending arrays ascending in N-1 calls', function() {
+ var arrayIn = _sort(ascending(100000), sortCounter);
+ expect(callCount).toBe(99999);
+ assertAscending(arrayIn);
+ });
+
+ it('sorts descending arrays ascending in N-1 calls', function() {
+ var arrayIn = _sort(descending(100000), sortCounter);
+ expect(callCount).toBe(99999);
+ assertAscending(arrayIn);
+ });
+
+ it('sorts ascending arrays descending in N-1 calls', function() {
+ var arrayIn = _sort(ascending(100000), sortCounterReversed);
+ expect(callCount).toBe(99999);
+ assertDescending(arrayIn);
+ });
+
+ it('sorts descending arrays descending in N-1 calls', function() {
+ var arrayIn = _sort(descending(100000), sortCounterReversed);
+ expect(callCount).toBe(99999);
+ assertDescending(arrayIn);
+ });
+
+ it('sorts random arrays ascending in a few more calls than bare sort', function() {
+ var arrayIn = _sort(rand(100000), sortCounter);
+ assertAscending(arrayIn);
+
+ var ourCallCount = callCount;
+ callCount = 0;
+ rand(100000).sort(sortCounter);
+ // in general this will be ~N*log_2(N)
+ expect(callCount).toBeGreaterThan(1e6);
+ // This number (2) is only repeatable because we used Lib.pseudoRandom
+ // should always be at least 2 and less than N - 1, and if
+ // the input array is really not sorted it will be close to 2. It will
+ // only be large if the array is sorted until near the end.
+ expect(ourCallCount - callCount).toBe(2);
+ });
+
+ it('sorts random arrays descending in a few more calls than bare sort', function() {
+ var arrayIn = _sort(rand(100000), sortCounterReversed);
+ assertDescending(arrayIn);
+
+ var ourCallCount = callCount;
+ callCount = 0;
+ rand(100000).sort(sortCounterReversed);
+ expect(callCount).toBeGreaterThan(1e6);
+ expect(ourCallCount - callCount).toBe(2);
+ });
+
+ it('supports short arrays', function() {
+ expect(_sort([], sortCounter)).toEqual([]);
+ expect(_sort([1], sortCounter)).toEqual([1]);
+ expect(callCount).toBe(0);
+
+ expect(_sort([1, 2], sortCounter)).toEqual([1, 2]);
+ expect(_sort([2, 3], sortCounterReversed)).toEqual([3, 2]);
+ expect(callCount).toBe(2);
+ });
+
+ function dupes() {
+ return [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 7, 8, 9];
+ }
+
+ it('still short-circuits in order with duplicates', function() {
+ expect(_sort(dupes(), sortCounter))
+ .toEqual(dupes());
+
+ expect(callCount).toEqual(18);
+
+ callCount = 0;
+ dupes().sort(sortCounter);
+ expect(callCount).toBeGreaterThan(18);
+ });
+
+ it('still short-circuits reversed with duplicates', function() {
+ expect(_sort(dupes(), sortCounterReversed))
+ .toEqual(dupes().reverse());
+
+ expect(callCount).toEqual(18);
+
+ callCount = 0;
+ dupes().sort(sortCounterReversed);
+ expect(callCount).toBeGreaterThan(18);
+ });
+ });
+
describe('relinkPrivateKeys', function() {
it('ignores customdata and ids', function() {
var fromContainer = {
diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js
index dffed81956d..74817411abf 100644
--- a/test/jasmine/tests/scatter_test.js
+++ b/test/jasmine/tests/scatter_test.js
@@ -832,6 +832,32 @@ describe('end-to-end scatter tests', function() {
.then(done);
});
+ it('correctly autoranges fill tonext traces across multiple subplots', function(done) {
+ Plotly.newPlot(gd, [
+ {y: [3, 4, 5], fill: 'tonexty'},
+ {y: [4, 5, 6], fill: 'tonexty'},
+ {y: [3, 4, 5], fill: 'tonexty', yaxis: 'y2'},
+ {y: [4, 5, 6], fill: 'tonexty', yaxis: 'y2'}
+ ], {})
+ .then(function() {
+ expect(gd._fullLayout.yaxis.range[0]).toBe(0);
+ // when we had a single `gd.firstscatter` this one was ~2.73
+ // even though the fill was correctly drawn down to zero
+ expect(gd._fullLayout.yaxis2.range[0]).toBe(0);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('correctly autoranges fill tonext traces with only one point', function(done) {
+ Plotly.newPlot(gd, [{y: [3], fill: 'tonexty'}])
+ .then(function() {
+ expect(gd._fullLayout.yaxis.range[0]).toBe(0);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('should work with typed arrays', function(done) {
function _assert(colors, sizes) {
var pts = d3.selectAll('.point');
@@ -978,6 +1004,129 @@ describe('end-to-end scatter tests', function() {
});
});
+describe('stacked area', function() {
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+ afterEach(destroyGraphDiv);
+ var mock = require('@mocks/stacked_area');
+
+ it('updates ranges correctly when traces are toggled', function(done) {
+ function checkRanges(ranges, msg) {
+ for(var axId in ranges) {
+ var axName = axId.charAt(0) + 'axis' + axId.slice(1);
+ expect(gd._fullLayout[axName].range)
+ .toBeCloseToArray(ranges[axId], 0.1, msg + ' - ' + axId);
+ }
+ }
+ Plotly.newPlot(gd, Lib.extendDeep({}, mock))
+ .then(function() {
+ // initial ranges, as in the baseline image
+ var xr = [1, 7];
+ checkRanges({
+ x: xr, x2: xr, x3: xr, x4: xr, x5: xr, x6: xr,
+ y: [0, 8.42], y2: [0, 10.53],
+ // TODO: for normalized data, perhaps we want to
+ // remove padding from the top (like we do from the zero)
+ // when data stay within the normalization limit?
+ // (y3&4 are more padded because they have markers)
+ y3: [0, 1.08], y4: [0, 1.08], y5: [0, 105.26], y6: [0, 105.26]
+ }, 'base case');
+
+ return Plotly.restyle(gd, 'visible', 'legendonly', [0, 3, 6, 9, 12, 15]);
+ })
+ .then(function() {
+ var xr = [2, 6];
+ checkRanges({
+ x: xr, x2: xr, x3: xr, x4: xr, x5: xr, x6: xr,
+ y: [0, 4.21], y2: [0, 5.26],
+ y3: [0, 1.08], y4: [0, 1.08], y5: [0, 105.26], y6: [0, 105.26]
+ }, 'bottom trace legendonly');
+
+ return Plotly.restyle(gd, 'visible', false, [0, 3, 6, 9, 12, 15]);
+ })
+ .then(function() {
+ var xr = [2, 6];
+ checkRanges({
+ x: xr, x2: xr, x3: xr, x4: xr, x5: xr, x6: xr,
+ // now we lose the explicit config from the bottom trace,
+ // which we kept when it was visible: 'legendonly'
+ y: [0, 4.21], y2: [0, 4.21],
+ y3: [0, 4.32], y4: [0, 1.08], y5: [0, 105.26], y6: [0, 5.26]
+ }, 'bottom trace visible: false');
+
+ // put the bottom traces back to legendonly so they still contribute
+ // config attributes, and hide the middles too
+ return Plotly.restyle(gd, 'visible', 'legendonly',
+ [0, 3, 6, 9, 12, 15, 1, 4, 7, 10, 13, 16]);
+ })
+ .then(function() {
+ var xr = [3, 5];
+ checkRanges({
+ x: xr, x2: xr, x3: xr, x4: xr, x5: xr, x6: xr,
+ y: [0, 2.11], y2: [0, 2.11],
+ y3: [0, 1.08], y4: [0, 1.08], y5: [0, 105.26], y6: [0, 105.26]
+ }, 'only top trace showing');
+
+ return Plotly.restyle(gd, 'visible', true, [0, 3, 6, 9, 12, 15]);
+ })
+ .then(function() {
+ var xr = [1, 7];
+ checkRanges({
+ x: xr, x2: xr, x3: xr, x4: xr, x5: xr, x6: xr,
+ y: [0, 7.37], y2: [0, 7.37],
+ y3: [0, 1.08], y4: [0, 1.08], y5: [0, 105.26], y6: [0, 105.26]
+ }, 'top and bottom showing');
+
+ return Plotly.restyle(gd, {x: null, y: null}, [0, 3, 6, 9, 12, 15]);
+ })
+ .then(function() {
+ return Plotly.restyle(gd, 'visible', true, [1, 4, 7, 10, 13, 16]);
+ })
+ .then(function() {
+ var xr = [2, 6];
+ // an invalid trace (no data) implicitly has visible: false, and is
+ // equivalent to explicit visible: false in removing stack config.
+ checkRanges({
+ x: xr, x2: xr, x3: xr, x4: xr, x5: xr, x6: xr,
+ y: [0, 4.21], y2: [0, 4.21],
+ y3: [0, 4.32], y4: [0, 1.08], y5: [0, 105.26], y6: [0, 5.26]
+ }, 'bottom trace *implicit* visible: false');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('does not stack on date axes', function(done) {
+ Plotly.newPlot(gd, [
+ {y: ['2016-01-01', '2017-01-01'], stackgroup: 'a'},
+ {y: ['2016-01-01', '2017-01-01'], stackgroup: 'a'}
+ ])
+ .then(function() {
+ expect(gd.layout.yaxis.range.map(function(v) { return v.slice(0, 4); }))
+ // if we had stacked, this would go into the 2060s since we'd be
+ // adding milliseconds since 1970
+ .toEqual(['2015', '2017']);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('does not stack on category axes', function(done) {
+ Plotly.newPlot(gd, [
+ {y: ['a', 'b'], stackgroup: 'a'},
+ {y: ['b', 'c'], stackgroup: 'a'}
+ ])
+ .then(function() {
+ // if we had stacked, we'd calculate a new category 3
+ // and autorange to ~[-0.2, 3.2]
+ expect(gd.layout.yaxis.range).toBeCloseToArray([-0.1, 2.1], 1);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+});
+
describe('scatter hoverPoints', function() {
afterEach(destroyGraphDiv);