diff --git a/lib/index.js b/lib/index.js
index 6ef0ea0266a..20d1b40ec86 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -21,6 +21,7 @@ Plotly.register([
require('./contour'),
require('./scatterternary'),
require('./violin'),
+ require('./waterfall'),
require('./pie'),
require('./sunburst'),
diff --git a/lib/waterfall.js b/lib/waterfall.js
new file mode 100644
index 00000000000..6d2bcac2fde
--- /dev/null
+++ b/lib/waterfall.js
@@ -0,0 +1,11 @@
+/**
+* Copyright 2012-2019, 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';
+
+module.exports = require('../src/traces/waterfall');
diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js
index f8a0131e4e0..05a56873b1d 100644
--- a/src/components/drawing/index.js
+++ b/src/components/drawing/index.js
@@ -110,7 +110,9 @@ drawing.hideOutsideRangePoints = function(traceGroups, subplot) {
var trace = d[0].trace;
var xcalendar = trace.xcalendar;
var ycalendar = trace.ycalendar;
- var selector = trace.type === 'bar' ? '.bartext' : '.point,.textpoint';
+ var selector = trace.type === 'bar' ? '.bartext' :
+ trace.type === 'waterfall' ? '.bartext,.line' :
+ '.point,.textpoint';
traceGroups.selectAll(selector).each(function(d) {
drawing.hideOutsideRangePoint(d, d3.select(this), xa, ya, xcalendar, ycalendar);
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index cd7260bf8d0..a48752efc8f 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -61,6 +61,7 @@ module.exports = function style(s, gd) {
.enter().append('g')
.classed('legendpoints', true);
})
+ .each(styleWaterfalls)
.each(styleBars)
.each(styleBoxes)
.each(stylePies)
@@ -241,6 +242,38 @@ module.exports = function style(s, gd) {
txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd);
}
+ function styleWaterfalls(d) {
+ var trace = d[0].trace;
+
+ var ptsData = [];
+ if(trace.type === 'waterfall' && trace.visible) {
+ ptsData = d[0].hasTotals ?
+ [['increasing', 'M-6,-6V6H0Z'], ['totals', 'M6,6H0L-6,-6H-0Z'], ['decreasing', 'M6,6V-6H0Z']] :
+ [['increasing', 'M-6,-6V6H6Z'], ['decreasing', 'M6,6V-6H-6Z']];
+ }
+
+ var pts = d3.select(this).select('g.legendpoints')
+ .selectAll('path.legendwaterfall')
+ .data(ptsData);
+ pts.enter().append('path').classed('legendwaterfall', true)
+ .attr('transform', 'translate(20,0)')
+ .style('stroke-miterlimit', 1);
+ pts.exit().remove();
+
+ pts.each(function(dd) {
+ var pt = d3.select(this);
+ var cont = trace[dd[0]].marker;
+
+ pt.attr('d', dd[1])
+ .style('stroke-width', cont.line.width + 'px')
+ .call(Color.fill, cont.color);
+
+ if(cont.line.width) {
+ pt.call(Color.stroke, cont.line.color);
+ }
+ });
+ }
+
function styleBars(d) {
var trace = d[0].trace;
var marker = trace.marker || {};
diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index bfbfc28c414..d4f1a2e4f34 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -16,10 +16,11 @@ var Registry = require('../registry');
var Lib = require('../lib');
var Plots = require('../plots/plots');
var AxisIds = require('../plots/cartesian/axis_ids');
-var cleanId = AxisIds.cleanId;
-var getFromTrace = AxisIds.getFromTrace;
var Color = require('../components/color');
+var cleanId = AxisIds.cleanId;
+var getFromTrace = AxisIds.getFromTrace;
+var traceIs = Registry.traceIs;
// clear the promise queue if one of them got rejected
exports.clearPromiseQueue = function(gd) {
@@ -290,7 +291,7 @@ exports.cleanData = function(data) {
// error_y.opacity is obsolete - merge into color
if(trace.error_y && 'opacity' in trace.error_y) {
var dc = Color.defaults;
- var yeColor = trace.error_y.color || (Registry.traceIs(trace, 'bar') ?
+ var yeColor = trace.error_y.color || (traceIs(trace, 'bar') ?
Color.defaultLine :
dc[tracei % dc.length]);
trace.error_y.color = Color.addOpacity(
@@ -302,8 +303,8 @@ exports.cleanData = function(data) {
// convert bardir to orientation, and put the data into
// the axes it's eventually going to be used with
if('bardir' in trace) {
- if(trace.bardir === 'h' && (Registry.traceIs(trace, 'bar') ||
- trace.type.substr(0, 9) === 'histogram')) {
+ if(trace.bardir === 'h' && (traceIs(trace, 'bar') ||
+ trace.type.substr(0, 9) === 'histogram')) {
trace.orientation = 'h';
exports.swapXYData(trace);
}
@@ -332,11 +333,11 @@ exports.cleanData = function(data) {
if(trace.yaxis) trace.yaxis = cleanId(trace.yaxis, 'y');
// scene ids scene1 -> scene
- if(Registry.traceIs(trace, 'gl3d') && trace.scene) {
+ if(traceIs(trace, 'gl3d') && trace.scene) {
trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene);
}
- if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) {
+ if(!traceIs(trace, 'pie') && !traceIs(trace, 'bar') && trace.type !== 'waterfall') {
if(Array.isArray(trace.textposition)) {
for(i = 0; i < trace.textposition.length; i++) {
trace.textposition[i] = cleanTextPosition(trace.textposition[i]);
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 1e0fee8609e..50ab0006933 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -2847,14 +2847,16 @@ function hasBarsOrFill(gd, ax) {
for(var i = 0; i < fullData.length; i++) {
var trace = fullData[i];
- if(trace.visible === true &&
- (trace.xaxis + trace.yaxis) === subplot &&
- (
- Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axLetter] ||
- trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter
- )
- ) {
- return true;
+ if(trace.visible === true && (trace.xaxis + trace.yaxis) === subplot) {
+ if(
+ (Registry.traceIs(trace, 'bar') || trace.type === 'waterfall') &&
+ trace.orientation === {x: 'h', y: 'v'}[axLetter]
+ ) return true;
+
+ if(
+ trace.fill &&
+ trace.fill.charAt(trace.fill.length - 1) === axLetter
+ ) return true;
}
}
return false;
diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js
index 51738431b6f..2b524c3d338 100644
--- a/src/plots/cartesian/constants.js
+++ b/src/plots/cartesian/constants.js
@@ -65,7 +65,7 @@ module.exports = {
traceLayerClasses: [
'heatmaplayer',
'contourcarpetlayer', 'contourlayer',
- 'barlayer',
+ 'waterfalllayer', 'barlayer',
'carpetlayer',
'violinlayer',
'boxlayer',
diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js
index 430e01dc4ea..8ff5a4678cd 100644
--- a/src/plots/cartesian/index.js
+++ b/src/plots/cartesian/index.js
@@ -261,7 +261,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback
);
// layers that allow `cliponaxis: false`
- if(className !== 'scatterlayer' && className !== 'barlayer') {
+ if(className !== 'scatterlayer' && className !== 'barlayer' && className !== 'waterfalllayer') {
Drawing.setClipUrl(sel, plotinfo.layerClipId, gd);
}
});
@@ -277,7 +277,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback
if(!gd._context.staticPlot) {
if(plotinfo._hasClipOnAxisFalse) {
plotinfo.clipOnAxisFalseTraces = plotinfo.plot
- .selectAll('.scatterlayer, .barlayer')
+ .selectAll('.scatterlayer, .barlayer, .waterfalllayer')
.selectAll('.trace');
}
diff --git a/src/traces/bar/arrays_to_calcdata.js b/src/traces/bar/arrays_to_calcdata.js
index fcc88ef81d0..a722f3dec04 100644
--- a/src/traces/bar/arrays_to_calcdata.js
+++ b/src/traces/bar/arrays_to_calcdata.js
@@ -6,12 +6,10 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var mergeArray = require('../../lib').mergeArray;
-
// arrayOk attributes, merge them into calcdata array
module.exports = function arraysToCalcdata(cd, trace) {
for(var i = 0; i < cd.length; i++) cd[i].i = i;
diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js
index 916e853eec5..d09f130bb08 100644
--- a/src/traces/bar/cross_trace_calc.js
+++ b/src/traces/bar/cross_trace_calc.js
@@ -237,6 +237,7 @@ function setOffsetAndWidth(gd, pa, sieve) {
var fullLayout = gd._fullLayout;
var bargap = fullLayout.bargap;
var bargroupgap = fullLayout.bargroupgap || 0;
+
var minDiff = sieve.minDiff;
var calcTraces = sieve.traces;
diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js
index b6715411524..e8e83ac35f0 100644
--- a/src/traces/bar/defaults.js
+++ b/src/traces/bar/defaults.js
@@ -13,17 +13,17 @@ var Color = require('../../components/color');
var Registry = require('../../registry');
var handleXYDefaults = require('../scatter/xy_defaults');
-var handleStyleDefaults = require('../bar/style_defaults');
+var handleStyleDefaults = require('./style_defaults');
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;
var attributes = require('./attributes');
+var coerceFont = Lib.coerceFont;
+
function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- var coerceFont = Lib.coerceFont;
-
var len = handleXYDefaults(traceIn, traceOut, layout, coerce);
if(!len) {
traceOut.visible = false;
@@ -39,34 +39,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
coerce('hovertext');
coerce('hovertemplate');
- var textPosition = coerce('textposition');
-
- var hasBoth = Array.isArray(textPosition) || textPosition === 'auto';
- var hasInside = hasBoth || textPosition === 'inside';
- var hasOutside = hasBoth || textPosition === 'outside';
-
- if(hasInside || hasOutside) {
- var textFont = coerceFont(coerce, 'textfont', layout.font);
-
- // Note that coercing `insidetextfont` is always needed –
- // even if `textposition` is `outside` for each trace – since
- // an outside label can become an inside one, for example because
- // of a bar being stacked on top of it.
- var insideTextFontDefault = Lib.extendFlat({}, textFont);
- var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
- var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
- if(isColorInheritedFromLayoutFont) {
- delete insideTextFontDefault.color;
- }
- coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
-
- if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont);
-
- coerce('constraintext');
- coerce('selected.textfont.color');
- coerce('unselected.textfont.color');
- coerce('cliponaxis');
- }
+ handleText(traceIn, traceOut, layout, coerce, true);
handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
@@ -126,20 +99,55 @@ function crossTraceDefaults(fullData, fullLayout) {
return Lib.coerce(traceOut._input, traceOut, attributes, attr);
}
- for(var i = 0; i < fullData.length; i++) {
- traceOut = fullData[i];
+ if(fullLayout.barmode === 'group') {
+ for(var i = 0; i < fullData.length; i++) {
+ traceOut = fullData[i];
- if(traceOut.type === 'bar') {
- traceIn = traceOut._input;
- if(fullLayout.barmode === 'group') {
+ if(traceOut.type === 'bar') {
+ traceIn = traceOut._input;
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
}
}
}
}
+function handleText(traceIn, traceOut, layout, coerce, moduleHasSelUnselected) {
+ var textPosition = coerce('textposition');
+ var hasBoth = Array.isArray(textPosition) || textPosition === 'auto';
+ var hasInside = hasBoth || textPosition === 'inside';
+ var hasOutside = hasBoth || textPosition === 'outside';
+
+ if(hasInside || hasOutside) {
+ var textFont = coerceFont(coerce, 'textfont', layout.font);
+
+ // Note that coercing `insidetextfont` is always needed –
+ // even if `textposition` is `outside` for each trace – since
+ // an outside label can become an inside one, for example because
+ // of a bar being stacked on top of it.
+ var insideTextFontDefault = Lib.extendFlat({}, textFont);
+ var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
+ var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
+ if(isColorInheritedFromLayoutFont) {
+ delete insideTextFontDefault.color;
+ }
+ coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
+
+ if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont);
+
+ coerce('constraintext');
+
+ if(moduleHasSelUnselected) {
+ coerce('selected.textfont.color');
+ coerce('unselected.textfont.color');
+ }
+
+ coerce('cliponaxis');
+ }
+}
+
module.exports = {
supplyDefaults: supplyDefaults,
crossTraceDefaults: crossTraceDefaults,
- handleGroupingDefaults: handleGroupingDefaults
+ handleGroupingDefaults: handleGroupingDefaults,
+ handleText: handleText
};
diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js
index 570fab66c8e..841d3ba00ab 100644
--- a/src/traces/bar/hover.js
+++ b/src/traces/bar/hover.js
@@ -15,6 +15,21 @@ var Color = require('../../components/color');
var fillHoverText = require('../scatter/fill_hover_text');
function hoverPoints(pointData, xval, yval, hovermode) {
+ var barPointData = hoverOnBars(pointData, xval, yval, hovermode);
+
+ if(barPointData) {
+ var cd = barPointData.cd;
+ var trace = cd[0].trace;
+ var di = cd[barPointData.index];
+
+ barPointData.color = getTraceColor(trace, di);
+ Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, barPointData);
+
+ return [barPointData];
+ }
+}
+
+function hoverOnBars(pointData, xval, yval, hovermode) {
var cd = pointData.cd;
var trace = cd[0].trace;
var t = cd[0].t;
@@ -133,12 +148,10 @@ function hoverPoints(pointData, xval, yval, hovermode) {
// in case of bars shifted within groups
pointData[posLetter + 'Spike'] = pa.c2p(di.p, true);
- pointData.color = getTraceColor(trace, di);
fillHoverText(di, trace, pointData);
- Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData);
-
pointData.hovertemplate = trace.hovertemplate;
- return [pointData];
+
+ return pointData;
}
function getTraceColor(trace, di) {
@@ -152,5 +165,6 @@ function getTraceColor(trace, di) {
module.exports = {
hoverPoints: hoverPoints,
+ hoverOnBars: hoverOnBars,
getTraceColor: getTraceColor
};
diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js
index c76d8fe0373..abc63612673 100644
--- a/src/traces/bar/plot.js
+++ b/src/traces/bar/plot.js
@@ -28,16 +28,24 @@ var style = require('./style');
// padding in pixels around text
var TEXTPAD = 3;
-module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
+module.exports = function plot(gd, plotinfo, cdModule, traceLayer) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
var fullLayout = gd._fullLayout;
- var bartraces = Lib.makeTraceGroups(barLayer, cdbar, 'trace bars').each(function(cd) {
+ var bartraces = Lib.makeTraceGroups(traceLayer, cdModule, 'trace bars').each(function(cd) {
var plotGroup = d3.select(this);
var cd0 = cd[0];
var trace = cd0.trace;
+ var adjustDir;
+ var adjustPixel = 0;
+ if(trace.type === 'waterfall' && trace.connector.visible && trace.connector.mode === 'between') {
+ adjustPixel = trace.connector.line.width / 2;
+ }
+
+ var isHorizontal = (trace.orientation === 'h');
+
if(!plotinfo.isRangePlot) cd0.node3 = plotGroup;
var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points');
@@ -57,7 +65,7 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
// log values go off-screen by plotwidth
// so you see them continue if you drag the plot
var x0, x1, y0, y1;
- if(trace.orientation === 'h') {
+ if(isHorizontal) {
y0 = ya.c2p(di.p0, true);
y1 = ya.c2p(di.p1, true);
x0 = xa.c2p(di.s0, true);
@@ -65,8 +73,7 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
// for selections
di.ct = [x1, (y0 + y1) / 2];
- }
- else {
+ } else {
x0 = xa.c2p(di.p0, true);
x1 = xa.c2p(di.p1, true);
y0 = ya.c2p(di.s0, true);
@@ -83,14 +90,43 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
return;
}
- var lw = (di.mlw + 1 || trace.marker.line.width + 1 ||
+ // in waterfall mode `between` we need to adjust bar end points to match the connector width
+ if(adjustPixel) {
+ if(isHorizontal) {
+ adjustDir = (x1 < x0) ? -1 : 1;
+ x0 -= adjustDir * adjustPixel;
+ x1 += adjustDir * adjustPixel;
+ } else {
+ adjustDir = (y1 < y0) ? -1 : 1;
+ y0 -= adjustDir * adjustPixel;
+ y1 += adjustDir * adjustPixel;
+ }
+ }
+
+ var lw;
+ var mc;
+ var prefix;
+
+ if(trace.type === 'waterfall') {
+ var cont = trace[di.dir].marker;
+ lw = cont.line.width;
+ mc = cont.color;
+ prefix = 'waterfall';
+ } else {
+ lw = (di.mlw + 1 || trace.marker.line.width + 1 ||
(di.trace ? di.trace.marker.line.width : 0) + 1) - 1;
+ mc = di.mc || trace.marker.color;
+ prefix = 'bar';
+ }
+
var offset = d3.round((lw / 2) % 1, 2);
+ var bargap = fullLayout[prefix + 'gap'];
+ var bargroupgap = fullLayout[prefix + 'groupgap'];
function roundWithLine(v) {
// if there are explicit gaps, don't round,
// it can make the gaps look crappy
- return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ?
+ return (bargap === 0 && bargroupgap === 0) ?
d3.round(Math.round(v) - offset, 2) : v;
}
@@ -111,7 +147,8 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
// pixelation. if the bars ARE fully opaque and have
// no line, expand to a full pixel to make sure we
// can see them
- var op = Color.opacity(di.mc || trace.marker.color);
+
+ var op = Color.opacity(mc);
var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible;
x0 = fixpx(x0, x1);
x1 = fixpx(x1, x0);
@@ -121,8 +158,7 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
Lib.ensureSingle(bar, 'path')
.style('vector-effect', 'non-scaling-stroke')
- .attr('d',
- 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z')
+ .attr('d', 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z')
.call(Drawing.setClipUrl, plotinfo.layerClipId, gd);
appendBarText(gd, bar, cd, i, x0, x1, y0, y1);
@@ -143,6 +179,7 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) {
};
function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
+ var fullLayout = gd._fullLayout;
var textPosition;
function appendTextNode(bar, text, textFont) {
@@ -174,13 +211,14 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
return;
}
- var layoutFont = gd._fullLayout.font;
+ var layoutFont = fullLayout.font;
var barColor = style.getBarColor(calcTrace[i], trace);
var insideTextFont = style.getInsideTextFont(trace, i, layoutFont, barColor);
var outsideTextFont = style.getOutsideTextFont(trace, i, layoutFont);
// compute text position
- var barmode = gd._fullLayout.barmode;
+ var prefix = trace.type === 'waterfall' ? 'waterfall' : 'bar';
+ var barmode = fullLayout[prefix + 'mode'];
var inStackMode = (barmode === 'stack');
var inRelativeMode = (barmode === 'relative');
var inStackOrRelativeMode = inStackMode || inRelativeMode;
@@ -220,14 +258,12 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
if(textHasSize &&
(fitsInside || fitsInsideIfRotated || fitsInsideIfShrunk)) {
textPosition = 'inside';
- }
- else {
+ } else {
textPosition = 'outside';
textSelection.remove();
textSelection = null;
}
- }
- else textPosition = 'inside';
+ } else textPosition = 'inside';
}
if(!textSelection) {
@@ -251,8 +287,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
constrained = trace.constraintext === 'both' || trace.constraintext === 'outside';
transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB,
orientation, constrained);
- }
- else {
+ } else {
constrained = trace.constraintext === 'both' || trace.constraintext === 'inside';
transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB,
orientation, constrained);
@@ -280,8 +315,7 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation, constr
textpad = TEXTPAD;
barWidth -= 2 * textpad;
barHeight -= 2 * textpad;
- }
- else textpad = 0;
+ } else textpad = 0;
// compute rotation and scale
var rotate,
@@ -291,18 +325,15 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation, constr
// no scale or rotation is required
rotate = false;
scale = 1;
- }
- else if(textWidth <= barHeight && textHeight <= barWidth) {
+ } else if(textWidth <= barHeight && textHeight <= barWidth) {
// only rotation is required
rotate = true;
scale = 1;
- }
- else if((textWidth < textHeight) === (barWidth < barHeight)) {
+ } else if((textWidth < textHeight) === (barWidth < barHeight)) {
// only scale is required
rotate = false;
scale = constrained ? Math.min(barWidth / textWidth, barHeight / textHeight) : 1;
- }
- else {
+ } else {
// both scale and rotation are required
rotate = true;
scale = constrained ? Math.min(barHeight / textWidth, barWidth / textHeight) : 1;
@@ -314,8 +345,7 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation, constr
if(rotate) {
targetWidth = scale * textHeight;
targetHeight = scale * textWidth;
- }
- else {
+ } else {
targetWidth = scale * textWidth;
targetHeight = scale * textHeight;
}
@@ -325,19 +355,16 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation, constr
// bar end is on the left hand side
targetX = x1 + textpad + targetWidth / 2;
targetY = (y0 + y1) / 2;
- }
- else {
+ } else {
targetX = x1 - textpad - targetWidth / 2;
targetY = (y0 + y1) / 2;
}
- }
- else {
+ } else {
if(y1 > y0) {
// bar end is on the bottom
targetX = (x0 + x1) / 2;
targetY = y1 - textpad - targetHeight / 2;
- }
- else {
+ } else {
targetX = (x0 + x1) / 2;
targetY = y1 + textpad + targetHeight / 2;
}
@@ -382,19 +409,16 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation, const
// bar end is on the left hand side
targetX = x1 - textpad - targetWidth / 2;
targetY = (y0 + y1) / 2;
- }
- else {
+ } else {
targetX = x1 + textpad + targetWidth / 2;
targetY = (y0 + y1) / 2;
}
- }
- else {
+ } else {
if(y1 > y0) {
// bar end is on the bottom
targetX = (x0 + x1) / 2;
targetY = y1 + textpad + targetHeight / 2;
- }
- else {
+ } else {
targetX = (x0 + x1) / 2;
targetY = y1 - textpad - targetHeight / 2;
}
diff --git a/src/traces/bar/style.js b/src/traces/bar/style.js
index be948a3877e..2492af523ab 100644
--- a/src/traces/bar/style.js
+++ b/src/traces/bar/style.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var d3 = require('d3');
@@ -22,7 +21,7 @@ var attributeOutsideTextFont = attributes.outsidetextfont;
var helpers = require('./helpers');
function style(gd, cd) {
- var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.trace.bars');
+ var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.barlayer').selectAll('g.trace');
var barcount = s.size();
var fullLayout = gd._fullLayout;
@@ -51,12 +50,12 @@ function style(gd, cd) {
}
function stylePoints(sel, trace, gd) {
- var pts = sel.selectAll('path');
- var txs = sel.selectAll('text');
-
- Drawing.pointStyle(pts, trace, gd);
+ Drawing.pointStyle(sel.selectAll('path'), trace, gd);
+ styleTextPoints(sel, trace, gd);
+}
- txs.each(function(d) {
+function styleTextPoints(sel, trace, gd) {
+ sel.selectAll('text').each(function(d) {
var tx = d3.select(this);
var font = determineFont(tx, d, trace, gd);
Drawing.font(tx, font);
@@ -160,11 +159,15 @@ function getFontValue(attributeDefinition, attributeValue, index, defaultValue)
}
function getBarColor(cd, trace) {
+ if(trace.type === 'waterfall') {
+ return trace[cd.dir].marker.color;
+ }
return cd.mc || trace.marker.color;
}
module.exports = {
style: style,
+ styleTextPoints: styleTextPoints,
styleOnSelect: styleOnSelect,
getInsideTextFont: getInsideTextFont,
getOutsideTextFont: getOutsideTextFont,
diff --git a/src/traces/waterfall/attributes.js b/src/traces/waterfall/attributes.js
new file mode 100644
index 00000000000..a4ba4419217
--- /dev/null
+++ b/src/traces/waterfall/attributes.js
@@ -0,0 +1,126 @@
+/**
+* Copyright 2012-2019, 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 barAttrs = require('../bar/attributes');
+var lineAttrs = require('../scatter/attributes').line;
+var extendFlat = require('../../lib/extend').extendFlat;
+
+function directionAttrs(dirTxt) {
+ return {
+ marker: {
+ color: extendFlat({}, barAttrs.marker.color, {
+ arrayOk: false,
+ editType: 'style',
+ description: 'Sets the marker color of all ' + dirTxt + ' values.'
+ }),
+ line: {
+ color: extendFlat({}, barAttrs.marker.line.color, {
+ arrayOk: false,
+ editType: 'style',
+ description: 'Sets the line color of all ' + dirTxt + ' values.'
+ }),
+ width: extendFlat({}, barAttrs.marker.line.width, {
+ arrayOk: false,
+ editType: 'style',
+ description: 'Sets the line width of all ' + dirTxt + ' values.'
+ }),
+ editType: 'style',
+ },
+ editType: 'style'
+ },
+ editType: 'style'
+ };
+}
+
+module.exports = {
+ measure: {
+ valType: 'data_array',
+ dflt: [],
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'An array containing types of values.',
+ 'By default the values are considered as \'relative\'.',
+ 'However; it is possible to use \'total\' to compute the sums.',
+ 'Also \'absolute\' could be applied to reset the computed total',
+ 'or to declare an initial value where needed.'
+ ].join(' ')
+ },
+
+ base: {
+ valType: 'number',
+ dflt: null,
+ arrayOk: false,
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets where the bar base is drawn (in position axis units).'
+ ].join(' ')
+ },
+
+ x: barAttrs.x,
+ x0: barAttrs.x0,
+ dx: barAttrs.dx,
+ y: barAttrs.y,
+ y0: barAttrs.y0,
+ dy: barAttrs.dy,
+
+ hovertext: barAttrs.hovertext,
+ hovertemplate: barAttrs.hovertemplate,
+
+ text: barAttrs.text,
+ textposition: barAttrs.textposition,
+ textfont: barAttrs.textfont,
+ insidetextfont: barAttrs.insidetextfont,
+ outsidetextfont: barAttrs.outsidetextfont,
+ constraintext: barAttrs.constraintext,
+
+ cliponaxis: barAttrs.cliponaxis,
+ orientation: barAttrs.orientation,
+
+ offset: barAttrs.offset,
+ width: barAttrs.width,
+
+ increasing: directionAttrs('increasing'),
+ decreasing: directionAttrs('decreasing'),
+ totals: directionAttrs('intermediate sums and total'),
+
+ connector: {
+ line: {
+ color: lineAttrs.color,
+ width: lineAttrs.width,
+ dash: lineAttrs.dash,
+ editType: 'plot'
+ },
+ mode: {
+ valType: 'enumerated',
+ values: ['spanning', 'between'],
+ dflt: 'between',
+ role: 'info',
+ editType: 'plot',
+ description: [
+ 'Sets the shape of connector lines.'
+ ].join(' ')
+ },
+ visible: {
+ valType: 'boolean',
+ dflt: true,
+ role: 'info',
+ editType: 'plot',
+ description: [
+ 'Determines if connector lines are drawn. '
+ ].join(' ')
+ },
+ editType: 'plot'
+ },
+
+ offsetgroup: barAttrs.offsetgroup,
+ alignmentgroup: barAttrs.offsetgroup
+};
diff --git a/src/traces/waterfall/calc.js b/src/traces/waterfall/calc.js
new file mode 100644
index 00000000000..d22c1121a86
--- /dev/null
+++ b/src/traces/waterfall/calc.js
@@ -0,0 +1,101 @@
+/**
+* Copyright 2012-2019, 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 Axes = require('../../plots/cartesian/axes');
+var mergeArray = require('../../lib').mergeArray;
+var calcSelection = require('../scatter/calc_selection');
+var BADNUM = require('../../constants/numerical').BADNUM;
+
+function isAbsolute(a) {
+ return (a === 'a' || a === 'absolute');
+}
+
+function isTotal(a) {
+ return (a === 't' || a === 'total');
+}
+
+module.exports = function calc(gd, trace) {
+ var xa = Axes.getFromId(gd, trace.xaxis || 'x');
+ var ya = Axes.getFromId(gd, trace.yaxis || 'y');
+ var size, pos;
+
+ if(trace.orientation === 'h') {
+ size = xa.makeCalcdata(trace, 'x');
+ pos = ya.makeCalcdata(trace, 'y');
+ } else {
+ size = ya.makeCalcdata(trace, 'y');
+ pos = xa.makeCalcdata(trace, 'x');
+ }
+
+ // create the "calculated data" to plot
+ var serieslen = Math.min(pos.length, size.length);
+ var cd = new Array(serieslen);
+
+ // set position and size (as well as for waterfall total size)
+ var previousSum = 0;
+ var newSize;
+ // trace-wide flags
+ var hasTotals = false;
+
+ for(var i = 0; i < serieslen; i++) {
+
+ var amount = size[i] || 0;
+
+ var connectToNext = false;
+ if(size[i] !== BADNUM || isTotal(trace.measure[i]) || isAbsolute(trace.measure[i])) {
+ if(i + 1 < serieslen && (size[i + 1] !== BADNUM || isTotal(trace.measure[i + 1]) || isAbsolute(trace.measure[i + 1]))) {
+ connectToNext = true;
+ }
+ }
+
+ var cdi = cd[i] = {
+ i: i,
+ p: pos[i],
+ s: amount,
+ rawS: amount,
+ cNext: connectToNext
+ };
+
+ if(isAbsolute(trace.measure[i])) {
+ previousSum = cdi.s;
+
+ cdi.isSum = true;
+ cdi.dir = 'totals';
+ cdi.s = previousSum;
+ } else if(isTotal(trace.measure[i])) {
+ cdi.isSum = true;
+ cdi.dir = 'totals';
+ cdi.s = previousSum;
+ } else {
+ // default: relative
+ cdi.isSum = false;
+ cdi.dir = cdi.rawS < 0 ? 'decreasing' : 'increasing';
+ newSize = cdi.s;
+ cdi.s = previousSum + newSize;
+ previousSum += newSize;
+ }
+
+ if(cdi.dir === 'totals') {
+ hasTotals = true;
+ }
+
+ if(trace.ids) {
+ cdi.id = String(trace.ids[i]);
+ }
+ }
+
+ cd[0].hasTotals = hasTotals;
+
+ mergeArray(trace.text, cd, 'tx');
+ mergeArray(trace.hovertext, cd, 'htx');
+ calcSelection(cd, trace);
+
+ return cd;
+};
diff --git a/src/traces/waterfall/cross_trace_calc.js b/src/traces/waterfall/cross_trace_calc.js
new file mode 100644
index 00000000000..31d3b25d178
--- /dev/null
+++ b/src/traces/waterfall/cross_trace_calc.js
@@ -0,0 +1,70 @@
+/**
+* Copyright 2012-2019, 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 setGroupPositions = require('../bar/cross_trace_calc').setGroupPositions;
+
+module.exports = function crossTraceCalc(gd, plotinfo) {
+ var fullLayout = gd._fullLayout;
+ var fullData = gd._fullData;
+ var calcdata = gd.calcdata;
+ var xa = plotinfo.xaxis;
+ var ya = plotinfo.yaxis;
+ var waterfalls = [];
+ var waterfallsVert = [];
+ var waterfallsHorz = [];
+ var cd, i;
+
+ for(i = 0; i < fullData.length; i++) {
+ var fullTrace = fullData[i];
+
+ if(
+ fullTrace.visible === true &&
+ fullTrace.xaxis === xa._id &&
+ fullTrace.yaxis === ya._id &&
+ fullTrace.type === 'waterfall'
+ ) {
+ cd = calcdata[i];
+
+ if(fullTrace.orientation === 'h') {
+ waterfallsHorz.push(cd);
+ } else {
+ waterfallsVert.push(cd);
+ }
+
+ waterfalls.push(cd);
+ }
+ }
+
+ // waterfall version of 'barmode', 'bargap' and 'bargroupgap'
+ var mockGd = {
+ _fullLayout: {
+ _axisMatchGroups: fullLayout._axisMatchGroups,
+ _alignmentOpts: fullLayout._alignmentOpts,
+ barmode: fullLayout.waterfallmode,
+ bargap: fullLayout.waterfallgap,
+ bargroupgap: fullLayout.waterfallgroupgap
+ }
+ };
+
+ setGroupPositions(mockGd, xa, ya, waterfallsVert);
+ setGroupPositions(mockGd, ya, xa, waterfallsHorz);
+
+ for(i = 0; i < waterfalls.length; i++) {
+ cd = waterfalls[i];
+
+ for(var j = 0; j < cd.length; j++) {
+ var di = cd[j];
+
+ if(di.isSum === false) {
+ di.s0 += (j === 0) ? 0 : cd[j - 1].s;
+ }
+ }
+ }
+};
diff --git a/src/traces/waterfall/defaults.js b/src/traces/waterfall/defaults.js
new file mode 100644
index 00000000000..cdf53eab8f6
--- /dev/null
+++ b/src/traces/waterfall/defaults.js
@@ -0,0 +1,89 @@
+/**
+* Copyright 2012-2019, 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 Lib = require('../../lib');
+
+var handleGroupingDefaults = require('../bar/defaults').handleGroupingDefaults;
+var handleText = require('../bar/defaults').handleText;
+var handleXYDefaults = require('../scatter/xy_defaults');
+var attributes = require('./attributes');
+var Color = require('../../components/color');
+
+var INCREASING_COLOR = '#3D9970';
+var DECREASING_COLOR = '#FF4136';
+var TOTALS_COLOR = '#4499FF';
+
+function handleDirection(coerce, direction, defaultColor) {
+ coerce(direction + '.marker.color', defaultColor);
+ coerce(direction + '.marker.line.color', Color.defaultLine);
+ coerce(direction + '.marker.line.width');
+}
+
+function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
+ }
+
+ var len = handleXYDefaults(traceIn, traceOut, layout, coerce);
+ if(!len) {
+ traceOut.visible = false;
+ return;
+ }
+
+ coerce('measure');
+
+ coerce('orientation', (traceOut.x && !traceOut.y) ? 'h' : 'v');
+ coerce('base');
+ coerce('offset');
+ coerce('width');
+
+ coerce('text');
+ coerce('hovertext');
+ coerce('hovertemplate');
+
+ handleText(traceIn, traceOut, layout, coerce, false);
+
+ handleDirection(coerce, 'increasing', INCREASING_COLOR);
+ handleDirection(coerce, 'decreasing', DECREASING_COLOR);
+ handleDirection(coerce, 'totals', TOTALS_COLOR);
+
+ var connectorVisible = coerce('connector.visible');
+ if(connectorVisible) {
+ coerce('connector.mode');
+ var connectorLineWidth = coerce('connector.line.width');
+ if(connectorLineWidth) {
+ coerce('connector.line.color');
+ coerce('connector.line.dash');
+ }
+ }
+}
+
+function crossTraceDefaults(fullData, fullLayout) {
+ var traceIn, traceOut;
+
+ function coerce(attr) {
+ return Lib.coerce(traceOut._input, traceOut, attributes, attr);
+ }
+
+ if(fullLayout.waterfallmode === 'group') {
+ for(var i = 0; i < fullData.length; i++) {
+ traceOut = fullData[i];
+ traceIn = traceOut._input;
+
+ handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
+ }
+ }
+}
+
+module.exports = {
+ supplyDefaults: supplyDefaults,
+ crossTraceDefaults: crossTraceDefaults,
+ handleGroupingDefaults: handleGroupingDefaults
+};
diff --git a/src/traces/waterfall/hover.js b/src/traces/waterfall/hover.js
new file mode 100644
index 00000000000..923668dabcf
--- /dev/null
+++ b/src/traces/waterfall/hover.js
@@ -0,0 +1,61 @@
+/**
+* Copyright 2012-2019, 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 Color = require('../../components/color');
+var hoverOnBars = require('../bar/hover').hoverOnBars;
+
+var DIRSYMBOL = {
+ increasing: '▲',
+ decreasing: '▼'
+};
+
+module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
+ var point = hoverOnBars(pointData, xval, yval, hovermode);
+ if(!point) return;
+
+ var cd = point.cd;
+ var trace = cd[0].trace;
+
+ // the closest data point
+ var index = point.index;
+ var di = cd[index];
+
+ var sizeLetter = (trace.orientation === 'h') ? 'x' : 'y';
+
+ var size = (di.isSum) ? di.b + di.s : di.rawS;
+
+ if(!di.isSum) {
+ // format delta numbers:
+ if(size > 0) {
+ point.extraText = size + ' ' + DIRSYMBOL.increasing;
+ } else if(size < 0) {
+ point.extraText = '(' + (-size) + ') ' + DIRSYMBOL.decreasing;
+ } else {
+ return;
+ }
+ // display initial value
+ point.extraText += '
Initial: ' + (di.b + di.s - size);
+ } else {
+ point[sizeLetter + 'LabelVal'] = size;
+ }
+
+ point.color = getTraceColor(trace, di);
+
+ return [point];
+};
+
+function getTraceColor(trace, di) {
+ var cont = trace[di.dir].marker;
+ var mc = cont.color;
+ var mlc = cont.line.color;
+ var mlw = cont.line.width;
+ if(Color.opacity(mc)) return mc;
+ else if(Color.opacity(mlc) && mlw) return mlc;
+}
diff --git a/src/traces/waterfall/index.js b/src/traces/waterfall/index.js
new file mode 100644
index 00000000000..1263189fced
--- /dev/null
+++ b/src/traces/waterfall/index.js
@@ -0,0 +1,40 @@
+/**
+* Copyright 2012-2019, 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 Waterfall = {};
+
+Waterfall.attributes = require('./attributes');
+Waterfall.layoutAttributes = require('./layout_attributes');
+Waterfall.supplyDefaults = require('./defaults').supplyDefaults;
+Waterfall.crossTraceDefaults = require('./defaults').crossTraceDefaults;
+Waterfall.supplyLayoutDefaults = require('./layout_defaults');
+Waterfall.calc = require('./calc');
+Waterfall.crossTraceCalc = require('./cross_trace_calc');
+Waterfall.plot = require('./plot');
+Waterfall.style = require('./style').style;
+Waterfall.hoverPoints = require('./hover');
+Waterfall.selectPoints = require('../bar/select');
+
+Waterfall.moduleType = 'trace';
+Waterfall.name = 'waterfall';
+Waterfall.basePlotModule = require('../../plots/cartesian');
+Waterfall.categories = ['cartesian', 'svg', 'oriented', 'showLegend', 'zoomScale'];
+Waterfall.meta = {
+ description: [
+ 'Draws waterfall trace which is useful graph to displays the',
+ 'contribution of various elements (either positive or negative)',
+ 'in a bar chart. The data visualized by the span of the bars is',
+ 'set in `y` if `orientation` is set th *v* (the default) and the',
+ 'labels are set in `x`.',
+ 'By setting `orientation` to *h*, the roles are interchanged.'
+ ].join(' ')
+};
+
+module.exports = Waterfall;
diff --git a/src/traces/waterfall/layout_attributes.js b/src/traces/waterfall/layout_attributes.js
new file mode 100644
index 00000000000..7d0a29b897c
--- /dev/null
+++ b/src/traces/waterfall/layout_attributes.js
@@ -0,0 +1,50 @@
+/**
+* Copyright 2012-2019, 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';
+
+module.exports = {
+ waterfallmode: {
+ valType: 'enumerated',
+ values: ['group', 'overlay'],
+ dflt: 'group',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Determines how bars at the same location coordinate',
+ 'are displayed on the graph.',
+ 'With *group*, the bars are plotted next to one another',
+ 'centered around the shared location.',
+ 'With *overlay*, the bars are plotted over one another,',
+ 'you might need to an *opacity* to see multiple bars.'
+ ].join(' ')
+ },
+ waterfallgap: {
+ valType: 'number',
+ min: 0,
+ max: 1,
+ role: 'style',
+ editType: 'calc',
+ description: [
+ 'Sets the gap (in plot fraction) between bars of',
+ 'adjacent location coordinates.'
+ ].join(' ')
+ },
+ waterfallgroupgap: {
+ valType: 'number',
+ min: 0,
+ max: 1,
+ dflt: 0,
+ role: 'style',
+ editType: 'calc',
+ description: [
+ 'Sets the gap (in plot fraction) between bars of',
+ 'the same location coordinate.'
+ ].join(' ')
+ }
+};
diff --git a/src/traces/waterfall/layout_defaults.js b/src/traces/waterfall/layout_defaults.js
new file mode 100644
index 00000000000..0179d7317a9
--- /dev/null
+++ b/src/traces/waterfall/layout_defaults.js
@@ -0,0 +1,35 @@
+/**
+* Copyright 2012-2019, 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 Lib = require('../../lib');
+var layoutAttributes = require('./layout_attributes');
+
+module.exports = function(layoutIn, layoutOut, fullData) {
+ var hasTraceType = false;
+
+ function coerce(attr, dflt) {
+ return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
+ }
+
+ for(var i = 0; i < fullData.length; i++) {
+ var trace = fullData[i];
+
+ if(trace.visible && trace.type === 'waterfall') {
+ hasTraceType = true;
+ break;
+ }
+ }
+
+ if(hasTraceType) {
+ coerce('waterfallmode');
+ coerce('waterfallgap', 0.2);
+ coerce('waterfallgroupgap');
+ }
+};
diff --git a/src/traces/waterfall/plot.js b/src/traces/waterfall/plot.js
new file mode 100644
index 00000000000..a285f0456df
--- /dev/null
+++ b/src/traces/waterfall/plot.js
@@ -0,0 +1,131 @@
+/**
+* Copyright 2012-2019, 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 d3 = require('d3');
+var Lib = require('../../lib');
+var Drawing = require('../../components/drawing');
+var barPlot = require('../bar/plot');
+
+module.exports = function plot(gd, plotinfo, cdModule, traceLayer) {
+ barPlot(gd, plotinfo, cdModule, traceLayer);
+ plotConnectors(gd, plotinfo, cdModule, traceLayer);
+};
+
+function plotConnectors(gd, plotinfo, cdModule, traceLayer) {
+ var xa = plotinfo.xaxis;
+ var ya = plotinfo.yaxis;
+
+ Lib.makeTraceGroups(traceLayer, cdModule, 'trace bars').each(function(cd) {
+ var plotGroup = d3.select(this);
+ var cd0 = cd[0];
+ var trace = cd0.trace;
+
+ var group = Lib.ensureSingle(plotGroup, 'g', 'lines');
+
+ if(!trace.connector || !trace.connector.visible) {
+ group.remove();
+ return;
+ }
+
+ var isHorizontal = (trace.orientation === 'h');
+ var mode = trace.connector.mode;
+
+ if(!plotinfo.isRangePlot) cd0.node3 = plotGroup;
+
+ var connectors = group.selectAll('g.line').data(Lib.identity);
+
+ connectors.enter().append('g')
+ .classed('line', true);
+
+ connectors.exit().remove();
+
+ var len = connectors.size();
+
+ connectors.each(function(di, i) {
+ // don't draw lines between nulls
+ if(i !== len - 1 && !di.cNext) return;
+
+ var connector = d3.select(this);
+ var shape = '';
+
+ var x0, y0;
+ var x1, y1;
+ var x2, y2;
+ var x3, y3;
+
+ var delta = 0;
+ if(i + 1 < len && Array.isArray(trace.offset)) {
+ delta -= trace.offset[i + 1] - trace.offset[i];
+ }
+
+ if(isHorizontal) {
+ x0 = xa.c2p(di.s1, true);
+ y0 = ya.c2p(di.p1, true);
+
+ x1 = xa.c2p(di.s0, true);
+ y1 = ya.c2p(di.p0, true);
+
+ x2 = xa.c2p(di.s1, true);
+ y2 = ya.c2p(di.p1, true);
+
+ if(i + 1 < len) {
+ x3 = xa.c2p(di.s0 + 1 - delta, true);
+ y3 = ya.c2p(di.p0 + 1 - delta, true);
+ }
+ } else {
+ x0 = xa.c2p(di.p1, true);
+ y0 = ya.c2p(di.s1, true);
+
+ x1 = xa.c2p(di.p0, true);
+ y1 = ya.c2p(di.s0, true);
+
+ x2 = xa.c2p(di.p1, true);
+ y2 = ya.c2p(di.s1, true);
+
+ if(i + 1 < len) {
+ x3 = xa.c2p(di.p0 + 1 - delta, true);
+ y3 = ya.c2p(di.s0 + 1 - delta, true);
+ }
+ }
+
+ if(mode === 'spanning') {
+ if(!di.isSum && i > 0) {
+ if(isHorizontal) {
+ shape += 'M' + x1 + ',' + y0 + 'V' + y1;
+ } else {
+ shape += 'M' + x0 + ',' + y1 + 'H' + x1;
+ }
+ }
+ }
+
+ if(mode !== 'between') {
+ if(di.isSum || i < len - 1) {
+ if(isHorizontal) {
+ shape += 'M' + x2 + ',' + y1 + 'V' + y2;
+ } else {
+ shape += 'M' + x1 + ',' + y2 + 'H' + x2;
+ }
+ }
+ }
+
+ if(x3 !== undefined && y3 !== undefined) {
+ if(isHorizontal) {
+ shape += 'M' + x2 + ',' + y2 + 'V' + y3;
+ } else {
+ shape += 'M' + x2 + ',' + y2 + 'H' + x3;
+ }
+ }
+
+ Lib.ensureSingle(connector, 'path')
+ .attr('d', shape)
+ .call(Drawing.setClipUrl, plotinfo.layerClipId, gd);
+ });
+ });
+}
diff --git a/src/traces/waterfall/style.js b/src/traces/waterfall/style.js
new file mode 100644
index 00000000000..12730615d9c
--- /dev/null
+++ b/src/traces/waterfall/style.js
@@ -0,0 +1,54 @@
+/**
+* Copyright 2012-2019, 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 d3 = require('d3');
+
+var Drawing = require('../../components/drawing');
+var Color = require('../../components/color');
+
+var styleTextPoints = require('../bar/style').styleTextPoints;
+
+function style(gd, cd) {
+ var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.waterfalllayer').selectAll('g.trace');
+
+ s.style('opacity', function(d) { return d[0].trace.opacity; });
+
+ s.each(function(d) {
+ var gTrace = d3.select(this);
+ var trace = d[0].trace;
+
+ gTrace.selectAll('.point > path').each(function(di) {
+ var cont = trace[di.dir].marker;
+
+ d3.select(this)
+ .call(Color.fill, cont.color)
+ .call(Color.stroke, cont.line.color)
+ .call(Drawing.dashLine, cont.line.dash, cont.line.width)
+ .style('opacity', trace.selectedpoints && !di.selected ? 0.3 : 1);
+ });
+
+ styleTextPoints(gTrace, trace, gd);
+
+ gTrace.selectAll('.lines').each(function() {
+ var sel = d3.select(this);
+ var connectorLine = trace.connector.line;
+
+ Drawing.lineGroupStyle(sel.selectAll('path'),
+ connectorLine.width,
+ connectorLine.color,
+ connectorLine.dash
+ );
+ });
+ });
+}
+
+module.exports = {
+ style: style
+};
diff --git a/test/image/baselines/waterfall-grouping-vs-defaults.png b/test/image/baselines/waterfall-grouping-vs-defaults.png
new file mode 100644
index 00000000000..7c023291515
Binary files /dev/null and b/test/image/baselines/waterfall-grouping-vs-defaults.png differ
diff --git a/test/image/baselines/waterfall-offsetgroups.png b/test/image/baselines/waterfall-offsetgroups.png
new file mode 100644
index 00000000000..63dd5bf28ee
Binary files /dev/null and b/test/image/baselines/waterfall-offsetgroups.png differ
diff --git a/test/image/baselines/waterfall_11.png b/test/image/baselines/waterfall_11.png
new file mode 100644
index 00000000000..e0c5465c7a3
Binary files /dev/null and b/test/image/baselines/waterfall_11.png differ
diff --git a/test/image/baselines/waterfall_and_bar.png b/test/image/baselines/waterfall_and_bar.png
new file mode 100644
index 00000000000..e1dda68661e
Binary files /dev/null and b/test/image/baselines/waterfall_and_bar.png differ
diff --git a/test/image/baselines/waterfall_and_histogram.png b/test/image/baselines/waterfall_and_histogram.png
new file mode 100644
index 00000000000..c0383c04e3c
Binary files /dev/null and b/test/image/baselines/waterfall_and_histogram.png differ
diff --git a/test/image/baselines/waterfall_attrs.png b/test/image/baselines/waterfall_attrs.png
new file mode 100644
index 00000000000..231255df571
Binary files /dev/null and b/test/image/baselines/waterfall_attrs.png differ
diff --git a/test/image/baselines/waterfall_cliponaxis-false.png b/test/image/baselines/waterfall_cliponaxis-false.png
new file mode 100644
index 00000000000..9b984b005b9
Binary files /dev/null and b/test/image/baselines/waterfall_cliponaxis-false.png differ
diff --git a/test/image/baselines/waterfall_custom.png b/test/image/baselines/waterfall_custom.png
new file mode 100644
index 00000000000..af16fd97824
Binary files /dev/null and b/test/image/baselines/waterfall_custom.png differ
diff --git a/test/image/baselines/waterfall_gap0.png b/test/image/baselines/waterfall_gap0.png
new file mode 100644
index 00000000000..7ea902fff82
Binary files /dev/null and b/test/image/baselines/waterfall_gap0.png differ
diff --git a/test/image/baselines/waterfall_line.png b/test/image/baselines/waterfall_line.png
new file mode 100644
index 00000000000..7e32aaad93a
Binary files /dev/null and b/test/image/baselines/waterfall_line.png differ
diff --git a/test/image/baselines/waterfall_months.png b/test/image/baselines/waterfall_months.png
new file mode 100644
index 00000000000..47a7907c292
Binary files /dev/null and b/test/image/baselines/waterfall_months.png differ
diff --git a/test/image/baselines/waterfall_multicategory.png b/test/image/baselines/waterfall_multicategory.png
new file mode 100644
index 00000000000..1fc8d5b86de
Binary files /dev/null and b/test/image/baselines/waterfall_multicategory.png differ
diff --git a/test/image/baselines/waterfall_nonnumeric_sizes.png b/test/image/baselines/waterfall_nonnumeric_sizes.png
new file mode 100644
index 00000000000..2ddd259f402
Binary files /dev/null and b/test/image/baselines/waterfall_nonnumeric_sizes.png differ
diff --git a/test/image/baselines/waterfall_profit-loss_2018_positive-negative.png b/test/image/baselines/waterfall_profit-loss_2018_positive-negative.png
new file mode 100644
index 00000000000..e913e3f156a
Binary files /dev/null and b/test/image/baselines/waterfall_profit-loss_2018_positive-negative.png differ
diff --git a/test/image/baselines/waterfall_profit-loss_2018vs2019_overlay.png b/test/image/baselines/waterfall_profit-loss_2018vs2019_overlay.png
new file mode 100644
index 00000000000..b2155e45d7b
Binary files /dev/null and b/test/image/baselines/waterfall_profit-loss_2018vs2019_overlay.png differ
diff --git a/test/image/baselines/waterfall_profit-loss_2018vs2019_rectangle.png b/test/image/baselines/waterfall_profit-loss_2018vs2019_rectangle.png
new file mode 100644
index 00000000000..7aae25906eb
Binary files /dev/null and b/test/image/baselines/waterfall_profit-loss_2018vs2019_rectangle.png differ
diff --git a/test/image/mocks/waterfall-grouping-vs-defaults.json b/test/image/mocks/waterfall-grouping-vs-defaults.json
new file mode 100644
index 00000000000..41c9d4242a5
--- /dev/null
+++ b/test/image/mocks/waterfall-grouping-vs-defaults.json
@@ -0,0 +1,61 @@
+{
+ "data": [
+ {
+ "type": "waterfall",
+ "y": [ 1, -2, 1 ],
+ "yaxis": "y2"
+ },
+ {
+ "type": "waterfall",
+ "y": [ 2, -1, 2 ]
+ },
+ {
+ "type": "waterfall",
+ "y": [ 1, -3, 0 ]
+ },
+ {
+ "type": "waterfall",
+ "y": [ 1, -2, 1 ],
+ "alignmentgroup": "top",
+ "hovertext": "alignmentgroup: top",
+ "xaxis": "x2",
+ "yaxis": "y2"
+ },
+ {
+ "type": "waterfall",
+ "y": [ 2, -1, 2 ],
+ "hovertext": "alignmentgroup: top
offsetgroup: 1",
+ "alignmentgroup": "bottom",
+ "offsetgroup": "1",
+ "xaxis": "x2"
+ },
+ {
+ "type": "waterfall",
+ "y": [ 1, -3, 0 ],
+ "hovertext": "alignmentgroup: top
offsetgroup: 2",
+ "alignmentgroup": "bottom",
+ "offsetgroup": "2",
+ "xaxis": "x2"
+ }
+ ],
+ "layout": {
+ "showlegend": false,
+ "grid": {
+ "rows": 2,
+ "columns": 2,
+ "roworder": "bottom to top"
+ },
+ "colorway": [ "blue", "orange", "green" ],
+ "margin": { "t": 20 },
+ "xaxis": {
+ "title": {
+ "text": "no alignmentgroup
no offsetgroup"
+ }
+ },
+ "xaxis2": {
+ "title": {
+ "text": "with alignmentgroup
with offsetgroup"
+ }
+ }
+ }
+}
diff --git a/test/image/mocks/waterfall-offsetgroups.json b/test/image/mocks/waterfall-offsetgroups.json
new file mode 100644
index 00000000000..66eb8e149ad
--- /dev/null
+++ b/test/image/mocks/waterfall-offsetgroups.json
@@ -0,0 +1,88 @@
+{
+ "data": [
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 1, -2, 3, 4 ],
+ "offsetgroup": 1,
+ "hovertext": "offsetgroup: 1"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 2, -3, 1, 5 ],
+ "offsetgroup": 2,
+ "hovertext": "offsetgroup: 2"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 1, -2, 3, 4 ],
+ "yaxis": "y2",
+ "offsetgroup": 1,
+ "hovertext": "offsetgroup: 1"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 2, -3, 1, 5 ],
+ "yaxis": "y2",
+ "offsetgroup": 2,
+ "hovertext": "offsetgroup: 2"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 1, -2, 3, 4 ],
+ "offsetgroup": 1,
+ "hovertext": "offsetgroup: 1",
+ "xaxis": "x2"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 2, -3, 1, 5 ],
+ "offsetgroup": 2,
+ "hovertext": "offsetgroup: 2",
+ "xaxis": "x2"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 1, -2, 3, 4 ],
+ "yaxis": "y2",
+ "offsetgroup": 3,
+ "hovertext": "offsetgroup: 3",
+ "xaxis": "x2"
+ },
+ {
+ "type": "waterfall",
+ "x": [ "A", "B", "C", "D" ],
+ "y": [ 2, -3, 1, 5 ],
+ "yaxis": "y2",
+ "offsetgroup": 4,
+ "hovertext": "offsetgroup: 4",
+ "xaxis": "x2"
+ }
+ ],
+ "layout": {
+ "showlegend": false,
+ "grid": {
+ "rows": 2,
+ "columns": 2
+ },
+ "title": {
+ "text": "Bar offset groups"
+ },
+ "xaxis": {
+ "title": {
+ "text": "two distinct offset groups"
+ }
+ },
+ "xaxis2": {
+ "title": {
+ "text": "four distinct offset groups"
+ }
+ }
+ }
+}
diff --git a/test/image/mocks/waterfall_11.json b/test/image/mocks/waterfall_11.json
new file mode 100644
index 00000000000..4b1137c77ec
--- /dev/null
+++ b/test/image/mocks/waterfall_11.json
@@ -0,0 +1,243 @@
+{
+ "data": [
+ {
+ "x": [
+ "Half Dose",
+ "Full Dose",
+ "Double Dose"
+ ],
+ "y": [
+ "13.23",
+ "-22.7",
+ "26.06"
+ ],
+ "name": "Orange Juice",
+ "type": "waterfall"
+ },
+ {
+ "x": [
+ "Half Dose",
+ "Full Dose",
+ "Double Dose"
+ ],
+ "y": [
+ "7.98",
+ "-16.77",
+ "26.14"
+ ],
+ "name": "Vitamin C",
+ "type": "waterfall"
+ },
+ {
+ "x": [
+ "Half Dose",
+ "Full Dose",
+ "Double Dose"
+ ],
+ "y": [
+ "1.4102837",
+ "-1.236752",
+ "0.8396031"
+ ],
+ "name": "Std Dev - OJ",
+ "visible": false,
+ "type": "waterfall"
+ },
+ {
+ "x": [
+ "Half Dose",
+ "Full Dose",
+ "Double Dose"
+ ],
+ "y": [
+ "0.868562",
+ "-0.7954104",
+ "1.5171757"
+ ],
+ "name": "Std Dev - VC",
+ "visible": false,
+ "type": "waterfall"
+ }
+ ],
+ "layout": {
+ "title": "Grouped Waterfall Chart",
+ "titlefont": {
+ "color": "",
+ "family": "",
+ "size": 16
+ },
+ "font": {
+ "family": "Arial, sans-serif",
+ "size": 12,
+ "color": "#000"
+ },
+ "showlegend": true,
+ "autosize": false,
+ "width": 600,
+ "height": 440,
+ "xaxis": {
+ "title": "Dose (mg)",
+ "titlefont": {
+ "color": "",
+ "family": "",
+ "size": 16
+ },
+ "range": [
+ -0.5,
+ 2.5
+ ],
+ "domain": [
+ 0,
+ 1
+ ],
+ "type": "category",
+ "rangemode": "normal",
+ "showgrid": true,
+ "zeroline": false,
+ "showline": false,
+ "autotick": true,
+ "nticks": 0,
+ "ticks": "",
+ "showticklabels": true,
+ "tick0": 0,
+ "dtick": 1,
+ "ticklen": 5,
+ "tickwidth": 1,
+ "tickcolor": "#000",
+ "tickangle": 0,
+ "tickfont": {
+ "family": "",
+ "size": 16,
+ "color": ""
+ },
+ "exponentformat": "e",
+ "showexponent": "all",
+ "gridcolor": "rgb(255, 255, 255)",
+ "gridwidth": 1.9,
+ "zerolinecolor": "#000",
+ "zerolinewidth": 1,
+ "linecolor": "#000",
+ "linewidth": 0.1,
+ "anchor": "y",
+ "position": 0,
+ "mirror": true,
+ "overlaying": false,
+ "autorange": true
+ },
+ "yaxis": {
+ "title": "Length",
+ "titlefont": {
+ "color": "",
+ "family": "",
+ "size": 16
+ },
+ "range": [
+ 0,
+ 29.11281652631579
+ ],
+ "domain": [
+ 0,
+ 1
+ ],
+ "type": "linear",
+ "rangemode": "normal",
+ "showgrid": true,
+ "zeroline": false,
+ "showline": false,
+ "autotick": true,
+ "nticks": 0,
+ "ticks": "",
+ "showticklabels": true,
+ "tick0": 0,
+ "dtick": 5,
+ "ticklen": 5,
+ "tickwidth": 1,
+ "tickcolor": "#000",
+ "tickangle": 0,
+ "tickfont": {
+ "family": "",
+ "size": 16,
+ "color": ""
+ },
+ "exponentformat": "e",
+ "showexponent": "all",
+ "gridcolor": "rgb(255, 255, 255)",
+ "gridwidth": 1.9,
+ "zerolinecolor": "#000",
+ "zerolinewidth": 1,
+ "linecolor": "#000",
+ "linewidth": 0.1,
+ "anchor": "x",
+ "position": 0,
+ "mirror": true,
+ "overlaying": false,
+ "autorange": true
+ },
+ "legend": {
+ "x": 1.02,
+ "y": 0.9319072504424065,
+ "traceorder": "normal",
+ "font": {
+ "family": "",
+ "size": 16,
+ "color": "rgb(0, 0, 0)"
+ },
+ "bgcolor": "rgba(255, 255, 255, 0)",
+ "bordercolor": "rgba(0, 0, 0, 0)",
+ "borderwidth": 1,
+ "xanchor": "left",
+ "yanchor": "auto"
+ },
+ "annotations": [
+ {
+ "x": 1.3479735318444994,
+ "y": 0.9982142857142856,
+ "xref": "paper",
+ "yref": "paper",
+ "text": "Supplement",
+ "font": {
+ "family": "",
+ "size": 18,
+ "color": ""
+ },
+ "align": "center",
+ "showarrow": false,
+ "arrowhead": 1,
+ "arrowsize": 1,
+ "arrowwidth": 0,
+ "arrowcolor": "",
+ "ax": -10,
+ "ay": -26.7109375,
+ "bordercolor": "",
+ "borderwidth": 1,
+ "borderpad": 1,
+ "bgcolor": "rgba(0,0,0,0)",
+ "opacity": 1,
+ "xanchor": "auto",
+ "yanchor": "auto",
+ "xatype": "category",
+ "yatype": "linear",
+ "tag": "",
+ "ref": "paper"
+ }
+ ],
+ "margin": {
+ "l": 80,
+ "r": 0,
+ "b": 80,
+ "t": 80,
+ "pad": 2,
+ "autoexpand": true
+ },
+ "paper_bgcolor": "#fff",
+ "plot_bgcolor": "rgb(217, 217, 217)",
+ "hovermode": "x",
+ "dragmode": "zoom",
+ "waterfallmode": "group",
+ "waterfallgap": 0.2,
+ "waterfallgroupgap": 0.1,
+ "boxmode": "overlay",
+ "separators": ".,",
+ "hidesources": false
+ }
+}
diff --git a/test/image/mocks/waterfall_and_bar.json b/test/image/mocks/waterfall_and_bar.json
new file mode 100644
index 00000000000..012d5e6f8de
--- /dev/null
+++ b/test/image/mocks/waterfall_and_bar.json
@@ -0,0 +1,124 @@
+{
+ "data": [
+ {
+ "name": "bar",
+ "type": "bar",
+ "orientation": "v",
+ "opacity": 0.5,
+ "x": [
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "y": [
+ 375,
+ 128,
+ 78,
+ 27,
+ null,
+ -327,
+ -12,
+ -78,
+ -12,
+ null,
+ 32,
+ 89,
+ null,
+ -45,
+ null
+ ]
+ }, {
+ "name": "waterfall",
+ "type": "waterfall",
+ "orientation": "v",
+ "opacity": 0.5,
+ "measure": [
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "total"
+ ],
+ "x": [
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "y": [
+ 375,
+ 128,
+ 78,
+ 27,
+ null,
+ -327,
+ -12,
+ -78,
+ -12,
+ null,
+ 32,
+ 89,
+ null,
+ -45,
+ null
+ ],
+ "connector": {
+ "mode": "spanning",
+ "line": {
+ "width": 2,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ }
+ }
+ ],
+ "layout": {
+ "title": {
+ "text": "waterfall chart with bar chart"
+ },
+ "xaxis": {
+ "type": "category"
+ },
+ "yaxis": {
+ "type": "linear"
+ },
+ "height": 600,
+ "width": 600,
+ "showlegend": true,
+ "bargap": 0,
+ "waterfallgap": 0.4
+ }
+}
diff --git a/test/image/mocks/waterfall_and_histogram.json b/test/image/mocks/waterfall_and_histogram.json
new file mode 100644
index 00000000000..0d30bd458f7
--- /dev/null
+++ b/test/image/mocks/waterfall_and_histogram.json
@@ -0,0 +1,15 @@
+{
+ "data": [
+ { "x": [1,1,1,1,1,2,2,2,3,3], "type": "histogram" },
+ { "x": [1,2,3], "y":[1,-2,3], "type": "waterfall", "connector": {"mode": "spanning"} },
+ { "x": [1,2,3], "y":[1,-2,3], "type": "waterfall", "yaxis":"y2", "connector": {"mode": "spanning"}},
+ { "x": [1,1,1,1,1,2,2,2,3,3], "type": "histogram", "yaxis":"y2" }
+ ],
+ "layout": {
+ "yaxis": { "domain": [0,0.49] },
+ "yaxis2": { "domain": [0.51, 1] },
+ "showlegend": false,
+ "bargap": 0.4,
+ "waterfallgap": 0
+ }
+}
diff --git a/test/image/mocks/waterfall_attrs.json b/test/image/mocks/waterfall_attrs.json
new file mode 100644
index 00000000000..956e0e39dbe
--- /dev/null
+++ b/test/image/mocks/waterfall_attrs.json
@@ -0,0 +1,58 @@
+{
+ "data": [
+ {
+ "width": [1, 0.8, 0.6, 0.4],
+ "text": [1, -2, 3333333333, 4],
+ "textposition": "outside",
+ "y": [1, -2, 3, 4],
+ "x": [1, 2, 3, 4],
+ "decreasing": { "marker": { "color": "Blue" } },
+ "increasing": { "marker": { "color": "Blue" } },
+ "totals": { "marker": { "color": "Blue" } },
+ "opacity": 0.5,
+ "connector": { "visible": false },
+ "type": "waterfall"
+ }, {
+ "width": [0.4, 0.6, 0.8, 1],
+ "text": ["Three", 2, "inside text", 0],
+ "textposition": "auto",
+ "textfont": { "size": [10]},
+ "y": [3, -2, 1, -1],
+ "x": [1, 2, 3, 4],
+ "decreasing": { "marker": { "color": "Orange" } },
+ "increasing": { "marker": { "color": "Orange" } },
+ "totals": { "marker": { "color": "Orange" } },
+ "opacity": 0.5,
+ "connector": { "visible": false },
+ "type": "waterfall"
+ }, {
+ "width": 1,
+ "text": [-1, 3, -2, -4],
+ "textposition": "inside",
+ "y": [-1, 3, -2, -4],
+ "x": [1, 2, 3, 4],
+ "decreasing": { "marker": { "color": "Green" } },
+ "increasing": { "marker": { "color": "Green" } },
+ "totals": { "marker": { "color": "Green" } },
+ "opacity": 0.5,
+ "connector": { "visible": false },
+ "type": "waterfall"
+ }, {
+ "text": [2, "outside text", -3, -2],
+ "textposition": "auto",
+ "y": [2, 0.25, -3, -2],
+ "x": [1, 2, 3, 4],
+ "decreasing": { "marker": { "color": "Red" } },
+ "increasing": { "marker": { "color": "Red" } },
+ "totals": { "marker": { "color": "Red" } },
+ "opacity": 0.5,
+ "connector": { "visible": false },
+ "type": "waterfall"
+ }
+ ],
+ "layout": {
+ "xaxis": { "showgrid": true },
+ "height": 800,
+ "width": 800
+ }
+}
diff --git a/test/image/mocks/waterfall_cliponaxis-false.json b/test/image/mocks/waterfall_cliponaxis-false.json
new file mode 100644
index 00000000000..1df4d99d49e
--- /dev/null
+++ b/test/image/mocks/waterfall_cliponaxis-false.json
@@ -0,0 +1,116 @@
+{
+ "data": [
+ {
+ "type": "waterfall",
+ "name": "not clipped",
+ "x": ["apple", "banana", "clementine"],
+ "y": [1.8, 0.2, 0.1],
+ "text": ["apple", "banana", "x"],
+ "textposition": "outside",
+ "cliponaxis": false,
+ "textfont": {
+ "size": [60, 40, 20]
+ },
+ "increasing": { "marker": {"color": "blue"} },
+ "decreasing": { "marker": {"color": "blue"} }
+ },
+ {
+ "type": "waterfall",
+ "name": "same but clipped",
+ "x": ["apple", "banana", "clementine"],
+ "y": [1.8, 0.2, 0.1],
+ "text": ["apple", "banana", "clementine"],
+ "textposition": "outside",
+ "cliponaxis": true,
+ "textfont": {
+ "size": [60, 40, 20]
+ },
+ "xaxis": "x2",
+ "yaxis": "y2",
+ "increasing": { "marker": {"color": "orange"} },
+ "decreasing": { "marker": {"color": "orange"} }
+ },
+ {
+ "type": "waterfall",
+ "name": "should not see text",
+ "x": ["banana"],
+ "y": [2],
+ "text": ["X"],
+ "textposition": "outside",
+ "cliponaxis": true,
+ "textfont": {"size": [20]},
+ "increasing": { "marker": {"color": "green"} },
+ "decreasing": { "marker": {"color": "green"} }
+ },
+ {
+ "type": "waterfall",
+ "name": "should see text",
+ "x": ["banana"],
+ "y": [1],
+ "text": ["banana"],
+ "textposition": "outside",
+ "cliponaxis": false,
+ "textfont": {"size": [20]},
+ "xaxis": "x3",
+ "yaxis": "y3",
+ "increasing": { "marker": {"color": "red"} },
+ "decreasing": { "marker": {"color": "red"} }
+ }
+ ],
+ "layout": {
+ "waterfallmode": "overlay",
+ "legend": {
+ "x": 0.5,
+ "xanchor": "center",
+ "y": -0.05,
+ "yanchor": "top"
+ },
+ "xaxis": {
+ "showline": true,
+ "showticklabels": false,
+ "mirror": true,
+ "layer": "below traces",
+ "domain": [0, 0.38]
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "showline": true,
+ "showticklabels": false,
+ "mirror": true,
+ "layer": "below traces",
+ "domain": [0.42, 0.80]
+ },
+ "xaxis3": {
+ "anchor": "y3",
+ "showline": true,
+ "showticklabels": false,
+ "mirror": true,
+ "layer": "below traces",
+ "domain": [0.84, 1]
+ },
+ "yaxis": {
+ "showline": true,
+ "mirror": true,
+ "layer": "below traces",
+ "range": [0, 2]
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "showline": true,
+ "mirror": true,
+ "layer": "below traces",
+ "range": [0, 2]
+ },
+ "yaxis3": {
+ "anchor": "x3",
+ "showline": true,
+ "mirror": true,
+ "layer": "below traces",
+ "range": [2, 0]
+ },
+ "width": 800,
+ "height": 400,
+ "margin": {"t": 40},
+ "dragmode": "pan"
+ }
+}
diff --git a/test/image/mocks/waterfall_custom.json b/test/image/mocks/waterfall_custom.json
new file mode 100644
index 00000000000..d757a79bced
--- /dev/null
+++ b/test/image/mocks/waterfall_custom.json
@@ -0,0 +1,59 @@
+{
+ "data": [
+ {
+ "name": "2018",
+ "type": "waterfall",
+ "orientation": "v",
+ "measure": [
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total"
+ ],
+ "width": [1.0, 0.6, 0.5, 0.7, 0.9, 0.8],
+ "offset": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5],
+ "x": [
+ "Sales",
+ "Consulting",
+ "Net revenue",
+ "Purchases",
+ "Other expenses",
+ "Cash"
+ ],
+ "y": [
+ 60,
+ 80,
+ 0,
+ -40,
+ -20,
+ 0
+ ],
+ "base": 1000,
+ "connector": {
+ "mode": "between",
+ "line": {
+ "width": 5,
+ "color": "rgb(63, 63, 63)"
+ }
+ }
+ }
+ ],
+ "layout": {
+ "title": {
+ "text": "Waterfall chart with custom bar widths and offsets"
+ },
+ "xaxis": {
+ "type": "category"
+ },
+ "yaxis": {
+ "type": "linear"
+ },
+ "margin": { "l": 150 },
+ "height": 800,
+ "width": 800,
+ "autosize": true,
+ "showlegend": true
+ }
+}
diff --git a/test/image/mocks/waterfall_gap0.json b/test/image/mocks/waterfall_gap0.json
new file mode 100644
index 00000000000..fbd415994e2
--- /dev/null
+++ b/test/image/mocks/waterfall_gap0.json
@@ -0,0 +1,40 @@
+{
+ "data": [
+ {
+ "x": [
+ "giraffes",
+ "orangutans",
+ "monkeys"
+ ],
+ "y": [
+ 20,
+ 14,
+ 23
+ ],
+ "name": "SF Zoo",
+ "type": "waterfall"
+ }
+ ],
+ "layout": {
+ "waterfallgap": 0,
+ "xaxis": {
+ "type": "category",
+ "range": [
+ -0.5,
+ 2.5
+ ],
+ "autorange": true
+ },
+ "yaxis": {
+ "type": "linear",
+ "range": [
+ 0,
+ 24.210526315789473
+ ],
+ "autorange": true
+ },
+ "height": 450,
+ "width": 1100,
+ "autosize": false
+ }
+}
diff --git a/test/image/mocks/waterfall_line.json b/test/image/mocks/waterfall_line.json
new file mode 100644
index 00000000000..b5b5d1f42ef
--- /dev/null
+++ b/test/image/mocks/waterfall_line.json
@@ -0,0 +1,42 @@
+{
+ "data": [
+ {
+ "x": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "y": [
+ 1.5,
+ 1,
+ 1.3,
+ 0.7,
+ 0.8,
+ 0.9
+ ],
+ "type": "scatter"
+ },
+ {
+ "x": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "y": [
+ 1,
+ 0.5,
+ 0.7,
+ -1.2,
+ 0.3,
+ 0.4
+ ],
+ "type": "waterfall"
+ }
+ ]
+}
diff --git a/test/image/mocks/waterfall_months.json b/test/image/mocks/waterfall_months.json
new file mode 100644
index 00000000000..c5f3e856486
--- /dev/null
+++ b/test/image/mocks/waterfall_months.json
@@ -0,0 +1,73 @@
+{
+ "data":
+ [{
+ "type": "waterfall",
+ "connector": {
+ "mode": "between",
+ "line": {
+ "width": 2,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ },
+ "decreasing": { "marker": { "color": "rgb(255, 0, 0)" } },
+ "increasing": { "marker": { "color": "rgb(0, 255, 0)" } },
+ "totals": { "marker": { "color": "rgb(0, 0, 63)" } },
+ "measure": ["absolute", "relative", "relative", "relative", "total", "relative", "relative", "relative", "total"],
+ "x": ["Initial", "Jan-Feb", "Mar-Apr", "May-Jun", "Mid-year", "Jul-Aug", "Sep-Oct", "Nov-Dec", "Full year"],
+ "y": [25, -1.1, -2.1, -3.1, null, -4.1, -5.1, -6.1, null]
+ }, {
+ "type": "waterfall",
+ "connector": {
+ "mode": "between",
+ "line": {
+ "width": 2,
+ "color": "rgb(63, 63, 63)",
+ "dash": 2
+ }
+ },
+ "decreasing": { "marker": { "color": "rgb(255, 63, 63)" } },
+ "increasing": { "marker": { "color": "rgb(63, 255, 63)" } },
+ "totals": { "marker": { "color": "rgb(0, 0, 127)" } },
+ "measure": ["absolute", "relative", "relative", "relative", "total", "relative", "relative", "relative", "total"],
+ "x": ["Initial", "Jan-Feb", "Mar-Apr", "May-Jun", "Mid-year", "Jul-Aug", "Sep-Oct", "Nov-Dec", "Full year"],
+ "y": [4, 1.2, 2.2, 3.2, null, 4.2, 5.2, 6.2, null]
+ }, {
+ "type": "waterfall",
+ "connector": {
+ "mode": "between",
+ "line": {
+ "width": 4,
+ "color": "rgb(127, 127, 127)",
+ "dash": 4
+ }
+ },
+ "decreasing": { "marker": { "color": "rgb(255, 127, 127)" } },
+ "increasing": { "marker": { "color": "rgb(127, 255, 127)" } },
+ "totals": { "marker": { "color": "rgb(0, 0, 191)" } },
+ "measure": ["absolute", "relative", "relative", "relative", "total", "relative", "relative", "relative", "total"],
+ "x": ["Initial", "Jan-Feb", "Mar-Apr", "May-Jun", "Mid-year", "Jul-Aug", "Sep-Oct", "Nov-Dec", "Full year"],
+ "y": [4, 6.3, 5.3, 4.3, null, 3.3, 2.3, 1.3, null]
+ }, {
+ "type": "waterfall",
+ "connector": {
+ "mode": "between",
+ "line": {
+ "width": 4,
+ "color": "rgb(191, 191, 191)",
+ "dash": 6
+ }
+ },
+ "decreasing": { "marker": { "color": "rgb(255, 191, 191)" } },
+ "increasing": { "marker": { "color": "rgb(191, 255, 191)" } },
+ "totals": { "marker": { "color": "rgb(0, 0, 255)" } },
+ "measure": ["absolute", "relative", "relative", "relative", "total", "relative", "relative", "relative", "total"],
+ "x": ["Initial", "Jan-Feb", "Mar-Apr", "May-Jun", "Mid-year", "Jul-Aug", "Sep-Oct", "Nov-Dec", "Full year"],
+ "y": [25, -6.4, -5.4, -4.4, null, -3.4, -2.4, -1.4, null]
+ }],
+ "layout": {
+ "width": 900,
+ "height": 450,
+ "showlegend": true
+ }
+}
diff --git a/test/image/mocks/waterfall_multicategory.json b/test/image/mocks/waterfall_multicategory.json
new file mode 100644
index 00000000000..675625d766c
--- /dev/null
+++ b/test/image/mocks/waterfall_multicategory.json
@@ -0,0 +1,31 @@
+{
+ "data": [
+ {
+ "type": "waterfall",
+ "x": [
+ ["2016", "2017", "2017", "2017", "2017", "2018", "2018", "2018", "2018"],
+ ["initial", "q1", "q2", "q3", "total", "q1", "q2", "q3", "total" ]
+ ],
+ "measure": ["absolute", "relative", "relative", "relative", "total", "relative", "relative", "relative", "total"],
+ "y": [1, 2, 3, -1, null, 1, 2, -4, null],
+ "base": 1000
+ },
+ {
+ "type": "waterfall",
+ "x": [
+ ["2016", "2017", "2017", "2017", "2017", "2018", "2018", "2018", "2018"],
+ ["initial", "q1", "q2", "q3", "total", "q1", "q2", "q3", "total" ]
+ ],
+ "measure": ["absolute", "relative", "relative", "relative", "total", "relative", "relative", "relative", "total"],
+ "y": [1.1, 2.2, 3.3, -1.1, null, 1.1, 2.2, -4.4, null],
+ "base": 1000
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "title": "MULTI-CATEGORY",
+ "tickfont": {"size": 16},
+ "ticks": "outside"
+ }
+ }
+}
diff --git a/test/image/mocks/waterfall_nonnumeric_sizes.json b/test/image/mocks/waterfall_nonnumeric_sizes.json
new file mode 100644
index 00000000000..2d3bd73d34a
--- /dev/null
+++ b/test/image/mocks/waterfall_nonnumeric_sizes.json
@@ -0,0 +1,23 @@
+{
+ "data": [
+ {
+ "x": [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f"
+ ],
+ "y": [
+ 1,
+ null,
+ "nonsense",
+ 14,
+ 0,
+ 1
+ ],
+ "type": "waterfall"
+ }
+ ]
+}
diff --git a/test/image/mocks/waterfall_profit-loss_2018_positive-negative.json b/test/image/mocks/waterfall_profit-loss_2018_positive-negative.json
new file mode 100644
index 00000000000..f85fb70a990
--- /dev/null
+++ b/test/image/mocks/waterfall_profit-loss_2018_positive-negative.json
@@ -0,0 +1,84 @@
+{
+ "data": [
+ {
+ "name": "2018",
+ "type": "waterfall",
+ "orientation": "h",
+ "measure": [
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "total"
+ ],
+ "y": [
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "x": [
+ 375,
+ 128,
+ 78,
+ 27,
+ null,
+ -327,
+ -12,
+ -78,
+ -12,
+ null,
+ 32,
+ 89,
+ null,
+ -45,
+ null
+ ],
+ "connector": {
+ "mode": "between",
+ "line": {
+ "width": 4,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ }
+ }
+ ],
+ "layout": {
+ "title": {
+ "text": "Profit and loss statement 2018
waterfall chart displaying positive and negative"
+ },
+ "yaxis": {
+ "type": "category",
+ "autorange": "reversed"
+ },
+ "xaxis": {
+ "type": "linear"
+ },
+ "margin": { "l": 150 },
+ "height": 800,
+ "width": 800,
+ "showlegend": true
+ }
+}
diff --git a/test/image/mocks/waterfall_profit-loss_2018vs2019_overlay.json b/test/image/mocks/waterfall_profit-loss_2018vs2019_overlay.json
new file mode 100644
index 00000000000..fff5e83282d
--- /dev/null
+++ b/test/image/mocks/waterfall_profit-loss_2018vs2019_overlay.json
@@ -0,0 +1,157 @@
+{
+ "data": [
+ {
+ "name": "2018",
+ "type": "waterfall",
+ "orientation": "h",
+ "measure": [
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "total"
+ ],
+ "y": [
+ "2018/2019",
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "x": [
+ 0,
+ 375,
+ 128,
+ 78,
+ 27,
+ null,
+ -327,
+ -12,
+ -78,
+ -12,
+ null,
+ 32,
+ 89,
+ null,
+ -45,
+ null
+ ],
+ "opacity": 0.5,
+ "connector": {
+ "mode": "spanning",
+ "line": {
+ "width": 2,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ }
+ },
+ {
+ "name": "2019",
+ "type": "waterfall",
+ "orientation": "h",
+ "measure": [
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "total"
+ ],
+ "y": [
+ "2018/2019",
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "x": [
+ 0,
+ 307,
+ 102,
+ 187,
+ 172,
+ null,
+ -302,
+ -121,
+ -187,
+ -121,
+ null,
+ 123,
+ 198,
+ null,
+ -53.7,
+ null
+ ],
+ "opacity": 0.5,
+ "connector": {
+ "mode": "spanning",
+ "line": {
+ "width": 2,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ }
+ }
+ ],
+ "layout": {
+ "waterfallmode": "overlay",
+ "title": {
+ "text": "Profit and loss statement 2018 vs 2019
overlaid waterfall chart"
+ },
+ "yaxis": {
+ "type": "category",
+ "autorange": "reversed"
+ },
+ "xaxis": {
+ "type": "linear"
+ },
+ "margin": { "l": 150 },
+ "height": 800,
+ "width": 800,
+ "showlegend": true
+ }
+}
diff --git a/test/image/mocks/waterfall_profit-loss_2018vs2019_rectangle.json b/test/image/mocks/waterfall_profit-loss_2018vs2019_rectangle.json
new file mode 100644
index 00000000000..a33d59b11f0
--- /dev/null
+++ b/test/image/mocks/waterfall_profit-loss_2018vs2019_rectangle.json
@@ -0,0 +1,155 @@
+{
+ "data": [
+ {
+ "name": "2018",
+ "type": "waterfall",
+ "orientation": "h",
+ "opacity": 0.5,
+ "measure": [
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "total"
+ ],
+ "y": [
+ "2018/2019",
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "x": [
+ 0,
+ 375,
+ 128,
+ 78,
+ 27,
+ null,
+ -327,
+ -12,
+ -78,
+ -12,
+ null,
+ 32,
+ 89,
+ null,
+ -45,
+ null
+ ],
+ "connector": {
+ "mode": "spanning",
+ "line": {
+ "width": 2,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ }
+ },
+ {
+ "name": "2019",
+ "type": "waterfall",
+ "orientation": "h",
+ "measure": [
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "relative",
+ "total",
+ "relative",
+ "total"
+ ],
+ "y": [
+ "2018/2019",
+ "Sales",
+ "Consulting",
+ "Maintenance",
+ "Other revenue",
+ "Net revenue",
+ "Purchases",
+ "Material expenses",
+ "Personnel expenses",
+ "Other expenses",
+ "Operating profit",
+ "Investment income",
+ "Financial income",
+ "Profit before tax",
+ "Income tax (15%)",
+ "Profit after tax"
+ ],
+ "x": [
+ 0,
+ 307,
+ 102,
+ 187,
+ 172,
+ null,
+ -302,
+ -121,
+ -187,
+ -121,
+ null,
+ 123,
+ 198,
+ null,
+ -53.7,
+ null
+ ],
+ "connector": {
+ "mode": "spanning",
+ "line": {
+ "width": 2,
+ "color": "rgb(0, 0, 0)",
+ "dash": 0
+ }
+ }
+ }
+ ],
+ "layout": {
+ "title": {
+ "text": "Profit and loss statement 2018 vs 2019
waterfall chart"
+ },
+ "yaxis": {
+ "type": "category",
+ "autorange": "reversed"
+ },
+ "xaxis": {
+ "type": "linear"
+ },
+ "margin": { "l": 150 },
+ "height": 800,
+ "width": 800,
+ "showlegend": true
+ }
+}
diff --git a/test/jasmine/assets/mock_lists.js b/test/jasmine/assets/mock_lists.js
index a52e0c7327b..01e4188f639 100644
--- a/test/jasmine/assets/mock_lists.js
+++ b/test/jasmine/assets/mock_lists.js
@@ -14,6 +14,7 @@ var svgMockList = [
['axes_enumerated_ticks', require('@mocks/axes_enumerated_ticks.json')],
['axes_visible-false', require('@mocks/axes_visible-false.json')],
['bar_and_histogram', require('@mocks/bar_and_histogram.json')],
+ ['waterfall', require('@mocks/waterfall_profit-loss_2018vs2019_rectangle.json')],
['basic_error_bar', require('@mocks/basic_error_bar.json')],
['binding', require('@mocks/binding.json')],
['cheater_smooth', require('@mocks/cheater_smooth.json')],
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 5f788d6d80a..4da03f92dda 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -2121,6 +2121,74 @@ describe('Test select box and lasso per trace:', function() {
.then(done);
}, LONG_TIMEOUT_INTERVAL);
+ it('@noCI @flaky should work for waterfall traces', function(done) {
+ var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']);
+ var assertSelectedPoints = makeAssertSelectedPoints();
+ var assertRanges = makeAssertRanges();
+ var assertLassoPoints = makeAssertLassoPoints();
+
+ var fig = Lib.extendDeep({}, require('@mocks/waterfall_profit-loss_2018_positive-negative'));
+ fig.layout.dragmode = 'lasso';
+ addInvisible(fig);
+
+ Plotly.plot(gd, fig)
+ .then(function() {
+ return _run(
+ [[400, 300], [200, 400], [400, 500], [600, 400], [500, 350]],
+ function() {
+ assertPoints([
+ [0, 281, 'Purchases'],
+ [0, 269, 'Material expenses'],
+ [0, 191, 'Personnel expenses'],
+ [0, 179, 'Other expenses']
+ ]);
+ assertSelectedPoints({
+ 0: [5, 6, 7, 8]
+ });
+ assertLassoPoints([
+ [289.8550724637681, 57.97101449275362, 289.8550724637681, 521.7391304347826, 405.7971014492753],
+ ['Net revenue', 'Personnel expenses', 'Operating profit', 'Personnel expenses', 'Material expenses']
+ ]);
+ },
+ null, LASSOEVENTS, 'waterfall lasso'
+ );
+ })
+ .then(function() {
+ return Plotly.relayout(gd, 'dragmode', 'select');
+ })
+ .then(function() {
+ // For some reason we need this to make the following tests pass
+ // on CI consistently. It appears that a double-click action
+ // is being confused with a mere click. See
+ // https://github.com/plotly/plotly.js/pull/2135#discussion_r148897529
+ // for more info.
+ return new Promise(function(resolve) {
+ setTimeout(resolve, 100);
+ });
+ })
+ .then(function() {
+ return _run(
+ [[300, 300], [400, 400]],
+ function() {
+ assertPoints([
+ [0, 281, 'Purchases', 269],
+ [0, 269, 'Material expenses', 269]
+ ]);
+ assertSelectedPoints({
+ 0: [5, 6]
+ });
+ assertRanges([
+ [173.91304347826087, 289.8550724637681],
+ ['Net revenue', 'Personnel expenses']
+ ]);
+ },
+ null, BOXEVENTS, 'waterfall select'
+ );
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('@flaky should work for bar traces', function(done) {
var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']);
var assertSelectedPoints = makeAssertSelectedPoints();
diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js
index 0c0ef5d0956..d31b20a10d9 100644
--- a/test/jasmine/tests/shapes_test.js
+++ b/test/jasmine/tests/shapes_test.js
@@ -1037,7 +1037,7 @@ describe('A fixed size shape', function() {
});
});
- it('being sized relative to data vertically is getting lower ' +
+ it('@flaky being sized relative to data vertically is getting lower ' +
'when being dragged to expand the y-axis',
function(done) {
layout.shapes[0].ysizemode = 'data';
diff --git a/test/jasmine/tests/waterfall_test.js b/test/jasmine/tests/waterfall_test.js
new file mode 100644
index 00000000000..db2e748bf95
--- /dev/null
+++ b/test/jasmine/tests/waterfall_test.js
@@ -0,0 +1,1459 @@
+var Plotly = require('@lib/index');
+
+var Waterfall = require('@src/traces/waterfall');
+var Lib = require('@src/lib');
+var Plots = require('@src/plots/plots');
+var Drawing = require('@src/components/drawing');
+
+var Axes = require('@src/plots/cartesian/axes');
+
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+var failTest = require('../assets/fail_test');
+var supplyAllDefaults = require('../assets/supply_defaults');
+var color = require('../../../src/components/color');
+var rgb = color.rgb;
+
+var customAssertions = require('../assets/custom_assertions');
+var assertHoverLabelContent = customAssertions.assertHoverLabelContent;
+var Fx = require('@src/components/fx');
+
+var d3 = require('d3');
+
+var WATERFALL_TEXT_SELECTOR = '.bars .bartext';
+
+describe('Waterfall.supplyDefaults', function() {
+ 'use strict';
+
+ var traceIn,
+ traceOut;
+
+ var defaultColor = '#444';
+
+ var supplyDefaults = Waterfall.supplyDefaults;
+
+ beforeEach(function() {
+ traceOut = {};
+ });
+
+ it('should set visible to false when x and y are empty', function() {
+ traceIn = {};
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(false);
+
+ traceIn = {
+ x: [],
+ y: []
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(false);
+ });
+
+ it('should set visible to false when x or y is empty', function() {
+ traceIn = {
+ x: []
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(false);
+
+ traceIn = {
+ x: [],
+ y: [1, 2, 3]
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(false);
+
+ traceIn = {
+ y: []
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(false);
+
+ traceIn = {
+ x: [1, 2, 3],
+ y: []
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(false);
+ });
+
+ [{letter: 'y', counter: 'x'}, {letter: 'x', counter: 'y'}].forEach(function(spec) {
+ var l = spec.letter;
+ var c = spec.counter;
+ var c0 = c + '0';
+ var dc = 'd' + c;
+ it('should be visible using ' + c0 + '/' + dc + ' if ' + c + ' is missing completely but ' + l + ' is present', function() {
+ traceIn = {};
+ traceIn[l] = [1, 2];
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.visible).toBe(undefined, l); // visible: true gets set above the module level
+ expect(traceOut._length).toBe(2, l);
+ expect(traceOut[c0]).toBe(0, c0);
+ expect(traceOut[dc]).toBe(1, dc);
+ expect(traceOut.orientation).toBe(l === 'x' ? 'h' : 'v', l);
+ });
+ });
+
+ it('should not set base, offset or width', function() {
+ traceIn = {
+ y: [1, 2, 3]
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.base).toBeUndefined();
+ expect(traceOut.offset).toBeUndefined();
+ expect(traceOut.width).toBeUndefined();
+ });
+
+ it('should coerce a non-negative width', function() {
+ traceIn = {
+ width: -1,
+ y: [1, 2, 3]
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.width).toBeUndefined();
+ });
+
+ it('should coerce textposition to none', function() {
+ traceIn = {
+ y: [1, 2, 3]
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+ expect(traceOut.textposition).toBe('none');
+ expect(traceOut.texfont).toBeUndefined();
+ expect(traceOut.insidetexfont).toBeUndefined();
+ expect(traceOut.outsidetexfont).toBeUndefined();
+ expect(traceOut.constraintext).toBeUndefined();
+ });
+
+ it('should default textfont to layout.font except for insidetextfont.color', function() {
+ traceIn = {
+ textposition: 'inside',
+ y: [1, 2, 3]
+ };
+ var layout = {
+ font: {family: 'arial', color: '#AAA', size: 13}
+ };
+ var layoutFontMinusColor = {family: 'arial', size: 13};
+
+ supplyDefaults(traceIn, traceOut, defaultColor, layout);
+
+ expect(traceOut.textposition).toBe('inside');
+ expect(traceOut.textfont).toEqual(layout.font);
+ expect(traceOut.textfont).not.toBe(layout.font);
+ expect(traceOut.insidetextfont).toEqual(layoutFontMinusColor);
+ expect(traceOut.insidetextfont).not.toBe(layout.font);
+ expect(traceOut.insidetextfont).not.toBe(traceOut.textfont);
+ expect(traceOut.outsidetexfont).toBeUndefined();
+ expect(traceOut.constraintext).toBe('both');
+ });
+
+ it('should not default insidetextfont.color to layout.font.color', function() {
+ traceIn = {
+ textposition: 'inside',
+ y: [1, 2, 3]
+ };
+ var layout = {
+ font: {family: 'arial', color: '#AAA', size: 13}
+ };
+
+ supplyDefaults(traceIn, traceOut, defaultColor, layout);
+
+ expect(traceOut.insidetextfont.family).toBe('arial');
+ expect(traceOut.insidetextfont.color).toBeUndefined();
+ expect(traceOut.insidetextfont.size).toBe(13);
+ });
+
+ it('should default insidetextfont.color to textfont.color', function() {
+ traceIn = {
+ textposition: 'inside',
+ y: [1, 2, 3],
+ textfont: {family: 'arial', color: '#09F', size: 20}
+ };
+
+ supplyDefaults(traceIn, traceOut, defaultColor, {});
+
+ expect(traceOut.insidetextfont.family).toBe('arial');
+ expect(traceOut.insidetextfont.color).toBe('#09F');
+ expect(traceOut.insidetextfont.size).toBe(20);
+ });
+
+ it('should inherit layout.calendar', function() {
+ traceIn = {
+ x: [1, 2, 3],
+ y: [1, 2, 3]
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'});
+
+ // we always fill calendar attributes, because it's hard to tell if
+ // we're on a date axis at this point.
+ expect(traceOut.xcalendar).toBe('islamic');
+ expect(traceOut.ycalendar).toBe('islamic');
+ });
+
+ it('should take its own calendars', function() {
+ traceIn = {
+ x: [1, 2, 3],
+ y: [1, 2, 3],
+ xcalendar: 'coptic',
+ ycalendar: 'ethiopian'
+ };
+ supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'});
+
+ expect(traceOut.xcalendar).toBe('coptic');
+ expect(traceOut.ycalendar).toBe('ethiopian');
+ });
+});
+
+describe('waterfall calc / crossTraceCalc', function() {
+ 'use strict';
+
+ it('should fill in calc pt fields (overlay case)', function() {
+ var gd = mockWaterfallPlot([{
+ y: [2, 1, 2]
+ }, {
+ y: [3, 1, null, 2, null],
+ measure: ['absolute', 'relative', 'total', 'relative', 'total']
+ }], {
+ waterfallmode: 'overlay'
+ });
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'w', [[0.8, 0.8, 0.8], [0.8, 0.8, 0.8, 0.8, 0.8]]);
+ assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2, 3, 4]]);
+ assertPointField(cd, 'y', [[2, 3, 5], [3, 4, 4, 6, 6]]);
+ assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0, 0, 0]]);
+ assertPointField(cd, 's', [[2, 3, 5], [3, 4, 4, 6, 6]]);
+ assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2, 3, 4]]);
+ assertPointField(cd, 'p0', [[-0.4, 0.6, 1.6], [-0.4, 0.6, 1.6, 2.6, 3.6]]);
+ assertPointField(cd, 'p1', [[0.4, 1.4, 2.4], [0.4, 1.4, 2.4, 3.4, 4.4]]);
+ assertPointField(cd, 's0', [[0, 2, 3], [0, 3, 0, 4, 0]]);
+ assertPointField(cd, 's1', [[2, 3, 5], [3, 4, 4, 6, 6]]);
+ assertPointField(cd, 'isSum', [[false, false, false], [true, false, true, false, true]]);
+ assertPointField(cd, 'rawS', [[2, 1, 2], [3, 1, 0, 2, 0]]);
+ assertPointField(cd, 'dir', [['increasing', 'increasing', 'increasing'], ['totals', 'increasing', 'totals', 'increasing', 'totals']]);
+ assertPointField(cd, 'cNext', [[true, true, false], [true, true, true, true, false]]);
+ assertPointField(cd, 'hasTotals', [[false, undefined, undefined], [true, undefined, undefined, undefined, undefined]]);
+ assertTraceField(cd, 't.barwidth', [0.8, 0.8]);
+ assertTraceField(cd, 't.poffset', [-0.4, -0.4]);
+ assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]);
+ });
+
+ it('should fill in calc pt fields (group case)', function() {
+ var gd = mockWaterfallPlot([{
+ y: [2, 1, 2]
+ }, {
+ y: [3, 1, null, 2, null],
+ measure: ['absolute', null, 'total', null, 'total']
+ }], {
+ waterfallmode: 'group',
+ // asumming default waterfallgap is 0.2
+ waterfallgroupgap: 0.1
+ });
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'w', [[0.36, 0.36, 0.36], [0.36, 0.36, 0.36, 0.36, 0.36]]);
+ assertPointField(cd, 'x', [[-0.2, 0.8, 1.8], [0.2, 1.2, 2.2, 3.2, 4.2]]);
+ assertPointField(cd, 'y', [[2, 3, 5], [3, 4, 4, 6, 6]]);
+ assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0, 0, 0]]);
+ assertPointField(cd, 's', [[2, 3, 5], [3, 4, 4, 6, 6]]);
+ assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2, 3, 4]]);
+ assertPointField(cd, 'p0', [[-0.38, 0.62, 1.62], [0.02, 1.02, 2.02, 3.02, 4.02]]);
+ assertPointField(cd, 'p1', [[-0.02, 0.98, 1.98], [0.38, 1.38, 2.38, 3.38, 4.38]]);
+ assertPointField(cd, 's0', [[0, 2, 3], [0, 3, 0, 4, 0]]);
+ assertPointField(cd, 's1', [[2, 3, 5], [3, 4, 4, 6, 6]]);
+ assertPointField(cd, 'isSum', [[false, false, false], [true, false, true, false, true]]);
+ assertPointField(cd, 'rawS', [[2, 1, 2], [3, 1, 0, 2, 0]]);
+ assertPointField(cd, 'dir', [['increasing', 'increasing', 'increasing'], ['totals', 'increasing', 'totals', 'increasing', 'totals']]);
+ assertPointField(cd, 'cNext', [[true, true, false], [true, true, true, true, false]]);
+ assertPointField(cd, 'hasTotals', [[false, undefined, undefined], [true, undefined, undefined, undefined, undefined]]);
+ assertTraceField(cd, 't.barwidth', [0.36, 0.36]);
+ assertTraceField(cd, 't.poffset', [-0.38, 0.02]);
+ assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]);
+ });
+});
+
+describe('Waterfall.calc', function() {
+ 'use strict';
+
+ it('should not exclude items with non-numeric x from calcdata (vertical case)', function() {
+ var gd = mockWaterfallPlot([{
+ x: [5, NaN, 15, 20, null, 21],
+ orientation: 'v',
+ }]);
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'x', [[5, NaN, 15, 20, NaN, 21]]);
+ });
+
+ it('should not exclude items with non-numeric y from calcdata (horizontal case)', function() {
+ var gd = mockWaterfallPlot([{
+ orientation: 'h',
+ y: [20, NaN, 23, 25, null, 26]
+ }]);
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'y', [[20, NaN, 23, 25, NaN, 26]]);
+ });
+
+ it('should not exclude items with non-numeric y from calcdata (to plots gaps correctly)', function() {
+ var gd = mockWaterfallPlot([{
+ x: ['a', 'b', 'c', 'd'],
+ y: [1, null, 'nonsense', 15]
+ }]);
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'x', [[0, 1, 2, 3]]);
+ assertPointField(cd, 'y', [[1, 1, 1, 16]]);
+ });
+
+ it('should not exclude items with non-numeric x from calcdata (to plots gaps correctly)', function() {
+ var gd = mockWaterfallPlot([{
+ x: [1, null, 'nonsense', 15],
+ y: [1, 2, 10, 30]
+ }]);
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'x', [[1, NaN, NaN, 15]]);
+ assertPointField(cd, 'y', [[1, 3, 13, 43]]);
+ });
+});
+
+describe('Waterfall.crossTraceCalc', function() {
+ 'use strict';
+
+ it('should guard against invalid offset items', function() {
+ var gd = mockWaterfallPlot([{
+ offset: [null, 0, 1],
+ y: [1, 2, 3]
+ }, {
+ offset: [null, 1],
+ y: [1, 2, 3]
+ }, {
+ offset: null,
+ y: [1]
+ }], {
+ waterfallgap: 0.2,
+ waterfallmode: 'overlay'
+ });
+
+ var cd = gd.calcdata;
+ assertArrayField(cd[0][0], 't.poffset', [-0.4, 0, 1]);
+ assertArrayField(cd[1][0], 't.poffset', [-0.4, 1, -0.4]);
+ assertArrayField(cd[2][0], 't.poffset', [-0.4]);
+ });
+
+ it('should work with *width* typed arrays', function() {
+ var w = [0.1, 0.4, 0.7];
+
+ var gd = mockWaterfallPlot([{
+ width: w,
+ y: [1, 2, 3]
+ }, {
+ width: new Float32Array(w),
+ y: [1, 2, 3]
+ }]);
+
+ var cd = gd.calcdata;
+ assertArrayField(cd[0][0], 't.barwidth', w);
+ assertArrayField(cd[1][0], 't.barwidth', w);
+ assertPointField(cd, 'x', [
+ [-0.2, 0.8, 1.8],
+ [0.2, 1.2, 2.2]
+ ]);
+ });
+
+ it('should work with *offset* typed arrays', function() {
+ var o = [0.1, 0.4, 0.7];
+
+ var gd = mockWaterfallPlot([{
+ offset: o,
+ y: [1, 2, 3]
+ }, {
+ offset: new Float32Array(o),
+ y: [1, 2, 3]
+ }]);
+
+ var cd = gd.calcdata;
+ assertArrayField(cd[0][0], 't.poffset', o);
+ assertArrayField(cd[1][0], 't.poffset', o);
+ assertPointField(cd, 'x', [
+ [0.5, 1.8, 3.1],
+ [0.5, 1.8, 3.099]
+ ]);
+ });
+
+ it('should guard against invalid width items', function() {
+ var gd = mockWaterfallPlot([{
+ width: [null, 1, 0.8],
+ y: [1, 2, 3]
+ }, {
+ width: [null, 1],
+ y: [1, 2, 3]
+ }, {
+ width: null,
+ y: [1]
+ }], {
+ waterfallgap: 0.2,
+ waterfallmode: 'overlay'
+ });
+
+ var cd = gd.calcdata;
+ assertArrayField(cd[0][0], 't.barwidth', [0.8, 1, 0.8]);
+ assertArrayField(cd[1][0], 't.barwidth', [0.8, 1, 0.8]);
+ assertArrayField(cd[2][0], 't.barwidth', [0.8]);
+ });
+
+ it('should guard against invalid width items (group case)', function() {
+ var gd = mockWaterfallPlot([{
+ width: [null, 0.1, 0.2],
+ y: [1, 2, 3]
+ }, {
+ width: [null, 0.1],
+ y: [1, 2, 3]
+ }, {
+ width: null,
+ y: [1]
+ }], {
+ waterfallgap: 0,
+ waterfallmode: 'group'
+ });
+
+ var cd = gd.calcdata;
+ assertArrayField(cd[0][0], 't.barwidth', [0.33, 0.1, 0.2]);
+ assertArrayField(cd[1][0], 't.barwidth', [0.33, 0.1, 0.33]);
+ assertArrayField(cd[2][0], 't.barwidth', [0.33]);
+ });
+
+ it('should not group traces that set offset', function() {
+ var gd = mockWaterfallPlot([{
+ y: [1, 2, 3]
+ }, {
+ y: [10, 20, 30]
+ }, {
+ offset: -1,
+ y: [-1, -2, -3]
+ }], {
+ waterfallgap: 0,
+ waterfallmode: 'group'
+ });
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0], [0, 0, 0]]);
+ assertPointField(cd, 's', [[1, 3, 6], [10, 30, 60], [-1, -3, -6]]);
+ assertPointField(cd, 'x', [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25], [-0.5, 0.5, 1.5]]);
+ assertPointField(cd, 'y', [[1, 3, 6], [10, 30, 60], [-1, -3, -6]]);
+ });
+
+ it('should draw traces separately in overlay mode', function() {
+ var gd = mockWaterfallPlot([{
+ y: [1, 2, 3]
+ }, {
+ y: [10, 20, 30]
+ }], {
+ waterfallgap: 0,
+ waterfallmode: 'overlay'
+ });
+
+ var cd = gd.calcdata;
+ assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]);
+ assertPointField(cd, 's', [[1, 3, 6], [10, 30, 60]]);
+ assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]);
+ assertPointField(cd, 'y', [[1, 3, 6], [10, 30, 60]]);
+ });
+
+ it('should expand position axis', function() {
+ var gd = mockWaterfallPlot([{
+ offset: 10,
+ width: 2,
+ y: [1.5, 1, 0.5]
+ }, {
+ offset: -5,
+ width: 2,
+ y: [-0.5, -1, -1.5]
+ }], {
+ waterfallgap: 0,
+ waterfallmode: 'overlay'
+ });
+
+ var xa = gd._fullLayout.xaxis;
+ var ya = gd._fullLayout.yaxis;
+ expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)');
+ expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)');
+ });
+
+ it('should expand size axis (overlay case)', function() {
+ var gd = mockWaterfallPlot([{
+ base: 7,
+ y: [3, 2, 1]
+ }, {
+ base: 2,
+ y: [1, 2, 3]
+ }, {
+ base: -2,
+ y: [-3, -2, -1]
+ }, {
+ base: -7,
+ y: [-1, -2, -3]
+ }], {
+ waterfallgap: 0,
+ waterfallmode: 'overlay'
+ });
+
+ var xa = gd._fullLayout.xaxis;
+ var ya = gd._fullLayout.yaxis;
+ expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)');
+ expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-14.44, 14.44], undefined, '(ya.range)');
+ });
+
+ it('should include explicit base in size axis range', function() {
+ var waterfallmodes = ['group', 'overlay'];
+ waterfallmodes.forEach(function(waterfallmode) {
+ var gd = mockWaterfallPlot([
+ {y: [3, 4, -5], base: 10}
+ ], {
+ waterfallmode: waterfallmode
+ });
+
+ var ya = gd._fullLayout.yaxis;
+ expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([9.611, 17.388]);
+ });
+ });
+
+ it('works with log axes (grouped waterfalls)', function() {
+ var gd = mockWaterfallPlot([
+ {y: [1, 10, 1e10, -1]},
+ {y: [2, 20, 2e10, -2]}
+ ], {
+ yaxis: {type: 'log'},
+ waterfallmode: 'group'
+ });
+
+ var ya = gd._fullLayout.yaxis;
+ expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-0.572, 10.873], undefined, '(ya.range)');
+ });
+
+ it('should ignore *base* on category axes', function() {
+ var gd = mockWaterfallPlot([
+ {x: ['a', 'b', 'c'], base: 10},
+ ]);
+
+ expect(gd._fullLayout.xaxis.type).toBe('category');
+ assertPointField(gd.calcdata, 'b', [[0, 0, 0]]);
+ });
+
+ it('should ignore *base* on multicategory axes', function() {
+ var gd = mockWaterfallPlot([
+ {x: [['a', 'a', 'b', 'b'], ['1', '2', '1', '2']], base: 10}
+ ]);
+
+ expect(gd._fullLayout.xaxis.type).toBe('multicategory');
+ assertPointField(gd.calcdata, 'b', [[0, 0, 0, 0]]);
+ });
+});
+
+describe('A waterfall plot', function() {
+ 'use strict';
+
+ var DARK = '#444';
+ var LIGHT = '#fff';
+
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function getAllTraceNodes(node) {
+ return node.querySelectorAll('g.points');
+ }
+
+ function getAllWaterfallNodes(node) {
+ return node.querySelectorAll('g.point');
+ }
+
+ function assertTextIsInsidePath(textNode, pathNode) {
+ var textBB = textNode.getBoundingClientRect();
+ var pathBB = pathNode.getBoundingClientRect();
+
+ expect(pathBB.left).not.toBeGreaterThan(textBB.left);
+ expect(textBB.right).not.toBeGreaterThan(pathBB.right);
+ expect(pathBB.top).not.toBeGreaterThan(textBB.top);
+ expect(textBB.bottom).not.toBeGreaterThan(pathBB.bottom);
+ }
+
+ function assertTextIsAbovePath(textNode, pathNode) {
+ var textBB = textNode.getBoundingClientRect();
+ var pathBB = pathNode.getBoundingClientRect();
+
+ expect(textBB.bottom).not.toBeGreaterThan(pathBB.top);
+ }
+
+ function assertTextIsBelowPath(textNode, pathNode) {
+ var textBB = textNode.getBoundingClientRect();
+ var pathBB = pathNode.getBoundingClientRect();
+
+ expect(pathBB.bottom).not.toBeGreaterThan(textBB.top);
+ }
+
+ function assertTextIsAfterPath(textNode, pathNode) {
+ var textBB = textNode.getBoundingClientRect();
+ var pathBB = pathNode.getBoundingClientRect();
+
+ expect(pathBB.right).not.toBeGreaterThan(textBB.left);
+ }
+
+ function assertTextFont(textNode, expectedFontProps, index) {
+ expect(textNode.style.fontFamily).toBe(expectedFontProps.family[index]);
+ expect(textNode.style.fontSize).toBe(expectedFontProps.size[index] + 'px');
+
+ var actualColorRGB = textNode.style.fill;
+ var expectedColorRGB = rgb(expectedFontProps.color[index]);
+ expect(actualColorRGB).toBe(expectedColorRGB);
+ }
+
+ function assertTextIsBeforePath(textNode, pathNode) {
+ var textBB = textNode.getBoundingClientRect();
+ var pathBB = pathNode.getBoundingClientRect();
+
+ expect(textBB.right).not.toBeGreaterThan(pathBB.left);
+ }
+
+ function assertTextFontColors(expFontColors, label) {
+ return function() {
+ var selection = d3.selectAll(WATERFALL_TEXT_SELECTOR);
+ expect(selection.size()).toBe(expFontColors.length);
+
+ selection.each(function(d, i) {
+ var expFontColor = expFontColors[i];
+ var isArray = Array.isArray(expFontColor);
+
+ expect(this.style.fill).toBe(isArray ? rgb(expFontColor[0]) : rgb(expFontColor),
+ (label || '') + ', fill for element ' + i);
+ expect(this.style.fillOpacity).toBe(isArray ? expFontColor[1] : '1',
+ (label || '') + ', fillOpacity for element ' + i);
+ });
+ };
+ }
+
+ it('should show texts (inside case)', function(done) {
+ var data = [{
+ y: [10, 20, 30],
+ type: 'waterfall',
+ text: ['1', 'Very very very very very long text'],
+ textposition: 'inside',
+ }];
+ var layout = {};
+
+ Plotly.plot(gd, data, layout).then(function() {
+ var traceNodes = getAllTraceNodes(gd);
+ var waterfallNodes = getAllWaterfallNodes(traceNodes[0]);
+ var foundTextNodes;
+
+ for(var i = 0; i < waterfallNodes.length; i++) {
+ var waterfallNode = waterfallNodes[i];
+ var pathNode = waterfallNode.querySelector('path');
+ var textNode = waterfallNode.querySelector('text');
+ if(textNode) {
+ foundTextNodes = true;
+ assertTextIsInsidePath(textNode, pathNode);
+ }
+ }
+
+ expect(foundTextNodes).toBe(true);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should show texts (horizontal case)', function(done) {
+ var data = [{
+ x: [10, -20, 30],
+ type: 'waterfall',
+ text: ['Very very very very very long text', -20],
+ textposition: 'outside',
+ }];
+ var layout = {};
+
+ Plotly.plot(gd, data, layout).then(function() {
+ var traceNodes = getAllTraceNodes(gd);
+ var waterfallNodes = getAllWaterfallNodes(traceNodes[0]);
+ var foundTextNodes;
+
+ for(var i = 0; i < waterfallNodes.length; i++) {
+ var waterfallNode = waterfallNodes[i];
+ var pathNode = waterfallNode.querySelector('path');
+ var textNode = waterfallNode.querySelector('text');
+ if(textNode) {
+ foundTextNodes = true;
+ if(data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode);
+ else assertTextIsBeforePath(textNode, pathNode);
+ }
+ }
+
+ expect(foundTextNodes).toBe(true);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ var insideTextTestsTrace = {
+ x: ['giraffes', 'orangutans', 'monkeys', 'elefants', 'spiders', 'snakes'],
+ y: [20, 14, 23, 10, 59, 15],
+ text: [20, 14, 23, 10, 59, 15],
+ type: 'waterfall',
+ textposition: 'auto',
+ marker: {
+ color: ['#ee1', '#eee', '#333', '#9467bd', '#dda', '#922'],
+ }
+ };
+
+ it('should take fill opacities into account when calculating contrasting inside text colors', function(done) {
+ var trace = {
+ x: [5, 10],
+ y: [5, -15],
+ text: ['Giraffes', 'Zebras'],
+ type: 'waterfall',
+ textposition: 'inside',
+ increasing: { marker: { color: 'rgba(0, 0, 0, 0.2)' } },
+ decreasing: { marker: { color: 'rgba(0, 0, 0, 0.8)' } }
+ };
+
+ Plotly.plot(gd, [trace])
+ .then(assertTextFontColors([DARK, LIGHT]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should use defined textfont.color for inside text instead of the contrasting default', function(done) {
+ var data = Lib.extendFlat({}, insideTextTestsTrace, { textfont: { color: '#09f' } });
+
+ Plotly.plot(gd, [data])
+ .then(assertTextFontColors(Lib.repeat('#09f', 6)))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to restyle', function(done) {
+ var mock = {
+ data: [
+ {
+ width: [1, 0.8, 0.6, 0.4],
+ text: [1, 2, 3333333333, 4],
+ textposition: 'outside',
+ y: [1, 2, 3, 4],
+ x: [1, 2, 3, 4],
+ type: 'waterfall'
+ }, {
+ width: [0.4, 0.6, 0.8, 1],
+ text: ['Three', 2, 'inside text', 0],
+ textposition: 'auto',
+ textfont: { size: [10] },
+ y: [3, 2, 1, 0],
+ x: [1, 2, 3, 4],
+ type: 'waterfall'
+ }, {
+ width: 1,
+ text: [-1, -3, -2, -4],
+ textposition: 'inside',
+ y: [-1, -3, -2, -4],
+ x: [1, 2, 3, 4],
+ type: 'waterfall'
+ }, {
+ text: [0, 'outside text', -3, -2],
+ textposition: 'auto',
+ y: [0, -0.25, -3, -2],
+ x: [1, 2, 3, 4],
+ type: 'waterfall'
+ }
+ ],
+ layout: {
+ xaxis: { showgrid: true },
+ yaxis: { range: [-6, 6] },
+ height: 400,
+ width: 400,
+ waterfallmode: 'overlay'
+ }
+ };
+
+ Plotly.plot(gd, mock.data, mock.layout).then(function() {
+ var cd = gd.calcdata;
+ assertPointField(cd, 'x', [
+ [1, 2, 3, 4], [1, 2, 3, 4],
+ [1, 2, 3, 4], [1, 2, 3, 4]]);
+ assertPointField(cd, 'y', [
+ [1, 3, 6, 10], [3, 5, 6, 6],
+ [-1, -4, -6, -10], [0, -0.25, -3.25, -5.25]]);
+ assertPointField(cd, 's', [
+ [1, 3, 6, 10], [3, 5, 6, 6],
+ [-1, -4, -6, -10], [0, -0.25, -3.25, -5.25]]);
+ assertPointField(cd, 'p', [
+ [1, 2, 3, 4], [1, 2, 3, 4],
+ [1, 2, 3, 4], [1, 2, 3, 4]]);
+ assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]);
+ assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]);
+ expect(cd[2][0].t.barwidth).toBe(1);
+ expect(cd[3][0].t.barwidth).toBe(0.8);
+ assertArrayField(cd[0][0], 't.poffset', [-0.5, -0.4, -0.3, -0.2]);
+ assertArrayField(cd[1][0], 't.poffset', [-0.2, -0.3, -0.4, -0.5]);
+ expect(cd[2][0].t.poffset).toBe(-0.5);
+ expect(cd[3][0].t.poffset).toBe(-0.4);
+ assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]);
+
+ return Plotly.restyle(gd, 'offset', 0);
+ }).then(function() {
+ var cd = gd.calcdata;
+ assertPointField(cd, 'x', [
+ [1.5, 2.4, 3.3, 4.2], [1.2, 2.3, 3.4, 4.5],
+ [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]);
+ assertPointField(cd, 'y', [
+ [1, 3, 6, 10], [3, 5, 6, 6],
+ [-1, -4, -6, -10], [0, -0.25, -3.25, -5.25]]);
+ assertPointField(cd, 's', [
+ [1, 3, 6, 10], [3, 5, 6, 6],
+ [-1, -4, -6, -10], [0, -0.25, -3.25, -5.25] ]);
+ assertPointField(cd, 'p', [
+ [1, 2, 3, 4], [1, 2, 3, 4],
+ [1, 2, 3, 4], [1, 2, 3, 4]]);
+ assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]);
+ assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]);
+ expect(cd[2][0].t.barwidth).toBe(1);
+ expect(cd[3][0].t.barwidth).toBe(0.8);
+ expect(cd[0][0].t.poffset).toBe(0);
+ expect(cd[1][0].t.poffset).toBe(0);
+ expect(cd[2][0].t.poffset).toBe(0);
+ expect(cd[3][0].t.poffset).toBe(0);
+ assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]);
+
+ var traceNodes = getAllTraceNodes(gd);
+ var trace0Waterfall3 = getAllWaterfallNodes(traceNodes[0])[3];
+ var path03 = trace0Waterfall3.querySelector('path');
+ var text03 = trace0Waterfall3.querySelector('text');
+ var trace1Waterfall2 = getAllWaterfallNodes(traceNodes[1])[2];
+ var path12 = trace1Waterfall2.querySelector('path');
+ var text12 = trace1Waterfall2.querySelector('text');
+ var trace2Waterfall0 = getAllWaterfallNodes(traceNodes[2])[0];
+ var path20 = trace2Waterfall0.querySelector('path');
+ var text20 = trace2Waterfall0.querySelector('text');
+ var trace3Waterfall0 = getAllWaterfallNodes(traceNodes[3])[0];
+ var path30 = trace3Waterfall0.querySelector('path');
+ var text30 = trace3Waterfall0.querySelector('text');
+
+ expect(text03.textContent).toBe('4');
+ expect(text12.textContent).toBe('inside text');
+ expect(text20.textContent).toBe('-1');
+ expect(text30.textContent).toBe('outside text');
+
+ assertTextIsAbovePath(text03, path03); // outside
+ assertTextIsInsidePath(text12, path12); // inside
+ assertTextIsInsidePath(text20, path20); // inside
+ assertTextIsBelowPath(text30, path30); // outside
+
+ // clear bounding box cache - somehow when you cache
+ // text size too early sometimes it changes later...
+ // we've had this issue before, where we've had to
+ // redraw annotations to get final sizes, I wish we
+ // could get some signal that fonts are really ready
+ // and not start drawing until then (or invalidate
+ // the bbox cache when that happens?)
+ // without this change, we get an error at
+ // assertTextIsInsidePath(text30, path30);
+ Drawing.savedBBoxes = {};
+
+ return Plotly.restyle(gd, 'textposition', 'inside');
+ }).then(function() {
+ var cd = gd.calcdata;
+ assertPointField(cd, 'x', [
+ [1.5, 2.4, 3.3, 4.2], [1.2, 2.3, 3.4, 4.5],
+ [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]);
+ assertPointField(cd, 'y', [
+ [1, 3, 6, 10], [3, 5, 6, 6],
+ [-1, -4, -6, -10], [0, -0.25, -3.25, -5.25]]);
+ assertPointField(cd, 's', [
+ [1, 3, 6, 10], [3, 5, 6, 6],
+ [-1, -4, -6, -10], [0, -0.25, -3.25, -5.25]]);
+ assertPointField(cd, 'p', [
+ [1, 2, 3, 4], [1, 2, 3, 4],
+ [1, 2, 3, 4], [1, 2, 3, 4]]);
+ assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]);
+ assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]);
+ expect(cd[2][0].t.barwidth).toBe(1);
+ expect(cd[3][0].t.barwidth).toBe(0.8);
+ expect(cd[0][0].t.poffset).toBe(0);
+ expect(cd[1][0].t.poffset).toBe(0);
+ expect(cd[2][0].t.poffset).toBe(0);
+ expect(cd[3][0].t.poffset).toBe(0);
+ assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]);
+
+ var traceNodes = getAllTraceNodes(gd);
+ var trace0Waterfall3 = getAllWaterfallNodes(traceNodes[0])[3];
+ var path03 = trace0Waterfall3.querySelector('path');
+ var text03 = trace0Waterfall3.querySelector('text');
+ var trace1Waterfall2 = getAllWaterfallNodes(traceNodes[1])[2];
+ var path12 = trace1Waterfall2.querySelector('path');
+ var text12 = trace1Waterfall2.querySelector('text');
+ var trace2Waterfall0 = getAllWaterfallNodes(traceNodes[2])[0];
+ var path20 = trace2Waterfall0.querySelector('path');
+ var text20 = trace2Waterfall0.querySelector('text');
+ var trace3Waterfall0 = getAllWaterfallNodes(traceNodes[3])[0];
+ var path30 = trace3Waterfall0.querySelector('path');
+ var text30 = trace3Waterfall0.querySelector('text');
+
+ expect(text03.textContent).toBe('4');
+ expect(text12.textContent).toBe('inside text');
+ expect(text20.textContent).toBe('-1');
+ expect(text30.textContent).toBe('outside text');
+
+ assertTextIsInsidePath(text03, path03); // inside
+ assertTextIsInsidePath(text12, path12); // inside
+ assertTextIsInsidePath(text20, path20); // inside
+ assertTextIsInsidePath(text30, path30); // inside
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to add/remove connector nodes on restyle', function(done) {
+ function _assertNumberOfWaterfallConnectorNodes(cnt) {
+ var sel = d3.select(gd).select('.waterfalllayer').selectAll('.line');
+ expect(sel.size()).toBe(cnt);
+ }
+
+ Plotly.plot(gd, [{
+ type: 'waterfall',
+ x: ['Initial', 'A', 'B', 'C', 'Total'],
+ y: [10, 2, 3, 5],
+ measure: ['absolute', 'relative', 'relative', 'relative', 'total'],
+ connector: { visible: false }
+ }])
+ .then(function() {
+ _assertNumberOfWaterfallConnectorNodes(0);
+ return Plotly.restyle(gd, 'connector.visible', true);
+ })
+ .then(function() {
+ _assertNumberOfWaterfallConnectorNodes(4);
+ return Plotly.restyle(gd, 'connector.visible', false);
+ })
+ .then(function() {
+ _assertNumberOfWaterfallConnectorNodes(0);
+ return Plotly.restyle(gd, 'connector.visible', true);
+ })
+ .then(function() {
+ _assertNumberOfWaterfallConnectorNodes(4);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should coerce text-related attributes', function(done) {
+ var data = [{
+ y: [10, 20, 30, 40],
+ type: 'waterfall',
+ text: ['T1P1', 'T1P2', 13, 14],
+ textposition: ['inside', 'outside', 'auto', 'BADVALUE'],
+ textfont: {
+ family: ['"comic sans"'],
+ color: ['red', 'green'],
+ },
+ insidetextfont: {
+ size: [8, 12, 16],
+ color: ['black'],
+ },
+ outsidetextfont: {
+ size: [null, 24, 32]
+ }
+ }];
+ var layout = {
+ font: {family: 'arial', color: 'blue', size: 13}
+ };
+
+ // Note: insidetextfont.color does NOT inherit from textfont.color
+ // since insidetextfont.color should be contrasting to bar's fill by default.
+ var contrastingLightColorVal = color.contrast('black');
+ var expected = {
+ y: [10, 20, 30, 40],
+ type: 'waterfall',
+ text: ['T1P1', 'T1P2', '13', '14'],
+ textposition: ['inside', 'outside', 'none'],
+ textfont: {
+ family: ['"comic sans"', 'arial'],
+ color: ['red', 'green'],
+ size: [13, 13]
+ },
+ insidetextfont: {
+ family: ['"comic sans"', 'arial', 'arial'],
+ color: ['black', 'green', contrastingLightColorVal],
+ size: [8, 12, 16]
+ },
+ outsidetextfont: {
+ family: ['"comic sans"', 'arial', 'arial'],
+ color: ['red', 'green', 'blue'],
+ size: [13, 24, 32]
+ }
+ };
+
+ Plotly.plot(gd, data, layout).then(function() {
+ var traceNodes = getAllTraceNodes(gd);
+ var waterfallNodes = getAllWaterfallNodes(traceNodes[0]);
+ var pathNodes = [
+ waterfallNodes[0].querySelector('path'),
+ waterfallNodes[1].querySelector('path'),
+ waterfallNodes[2].querySelector('path'),
+ waterfallNodes[3].querySelector('path')
+ ];
+ var textNodes = [
+ waterfallNodes[0].querySelector('text'),
+ waterfallNodes[1].querySelector('text'),
+ waterfallNodes[2].querySelector('text'),
+ waterfallNodes[3].querySelector('text')
+ ];
+ var i;
+
+ // assert waterfall texts
+ for(i = 0; i < 3; i++) {
+ expect(textNodes[i].textContent).toBe(expected.text[i]);
+ }
+
+ // assert waterfall positions
+ assertTextIsInsidePath(textNodes[0], pathNodes[0]); // inside
+ assertTextIsAbovePath(textNodes[1], pathNodes[1]); // outside
+ assertTextIsInsidePath(textNodes[2], pathNodes[2]); // auto -> inside
+ expect(textNodes[3]).toBe(null); // BADVALUE -> none
+
+ // assert fonts
+ assertTextFont(textNodes[0], expected.insidetextfont, 0);
+ assertTextFont(textNodes[1], expected.outsidetextfont, 1);
+ assertTextFont(textNodes[2], expected.insidetextfont, 2);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to add/remove text node on restyle', function(done) {
+ function _assertNumberOfWaterfallTextNodes(cnt) {
+ var sel = d3.select(gd).select('.waterfalllayer').selectAll('text');
+ expect(sel.size()).toBe(cnt);
+ }
+
+ Plotly.plot(gd, [{
+ type: 'waterfall',
+ x: ['Product A', 'Product B', 'Product C'],
+ y: [20, 14, 23],
+ text: [20, 14, 23],
+ textposition: 'auto'
+ }])
+ .then(function() {
+ _assertNumberOfWaterfallTextNodes(3);
+ return Plotly.restyle(gd, 'textposition', 'none');
+ })
+ .then(function() {
+ _assertNumberOfWaterfallTextNodes(0);
+ return Plotly.restyle(gd, 'textposition', 'auto');
+ })
+ .then(function() {
+ _assertNumberOfWaterfallTextNodes(3);
+ return Plotly.restyle(gd, 'text', [[null, 0, '']]);
+ })
+ .then(function() {
+ // N.B. that '0' should be there!
+ _assertNumberOfWaterfallTextNodes(1);
+ return Plotly.restyle(gd, 'text', 'yo!');
+ })
+ .then(function() {
+ _assertNumberOfWaterfallTextNodes(3);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to react with new text colors', function(done) {
+ Plotly.react(gd, [{
+ type: 'waterfall',
+ y: [1, 2, 3],
+ text: ['A', 'B', 'C'],
+ textposition: 'inside'
+ }])
+ .then(assertTextFontColors(['rgb(255, 255, 255)', 'rgb(255, 255, 255)', 'rgb(255, 255, 255)']))
+ .then(function() {
+ gd.data[0].insidetextfont = {color: 'red'};
+ return Plotly.react(gd, gd.data);
+ })
+ .then(assertTextFontColors(['rgb(255, 0, 0)', 'rgb(255, 0, 0)', 'rgb(255, 0, 0)']))
+ .then(function() {
+ delete gd.data[0].insidetextfont.color;
+ gd.data[0].textfont = {color: 'blue'};
+ return Plotly.react(gd, gd.data);
+ })
+ .then(assertTextFontColors(['rgb(0, 0, 255)', 'rgb(0, 0, 255)', 'rgb(0, 0, 255)']))
+ .then(function() {
+ gd.data[0].textposition = 'outside';
+ return Plotly.react(gd, gd.data);
+ })
+ .then(assertTextFontColors(['rgb(0, 0, 255)', 'rgb(0, 0, 255)', 'rgb(0, 0, 255)']))
+ .then(function() {
+ gd.data[0].outsidetextfont = {color: 'red'};
+ return Plotly.react(gd, gd.data);
+ })
+ .then(assertTextFontColors(['rgb(255, 0, 0)', 'rgb(255, 0, 0)', 'rgb(255, 0, 0)']))
+ .catch(failTest)
+ .then(done);
+ });
+});
+
+describe('waterfall visibility toggling:', function() {
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function _assert(msg, xrng, yrng, calls) {
+ var fullLayout = gd._fullLayout;
+ expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng');
+ expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng');
+
+ var crossTraceCalc = gd._fullData[0]._module.crossTraceCalc;
+ expect(crossTraceCalc).toHaveBeenCalledTimes(calls);
+ crossTraceCalc.calls.reset();
+ }
+
+ it('should update axis range according to visible edits (group case)', function(done) {
+ Plotly.plot(gd, [
+ {type: 'waterfall', x: [1, 2, 3], y: [0.5, 1, 0.5]},
+ {type: 'waterfall', x: [1, 2, 3], y: [-0.5, -1, -0.5]}
+ ])
+ .then(function() {
+ spyOn(gd._fullData[0]._module, 'crossTraceCalc').and.callThrough();
+
+ _assert('base', [0.5, 3.5], [-2.222, 2.222], 0);
+ expect(gd._fullLayout.legend.traceorder).toBe('normal');
+ return Plotly.restyle(gd, 'visible', false, [1]);
+ })
+ .then(function() {
+ _assert('visible [true,false]', [0.5, 3.5], [0, 2.105], 1);
+ return Plotly.restyle(gd, 'visible', false, [0]);
+ })
+ .then(function() {
+ _assert('both invisible', [0.5, 3.5], [0, 2.105], 0);
+ return Plotly.restyle(gd, 'visible', 'legendonly');
+ })
+ .then(function() {
+ _assert('both legendonly', [0.5, 3.5], [0, 2.105], 0);
+ expect(gd._fullLayout.legend.traceorder).toBe('normal');
+ return Plotly.restyle(gd, 'visible', true, [1]);
+ })
+ .then(function() {
+ _assert('visible [false,true]', [0.5, 3.5], [-2.105, 0], 1);
+ return Plotly.restyle(gd, 'visible', true);
+ })
+ .then(function() {
+ _assert('back to both visible', [0.5, 3.5], [-2.222, 2.222], 1);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+});
+
+describe('waterfall hover', function() {
+ 'use strict';
+
+ var gd;
+
+ afterEach(destroyGraphDiv);
+
+ function getPointData(gd) {
+ var cd = gd.calcdata;
+ var subplot = gd._fullLayout._plots.xy;
+
+ return {
+ index: false,
+ distance: 20,
+ cd: cd[0],
+ trace: cd[0][0].trace,
+ xa: subplot.xaxis,
+ ya: subplot.yaxis,
+ maxHoverDistance: 20
+ };
+ }
+
+ function _hover(gd, xval, yval, hovermode) {
+ var pointData = getPointData(gd);
+ var pts = Waterfall.hoverPoints(pointData, xval, yval, hovermode);
+ if(!pts) return false;
+
+ var pt = pts[0];
+
+ return {
+ style: [pt.index, pt.color, pt.xLabelVal, pt.yLabelVal],
+ pos: [pt.x0, pt.x1, pt.y0, pt.y1],
+ text: pt.text
+ };
+ }
+
+ function assertPos(actual, expected) {
+ var TOL = 5;
+
+ actual.forEach(function(p, i) {
+ expect(p).toBeWithin(expected[i], TOL);
+ });
+ }
+
+ describe('with orientation *v*', function() {
+ beforeAll(function(done) {
+ gd = createGraphDiv();
+
+ var mock = Lib.extendDeep({}, require('@mocks/waterfall_11.json'));
+
+ Plotly.plot(gd, mock.data, mock.layout)
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should return the correct hover point data (case x)', function() {
+ var out = _hover(gd, 0, 0, 'x');
+
+ expect(out.style).toEqual([0, '#3D9970', 0, 13.23]);
+ assertPos(out.pos, [11.87, 106.8, 52.71, 52.71]);
+ });
+
+ it('should return the correct hover point data (case closest)', function() {
+ var out = _hover(gd, -0.2, 12, 'closest');
+
+ expect(out.style).toEqual([0, '#3D9970', 0, 13.23]);
+ assertPos(out.pos, [11.87, 59.33, 52.71, 52.71]);
+ });
+ });
+
+ describe('text labels', function() {
+
+ it('should show \'hovertext\' items when present, \'text\' if not', function(done) {
+ gd = createGraphDiv();
+
+ var mock = Lib.extendDeep({}, require('@mocks/text_chart_arrays'));
+ mock.data.forEach(function(t) { t.type = 'waterfall'; });
+
+ Plotly.plot(gd, mock).then(function() {
+ var out = _hover(gd, -0.25, 0.5, 'closest');
+ expect(out.text).toEqual('Hover text\nA', 'hover text');
+
+ return Plotly.restyle(gd, 'hovertext', null);
+ })
+ .then(function() {
+ var out = _hover(gd, -0.25, 0.5, 'closest');
+ expect(out.text).toEqual('Text\nA', 'hover text');
+
+ return Plotly.restyle(gd, 'text', ['APPLE', 'BANANA', 'ORANGE']);
+ })
+ .then(function() {
+ var out = _hover(gd, -0.25, 0.5, 'closest');
+ expect(out.text).toEqual('APPLE', 'hover text');
+
+ return Plotly.restyle(gd, 'hovertext', ['apple', 'banana', 'orange']);
+ })
+ .then(function() {
+ var out = _hover(gd, -0.25, 0.5, 'closest');
+ expect(out.text).toEqual('apple', 'hover text');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should use hovertemplate if specified', function(done) {
+ gd = createGraphDiv();
+
+ var mock = Lib.extendDeep({}, require('@mocks/text_chart_arrays'));
+ mock.data.forEach(function(t) {
+ t.type = 'waterfall';
+ t.hovertemplate = '%{y}';
+ });
+
+ function _hover() {
+ var evt = { xpx: 125, ypx: 150 };
+ Fx.hover('graph', evt, 'xy');
+ }
+
+ Plotly.plot(gd, mock)
+ .then(_hover)
+ .then(function() {
+ assertHoverLabelContent({
+ nums: ['1', '2', '1.5'],
+ name: ['', '', ''],
+ axis: '0'
+ });
+ // return Plotly.restyle(gd, 'text', ['APPLE', 'BANANA', 'ORANGE']);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+
+ describe('with special width/offset combinations', function() {
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ it('should return correct hover data (single waterfall, trace width)', function(done) {
+ Plotly.plot(gd, [{
+ type: 'waterfall',
+ x: [1],
+ y: [2],
+ width: 10,
+ marker: { color: 'red' }
+ }], {
+ xaxis: { range: [-200, 200] }
+ })
+ .then(function() {
+ // all these x, y, hovermode should give the same (the only!) hover label
+ [
+ [0, 0, 'closest'],
+ [-3.9, 1, 'closest'],
+ [5.9, 1.9, 'closest'],
+ [-3.9, -10, 'x'],
+ [5.9, 19, 'x']
+ ].forEach(function(hoverSpec) {
+ var out = _hover(gd, hoverSpec[0], hoverSpec[1], hoverSpec[2]);
+
+ expect(out.style).toEqual([0, '#3D9970', 1, 2], hoverSpec);
+ assertPos(out.pos, [264, 278, 14, 14], hoverSpec);
+ });
+
+ // then a few that are off the edge so yield nothing
+ [
+ [1, -0.1, 'closest'],
+ [1, 2.1, 'closest'],
+ [-4.1, 1, 'closest'],
+ [6.1, 1, 'closest'],
+ [-4.1, 1, 'x'],
+ [6.1, 1, 'x']
+ ].forEach(function(hoverSpec) {
+ var out = _hover(gd, hoverSpec[0], hoverSpec[1], hoverSpec[2]);
+
+ expect(out).toBe(false, hoverSpec);
+ });
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should return correct hover data (two waterfalls, array width)', function(done) {
+ Plotly.plot(gd, [{
+ type: 'waterfall',
+ x: [1, 200],
+ y: [2, 1],
+ width: [10, 20],
+ marker: { color: 'red' }
+ }, {
+ type: 'waterfall',
+ x: [1, 200],
+ y: [1, 2],
+ width: [20, 10],
+ marker: { color: 'green' }
+ }], {
+ xaxis: { range: [-200, 300] },
+ width: 500,
+ height: 500
+ })
+ .then(function() {
+ var out = _hover(gd, -36, 1.5, 'closest');
+
+ expect(out.style).toEqual([0, '#3D9970', 1, 2]);
+ assertPos(out.pos, [99, 106, 117.33, 117.33]);
+
+ out = _hover(gd, 164, 0.8, 'closest');
+
+ expect(out.style).toEqual([1, '#3D9970', 200, 3]);
+ assertPos(out.pos, [222, 235, 16, 16]);
+
+ out = _hover(gd, 125, 0.8, 'x');
+
+ expect(out.style).toEqual([1, '#3D9970', 200, 3]);
+ assertPos(out.pos, [222, 280, 16, 16]);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('positions labels correctly w.r.t. narrow waterfalls', function(done) {
+ Plotly.newPlot(gd, [{
+ x: [0, 10, 20],
+ y: [1, 3, 2],
+ type: 'waterfall',
+ width: 1
+ }], {
+ width: 500,
+ height: 500,
+ margin: {l: 100, r: 100, t: 100, b: 100}
+ })
+ .then(function() {
+ // you can still hover over the gap (14) but the label will
+ // get pushed in to the bar
+ var out = _hover(gd, 14, 2, 'x');
+ assertPos(out.pos, [145, 155, 110, 110]);
+
+ // in closest mode you must be over the bar though
+ out = _hover(gd, 14, 2, 'closest');
+ expect(out).toBe(false);
+
+ // now for a single waterfall trace, closest and compare modes give the same
+ // positioning of hover labels
+ out = _hover(gd, 10, 2, 'closest');
+ assertPos(out.pos, [145, 155, 110, 110]);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+});
+
+function mockWaterfallPlot(dataWithoutTraceType, layout) {
+ var traceTemplate = { type: 'waterfall' };
+
+ var dataWithTraceType = dataWithoutTraceType.map(function(trace) {
+ return Lib.extendFlat({}, traceTemplate, trace);
+ });
+
+ var gd = {
+ data: dataWithTraceType,
+ layout: layout || {},
+ calcdata: [],
+ _context: {locale: 'en', locales: {}}
+ };
+
+ supplyAllDefaults(gd);
+ Plots.doCalcdata(gd);
+
+ return gd;
+}
+
+function assertArrayField(calcData, prop, expectation) {
+ var values = Lib.nestedProperty(calcData, prop).get();
+ if(!Array.isArray(values)) values = [values];
+
+ expect(values).toBeCloseToArray(expectation, undefined, '(field ' + prop + ')');
+}
+
+function assertPointField(calcData, prop, expectation) {
+ var values = [];
+
+ calcData.forEach(function(calcTrace) {
+ var vals = calcTrace.map(function(pt) {
+ return Lib.nestedProperty(pt, prop).get();
+ });
+
+ values.push(vals);
+ });
+
+ expect(values).toBeCloseTo2DArray(expectation, undefined, '(field ' + prop + ')');
+}
+
+function assertTraceField(calcData, prop, expectation) {
+ var values = calcData.map(function(calcTrace) {
+ return Lib.nestedProperty(calcTrace[0], prop).get();
+ });
+
+ expect(values).toBeCloseToArray(expectation, undefined, '(field ' + prop + ')');
+}