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/plots/plots.js b/src/plots/plots.js
index 908289fa72c..2063f505e48 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;
@@ -1178,6 +1181,14 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac
plots.supplyTransformDefaults(traceIn, traceOut, layout);
}
+ else if(_module && Registry.traceIs(traceOut, 'alwaysSupplyDefaults')) {
+ // Some types need *something* from supplyDefaults always, even if
+ // visible: false. Looking at you scatter: stack options even from
+ // hidden traces can control other traces in the stack.
+ // These types should bail out ASAP if visible is false.
+ // But we don't need any other cross-module attrs ^^ in this case.
+ _module.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ }
return traceOut;
};
@@ -1556,7 +1567,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;
@@ -2421,8 +2431,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/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..c2856da6def 100644
--- a/src/traces/scatter/attributes.js
+++ b/src/traces/scatter/attributes.js
@@ -72,6 +72,69 @@ 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. 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.',
+ '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.'
+ ].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.',
+ '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 +177,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 +276,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..4bff13e0e5f
--- /dev/null
+++ b/src/traces/scatter/cross_trace_calc.js
@@ -0,0 +1,185 @@
+/**
+* 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]];
+ if(cd.length !== serieslen) {
+ // TODO: verify this never happens and remove
+ throw new Error('length mismatch!');
+ }
+ 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..955a2a82d5f 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 stackGroupOpts = handleStackDefaults(traceIn, traceOut, layout, coerce);
+
+ var len = handleXYDefaults(traceIn, traceOut, layout, coerce);
+ if(!len) traceOut.visible = false;
+
+ if(!traceOut.visible) return;
+ 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..6df0d85ed7a 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', 'alwaysSupplyDefaults'
+];
Scatter.meta = {
description: [
'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.',
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..75cdd99a5dc
--- /dev/null
+++ b/src/traces/scatter/stack_defaults.js
@@ -0,0 +1,101 @@
+/**
+* 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) {
+ // TODO: this will show a value here even if it's invalid
+ // in which case it will revert to default. Is that what we
+ // want, or should we only take the first *valid* value?
+ groupOpts[attrFound] = true;
+
+ // TODO: should we copy the new value to the first trace instead?
+ // if we did that, the editor could show controls in the
+ // first trace always; otherwise it would only show controls
+ // in a later trace, if that trace happens to already have a value...
+ // I think it's probably better to ignore the editor for this
+ // purpose though, as it's probably going to have to use some
+ // special logic anyway, it will likely want to just
+ // take the value out of layout._scatterStackOpts, 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/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_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/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_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/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js
index dffed81956d..e1711b395d9 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,109 @@ 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,
+ 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 visible: false');
+
+ return Plotly.restyle(gd, 'visible', false, [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');
+ })
+ .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);