Skip to content

Commit 978303e

Browse files
authored
Merge pull request #3531 from plotly/waterfall-new-trace
Waterfall charts
2 parents c740e3f + 1d275f5 commit 978303e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3862
-110
lines changed

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Plotly.register([
2121
require('./contour'),
2222
require('./scatterternary'),
2323
require('./violin'),
24+
require('./waterfall'),
2425

2526
require('./pie'),
2627
require('./sunburst'),

lib/waterfall.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = require('../src/traces/waterfall');

src/components/drawing/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ drawing.hideOutsideRangePoints = function(traceGroups, subplot) {
110110
var trace = d[0].trace;
111111
var xcalendar = trace.xcalendar;
112112
var ycalendar = trace.ycalendar;
113-
var selector = trace.type === 'bar' ? '.bartext' : '.point,.textpoint';
113+
var selector = trace.type === 'bar' ? '.bartext' :
114+
trace.type === 'waterfall' ? '.bartext,.line' :
115+
'.point,.textpoint';
114116

115117
traceGroups.selectAll(selector).each(function(d) {
116118
drawing.hideOutsideRangePoint(d, d3.select(this), xa, ya, xcalendar, ycalendar);

src/components/legend/style.js

+33
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ module.exports = function style(s, gd) {
6161
.enter().append('g')
6262
.classed('legendpoints', true);
6363
})
64+
.each(styleWaterfalls)
6465
.each(styleBars)
6566
.each(styleBoxes)
6667
.each(stylePies)
@@ -241,6 +242,38 @@ module.exports = function style(s, gd) {
241242
txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd);
242243
}
243244

245+
function styleWaterfalls(d) {
246+
var trace = d[0].trace;
247+
248+
var ptsData = [];
249+
if(trace.type === 'waterfall' && trace.visible) {
250+
ptsData = d[0].hasTotals ?
251+
[['increasing', 'M-6,-6V6H0Z'], ['totals', 'M6,6H0L-6,-6H-0Z'], ['decreasing', 'M6,6V-6H0Z']] :
252+
[['increasing', 'M-6,-6V6H6Z'], ['decreasing', 'M6,6V-6H-6Z']];
253+
}
254+
255+
var pts = d3.select(this).select('g.legendpoints')
256+
.selectAll('path.legendwaterfall')
257+
.data(ptsData);
258+
pts.enter().append('path').classed('legendwaterfall', true)
259+
.attr('transform', 'translate(20,0)')
260+
.style('stroke-miterlimit', 1);
261+
pts.exit().remove();
262+
263+
pts.each(function(dd) {
264+
var pt = d3.select(this);
265+
var cont = trace[dd[0]].marker;
266+
267+
pt.attr('d', dd[1])
268+
.style('stroke-width', cont.line.width + 'px')
269+
.call(Color.fill, cont.color);
270+
271+
if(cont.line.width) {
272+
pt.call(Color.stroke, cont.line.color);
273+
}
274+
});
275+
}
276+
244277
function styleBars(d) {
245278
var trace = d[0].trace;
246279
var marker = trace.marker || {};

src/plot_api/helpers.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ var Registry = require('../registry');
1616
var Lib = require('../lib');
1717
var Plots = require('../plots/plots');
1818
var AxisIds = require('../plots/cartesian/axis_ids');
19-
var cleanId = AxisIds.cleanId;
20-
var getFromTrace = AxisIds.getFromTrace;
2119
var Color = require('../components/color');
2220

21+
var cleanId = AxisIds.cleanId;
22+
var getFromTrace = AxisIds.getFromTrace;
23+
var traceIs = Registry.traceIs;
2324

2425
// clear the promise queue if one of them got rejected
2526
exports.clearPromiseQueue = function(gd) {
@@ -290,7 +291,7 @@ exports.cleanData = function(data) {
290291
// error_y.opacity is obsolete - merge into color
291292
if(trace.error_y && 'opacity' in trace.error_y) {
292293
var dc = Color.defaults;
293-
var yeColor = trace.error_y.color || (Registry.traceIs(trace, 'bar') ?
294+
var yeColor = trace.error_y.color || (traceIs(trace, 'bar') ?
294295
Color.defaultLine :
295296
dc[tracei % dc.length]);
296297
trace.error_y.color = Color.addOpacity(
@@ -302,8 +303,8 @@ exports.cleanData = function(data) {
302303
// convert bardir to orientation, and put the data into
303304
// the axes it's eventually going to be used with
304305
if('bardir' in trace) {
305-
if(trace.bardir === 'h' && (Registry.traceIs(trace, 'bar') ||
306-
trace.type.substr(0, 9) === 'histogram')) {
306+
if(trace.bardir === 'h' && (traceIs(trace, 'bar') ||
307+
trace.type.substr(0, 9) === 'histogram')) {
307308
trace.orientation = 'h';
308309
exports.swapXYData(trace);
309310
}
@@ -332,11 +333,11 @@ exports.cleanData = function(data) {
332333
if(trace.yaxis) trace.yaxis = cleanId(trace.yaxis, 'y');
333334

334335
// scene ids scene1 -> scene
335-
if(Registry.traceIs(trace, 'gl3d') && trace.scene) {
336+
if(traceIs(trace, 'gl3d') && trace.scene) {
336337
trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene);
337338
}
338339

339-
if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) {
340+
if(!traceIs(trace, 'pie') && !traceIs(trace, 'bar') && trace.type !== 'waterfall') {
340341
if(Array.isArray(trace.textposition)) {
341342
for(i = 0; i < trace.textposition.length; i++) {
342343
trace.textposition[i] = cleanTextPosition(trace.textposition[i]);

src/plots/cartesian/axes.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -2847,14 +2847,16 @@ function hasBarsOrFill(gd, ax) {
28472847
for(var i = 0; i < fullData.length; i++) {
28482848
var trace = fullData[i];
28492849

2850-
if(trace.visible === true &&
2851-
(trace.xaxis + trace.yaxis) === subplot &&
2852-
(
2853-
Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axLetter] ||
2854-
trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter
2855-
)
2856-
) {
2857-
return true;
2850+
if(trace.visible === true && (trace.xaxis + trace.yaxis) === subplot) {
2851+
if(
2852+
(Registry.traceIs(trace, 'bar') || trace.type === 'waterfall') &&
2853+
trace.orientation === {x: 'h', y: 'v'}[axLetter]
2854+
) return true;
2855+
2856+
if(
2857+
trace.fill &&
2858+
trace.fill.charAt(trace.fill.length - 1) === axLetter
2859+
) return true;
28582860
}
28592861
}
28602862
return false;

src/plots/cartesian/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ module.exports = {
6565
traceLayerClasses: [
6666
'heatmaplayer',
6767
'contourcarpetlayer', 'contourlayer',
68-
'barlayer',
68+
'waterfalllayer', 'barlayer',
6969
'carpetlayer',
7070
'violinlayer',
7171
'boxlayer',

src/plots/cartesian/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback
261261
);
262262

263263
// layers that allow `cliponaxis: false`
264-
if(className !== 'scatterlayer' && className !== 'barlayer') {
264+
if(className !== 'scatterlayer' && className !== 'barlayer' && className !== 'waterfalllayer') {
265265
Drawing.setClipUrl(sel, plotinfo.layerClipId, gd);
266266
}
267267
});
@@ -277,7 +277,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback
277277
if(!gd._context.staticPlot) {
278278
if(plotinfo._hasClipOnAxisFalse) {
279279
plotinfo.clipOnAxisFalseTraces = plotinfo.plot
280-
.selectAll('.scatterlayer, .barlayer')
280+
.selectAll('.scatterlayer, .barlayer, .waterfalllayer')
281281
.selectAll('.trace');
282282
}
283283

src/traces/bar/arrays_to_calcdata.js

-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

1211
var mergeArray = require('../../lib').mergeArray;
1312

14-
1513
// arrayOk attributes, merge them into calcdata array
1614
module.exports = function arraysToCalcdata(cd, trace) {
1715
for(var i = 0; i < cd.length; i++) cd[i].i = i;

src/traces/bar/cross_trace_calc.js

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ function setOffsetAndWidth(gd, pa, sieve) {
237237
var fullLayout = gd._fullLayout;
238238
var bargap = fullLayout.bargap;
239239
var bargroupgap = fullLayout.bargroupgap || 0;
240+
240241
var minDiff = sieve.minDiff;
241242
var calcTraces = sieve.traces;
242243

src/traces/bar/defaults.js

+45-37
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ var Color = require('../../components/color');
1313
var Registry = require('../../registry');
1414

1515
var handleXYDefaults = require('../scatter/xy_defaults');
16-
var handleStyleDefaults = require('../bar/style_defaults');
16+
var handleStyleDefaults = require('./style_defaults');
1717
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;
1818
var attributes = require('./attributes');
1919

20+
var coerceFont = Lib.coerceFont;
21+
2022
function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
2123
function coerce(attr, dflt) {
2224
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
2325
}
2426

25-
var coerceFont = Lib.coerceFont;
26-
2727
var len = handleXYDefaults(traceIn, traceOut, layout, coerce);
2828
if(!len) {
2929
traceOut.visible = false;
@@ -39,34 +39,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
3939
coerce('hovertext');
4040
coerce('hovertemplate');
4141

42-
var textPosition = coerce('textposition');
43-
44-
var hasBoth = Array.isArray(textPosition) || textPosition === 'auto';
45-
var hasInside = hasBoth || textPosition === 'inside';
46-
var hasOutside = hasBoth || textPosition === 'outside';
47-
48-
if(hasInside || hasOutside) {
49-
var textFont = coerceFont(coerce, 'textfont', layout.font);
50-
51-
// Note that coercing `insidetextfont` is always needed –
52-
// even if `textposition` is `outside` for each trace – since
53-
// an outside label can become an inside one, for example because
54-
// of a bar being stacked on top of it.
55-
var insideTextFontDefault = Lib.extendFlat({}, textFont);
56-
var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
57-
var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
58-
if(isColorInheritedFromLayoutFont) {
59-
delete insideTextFontDefault.color;
60-
}
61-
coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
62-
63-
if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont);
64-
65-
coerce('constraintext');
66-
coerce('selected.textfont.color');
67-
coerce('unselected.textfont.color');
68-
coerce('cliponaxis');
69-
}
42+
handleText(traceIn, traceOut, layout, coerce, true);
7043

7144
handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
7245

@@ -126,20 +99,55 @@ function crossTraceDefaults(fullData, fullLayout) {
12699
return Lib.coerce(traceOut._input, traceOut, attributes, attr);
127100
}
128101

129-
for(var i = 0; i < fullData.length; i++) {
130-
traceOut = fullData[i];
102+
if(fullLayout.barmode === 'group') {
103+
for(var i = 0; i < fullData.length; i++) {
104+
traceOut = fullData[i];
131105

132-
if(traceOut.type === 'bar') {
133-
traceIn = traceOut._input;
134-
if(fullLayout.barmode === 'group') {
106+
if(traceOut.type === 'bar') {
107+
traceIn = traceOut._input;
135108
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
136109
}
137110
}
138111
}
139112
}
140113

114+
function handleText(traceIn, traceOut, layout, coerce, moduleHasSelUnselected) {
115+
var textPosition = coerce('textposition');
116+
var hasBoth = Array.isArray(textPosition) || textPosition === 'auto';
117+
var hasInside = hasBoth || textPosition === 'inside';
118+
var hasOutside = hasBoth || textPosition === 'outside';
119+
120+
if(hasInside || hasOutside) {
121+
var textFont = coerceFont(coerce, 'textfont', layout.font);
122+
123+
// Note that coercing `insidetextfont` is always needed –
124+
// even if `textposition` is `outside` for each trace – since
125+
// an outside label can become an inside one, for example because
126+
// of a bar being stacked on top of it.
127+
var insideTextFontDefault = Lib.extendFlat({}, textFont);
128+
var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
129+
var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
130+
if(isColorInheritedFromLayoutFont) {
131+
delete insideTextFontDefault.color;
132+
}
133+
coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
134+
135+
if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont);
136+
137+
coerce('constraintext');
138+
139+
if(moduleHasSelUnselected) {
140+
coerce('selected.textfont.color');
141+
coerce('unselected.textfont.color');
142+
}
143+
144+
coerce('cliponaxis');
145+
}
146+
}
147+
141148
module.exports = {
142149
supplyDefaults: supplyDefaults,
143150
crossTraceDefaults: crossTraceDefaults,
144-
handleGroupingDefaults: handleGroupingDefaults
151+
handleGroupingDefaults: handleGroupingDefaults,
152+
handleText: handleText
145153
};

src/traces/bar/hover.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ var Color = require('../../components/color');
1515
var fillHoverText = require('../scatter/fill_hover_text');
1616

1717
function hoverPoints(pointData, xval, yval, hovermode) {
18+
var barPointData = hoverOnBars(pointData, xval, yval, hovermode);
19+
20+
if(barPointData) {
21+
var cd = barPointData.cd;
22+
var trace = cd[0].trace;
23+
var di = cd[barPointData.index];
24+
25+
barPointData.color = getTraceColor(trace, di);
26+
Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, barPointData);
27+
28+
return [barPointData];
29+
}
30+
}
31+
32+
function hoverOnBars(pointData, xval, yval, hovermode) {
1833
var cd = pointData.cd;
1934
var trace = cd[0].trace;
2035
var t = cd[0].t;
@@ -133,12 +148,10 @@ function hoverPoints(pointData, xval, yval, hovermode) {
133148
// in case of bars shifted within groups
134149
pointData[posLetter + 'Spike'] = pa.c2p(di.p, true);
135150

136-
pointData.color = getTraceColor(trace, di);
137151
fillHoverText(di, trace, pointData);
138-
Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData);
139-
140152
pointData.hovertemplate = trace.hovertemplate;
141-
return [pointData];
153+
154+
return pointData;
142155
}
143156

144157
function getTraceColor(trace, di) {
@@ -152,5 +165,6 @@ function getTraceColor(trace, di) {
152165

153166
module.exports = {
154167
hoverPoints: hoverPoints,
168+
hoverOnBars: hoverOnBars,
155169
getTraceColor: getTraceColor
156170
};

0 commit comments

Comments
 (0)