diff --git a/lib/funnelarea.js b/lib/funnelarea.js
new file mode 100644
index 00000000000..c677c565879
--- /dev/null
+++ b/lib/funnelarea.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/funnelarea');
diff --git a/lib/index-finance.js b/lib/index-finance.js
index b1aa2ac0c73..4a590fde2bd 100644
--- a/lib/index-finance.js
+++ b/lib/index-finance.js
@@ -14,6 +14,7 @@ Plotly.register([
require('./bar'),
require('./histogram'),
require('./pie'),
+ require('./funnelarea'),
require('./ohlc'),
require('./candlestick'),
require('./funnel'),
diff --git a/lib/index.js b/lib/index.js
index 210bc566a93..c3f2ab67bdd 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -26,6 +26,7 @@ Plotly.register([
require('./pie'),
require('./sunburst'),
+ require('./funnelarea'),
require('./scatter3d'),
require('./surface'),
diff --git a/src/components/fx/calc.js b/src/components/fx/calc.js
index 4d397764e4f..2fb7193e8d1 100644
--- a/src/components/fx/calc.js
+++ b/src/components/fx/calc.js
@@ -28,7 +28,7 @@ module.exports = function calc(gd) {
// don't include hover calc fields for pie traces
// as calcdata items might be sorted by value and
// won't match the data array order.
- if(Registry.traceIs(trace, 'pie')) continue;
+ if(Registry.traceIs(trace, 'pie-like')) continue;
var fillFn = Registry.traceIs(trace, '2dMap') ? paste : Lib.fillArray;
diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js
index c08fdb2ec8d..955c94f9f53 100644
--- a/src/components/legend/defaults.js
+++ b/src/components/legend/defaults.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Registry = require('../../registry');
@@ -42,7 +41,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
legendReallyHasATrace = true;
// Always show the legend by default if there's a pie,
// or if there's only one trace but it's explicitly shown
- if(Registry.traceIs(trace, 'pie') ||
+ if(Registry.traceIs(trace, 'pie-like') ||
trace._input.showlegend === true
) {
legendTraceCount++;
diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js
index 50558b1513f..eb97fee7a87 100644
--- a/src/components/legend/draw.js
+++ b/src/components/legend/draw.js
@@ -58,8 +58,8 @@ module.exports = function draw(gd) {
for(var j = 0; j < legendData[i].length; j++) {
var item = legendData[i][j][0];
var trace = item.trace;
- var isPie = Registry.traceIs(trace, 'pie');
- var name = isPie ? item.label : trace.name;
+ var isPieLike = Registry.traceIs(trace, 'pie-like');
+ var name = isPieLike ? item.label : trace.name;
maxLength = Math.max(maxLength, name && name.length || 0);
}
}
@@ -110,7 +110,7 @@ module.exports = function draw(gd) {
traces.style('opacity', function(d) {
var trace = d[0].trace;
- if(Registry.traceIs(trace, 'pie')) {
+ if(Registry.traceIs(trace, 'pie-like')) {
return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
} else {
return trace.visible === 'legendonly' ? 0.5 : 1;
@@ -375,7 +375,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
if(trace._group) {
evtData.group = trace._group;
}
- if(trace.type === 'pie') {
+ if(Registry.traceIs(trace, 'pie-like')) {
evtData.label = legendItem.datum()[0].label;
}
@@ -399,11 +399,11 @@ function drawTexts(g, gd, maxLength) {
var legendItem = g.data()[0][0];
var fullLayout = gd._fullLayout;
var trace = legendItem.trace;
- var isPie = Registry.traceIs(trace, 'pie');
+ var isPieLike = Registry.traceIs(trace, 'pie-like');
var traceIndex = trace.index;
- var isEditable = gd._context.edits.legendText && !isPie;
+ var isEditable = gd._context.edits.legendText && !isPieLike;
- var name = isPie ? legendItem.label : trace.name;
+ var name = isPieLike ? legendItem.label : trace.name;
if(trace._meta) {
name = Lib.templateString(name, trace._meta);
}
diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js
index 2c2c7cd1190..0e9c4599d63 100644
--- a/src/components/legend/get_legend_data.js
+++ b/src/components/legend/get_legend_data.js
@@ -6,13 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Registry = require('../../registry');
var helpers = require('./helpers');
-
module.exports = function getLegendData(calcdata, opts) {
var lgroupToTraces = {};
var lgroups = [];
@@ -45,7 +43,7 @@ module.exports = function getLegendData(calcdata, opts) {
if(!trace.visible || !trace.showlegend) continue;
- if(Registry.traceIs(trace, 'pie')) {
+ if(Registry.traceIs(trace, 'pie-like')) {
if(!slicesShown[lgroup]) slicesShown[lgroup] = {};
for(j = 0; j < cd.length; j++) {
diff --git a/src/components/legend/handle_click.js b/src/components/legend/handle_click.js
index 6bc98641108..902223b2a73 100644
--- a/src/components/legend/handle_click.js
+++ b/src/components/legend/handle_click.js
@@ -104,7 +104,7 @@ module.exports = function handleClick(g, gd, numClicks) {
}
}
- if(Registry.traceIs(fullTrace, 'pie')) {
+ if(Registry.traceIs(fullTrace, 'pie-like')) {
var thisLabel = legendItem.label;
var thisLabelIndex = hiddenSlices.indexOf(thisLabel);
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index 9d3c7077af9..75f8abb4dc7 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -87,6 +87,7 @@ module.exports = function style(s, gd) {
.each(styleFunnels)
.each(styleBars)
.each(styleBoxes)
+ .each(styleFunnelareas)
.each(stylePies)
.each(styleLines)
.each(stylePoints)
@@ -307,14 +308,14 @@ module.exports = function style(s, gd) {
}
function styleBars(d) {
- styleBarFamily(d, this);
+ styleBarLike(d, this);
}
function styleFunnels(d) {
- styleBarFamily(d, this, 'funnel');
+ styleBarLike(d, this, 'funnel');
}
- function styleBarFamily(d, lThis, desiredType) {
+ function styleBarLike(d, lThis, desiredType) {
var trace = d[0].trace;
var marker = trace.marker || {};
var markerLine = marker.line || {};
@@ -435,13 +436,24 @@ module.exports = function style(s, gd) {
}
function stylePies(d) {
+ stylePieLike(d, this, 'pie');
+ }
+
+ function styleFunnelareas(d) {
+ stylePieLike(d, this, 'funnelarea');
+ }
+
+ function stylePieLike(d, lThis, desiredType) {
var d0 = d[0];
var trace = d0.trace;
- var pts = d3.select(this).select('g.legendpoints')
- .selectAll('path.legendpie')
- .data(Registry.traceIs(trace, 'pie') && trace.visible ? [d] : []);
- pts.enter().append('path').classed('legendpie', true)
+ var isVisible = (!desiredType) ? Registry.traceIs(trace, desiredType) :
+ (trace.type === desiredType && trace.visible);
+
+ var pts = d3.select(lThis).select('g.legendpoints')
+ .selectAll('path.legend' + desiredType)
+ .data(isVisible ? [d] : []);
+ pts.enter().append('path').classed('legend' + desiredType, true)
.attr('d', 'M6,6H-6V-6H6Z')
.attr('transform', 'translate(20,0)');
pts.exit().remove();
diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js
index 7a22f8f40a9..89e9ce3a6c1 100644
--- a/src/components/modebar/manage.js
+++ b/src/components/modebar/manage.js
@@ -80,6 +80,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) {
var hasGL3D = fullLayout._has('gl3d');
var hasGeo = fullLayout._has('geo');
var hasPie = fullLayout._has('pie');
+ var hasFunnelarea = fullLayout._has('funnelarea');
var hasGL2D = fullLayout._has('gl2d');
var hasTernary = fullLayout._has('ternary');
var hasMapbox = fullLayout._has('mapbox');
@@ -113,7 +114,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) {
var resetGroup = [];
var dragModeGroup = [];
- if((hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) {
+ if((hasCartesian || hasGL2D || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) {
// graphs with more than one plot types get 'union buttons'
// which reset the view or toggle hover labels across all subplots.
hoverGroup = ['toggleHover'];
diff --git a/src/lib/index.js b/src/lib/index.js
index 13cf3c86eb2..850b8aa88e6 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -1159,3 +1159,15 @@ lib.fillText = function(calcPt, trace, contOut) {
lib.isValidTextValue = function(v) {
return v || v === 0;
};
+
+lib.formatPercent = function(ratio, n) {
+ n = n || 0;
+ var str = (Math.round(100 * ratio * Math.pow(10, n)) * Math.pow(0.1, n)).toFixed(n) + '%';
+ for(var i = 0; i < n; i++) {
+ if(str.indexOf('.') !== -1) {
+ str = str.replace('0%', '%');
+ str = str.replace('.%', '%');
+ }
+ }
+ return str;
+};
diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index 45bd5b69835..8028d17211d 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var isNumeric = require('fast-isnumeric');
@@ -328,7 +327,7 @@ exports.cleanData = function(data) {
trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene);
}
- if(!traceIs(trace, 'pie') && !traceIs(trace, 'bar-like')) {
+ if(!traceIs(trace, 'pie-like') && !traceIs(trace, 'bar-like')) {
if(Array.isArray(trace.textposition)) {
for(i = 0; i < trace.textposition.length; i++) {
trace.textposition[i] = cleanTextPosition(trace.textposition[i]);
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index a48bd528ccd..33a1a000cb7 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -6,10 +6,8 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
-
var d3 = require('d3');
var isNumeric = require('fast-isnumeric');
var hasHover = require('has-hover');
@@ -1634,7 +1632,10 @@ function _restyle(gd, aobj, traces) {
doextra(prefixDot + 'len', innerContFull.len *
(newVal === 'fraction' ? 1 / lennorm : lennorm), i);
}
- } else if(ai === 'type' && (newVal === 'pie') !== (oldVal === 'pie')) {
+ } else if(ai === 'type' && (
+ (newVal === 'pie') !== (oldVal === 'pie') ||
+ (newVal === 'funnelarea') !== (oldVal === 'funnelarea')
+ )) {
var labelsTo = 'x';
var valuesTo = 'y';
if((newVal === 'bar' || oldVal === 'bar') && cont.orientation === 'h') {
@@ -1645,7 +1646,7 @@ function _restyle(gd, aobj, traces) {
Lib.swapAttrs(cont, ['d?', '?0'], 'label', labelsTo);
Lib.swapAttrs(cont, ['?', '?src'], 'values', valuesTo);
- if(oldVal === 'pie') {
+ if(oldVal === 'pie' || oldVal === 'funnelarea') {
nestedProperty(cont, 'marker.color')
.set(nestedProperty(cont, 'marker.colors').get());
@@ -3769,10 +3770,13 @@ function makePlotFramework(gd) {
// single geo layer for the whole plot
fullLayout._geolayer = fullLayout._paper.append('g').classed('geolayer', true);
+ // single funnelarea layer for the whole plot
+ fullLayout._funnelarealayer = fullLayout._paper.append('g').classed('funnelarealayer', true);
+
// single pie layer for the whole plot
fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);
- // single sunbursrt layer for the whole plot
+ // single sunburst layer for the whole plot
fullLayout._sunburstlayer = fullLayout._paper.append('g').classed('sunburstlayer', true);
// fill in image server scrape-svg
diff --git a/src/plots/plots.js b/src/plots/plots.js
index c41cdd046d0..ae82ecaa62f 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -2758,9 +2758,10 @@ plots.doCalcdata = function(gd, traces) {
gd._hmpixcount = 0;
gd._hmlumcount = 0;
- // for sharing colors across pies / sunbursts (and for legend)
+ // for sharing colors across pies / sunbursts / funnelarea (and for legend)
fullLayout._piecolormap = {};
fullLayout._sunburstcolormap = {};
+ fullLayout._funnelareacolormap = {};
// If traces were specified and this trace was not included,
// then transfer it over from the old calcdata:
diff --git a/src/snapshot/cloneplot.js b/src/snapshot/cloneplot.js
index 239ad267dc2..2fc8773d6d2 100644
--- a/src/snapshot/cloneplot.js
+++ b/src/snapshot/cloneplot.js
@@ -6,9 +6,9 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
+var Registry = require('../registry');
var Lib = require('../lib');
var extendFlat = Lib.extendFlat;
@@ -89,7 +89,7 @@ module.exports = function clonePlot(graphObj, options) {
var trace = newData[i];
trace.showscale = false;
if(trace.marker) trace.marker.showscale = false;
- if(trace.type === 'pie') trace.textposition = 'none';
+ if(Registry.traceIs(trace, 'pie-like')) trace.textposition = 'none';
}
}
diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js
index 180ec4cdcf7..5c391df606f 100644
--- a/src/traces/bar/attributes.js
+++ b/src/traces/bar/attributes.js
@@ -88,7 +88,7 @@ module.exports = {
valType: 'enumerated',
values: ['end', 'middle', 'start'],
dflt: 'end',
- role: 'info', // TODO: or style ?
+ role: 'info',
editType: 'plot',
description: [
'Determines if texts are kept at center or start/end points in `textposition` *inside* mode.'
@@ -98,7 +98,7 @@ module.exports = {
textangle: {
valType: 'angle',
dflt: 'auto',
- role: 'info', // TODO: or style ?
+ role: 'info',
editType: 'plot',
description: [
'Sets the angle of the tick labels with respect to the bar.',
diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js
index a570b50325a..e6df70f4562 100644
--- a/src/traces/bar/defaults.js
+++ b/src/traces/bar/defaults.js
@@ -39,7 +39,15 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
coerce('hovertext');
coerce('hovertemplate');
- handleText(traceIn, traceOut, layout, coerce, true);
+ var textposition = coerce('textposition');
+ handleText(traceIn, traceOut, layout, coerce, textposition, {
+ moduleHasSelected: true,
+ moduleHasUnselected: true,
+ moduleHasConstrain: true,
+ moduleHasCliponaxis: true,
+ moduleHasTextangle: true,
+ moduleHasInsideanchor: true
+ });
handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
@@ -111,20 +119,27 @@ function crossTraceDefaults(fullData, fullLayout) {
}
}
-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';
+function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
+ opts = opts || {};
+ var moduleHasSelected = !(opts.moduleHasSelected === false);
+ var moduleHasUnselected = !(opts.moduleHasUnselected === false);
+ var moduleHasConstrain = !(opts.moduleHasConstrain === false);
+ var moduleHasCliponaxis = !(opts.moduleHasCliponaxis === false);
+ var moduleHasTextangle = !(opts.moduleHasTextangle === false);
+ var moduleHasInsideanchor = !(opts.moduleHasInsideanchor === false);
+
+ 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);
+ var dfltFont = 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 insideTextFontDefault = Lib.extendFlat({}, dfltFont);
var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
if(isColorInheritedFromLayoutFont) {
@@ -132,21 +147,18 @@ function handleText(traceIn, traceOut, layout, coerce, moduleHasSelUnselected) {
}
coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
- if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont);
-
- coerce('constraintext');
+ if(hasOutside) coerceFont(coerce, 'outsidetextfont', dfltFont);
- if(moduleHasSelUnselected) {
- coerce('selected.textfont.color');
- coerce('unselected.textfont.color');
- }
- coerce('cliponaxis');
- coerce('textangle');
+ if(moduleHasSelected) coerce('selected.textfont.color');
+ if(moduleHasUnselected) coerce('unselected.textfont.color');
+ if(moduleHasConstrain) coerce('constraintext');
+ if(moduleHasCliponaxis) coerce('cliponaxis');
+ if(moduleHasTextangle) coerce('textangle');
}
if(hasInside) {
- coerce('insidetextanchor');
+ if(moduleHasInsideanchor) coerce('insidetextanchor');
}
}
diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js
index dd9a8bbf72b..5560603cee6 100644
--- a/src/traces/bar/index.js
+++ b/src/traces/bar/index.js
@@ -8,34 +8,32 @@
'use strict';
-var Bar = {};
+module.exports = {
+ attributes: require('./attributes'),
+ layoutAttributes: require('./layout_attributes'),
+ supplyDefaults: require('./defaults').supplyDefaults,
+ crossTraceDefaults: require('./defaults').crossTraceDefaults,
+ supplyLayoutDefaults: require('./layout_defaults'),
+ calc: require('./calc'),
+ crossTraceCalc: require('./cross_trace_calc').crossTraceCalc,
+ colorbar: require('../scatter/marker_colorbar'),
+ arraysToCalcdata: require('./arrays_to_calcdata'),
+ plot: require('./plot').plot,
+ style: require('./style').style,
+ styleOnSelect: require('./style').styleOnSelect,
+ hoverPoints: require('./hover').hoverPoints,
+ selectPoints: require('./select'),
-Bar.attributes = require('./attributes');
-Bar.layoutAttributes = require('./layout_attributes');
-Bar.supplyDefaults = require('./defaults').supplyDefaults;
-Bar.crossTraceDefaults = require('./defaults').crossTraceDefaults;
-Bar.supplyLayoutDefaults = require('./layout_defaults');
-Bar.calc = require('./calc');
-Bar.crossTraceCalc = require('./cross_trace_calc').crossTraceCalc;
-Bar.colorbar = require('../scatter/marker_colorbar');
-Bar.arraysToCalcdata = require('./arrays_to_calcdata');
-Bar.plot = require('./plot');
-Bar.style = require('./style').style;
-Bar.styleOnSelect = require('./style').styleOnSelect;
-Bar.hoverPoints = require('./hover').hoverPoints;
-Bar.selectPoints = require('./select');
-
-Bar.moduleType = 'trace';
-Bar.name = 'bar';
-Bar.basePlotModule = require('../../plots/cartesian');
-Bar.categories = ['bar-like', 'cartesian', 'svg', 'bar', 'oriented', 'errorBarsOK', 'showLegend', 'zoomScale'];
-Bar.meta = {
- description: [
- '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(' ')
+ moduleType: 'trace',
+ name: 'bar',
+ basePlotModule: require('../../plots/cartesian'),
+ categories: ['bar-like', 'cartesian', 'svg', 'bar', 'oriented', 'errorBarsOK', 'showLegend', 'zoomScale'],
+ meta: {
+ description: [
+ '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 = Bar;
diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js
index 31cc64ed3ff..778ac7ebd0b 100644
--- a/src/traces/bar/plot.js
+++ b/src/traces/bar/plot.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var d3 = require('d3');
@@ -50,7 +49,7 @@ function getXY(di, xa, ya, isHorizontal) {
return isHorizontal ? [s, p] : [p, s];
}
-module.exports = function plot(gd, plotinfo, cdModule, traceLayer, opts) {
+function plot(gd, plotinfo, cdModule, traceLayer, opts) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
var fullLayout = gd._fullLayout;
@@ -193,7 +192,7 @@ module.exports = function plot(gd, plotinfo, cdModule, traceLayer, opts) {
// error bars are on the top
Registry.getComponentMethod('errorbars', 'plot')(gd, bartraces, plotinfo);
-};
+}
function appendBarText(gd, plotinfo, bar, calcTrace, i, x0, x1, y0, y1, opts) {
var xa = plotinfo.xaxis;
@@ -337,15 +336,22 @@ function appendBarText(gd, plotinfo, bar, calcTrace, i, x0, x1, y0, y1, opts) {
trace.constraintext === 'both' ||
trace.constraintext === 'outside';
- transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB,
- isHorizontal, constrained, trace.textangle);
+ transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, {
+ isHorizontal: isHorizontal,
+ constrained: constrained,
+ angle: trace.textangle
+ });
} else {
constrained =
trace.constraintext === 'both' ||
trace.constraintext === 'inside';
- transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB,
- isHorizontal, constrained, trace.textangle, trace.insidetextanchor);
+ transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, {
+ isHorizontal: isHorizontal,
+ constrained: constrained,
+ angle: trace.textangle,
+ anchor: trace.insidetextanchor
+ });
}
textSelection.attr('transform', transform);
@@ -355,7 +361,12 @@ function getRotationFromAngle(angle) {
return (angle === 'auto') ? 0 : angle;
}
-function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, isHorizontal, constrained, angle, anchor) {
+function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, opts) {
+ var isHorizontal = !!opts.isHorizontal;
+ var constrained = !!opts.constrained;
+ var angle = opts.angle || 0;
+ var anchor = opts.anchor || 0;
+
var textWidth = textBB.width;
var textHeight = textBB.height;
var lx = Math.abs(x1 - x0);
@@ -428,7 +439,11 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, isHorizontal, const
return getTransform(textX, textY, targetX, targetY, scale, rotation);
}
-function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, isHorizontal, constrained, angle) {
+function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, opts) {
+ var isHorizontal = !!opts.isHorizontal;
+ var constrained = !!opts.constrained;
+ var angle = opts.angle || 0;
+
var textWidth = textBB.width;
var textHeight = textBB.height;
var lx = Math.abs(x1 - x0);
@@ -570,17 +585,17 @@ function calcTextinfo(calcTrace, index, xa, ya) {
var hasMultiplePercents = nPercent > 1;
if(hasFlag('percent initial')) {
- tx = formatPercent(cdi.begR);
+ tx = Lib.formatPercent(cdi.begR);
if(hasMultiplePercents) tx += ' of initial';
text.push(tx);
}
if(hasFlag('percent previous')) {
- tx = formatPercent(cdi.difR);
+ tx = Lib.formatPercent(cdi.difR);
if(hasMultiplePercents) tx += ' of previous';
text.push(tx);
}
if(hasFlag('percent total')) {
- tx = formatPercent(cdi.sumR);
+ tx = Lib.formatPercent(cdi.sumR);
if(hasMultiplePercents) tx += ' of total';
text.push(tx);
}
@@ -589,6 +604,8 @@ function calcTextinfo(calcTrace, index, xa, ya) {
return text.join('
');
}
-function formatPercent(ratio) {
- return Math.round(100 * ratio) + '%';
-}
+module.exports = {
+ plot: plot,
+ getTransformToMoveInsideBar: getTransformToMoveInsideBar,
+ getTransformToMoveOutsideBar: getTransformToMoveOutsideBar
+};
diff --git a/src/traces/funnel/defaults.js b/src/traces/funnel/defaults.js
index 6e8a0d8a62a..f4ae0a1fd71 100644
--- a/src/traces/funnel/defaults.js
+++ b/src/traces/funnel/defaults.js
@@ -36,7 +36,15 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
coerce('hovertext');
coerce('hovertemplate');
- handleText(traceIn, traceOut, layout, coerce, false);
+ var textposition = coerce('textposition');
+ handleText(traceIn, traceOut, layout, coerce, textposition, {
+ moduleHasSelected: false,
+ moduleHasUnselected: false,
+ moduleHasConstrain: true,
+ moduleHasCliponaxis: true,
+ moduleHasTextangle: true,
+ moduleHasInsideanchor: true
+ });
if(traceOut.textposition !== 'none') {
coerce('textinfo', Array.isArray(text) ? 'text+value' : 'value');
diff --git a/src/traces/funnel/hover.js b/src/traces/funnel/hover.js
index a31455f0efc..42935a87412 100644
--- a/src/traces/funnel/hover.js
+++ b/src/traces/funnel/hover.js
@@ -10,6 +10,7 @@
var opacity = require('../../components/color').opacity;
var hoverOnBars = require('../bar/hover').hoverOnBars;
+var formatPercent = require('../../lib').formatPercent;
module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
var point = hoverOnBars(pointData, xval, yval, hovermode);
@@ -29,9 +30,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
// display ratio to initial value
point.extraText = [
- formatPercent(di.begR) + ' of initial',
- formatPercent(di.difR) + ' of previous',
- formatPercent(di.sumR) + ' of total'
+ formatPercent(di.begR, 1) + ' of initial',
+ formatPercent(di.difR, 1) + ' of previous',
+ formatPercent(di.sumR, 1) + ' of total'
].join('
');
// TODO: Should we use pieHelpers.formatPieValue instead ?
@@ -48,7 +49,3 @@ function getTraceColor(trace, di) {
if(opacity(mc)) return mc;
else if(opacity(mlc) && mlw) return mlc;
}
-
-function formatPercent(ratio) {
- return ((Math.round(1000 * ratio) * 0.1).toFixed(1) + '%').replace('.0%', '%');
-}
diff --git a/src/traces/funnel/plot.js b/src/traces/funnel/plot.js
index 70523e5b676..39290306f82 100644
--- a/src/traces/funnel/plot.js
+++ b/src/traces/funnel/plot.js
@@ -11,7 +11,7 @@
var d3 = require('d3');
var Lib = require('../../lib');
var Drawing = require('../../components/drawing');
-var barPlot = require('../bar/plot');
+var barPlot = require('../bar/plot').plot;
module.exports = function plot(gd, plotinfo, cdModule, traceLayer) {
var fullLayout = gd._fullLayout;
diff --git a/src/traces/funnelarea/attributes.js b/src/traces/funnelarea/attributes.js
new file mode 100644
index 00000000000..7c995d007fb
--- /dev/null
+++ b/src/traces/funnelarea/attributes.js
@@ -0,0 +1,106 @@
+/**
+* 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 pieAttrs = require('../pie/attributes');
+var plotAttrs = require('../../plots/attributes');
+var domainAttrs = require('../../plots/domain').attributes;
+var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
+
+var extendFlat = require('../../lib/extend').extendFlat;
+
+module.exports = {
+ labels: pieAttrs.labels,
+ // equivalent of x0 and dx, if label is missing
+ label0: pieAttrs.label0,
+ dlabel: pieAttrs.dlabel,
+ values: pieAttrs.values,
+
+ marker: {
+ colors: pieAttrs.marker.colors,
+ line: {
+ color: extendFlat({}, pieAttrs.marker.line.color, {
+ dflt: null,
+ description: [
+ 'Sets the color of the line enclosing each sector.',
+ 'Defaults to the `paper_bgcolor` value.'
+ ].join(' ')
+ }),
+ width: extendFlat({}, pieAttrs.marker.line.width, {dflt: 1}),
+ editType: 'calc'
+ },
+ editType: 'calc'
+ },
+
+ text: pieAttrs.text,
+ hovertext: pieAttrs.hovertext,
+
+ scalegroup: extendFlat({}, pieAttrs.scalegroup, {
+ description: [
+ 'If there are multiple funnelareas that should be sized according to',
+ 'their totals, link them by providing a non-empty group id here',
+ 'shared by every trace in the same group.'
+ ].join(' ')
+ }),
+
+ textinfo: extendFlat({}, pieAttrs.textinfo, {
+ flags: ['label', 'text', 'value', 'percent']
+ }),
+
+ hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
+ flags: ['label', 'text', 'value', 'percent', 'name']
+ }),
+
+ hovertemplate: hovertemplateAttrs({}, {
+ keys: ['label', 'color', 'value', 'percent', 'text']
+ }),
+
+ textposition: extendFlat({}, pieAttrs.textposition, {
+ values: ['inside', 'none'],
+ dflt: 'inside'
+ }),
+
+ textfont: pieAttrs.textfont,
+ insidetextfont: pieAttrs.insidetextfont,
+
+ title: {
+ text: pieAttrs.title.text,
+ font: pieAttrs.title.font,
+ position: extendFlat({}, pieAttrs.title.position, {
+ values: ['top left', 'top center', 'top right'],
+ dflt: 'top center'
+ }),
+ editType: 'plot'
+ },
+
+ domain: domainAttrs({name: 'funnelarea', trace: true, editType: 'calc'}),
+
+ aspectratio: {
+ valType: 'number',
+ role: 'info',
+ min: 0,
+ dflt: 1,
+ editType: 'plot',
+ description: [
+ 'Sets the ratio between height and width'
+ ].join(' ')
+ },
+
+ baseratio: {
+ valType: 'number',
+ role: 'info',
+ min: 0,
+ max: 1,
+ dflt: 0.333,
+ editType: 'plot',
+ description: [
+ 'Sets the ratio between bottom length and maximum top length.'
+ ].join(' ')
+ }
+};
diff --git a/src/traces/funnelarea/base_plot.js b/src/traces/funnelarea/base_plot.js
new file mode 100644
index 00000000000..739fbc3050e
--- /dev/null
+++ b/src/traces/funnelarea/base_plot.js
@@ -0,0 +1,29 @@
+/**
+* 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 Registry = require('../../registry');
+var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
+
+exports.name = 'funnelarea';
+
+exports.plot = function(gd) {
+ var Funnelarea = Registry.getModule('funnelarea');
+ var cdFunnelarea = getModuleCalcData(gd.calcdata, Funnelarea)[0];
+ Funnelarea.plot(gd, cdFunnelarea);
+};
+
+exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
+ var hadFunnelarea = (oldFullLayout._has && oldFullLayout._has('funnelarea'));
+ var hasFunnelarea = (newFullLayout._has && newFullLayout._has('funnelarea'));
+
+ if(hadFunnelarea && !hasFunnelarea) {
+ oldFullLayout._funnelarealayer.selectAll('g.trace').remove();
+ }
+};
diff --git a/src/traces/funnelarea/calc.js b/src/traces/funnelarea/calc.js
new file mode 100644
index 00000000000..994c80c297a
--- /dev/null
+++ b/src/traces/funnelarea/calc.js
@@ -0,0 +1,24 @@
+/**
+* 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 pieCalc = require('../pie/calc');
+
+function calc(gd, trace) {
+ return pieCalc.calc(gd, trace);
+}
+
+function crossTraceCalc(gd) {
+ pieCalc.crossTraceCalc(gd, { type: 'funnelarea' });
+}
+
+module.exports = {
+ calc: calc,
+ crossTraceCalc: crossTraceCalc
+};
diff --git a/src/traces/funnelarea/defaults.js b/src/traces/funnelarea/defaults.js
new file mode 100644
index 00000000000..ae20aba2d6f
--- /dev/null
+++ b/src/traces/funnelarea/defaults.js
@@ -0,0 +1,75 @@
+/**
+* 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 attributes = require('./attributes');
+var handleDomainDefaults = require('../../plots/domain').defaults;
+var handleText = require('../bar/defaults').handleText;
+
+module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
+ }
+
+ var len;
+ var vals = coerce('values');
+ var hasVals = Lib.isArrayOrTypedArray(vals);
+ var labels = coerce('labels');
+ if(Array.isArray(labels)) {
+ len = labels.length;
+ if(hasVals) len = Math.min(len, vals.length);
+ } else if(hasVals) {
+ len = vals.length;
+
+ coerce('label0');
+ coerce('dlabel');
+ }
+
+ if(!len) {
+ traceOut.visible = false;
+ return;
+ }
+ traceOut._length = len;
+
+ var lineWidth = coerce('marker.line.width');
+ if(lineWidth) coerce('marker.line.color', layout.paper_bgcolor);
+
+ coerce('marker.colors');
+
+ coerce('scalegroup');
+
+ var textData = coerce('text');
+ var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent');
+ coerce('hovertext');
+ coerce('hovertemplate');
+
+ if(textInfo && textInfo !== 'none') {
+ var textposition = coerce('textposition');
+ handleText(traceIn, traceOut, layout, coerce, textposition, {
+ moduleHasSelected: false,
+ moduleHasUnselected: false,
+ moduleHasConstrain: false,
+ moduleHasCliponaxis: false,
+ moduleHasTextangle: false,
+ moduleHasInsideanchor: false
+ });
+ }
+
+ handleDomainDefaults(traceOut, layout, coerce);
+
+ var title = coerce('title.text');
+ if(title) {
+ coerce('title.position');
+ Lib.coerceFont(coerce, 'title.font', layout.font);
+ }
+
+ coerce('aspectratio');
+ coerce('baseratio');
+};
diff --git a/src/traces/funnelarea/index.js b/src/traces/funnelarea/index.js
new file mode 100644
index 00000000000..f8b6790e736
--- /dev/null
+++ b/src/traces/funnelarea/index.js
@@ -0,0 +1,34 @@
+/**
+* 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 = {
+ moduleType: 'trace',
+ name: 'funnelarea',
+ basePlotModule: require('./base_plot'),
+ categories: ['pie-like', 'funnelarea', 'showLegend'],
+
+ attributes: require('./attributes'),
+ layoutAttributes: require('./layout_attributes'),
+ supplyDefaults: require('./defaults'),
+ supplyLayoutDefaults: require('./layout_defaults'),
+
+ calc: require('./calc').calc,
+ crossTraceCalc: require('./calc').crossTraceCalc,
+
+ plot: require('./plot'),
+ style: require('./style'),
+ styleOne: require('../pie/style_one'),
+
+ meta: {
+ description: [
+ 'TODO'
+ ].join(' ')
+ }
+};
diff --git a/src/traces/funnelarea/layout_attributes.js b/src/traces/funnelarea/layout_attributes.js
new file mode 100644
index 00000000000..dc19b922a68
--- /dev/null
+++ b/src/traces/funnelarea/layout_attributes.js
@@ -0,0 +1,43 @@
+/**
+* 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 hiddenlabels = require('../pie/layout_attributes').hiddenlabels;
+
+module.exports = {
+ hiddenlabels: hiddenlabels,
+
+ funnelareacolorway: {
+ valType: 'colorlist',
+ role: 'style',
+ editType: 'calc',
+ description: [
+ 'Sets the default funnelarea slice colors. Defaults to the main',
+ '`colorway` used for trace colors. If you specify a new',
+ 'list here it can still be extended with lighter and darker',
+ 'colors, see `extendfunnelareacolors`.'
+ ].join(' ')
+ },
+ extendfunnelareacolors: {
+ valType: 'boolean',
+ dflt: true,
+ role: 'style',
+ editType: 'calc',
+ description: [
+ 'If `true`, the funnelarea slice colors (whether given by `funnelareacolorway` or',
+ 'inherited from `colorway`) will be extended to three times its',
+ 'original length by first repeating every color 20% lighter then',
+ 'each color 20% darker. This is intended to reduce the likelihood',
+ 'of reusing the same color when you have many slices, but you can',
+ 'set `false` to disable.',
+ 'Colors provided in the trace, using `marker.colors`, are never',
+ 'extended.'
+ ].join(' ')
+ }
+};
diff --git a/src/traces/funnelarea/layout_defaults.js b/src/traces/funnelarea/layout_defaults.js
new file mode 100644
index 00000000000..5665bf6cd07
--- /dev/null
+++ b/src/traces/funnelarea/layout_defaults.js
@@ -0,0 +1,23 @@
+/**
+* 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 supplyLayoutDefaults(layoutIn, layoutOut) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
+ }
+
+ coerce('hiddenlabels');
+ coerce('funnelareacolorway', layoutOut.colorway);
+ coerce('extendfunnelareacolors');
+};
diff --git a/src/traces/funnelarea/plot.js b/src/traces/funnelarea/plot.js
new file mode 100644
index 00000000000..4e0667fa96e
--- /dev/null
+++ b/src/traces/funnelarea/plot.js
@@ -0,0 +1,284 @@
+/**
+* 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 Lib = require('../../lib');
+var svgTextUtils = require('../../lib/svg_text_utils');
+
+var barPlot = require('../bar/plot');
+var getTransformToMoveInsideBar = barPlot.getTransformToMoveInsideBar;
+
+var pieHelpers = require('../pie/helpers');
+var piePlot = require('../pie/plot');
+
+var attachFxHandlers = piePlot.attachFxHandlers;
+var determineInsideTextFont = piePlot.determineInsideTextFont;
+
+var layoutAreas = piePlot.layoutAreas;
+var prerenderTitles = piePlot.prerenderTitles;
+var positionTitleOutside = piePlot.positionTitleOutside;
+
+module.exports = function plot(gd, cdModule) {
+ var fullLayout = gd._fullLayout;
+
+ prerenderTitles(cdModule, gd);
+ layoutAreas(cdModule, fullLayout._size);
+
+ Lib.makeTraceGroups(fullLayout._funnelarealayer, cdModule, 'trace').each(function(cd) {
+ var plotGroup = d3.select(this);
+ var cd0 = cd[0];
+ var trace = cd0.trace;
+
+ setCoords(cd);
+
+ plotGroup.each(function() {
+ var slices = d3.select(this).selectAll('g.slice').data(cd);
+
+ slices.enter().append('g')
+ .classed('slice', true);
+ slices.exit().remove();
+
+ slices.each(function(pt) {
+ if(pt.hidden) {
+ d3.select(this).selectAll('path,g').remove();
+ return;
+ }
+
+ // to have consistent event data compared to other traces
+ pt.pointNumber = pt.i;
+ pt.curveNumber = trace.index;
+
+ var cx = cd0.cx;
+ var cy = cd0.cy;
+ var sliceTop = d3.select(this);
+ var slicePath = sliceTop.selectAll('path.surface').data([pt]);
+
+ slicePath.enter().append('path')
+ .classed('surface', true)
+ .style({'pointer-events': 'all'});
+
+ sliceTop.call(attachFxHandlers, gd, cd);
+
+ var shape =
+ 'M' + (cx + pt.TR[0]) + ',' + (cy + pt.TR[1]) +
+ line(pt.TR, pt.BR) +
+ line(pt.BR, pt.BL) +
+ line(pt.BL, pt.TL) +
+ 'Z';
+
+ slicePath.attr('d', shape);
+
+ // add text
+ var textPosition = pieHelpers.castOption(trace.textposition, pt.pts);
+ var sliceTextGroup = sliceTop.selectAll('g.slicetext')
+ .data(pt.text && (textPosition !== 'none') ? [0] : []);
+
+ sliceTextGroup.enter().append('g')
+ .classed('slicetext', true);
+ sliceTextGroup.exit().remove();
+
+ sliceTextGroup.each(function() {
+ var sliceText = Lib.ensureSingle(d3.select(this), 'text', '', function(s) {
+ // prohibit tex interpretation until we can handle
+ // tex and regular text together
+ s.attr('data-notex', 1);
+ });
+
+ sliceText.text(pt.text)
+ .attr({
+ 'class': 'slicetext',
+ transform: '',
+ 'text-anchor': 'middle'
+ })
+ .call(Drawing.font, determineInsideTextFont(trace, pt, gd._fullLayout.font))
+ .call(svgTextUtils.convertToTspans, gd);
+
+ // position the text relative to the slice
+ var textBB = Drawing.bBox(sliceText.node());
+ var transform;
+
+ var x0, x1;
+ var y0 = Math.min(pt.BL[1], pt.BR[1]);
+ var y1 = Math.max(pt.TL[1], pt.TR[1]);
+
+ x0 = Math.max(pt.TL[0], pt.BL[0]);
+ x1 = Math.min(pt.TR[0], pt.BR[0]);
+
+ transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, {
+ isHorizontal: true,
+ constrained: true,
+ angle: 0,
+ anchor: 'middle'
+ });
+
+ sliceText.attr('transform',
+ 'translate(' + cx + ',' + cy + ')' + transform
+ );
+ });
+ });
+
+ // add the title
+ var titleTextGroup = d3.select(this).selectAll('g.titletext')
+ .data(trace.title.text ? [0] : []);
+
+ titleTextGroup.enter().append('g')
+ .classed('titletext', true);
+ titleTextGroup.exit().remove();
+
+ titleTextGroup.each(function() {
+ var titleText = Lib.ensureSingle(d3.select(this), 'text', '', function(s) {
+ // prohibit tex interpretation as above
+ s.attr('data-notex', 1);
+ });
+
+ var txt = trace.title.text;
+ if(trace._meta) {
+ txt = Lib.templateString(txt, trace._meta);
+ }
+
+ titleText.text(txt)
+ .attr({
+ 'class': 'titletext',
+ transform: '',
+ 'text-anchor': 'middle',
+ })
+ .call(Drawing.font, trace.title.font)
+ .call(svgTextUtils.convertToTspans, gd);
+
+ var transform = positionTitleOutside(cd0, fullLayout._size);
+
+ titleText.attr('transform',
+ 'translate(' + transform.x + ',' + transform.y + ')' +
+ (transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
+ 'translate(' + transform.tx + ',' + transform.ty + ')');
+ });
+ });
+ });
+};
+
+function line(a, b) {
+ var dx = b[0] - a[0];
+ var dy = b[1] - a[1];
+
+ return 'l' + dx + ',' + dy;
+}
+
+function getBetween(a, b) {
+ return [
+ 0.5 * (a[0] + b[0]),
+ 0.5 * (a[1] + b[1])
+ ];
+}
+
+function setCoords(cd) {
+ if(!cd.length) return;
+
+ var cd0 = cd[0];
+ var trace = cd0.trace;
+
+ var aspectratio = trace.aspectratio;
+
+ var h = trace.baseratio;
+ if(h > 0.999) h = 0.999; // TODO: may handle this case separately
+ var h2 = Math.pow(h, 2);
+
+ var v1 = cd0.vTotal;
+ var v0 = v1 * h2 / (1 - h2);
+
+ var totalValues = v1;
+ var sumSteps = v0 / v1;
+
+ function calcPos() {
+ var q = Math.sqrt(sumSteps);
+ return {
+ x: q,
+ y: -q
+ };
+ }
+
+ function getPoint() {
+ var pos = calcPos();
+ return [pos.x, pos.y];
+ }
+
+ var p;
+ var allPoints = [];
+ allPoints.push(getPoint());
+
+ var i, cdi;
+ for(i = cd.length - 1; i > -1; i--) {
+ cdi = cd[i];
+ if(cdi.hidden) continue;
+
+ var step = cdi.v / totalValues;
+ sumSteps += step;
+
+ allPoints.push(getPoint());
+ }
+
+ var minY = Infinity;
+ var maxY = -Infinity;
+ for(i = 0; i < allPoints.length; i++) {
+ p = allPoints[i];
+ minY = Math.min(minY, p[1]);
+ maxY = Math.max(maxY, p[1]);
+ }
+
+ // center the shape
+ for(i = 0; i < allPoints.length; i++) {
+ allPoints[i][1] -= (maxY + minY) / 2;
+ }
+
+ var lastX = allPoints[allPoints.length - 1][0];
+
+ // get pie r
+ var r = cd0.r;
+
+ var rY = (maxY - minY) / 2;
+ var scaleX = r / lastX;
+ var scaleY = r / rY * aspectratio;
+
+ // set funnelarea r
+ cd0.r = scaleY * rY;
+
+ // scale the shape
+ for(i = 0; i < allPoints.length; i++) {
+ allPoints[i][0] *= scaleX;
+ allPoints[i][1] *= scaleY;
+ }
+
+ // record first position
+ p = allPoints[0];
+ var prevLeft = [-p[0], p[1]];
+ var prevRight = [p[0], p[1]];
+
+ var n = 0; // note we skip the very first point.
+ for(i = cd.length - 1; i > -1; i--) {
+ cdi = cd[i];
+ if(cdi.hidden) continue;
+
+ n += 1;
+ var x = allPoints[n][0];
+ var y = allPoints[n][1];
+
+ cdi.TL = [-x, y];
+ cdi.TR = [x, y];
+
+ cdi.BL = prevLeft;
+ cdi.BR = prevRight;
+
+ cdi.pxmid = getBetween(cdi.TR, cdi.BR);
+
+ prevLeft = cdi.TL;
+ prevRight = cdi.TR;
+ }
+}
diff --git a/src/traces/funnelarea/style.js b/src/traces/funnelarea/style.js
new file mode 100644
index 00000000000..af5455a7613
--- /dev/null
+++ b/src/traces/funnelarea/style.js
@@ -0,0 +1,27 @@
+/**
+* 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 styleOne = require('../pie/style_one');
+
+module.exports = function style(gd) {
+ gd._fullLayout._funnelarealayer.selectAll('.trace').each(function(cd) {
+ var cd0 = cd[0];
+ var trace = cd0.trace;
+ var traceSelection = d3.select(this);
+
+ traceSelection.style({opacity: trace.opacity});
+
+ traceSelection.selectAll('path.surface').each(function(pt) {
+ d3.select(this).call(styleOne, pt, trace);
+ });
+ });
+};
diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js
index b7a99e7589b..b8b22a50fc1 100644
--- a/src/traces/histogram/index.js
+++ b/src/traces/histogram/index.js
@@ -29,7 +29,7 @@ module.exports = {
supplyLayoutDefaults: require('../bar/layout_defaults'),
calc: require('./calc').calc,
crossTraceCalc: require('../bar/cross_trace_calc').crossTraceCalc,
- plot: require('../bar/plot'),
+ plot: require('../bar/plot').plot,
layerName: 'barlayer',
style: require('../bar/style').style,
styleOnSelect: require('../bar/style').styleOnSelect,
diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js
index 744a344b532..475ae885fb0 100644
--- a/src/traces/pie/attributes.js
+++ b/src/traces/pie/attributes.js
@@ -8,16 +8,16 @@
'use strict';
-var colorAttrs = require('../../components/color/attributes');
-var fontAttrs = require('../../plots/font_attributes');
var plotAttrs = require('../../plots/attributes');
-var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
var domainAttrs = require('../../plots/domain').attributes;
+var fontAttrs = require('../../plots/font_attributes');
+var colorAttrs = require('../../components/color/attributes');
+var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
var extendFlat = require('../../lib/extend').extendFlat;
var textFontAttrs = fontAttrs({
- editType: 'calc',
+ editType: 'plot',
arrayOk: true,
colorEditType: 'plot',
description: 'Sets the font used for `textinfo`.'
@@ -60,7 +60,7 @@ module.exports = {
valType: 'data_array',
editType: 'calc',
description: [
- 'Sets the values of the sectors of this pie chart.',
+ 'Sets the values of the sectors.',
'If omitted, we count occurrences of each label.'
].join(' ')
},
@@ -70,7 +70,7 @@ module.exports = {
valType: 'data_array', // TODO 'color_array' ?
editType: 'calc',
description: [
- 'Sets the color of each sector of this pie chart.',
+ 'Sets the color of each sector.',
'If not specified, the default trace color set is used',
'to pick the sector colors.'
].join(' ')
@@ -140,7 +140,7 @@ module.exports = {
dflt: '',
editType: 'calc',
description: [
- 'If there are multiple pies that should be sized according to',
+ 'If there are multiple pie charts that should be sized according to',
'their totals, link them by providing a non-empty group id here',
'shared by every trace in the same group.'
].join(' ')
@@ -169,7 +169,7 @@ module.exports = {
values: ['inside', 'outside', 'auto', 'none'],
dflt: 'auto',
arrayOk: true,
- editType: 'calc',
+ editType: 'plot',
description: [
'Specifies the location of the `textinfo`.'
].join(' ')
@@ -178,10 +178,10 @@ module.exports = {
description: 'Sets the font used for `textinfo`.'
}),
insidetextfont: extendFlat({}, textFontAttrs, {
- description: 'Sets the font used for `textinfo` lying inside the pie.'
+ description: 'Sets the font used for `textinfo` lying inside the sector.'
}),
outsidetextfont: extendFlat({}, textFontAttrs, {
- description: 'Sets the font used for `textinfo` lying outside the pie.'
+ description: 'Sets the font used for `textinfo` lying outside the sector.'
}),
title: {
@@ -189,9 +189,9 @@ module.exports = {
valType: 'string',
dflt: '',
role: 'info',
- editType: 'calc',
+ editType: 'plot',
description: [
- 'Sets the title of the pie chart.',
+ 'Sets the title of the chart.',
'If it is empty, no title is displayed.',
'Note that before the existence of `title.text`, the title\'s',
'contents used to be defined as the `title` attribute itself.',
@@ -213,7 +213,7 @@ module.exports = {
'bottom left', 'bottom center', 'bottom right'
],
role: 'info',
- editType: 'calc',
+ editType: 'plot',
description: [
'Specifies the location of the `title`.',
'Note that the title\'s position used to be set',
@@ -221,7 +221,7 @@ module.exports = {
].join(' ')
},
- editType: 'calc'
+ editType: 'plot'
},
// position and shape
diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js
index 03d7fa81ae3..461094aac19 100644
--- a/src/traces/pie/calc.js
+++ b/src/traces/pie/calc.js
@@ -16,20 +16,20 @@ var Color = require('../../components/color');
var helpers = require('./helpers');
var isValidTextValue = require('../../lib').isValidTextValue;
-var pieExtendedColorWays = {};
+var extendedColorWayList = {};
function calc(gd, trace) {
- var vals = trace.values;
- var hasVals = isArrayOrTypedArray(vals) && vals.length;
- var labels = trace.labels;
- var colors = trace.marker.colors || [];
var cd = [];
+
var fullLayout = gd._fullLayout;
- var allThisTraceLabels = {};
- var vTotal = 0;
var hiddenLabels = fullLayout.hiddenlabels || [];
- var i, v, label, hidden, pt;
+ var labels = trace.labels;
+ var colors = trace.marker.colors || [];
+ var vals = trace.values;
+ var hasVals = isArrayOrTypedArray(vals) && vals.length;
+
+ var i, pt;
if(trace.dlabel) {
labels = new Array(vals.length);
@@ -38,10 +38,14 @@ function calc(gd, trace) {
}
}
- var pullColor = makePullColorFn(fullLayout._piecolormap);
+ var allThisTraceLabels = {};
+ var pullColor = makePullColorFn(fullLayout['_' + trace.type + 'colormap']);
var seriesLen = (hasVals ? vals : labels).length;
+ var vTotal = 0;
+ var isAggregated = false;
for(i = 0; i < seriesLen; i++) {
+ var v, label, hidden;
if(hasVals) {
v = vals[i];
if(!isNumeric(v)) continue;
@@ -70,6 +74,8 @@ function calc(gd, trace) {
hidden: hidden
});
} else {
+ isAggregated = true;
+
pt = cd[thisLabelIndex];
pt.v += v;
pt.pts.push(i);
@@ -81,31 +87,35 @@ function calc(gd, trace) {
}
}
- if(trace.sort) cd.sort(function(a, b) { return b.v - a.v; });
+ var shouldSort = (trace.type === 'funnelarea') ? isAggregated : trace.sort;
+ if(shouldSort) cd.sort(function(a, b) { return b.v - a.v; });
// include the sum of all values in the first point
if(cd[0]) cd[0].vTotal = vTotal;
// now insert text
- if(trace.textinfo && trace.textinfo !== 'none') {
- var hasLabel = trace.textinfo.indexOf('label') !== -1;
- var hasText = trace.textinfo.indexOf('text') !== -1;
- var hasValue = trace.textinfo.indexOf('value') !== -1;
- var hasPercent = trace.textinfo.indexOf('percent') !== -1;
- var separators = fullLayout.separators;
+ var textinfo = trace.textinfo;
+ if(textinfo && textinfo !== 'none') {
+ var parts = textinfo.split('+');
+ var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; };
+ var hasLabel = hasFlag('label');
+ var hasText = hasFlag('text');
+ var hasValue = hasFlag('value');
+ var hasPercent = hasFlag('percent');
- var thisText;
+ var separators = fullLayout.separators;
+ var text;
for(i = 0; i < cd.length; i++) {
pt = cd[i];
- thisText = hasLabel ? [pt.label] : [];
+ text = hasLabel ? [pt.label] : [];
if(hasText) {
var tx = helpers.getFirstFilled(trace.text, pt.pts);
- if(isValidTextValue(tx)) thisText.push(tx);
+ if(isValidTextValue(tx)) text.push(tx);
}
- if(hasValue) thisText.push(helpers.formatPieValue(pt.v, separators));
- if(hasPercent) thisText.push(helpers.formatPiePercent(pt.v / vTotal, separators));
- pt.text = thisText.join('
');
+ if(hasValue) text.push(helpers.formatPieValue(pt.v, separators));
+ if(hasPercent) text.push(helpers.formatPiePercent(pt.v / vTotal, separators));
+ pt.text = text.join('
');
}
}
@@ -133,20 +143,24 @@ function makePullColorFn(colorMap) {
* This is done after sorting, so we pick defaults
* in the order slices will be displayed
*/
-function crossTraceCalc(gd) {
+function crossTraceCalc(gd, plotinfo) { // TODO: should we name the second argument opts?
+ var desiredType = (plotinfo || {}).type;
+ if(!desiredType) desiredType = 'pie';
+
var fullLayout = gd._fullLayout;
var calcdata = gd.calcdata;
- var pieColorWay = fullLayout.piecolorway;
- var colorMap = fullLayout._piecolormap;
+ var colorWay = fullLayout[desiredType + 'colorway'];
+ var colorMap = fullLayout['_' + desiredType + 'colormap'];
- if(fullLayout.extendpiecolors) {
- pieColorWay = generateExtendedColors(pieColorWay, pieExtendedColorWays);
+ if(fullLayout['extend' + desiredType + 'colors']) {
+ colorWay = generateExtendedColors(colorWay, extendedColorWayList);
}
var dfltColorCount = 0;
for(var i = 0; i < calcdata.length; i++) {
var cd = calcdata[i];
- if(cd[0].trace.type !== 'pie') continue;
+ var traceType = cd[0].trace.type;
+ if(traceType !== desiredType) continue;
for(var j = 0; j < cd.length; j++) {
var pt = cd[j];
@@ -155,7 +169,7 @@ function crossTraceCalc(gd) {
if(colorMap[pt.label]) {
pt.color = colorMap[pt.label];
} else {
- colorMap[pt.label] = pt.color = pieColorWay[dfltColorCount % pieColorWay.length];
+ colorMap[pt.label] = pt.color = colorWay[dfltColorCount % colorWay.length];
dfltColorCount++;
}
}
@@ -170,21 +184,21 @@ function crossTraceCalc(gd) {
function generateExtendedColors(colorList, extendedColorWays) {
var i;
var colorString = JSON.stringify(colorList);
- var pieColors = extendedColorWays[colorString];
- if(!pieColors) {
- pieColors = colorList.slice();
+ var colors = extendedColorWays[colorString];
+ if(!colors) {
+ colors = colorList.slice();
for(i = 0; i < colorList.length; i++) {
- pieColors.push(tinycolor(colorList[i]).lighten(20).toHexString());
+ colors.push(tinycolor(colorList[i]).lighten(20).toHexString());
}
for(i = 0; i < colorList.length; i++) {
- pieColors.push(tinycolor(colorList[i]).darken(20).toHexString());
+ colors.push(tinycolor(colorList[i]).darken(20).toHexString());
}
- extendedColorWays[colorString] = pieColors;
+ extendedColorWays[colorString] = colors;
}
- return pieColors;
+ return colors;
}
module.exports = {
diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js
index 52aea7bfc7b..3e94c24483d 100644
--- a/src/traces/pie/defaults.js
+++ b/src/traces/pie/defaults.js
@@ -11,15 +11,14 @@
var Lib = require('../../lib');
var attributes = require('./attributes');
var handleDomainDefaults = require('../../plots/domain').defaults;
+var handleText = require('../bar/defaults').handleText;
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- var coerceFont = Lib.coerceFont;
var len;
-
var vals = coerce('values');
var hasVals = Lib.isArrayOrTypedArray(vals);
var labels = coerce('labels');
@@ -53,24 +52,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('hovertemplate');
if(textInfo && textInfo !== 'none') {
- 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 dfltFont = coerceFont(coerce, 'textfont', layout.font);
- if(hasInside) {
- var insideTextFontDefault = Lib.extendFlat({}, dfltFont);
- var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
- var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
- if(isColorInheritedFromLayoutFont) {
- delete insideTextFontDefault.color;
- }
- coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
- }
- if(hasOutside) coerceFont(coerce, 'outsidetextfont', dfltFont);
- }
+ var textposition = coerce('textposition');
+ handleText(traceIn, traceOut, layout, coerce, textposition, {
+ moduleHasSelected: false,
+ moduleHasUnselected: false,
+ moduleHasConstrain: false,
+ moduleHasCliponaxis: false,
+ moduleHasTextangle: false,
+ moduleHasInsideanchor: false
+ });
}
handleDomainDefaults(traceOut, layout, coerce);
@@ -80,12 +70,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
if(title) {
var titlePosition = coerce('title.position', hole ? 'middle center' : 'top center');
if(!hole && titlePosition === 'middle center') traceOut.title.position = 'top center';
- coerceFont(coerce, 'title.font', layout.font);
+ Lib.coerceFont(coerce, 'title.font', layout.font);
}
coerce('sort');
coerce('direction');
coerce('rotation');
-
coerce('pull');
};
diff --git a/src/traces/pie/event_data.js b/src/traces/pie/event_data.js
index 2583a24628d..ea181189f3d 100644
--- a/src/traces/pie/event_data.js
+++ b/src/traces/pie/event_data.js
@@ -6,12 +6,10 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var appendArrayMultiPointValues = require('../../components/fx/helpers').appendArrayMultiPointValues;
-
// Note: like other eventData routines, this creates the data for hover/unhover/click events
// but it has a different API and goes through a totally different pathway.
// So to ensure it doesn't get misused, it's not attached to the Pie module.
@@ -39,5 +37,11 @@ module.exports = function eventData(pt, trace) {
// so added data will be arrays matching the pointNumbers array.
appendArrayMultiPointValues(out, trace, pt.pts);
+ // don't include obsolete fields in new funnelarea traces
+ if(trace.type === 'funnelarea') {
+ delete out.v;
+ delete out.i;
+ }
+
return out;
};
diff --git a/src/traces/pie/index.js b/src/traces/pie/index.js
index 71b2594aa01..0597ab64f2e 100644
--- a/src/traces/pie/index.js
+++ b/src/traces/pie/index.js
@@ -24,7 +24,7 @@ module.exports = {
moduleType: 'trace',
name: 'pie',
basePlotModule: require('./base_plot'),
- categories: ['pie', 'showLegend'],
+ categories: ['pie-like', 'pie', 'showLegend'],
meta: {
description: [
'A data visualized by the sectors of the pie is set in `values`.',
diff --git a/src/traces/pie/layout_attributes.js b/src/traces/pie/layout_attributes.js
index 09100b38e5c..87dd5971384 100644
--- a/src/traces/pie/layout_attributes.js
+++ b/src/traces/pie/layout_attributes.js
@@ -9,14 +9,16 @@
'use strict';
module.exports = {
- /**
- * hiddenlabels is the pie chart analog of visible:'legendonly'
- * but it can contain many labels, and can hide slices
- * from several pies simultaneously
- */
hiddenlabels: {
valType: 'data_array',
- editType: 'calc'
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'hiddenlabels is the funnelarea & pie chart analog of',
+ 'visible:\'legendonly\'',
+ 'but it can contain many labels, and can simultaneously',
+ 'hide slices from several pies/funnelarea charts'
+ ].join(' ')
},
piecolorway: {
valType: 'colorlist',
diff --git a/src/traces/pie/layout_defaults.js b/src/traces/pie/layout_defaults.js
index a38ef5890e2..9aeebbe5ee3 100644
--- a/src/traces/pie/layout_defaults.js
+++ b/src/traces/pie/layout_defaults.js
@@ -16,6 +16,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
function coerce(attr, dflt) {
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}
+
coerce('hiddenlabels');
coerce('piecolorway', layoutOut.colorway);
coerce('extendpiecolors');
diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js
index 05bf81e8bf8..bcc67a99d51 100644
--- a/src/traces/pie/plot.js
+++ b/src/traces/pie/plot.js
@@ -19,14 +19,14 @@ var svgTextUtils = require('../../lib/svg_text_utils');
var helpers = require('./helpers');
var eventData = require('./event_data');
-function plot(gd, cdpie) {
+function plot(gd, cdModule) {
var fullLayout = gd._fullLayout;
- prerenderTitles(cdpie, gd);
- scalePies(cdpie, fullLayout._size);
+ prerenderTitles(cdModule, gd);
+ layoutAreas(cdModule, fullLayout._size);
- var pieGroups = Lib.makeTraceGroups(fullLayout._pielayer, cdpie, 'trace').each(function(cd) {
- var pieGroup = d3.select(this);
+ var plotGroups = Lib.makeTraceGroups(fullLayout._pielayer, cdModule, 'trace').each(function(cd) {
+ var plotGroup = d3.select(this);
var cd0 = cd[0];
var trace = cd0.trace;
@@ -34,9 +34,9 @@ function plot(gd, cdpie) {
// TODO: miter might look better but can sometimes cause problems
// maybe miter with a small-ish stroke-miterlimit?
- pieGroup.attr('stroke-linejoin', 'round');
+ plotGroup.attr('stroke-linejoin', 'round');
- pieGroup.each(function() {
+ plotGroup.each(function() {
var slices = d3.select(this).selectAll('g.slice').data(cd);
slices.enter().append('g')
@@ -84,9 +84,12 @@ function plot(gd, cdpie) {
pt.cyFinal = cy;
function arc(start, finish, cw, scale) {
- return 'a' + (scale * cd0.r) + ',' + (scale * cd0.r) + ' 0 ' +
- pt.largeArc + (cw ? ' 1 ' : ' 0 ') +
- (scale * (finish[0] - start[0])) + ',' + (scale * (finish[1] - start[1]));
+ var dx = scale * (finish[0] - start[0]);
+ var dy = scale * (finish[1] - start[1]);
+
+ return 'a' +
+ (scale * cd0.r) + ',' + (scale * cd0.r) + ' 0 ' +
+ pt.largeArc + (cw ? ' 1 ' : ' 0 ') + dx + ',' + dy;
}
var hole = trace.hole;
@@ -246,7 +249,7 @@ function plot(gd, cdpie) {
// I have no idea why we haven't seen this in other contexts. Also, sometimes
// it gets the initial draw correct but on redraw it gets confused.
setTimeout(function() {
- pieGroups.selectAll('tspan').each(function() {
+ plotGroups.selectAll('tspan').each(function() {
var s = d3.select(this);
if(s.attr('dy')) s.attr('dy', s.attr('dy'));
});
@@ -344,24 +347,24 @@ function attachFxHandlers(sliceTop, gd, cd) {
// in case we dragged over the pie from another subplot,
// or if hover is turned off
if(trace2.hovertemplate || (hoverinfo !== 'none' && hoverinfo !== 'skip' && hoverinfo)) {
- var rInscribed = pt.rInscribed;
+ var rInscribed = pt.rInscribed || 0;
var hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed);
var hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed);
var separators = fullLayout2.separators;
- var thisText = [];
+ var text = [];
- if(hoverinfo && hoverinfo.indexOf('label') !== -1) thisText.push(pt.label);
+ if(hoverinfo && hoverinfo.indexOf('label') !== -1) text.push(pt.label);
pt.text = helpers.castOption(trace2.hovertext || trace2.text, pt.pts);
if(hoverinfo && hoverinfo.indexOf('text') !== -1) {
var tx = pt.text;
- if(Lib.isValidTextValue(tx)) thisText.push(tx);
+ if(Lib.isValidTextValue(tx)) text.push(tx);
}
pt.value = pt.v;
pt.valueLabel = helpers.formatPieValue(pt.v, separators);
- if(hoverinfo && hoverinfo.indexOf('value') !== -1) thisText.push(pt.valueLabel);
+ if(hoverinfo && hoverinfo.indexOf('value') !== -1) text.push(pt.valueLabel);
pt.percent = pt.v / cd0.vTotal;
pt.percentLabel = helpers.formatPiePercent(pt.percent, separators);
- if(hoverinfo && hoverinfo.indexOf('percent') !== -1) thisText.push(pt.percentLabel);
+ if(hoverinfo && hoverinfo.indexOf('percent') !== -1) text.push(pt.percentLabel);
var hoverLabel = trace2.hoverlabel;
var hoverFont = hoverLabel.font;
@@ -371,7 +374,7 @@ function attachFxHandlers(sliceTop, gd, cd) {
x0: hoverCenterX - rInscribed * cd0.r,
x1: hoverCenterX + rInscribed * cd0.r,
y: hoverCenterY,
- text: thisText.join('
'),
+ text: text.join('
'),
name: (trace2.hovertemplate || hoverinfo.indexOf('name') !== -1) ? trace2.name : undefined,
idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right',
color: helpers.castOption(hoverLabel.bgcolor, pt.pts) || pt.color,
@@ -437,17 +440,20 @@ function attachFxHandlers(sliceTop, gd, cd) {
}
function determineOutsideTextFont(trace, pt, layoutFont) {
- var color = helpers.castOption(trace.outsidetextfont.color, pt.pts) ||
- helpers.castOption(trace.textfont.color, pt.pts) ||
- layoutFont.color;
+ var color =
+ helpers.castOption(trace.outsidetextfont.color, pt.pts) ||
+ helpers.castOption(trace.textfont.color, pt.pts) ||
+ layoutFont.color;
- var family = helpers.castOption(trace.outsidetextfont.family, pt.pts) ||
- helpers.castOption(trace.textfont.family, pt.pts) ||
- layoutFont.family;
+ var family =
+ helpers.castOption(trace.outsidetextfont.family, pt.pts) ||
+ helpers.castOption(trace.textfont.family, pt.pts) ||
+ layoutFont.family;
- var size = helpers.castOption(trace.outsidetextfont.size, pt.pts) ||
- helpers.castOption(trace.textfont.size, pt.pts) ||
- layoutFont.size;
+ var size =
+ helpers.castOption(trace.outsidetextfont.size, pt.pts) ||
+ helpers.castOption(trace.textfont.size, pt.pts) ||
+ layoutFont.size;
return {
color: color,
@@ -466,13 +472,15 @@ function determineInsideTextFont(trace, pt, layoutFont) {
customColor = helpers.castOption(trace._input.textfont.color, pt.pts);
}
- var family = helpers.castOption(trace.insidetextfont.family, pt.pts) ||
- helpers.castOption(trace.textfont.family, pt.pts) ||
- layoutFont.family;
+ var family =
+ helpers.castOption(trace.insidetextfont.family, pt.pts) ||
+ helpers.castOption(trace.textfont.family, pt.pts) ||
+ layoutFont.family;
- var size = helpers.castOption(trace.insidetextfont.size, pt.pts) ||
- helpers.castOption(trace.textfont.size, pt.pts) ||
- layoutFont.size;
+ var size =
+ helpers.castOption(trace.insidetextfont.size, pt.pts) ||
+ helpers.castOption(trace.textfont.size, pt.pts) ||
+ layoutFont.size;
return {
color: customColor || Color.contrast(pt.color),
@@ -481,12 +489,12 @@ function determineInsideTextFont(trace, pt, layoutFont) {
};
}
-function prerenderTitles(cdpie, gd) {
+function prerenderTitles(cdModule, gd) {
var cd0, trace;
// Determine the width and height of the title for each pie.
- for(var i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
+ for(var i = 0; i < cdModule.length; i++) {
+ cd0 = cdModule[i][0];
trace = cd0.trace;
if(trace.title.text) {
@@ -607,7 +615,7 @@ function positionTitleInside(cd0) {
function positionTitleOutside(cd0, plotSize) {
var scaleX = 1;
var scaleY = 1;
- var maxWidth, maxPull;
+ var maxPull;
var trace = cd0.trace;
// position of the baseline point of the text box in the plot, before scaling.
@@ -636,16 +644,19 @@ function positionTitleOutside(cd0, plotSize) {
topMiddle.y += (1 + maxPull) * cd0.r;
}
+ var rx = applyAspectRatio(cd0.r, cd0.trace.aspectratio);
+
+ var maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2;
if(trace.title.position.indexOf('left') !== -1) {
// we start the text at the left edge of the pie
- maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
- topMiddle.x -= (1 + maxPull) * cd0.r;
+ maxWidth = maxWidth + rx;
+ topMiddle.x -= (1 + maxPull) * rx;
translate.tx += cd0.titleBox.width / 2;
} else if(trace.title.position.indexOf('center') !== -1) {
- maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
+ maxWidth *= 2;
} else if(trace.title.position.indexOf('right') !== -1) {
- maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
- topMiddle.x += (1 + maxPull) * cd0.r;
+ maxWidth = maxWidth + rx;
+ topMiddle.x += (1 + maxPull) * rx;
translate.tx -= cd0.titleBox.width / 2;
}
scaleX = maxWidth / cd0.titleBox.width;
@@ -659,6 +670,10 @@ function positionTitleOutside(cd0, plotSize) {
};
}
+function applyAspectRatio(x, aspectratio) {
+ return x / ((aspectratio === undefined) ? 1 : aspectratio);
+}
+
function getTitleSpace(cd0, plotSize) {
var trace = cd0.trace;
var pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
@@ -668,6 +683,8 @@ function getTitleSpace(cd0, plotSize) {
function getMaxPull(trace) {
var maxPull = trace.pull;
+ if(!maxPull) return 0;
+
var j;
if(Array.isArray(maxPull)) {
maxPull = 0;
@@ -783,30 +800,32 @@ function scootLabels(quadrants, trace) {
}
}
-function scalePies(cdpie, plotSize) {
+function layoutAreas(cdModule, plotSize) {
var scaleGroups = [];
- var pieBoxWidth, pieBoxHeight, i, j, cd0, trace,
- maxPull, scaleGroup, minPxPerValUnit;
-
- // first figure out the center and maximum radius for each pie
- for(i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
- trace = cd0.trace;
+ // figure out the center and maximum radius
+ for(var i = 0; i < cdModule.length; i++) {
+ var cd0 = cdModule[i][0];
+ var trace = cd0.trace;
- pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
- pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
+ var domain = trace.domain;
+ var width = plotSize.w * (domain.x[1] - domain.x[0]);
+ var height = plotSize.h * (domain.y[1] - domain.y[0]);
// leave some space for the title, if it will be displayed outside
if(trace.title.text && trace.title.position !== 'middle center') {
- pieBoxHeight -= getTitleSpace(cd0, plotSize);
+ height -= getTitleSpace(cd0, plotSize);
}
- maxPull = getMaxPull(trace);
+ var rx = width / 2;
+ var ry = height / 2;
+ if(trace.type === 'funnelarea' && !trace.scalegroup) {
+ ry /= trace.aspectratio;
+ }
- cd0.r = Math.min(pieBoxWidth, pieBoxHeight) / (2 + 2 * maxPull);
+ cd0.r = Math.min(rx, ry) / (1 + getMaxPull(trace));
cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2;
- cd0.cy = plotSize.t + plotSize.h * (1 - trace.domain.y[0]) - pieBoxHeight / 2;
+ cd0.cy = plotSize.t + plotSize.h * (1 - trace.domain.y[0]) - height / 2;
if(trace.title.text && trace.title.position.indexOf('bottom') !== -1) {
cd0.cy -= getTitleSpace(cd0, plotSize);
}
@@ -816,23 +835,56 @@ function scalePies(cdpie, plotSize) {
}
}
- // Then scale any pies that are grouped
- for(j = 0; j < scaleGroups.length; j++) {
- minPxPerValUnit = Infinity;
- scaleGroup = scaleGroups[j];
+ groupScale(cdModule, scaleGroups);
+}
+
+function groupScale(cdModule, scaleGroups) {
+ var cd0, i, trace;
- for(i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
- if(cd0.trace.scalegroup === scaleGroup) {
- minPxPerValUnit = Math.min(minPxPerValUnit,
- cd0.r * cd0.r / cd0.vTotal);
+ // scale those that are grouped
+ for(var k = 0; k < scaleGroups.length; k++) {
+ var min = Infinity;
+ var g = scaleGroups[k];
+
+ for(i = 0; i < cdModule.length; i++) {
+ cd0 = cdModule[i][0];
+ trace = cd0.trace;
+
+ if(trace.scalegroup === g) {
+ var area;
+ if(trace.type === 'pie') {
+ area = cd0.r * cd0.r;
+ } else if(trace.type === 'funnelarea') {
+ var rx, ry;
+
+ if(trace.aspectratio > 1) {
+ rx = cd0.r;
+ ry = rx / trace.aspectratio;
+ } else {
+ ry = cd0.r;
+ rx = ry * trace.aspectratio;
+ }
+
+ rx *= (1 + trace.baseratio) / 2;
+
+ area = rx * ry;
+ }
+
+ min = Math.min(min, area / cd0.vTotal);
}
}
- for(i = 0; i < cdpie.length; i++) {
- cd0 = cdpie[i][0];
- if(cd0.trace.scalegroup === scaleGroup) {
- cd0.r = Math.sqrt(minPxPerValUnit * cd0.vTotal);
+ for(i = 0; i < cdModule.length; i++) {
+ cd0 = cdModule[i][0];
+ trace = cd0.trace;
+ if(trace.scalegroup === g) {
+ var v = min * cd0.vTotal;
+ if(trace.type === 'funnelarea') {
+ v /= (1 + trace.baseratio) / 2;
+ v /= trace.aspectratio;
+ }
+
+ cd0.r = Math.sqrt(v);
}
}
}
@@ -891,5 +943,10 @@ function setCoords(cd) {
module.exports = {
plot: plot,
- transformInsideText: transformInsideText
+ transformInsideText: transformInsideText,
+ determineInsideTextFont: determineInsideTextFont,
+ positionTitleOutside: positionTitleOutside,
+ prerenderTitles: prerenderTitles,
+ layoutAreas: layoutAreas,
+ attachFxHandlers: attachFxHandlers,
};
diff --git a/src/traces/sunburst/defaults.js b/src/traces/sunburst/defaults.js
index bea2a98da84..5a8a1d3bc2f 100644
--- a/src/traces/sunburst/defaults.js
+++ b/src/traces/sunburst/defaults.js
@@ -11,8 +11,7 @@
var Lib = require('../../lib');
var attributes = require('./attributes');
var handleDomainDefaults = require('../../plots/domain').defaults;
-
-var coerceFont = Lib.coerceFont;
+var handleText = require('../bar/defaults').handleText;
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
@@ -46,15 +45,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('hovertext');
coerce('hovertemplate');
- var dfltFont = coerceFont(coerce, 'textfont', layout.font);
- var insideTextFontDefault = Lib.extendFlat({}, dfltFont);
- var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color;
- var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet;
- if(isColorInheritedFromLayoutFont) {
- delete insideTextFontDefault.color;
- }
- coerceFont(coerce, 'insidetextfont', insideTextFontDefault);
- coerceFont(coerce, 'outsidetextfont', dfltFont);
+ var textposition = 'auto';
+ handleText(traceIn, traceOut, layout, coerce, textposition, {
+ moduleHasSelected: false,
+ moduleHasUnselected: false,
+ moduleHasConstrain: false,
+ moduleHasCliponaxis: false,
+ moduleHasTextangle: false,
+ moduleHasInsideanchor: false
+ });
handleDomainDefaults(traceOut, layout, coerce);
diff --git a/src/traces/waterfall/defaults.js b/src/traces/waterfall/defaults.js
index 98c60f2718d..b56e488a115 100644
--- a/src/traces/waterfall/defaults.js
+++ b/src/traces/waterfall/defaults.js
@@ -49,7 +49,16 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
coerce('hovertext');
coerce('hovertemplate');
- handleText(traceIn, traceOut, layout, coerce, false);
+ var textposition = coerce('textposition');
+ handleText(traceIn, traceOut, layout, coerce, textposition, {
+ moduleHasSelected: false,
+ moduleHasUnselected: false,
+ moduleHasConstrain: true,
+ moduleHasCliponaxis: true,
+ moduleHasTextangle: true,
+ moduleHasInsideanchor: true
+ });
+
if(traceOut.textposition !== 'none') {
coerce('textinfo');
diff --git a/src/traces/waterfall/plot.js b/src/traces/waterfall/plot.js
index c29e6afbf80..61a7ab6580c 100644
--- a/src/traces/waterfall/plot.js
+++ b/src/traces/waterfall/plot.js
@@ -11,7 +11,7 @@
var d3 = require('d3');
var Lib = require('../../lib');
var Drawing = require('../../components/drawing');
-var barPlot = require('../bar/plot');
+var barPlot = require('../bar/plot').plot;
module.exports = function plot(gd, plotinfo, cdModule, traceLayer) {
var fullLayout = gd._fullLayout;
diff --git a/test/image/baselines/funnelarea_aggregated.png b/test/image/baselines/funnelarea_aggregated.png
new file mode 100644
index 00000000000..17700dd9f46
Binary files /dev/null and b/test/image/baselines/funnelarea_aggregated.png differ
diff --git a/test/image/baselines/funnelarea_fonts.png b/test/image/baselines/funnelarea_fonts.png
new file mode 100644
index 00000000000..5837993111b
Binary files /dev/null and b/test/image/baselines/funnelarea_fonts.png differ
diff --git a/test/image/baselines/funnelarea_label0_dlabel.png b/test/image/baselines/funnelarea_label0_dlabel.png
new file mode 100644
index 00000000000..5f7b5bd3f76
Binary files /dev/null and b/test/image/baselines/funnelarea_label0_dlabel.png differ
diff --git a/test/image/baselines/funnelarea_labels_colors_text.png b/test/image/baselines/funnelarea_labels_colors_text.png
new file mode 100644
index 00000000000..eed4f89dc51
Binary files /dev/null and b/test/image/baselines/funnelarea_labels_colors_text.png differ
diff --git a/test/image/baselines/funnelarea_line_width.png b/test/image/baselines/funnelarea_line_width.png
new file mode 100644
index 00000000000..680837aefc4
Binary files /dev/null and b/test/image/baselines/funnelarea_line_width.png differ
diff --git a/test/image/baselines/funnelarea_no_scalegroup_various_domain.png b/test/image/baselines/funnelarea_no_scalegroup_various_domain.png
new file mode 100644
index 00000000000..e92deb938c0
Binary files /dev/null and b/test/image/baselines/funnelarea_no_scalegroup_various_domain.png differ
diff --git a/test/image/baselines/funnelarea_no_scalegroup_various_ratios.png b/test/image/baselines/funnelarea_no_scalegroup_various_ratios.png
new file mode 100644
index 00000000000..4da55b0616f
Binary files /dev/null and b/test/image/baselines/funnelarea_no_scalegroup_various_ratios.png differ
diff --git a/test/image/baselines/funnelarea_no_scalegroup_various_ratios_and_domain.png b/test/image/baselines/funnelarea_no_scalegroup_various_ratios_and_domain.png
new file mode 100644
index 00000000000..976067ae338
Binary files /dev/null and b/test/image/baselines/funnelarea_no_scalegroup_various_ratios_and_domain.png differ
diff --git a/test/image/baselines/funnelarea_pie_colorways.png b/test/image/baselines/funnelarea_pie_colorways.png
new file mode 100644
index 00000000000..7261a92d424
Binary files /dev/null and b/test/image/baselines/funnelarea_pie_colorways.png differ
diff --git a/test/image/baselines/funnelarea_scalegroup_two.png b/test/image/baselines/funnelarea_scalegroup_two.png
new file mode 100644
index 00000000000..ffc15add2a4
Binary files /dev/null and b/test/image/baselines/funnelarea_scalegroup_two.png differ
diff --git a/test/image/baselines/funnelarea_scalegroup_various_ratios.png b/test/image/baselines/funnelarea_scalegroup_various_ratios.png
new file mode 100644
index 00000000000..01e70d18c84
Binary files /dev/null and b/test/image/baselines/funnelarea_scalegroup_various_ratios.png differ
diff --git a/test/image/baselines/funnelarea_scalegroup_various_ratios_and_domain.png b/test/image/baselines/funnelarea_scalegroup_various_ratios_and_domain.png
new file mode 100644
index 00000000000..b696ba6e5ff
Binary files /dev/null and b/test/image/baselines/funnelarea_scalegroup_various_ratios_and_domain.png differ
diff --git a/test/image/baselines/funnelarea_simple.png b/test/image/baselines/funnelarea_simple.png
new file mode 100644
index 00000000000..97dbfef4f91
Binary files /dev/null and b/test/image/baselines/funnelarea_simple.png differ
diff --git a/test/image/baselines/funnelarea_style.png b/test/image/baselines/funnelarea_style.png
new file mode 100644
index 00000000000..f8304da905f
Binary files /dev/null and b/test/image/baselines/funnelarea_style.png differ
diff --git a/test/image/baselines/funnelarea_title_multiple.png b/test/image/baselines/funnelarea_title_multiple.png
new file mode 100644
index 00000000000..20834fca5a1
Binary files /dev/null and b/test/image/baselines/funnelarea_title_multiple.png differ
diff --git a/test/image/baselines/funnelarea_with_other_traces.png b/test/image/baselines/funnelarea_with_other_traces.png
new file mode 100644
index 00000000000..0e9d6d123fe
Binary files /dev/null and b/test/image/baselines/funnelarea_with_other_traces.png differ
diff --git a/test/image/mocks/funnelarea_aggregated.json b/test/image/mocks/funnelarea_aggregated.json
new file mode 100644
index 00000000000..9b9eaf1fa03
--- /dev/null
+++ b/test/image/mocks/funnelarea_aggregated.json
@@ -0,0 +1,64 @@
+{
+ "data": [
+ {
+ "labels": [
+ "Alice",
+ "Bob",
+ "Charlie",
+ "Charlie",
+ "Charlie",
+ "Alice"
+ ],
+ "type": "funnelarea",
+ "sort": true,
+ "domain": {
+ "x": [
+ 0,
+ 0.4
+ ]
+ }
+ },
+ {
+ "labels": [
+ "Alice",
+ "Alice",
+ "Allison",
+ "Alys",
+ "Elise",
+ "Allison",
+ "Alys",
+ "Alys"
+ ],
+ "values": [
+ 10,
+ 20,
+ 30,
+ 40,
+ 50,
+ 60,
+ 70,
+ 80
+ ],
+ "marker": {
+ "colors": [
+ "",
+ "",
+ "#ccc"
+ ]
+ },
+ "type": "funnelarea",
+ "sort": true,
+ "domain": {
+ "x": [
+ 0.5,
+ 1
+ ]
+ },
+ "textinfo": "label+value+percent total"
+ }
+ ],
+ "layout": {
+ "height": 300,
+ "width": 500
+ }
+}
diff --git a/test/image/mocks/funnelarea_fonts.json b/test/image/mocks/funnelarea_fonts.json
new file mode 100644
index 00000000000..521d04652cd
--- /dev/null
+++ b/test/image/mocks/funnelarea_fonts.json
@@ -0,0 +1,54 @@
+{
+ "data": [
+ {
+ "values": [
+ 3,
+ 1
+ ],
+ "text": [
+ "inherit from
global...",
+ "font"
+ ],
+ "type": "funnelarea",
+ "domain": {
+ "x": [
+ 0,
+ 0.4
+ ]
+ }
+ },
+ {
+ "values": [
+ 3,
+ 1
+ ],
+ "text": [
+ "a font of...",
+ "my own"
+ ],
+ "type": "funnelarea",
+ "domain": {
+ "x": [
+ 0.6,
+ 1
+ ]
+ },
+ "textfont": {
+ "family": "Times New Roman, Times, serif",
+ "size": 15,
+ "color": "#700"
+ }
+ }
+
+ ],
+ "layout": {
+ "height": 300,
+ "width": 500,
+ "font": {
+ "family": "Old Standard TT, serif",
+ "size": 20
+ },
+ "showlegend": true,
+ "separators": "∆|"
+ }
+}
diff --git a/test/image/mocks/funnelarea_label0_dlabel.json b/test/image/mocks/funnelarea_label0_dlabel.json
new file mode 100644
index 00000000000..92648cae8bd
--- /dev/null
+++ b/test/image/mocks/funnelarea_label0_dlabel.json
@@ -0,0 +1,20 @@
+{
+ "data": [
+ {
+ "values": [
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ ],
+ "label0": 20,
+ "dlabel": 5,
+ "type": "funnelarea"
+ }
+ ],
+ "layout": {
+ "height": 300,
+ "width": 400
+ }
+}
diff --git a/test/image/mocks/funnelarea_labels_colors_text.json b/test/image/mocks/funnelarea_labels_colors_text.json
new file mode 100644
index 00000000000..06485cf6854
--- /dev/null
+++ b/test/image/mocks/funnelarea_labels_colors_text.json
@@ -0,0 +1,43 @@
+{
+ "data": [
+ {
+ "values": [
+ 500000,
+ 400000,
+ 300000,
+ 200000,
+ 100000
+ ],
+ "labels": [
+ "apples",
+ "oranges",
+ "blueberries",
+ "lemons",
+ "watermelon"
+ ],
+ "marker": {
+ "colors": [
+ "rgba(255,0,0,0.5)",
+ "orange",
+ "blue",
+ "yellow",
+ "green"
+ ]
+ },
+ "text": [
+ "red delicious",
+ "mandarin",
+ "high bush",
+ "meyer",
+ "jubilee"
+ ],
+ "textinfo": "label+text+value+percent total",
+ "type": "funnelarea"
+ }
+ ],
+ "layout": {
+ "height": 400,
+ "width": 500,
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/funnelarea_line_width.json b/test/image/mocks/funnelarea_line_width.json
new file mode 100644
index 00000000000..7f41dfaadf9
--- /dev/null
+++ b/test/image/mocks/funnelarea_line_width.json
@@ -0,0 +1,33 @@
+{
+ "data": [
+ {
+ "marker": {
+ "line": {
+ "color": ["#F00", "#FF0", "#0F0", "#0FF", "#00F"],
+ "width": [0, 0, 8, 0, 8]
+ }
+ },
+ "values": [
+ 2.02,
+ 3.003,
+ 4.0004,
+ 1.1,
+ 0
+ ],
+ "labels": [
+ "1st",
+ "2nd",
+ "3rd",
+ "4th",
+ "include 0!"
+ ],
+ "textposition": "none",
+ "type": "funnelarea"
+ }
+ ],
+ "layout": {
+ "showlegend": true,
+ "height": 400,
+ "width": 600
+ }
+}
diff --git a/test/image/mocks/funnelarea_no_scalegroup_various_domain.json b/test/image/mocks/funnelarea_no_scalegroup_various_domain.json
new file mode 100644
index 00000000000..775699b03c2
--- /dev/null
+++ b/test/image/mocks/funnelarea_no_scalegroup_various_domain.json
@@ -0,0 +1,137 @@
+{
+ "data": [
+ {
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "11 - 16 no group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0, 0.5]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "title": {
+ "position": "top center",
+ "text": "11 - 16 no group"
+ },
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0, 0.5]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "11 - 16 no group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0, 0.5]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["1", "4"],
+ "width": 800,
+ "height": 800,
+ "shapes": [
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0.8, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0.8, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0.8, "y1": 1 }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_no_scalegroup_various_ratios.json b/test/image/mocks/funnelarea_no_scalegroup_various_ratios.json
new file mode 100644
index 00000000000..1d3debc90d0
--- /dev/null
+++ b/test/image/mocks/funnelarea_no_scalegroup_various_ratios.json
@@ -0,0 +1,155 @@
+{
+ "data": [
+ {
+ "baseratio": 0,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "11 - 16 no group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0, 0.3]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "title": {
+ "position": "top center",
+ "text": "11 - 16 no group"
+ },
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0, 0.3]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "11 - 16 no group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0, 0.3]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["1", "4"],
+ "width": 800,
+ "height": 800,
+ "shapes": [
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0.7, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0.7, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0.7, "y1": 1 }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_no_scalegroup_various_ratios_and_domain.json b/test/image/mocks/funnelarea_no_scalegroup_various_ratios_and_domain.json
new file mode 100644
index 00000000000..5d740010bde
--- /dev/null
+++ b/test/image/mocks/funnelarea_no_scalegroup_various_ratios_and_domain.json
@@ -0,0 +1,155 @@
+{
+ "data": [
+ {
+ "baseratio": 0,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "11 - 16 no group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0, 0.5]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "title": {
+ "position": "top center",
+ "text": "11 - 16 no group"
+ },
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0, 0.5]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "1 - 6 no group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "11 - 16 no group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 no group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0, 0.5]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["1", "4"],
+ "width": 800,
+ "height": 800,
+ "shapes": [
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0.8, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0.8, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0.8, "y1": 1 }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_pie_colorways.json b/test/image/mocks/funnelarea_pie_colorways.json
new file mode 100644
index 00000000000..a2fc3a5da7f
--- /dev/null
+++ b/test/image/mocks/funnelarea_pie_colorways.json
@@ -0,0 +1,64 @@
+{
+ "data": [
+ {
+ "type": "pie",
+ "labels": [
+ "Lead",
+ "Pipeline",
+ "Proposal",
+ "Negotiation",
+ "Closed (Won)"
+ ],
+ "values": [
+ 23,
+ 15,
+ 11,
+ 6,
+ 2.1
+ ],
+ "domain": {
+ "row": 0,
+ "column": 0
+ }
+ },
+ {
+ "type": "funnelarea",
+ "labels": [
+ "Lead",
+ "Pipeline",
+ "Proposal",
+ "Negotiation",
+ "Closed (Won)"
+ ],
+ "values": [
+ 23,
+ 15,
+ 11,
+ 6,
+ 2.1
+ ],
+ "domain": {
+ "row": 0,
+ "column": 1
+ }
+ }
+ ],
+ "layout": {
+ "grid": {
+ "rows": 1,
+ "columns": 2
+ },
+ "piecolorway": [
+ "#7fc97f",
+ "#beaed4",
+ "#fdc086"
+ ],
+ "funnelareacolorway": [
+ "#1b9e77",
+ "#d95f02",
+ "#7570b3"
+ ],
+ "width": 500,
+ "height": 300
+ }
+}
diff --git a/test/image/mocks/funnelarea_scalegroup_two.json b/test/image/mocks/funnelarea_scalegroup_two.json
new file mode 100644
index 00000000000..cc30fd59e45
--- /dev/null
+++ b/test/image/mocks/funnelarea_scalegroup_two.json
@@ -0,0 +1,146 @@
+{
+ "data": [
+ {
+ "type": "funnelarea",
+ "scalegroup": "first",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "1 - 6 in first group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "first",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "11 - 16 in first group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "first",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in first group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0, 0.3]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "first",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in first group"
+ },
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "first",
+ "title": {
+ "position": "top center",
+ "text": "110 - 160 in first group"
+ },
+ "values": [160, 150, 140, 130, 120, 110],
+ "textinfo": "value",
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "first",
+ "values": [600, 500, 400, 300, 200, 100],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "100 - 600 in first group"
+ },
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0, 0.3]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "second",
+ "values": [6, 5, 4, 3, 2, 1],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "1 - 6 in second group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "second",
+ "values": [16, 15, 14, 13, 12, 11],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "11 - 16 in second group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "type": "funnelarea",
+ "scalegroup": "second",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in second group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0, 0.3]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["1", "4"],
+ "width": 800,
+ "height": 800,
+ "shapes": [
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0.7, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0.7, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0.7, "y1": 1 }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_scalegroup_various_ratios.json b/test/image/mocks/funnelarea_scalegroup_various_ratios.json
new file mode 100644
index 00000000000..f2e9a9c3fbc
--- /dev/null
+++ b/test/image/mocks/funnelarea_scalegroup_various_ratios.json
@@ -0,0 +1,164 @@
+{
+ "data": [
+ {
+ "baseratio": 0,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.7, 1],
+ "y": [0, 0.3]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in one group"
+ },
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.35, 0.65],
+ "y": [0, 0.3]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0.7, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0.35, 0.65]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0, 0.3],
+ "y": [0, 0.3]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["1", "4"],
+ "width": 800,
+ "height": 800,
+ "shapes": [
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.3, "y0": 0.7, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0.35, "x1": 0.65, "y0": 0.7, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0, "y1": 0.3 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0.35, "y1": 0.65 },
+ { "type": "rect", "layer": "below", "x0": 0.7, "x1": 1, "y0": 0.7, "y1": 1 }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_scalegroup_various_ratios_and_domain.json b/test/image/mocks/funnelarea_scalegroup_various_ratios_and_domain.json
new file mode 100644
index 00000000000..19c8d3018b4
--- /dev/null
+++ b/test/image/mocks/funnelarea_scalegroup_various_ratios_and_domain.json
@@ -0,0 +1,164 @@
+{
+ "data": [
+ {
+ "baseratio": 0,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 2,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top right",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.8, 1],
+ "y": [0, 0.5]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in one group"
+ },
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 1,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top center",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0.5, 0.8],
+ "y": [0, 0.5]
+ }
+ },
+ {
+ "baseratio": 0,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.8, 1]
+ }
+ },
+ {
+ "baseratio": 0.5,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.5, 0.8]
+ }
+ },
+ {
+ "baseratio": 1,
+ "aspectratio": 0.5,
+ "type": "funnelarea",
+ "scalegroup": "one",
+ "values": [60, 50, 40, 30, 20, 10],
+ "textinfo": "value",
+ "title": {
+ "position": "top left",
+ "text": "10 - 60 in one group"
+ },
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0, 0.5]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["1", "4"],
+ "width": 800,
+ "height": 800,
+ "shapes": [
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0, "x1": 0.5, "y0": 0.8, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0.5, "x1": 0.8, "y0": 0.8, "y1": 1 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0, "y1": 0.5 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0.5, "y1": 0.8 },
+ { "type": "rect", "layer": "below", "x0": 0.8, "x1": 1, "y0": 0.8, "y1": 1 }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_simple.json b/test/image/mocks/funnelarea_simple.json
new file mode 100644
index 00000000000..c445e7bb3f2
--- /dev/null
+++ b/test/image/mocks/funnelarea_simple.json
@@ -0,0 +1,18 @@
+{
+ "data": [
+ {
+ "values": [
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ ],
+ "type": "funnelarea"
+ }
+ ],
+ "layout": {
+ "height": 300,
+ "width": 400
+ }
+}
diff --git a/test/image/mocks/funnelarea_style.json b/test/image/mocks/funnelarea_style.json
new file mode 100644
index 00000000000..c512d0230bd
--- /dev/null
+++ b/test/image/mocks/funnelarea_style.json
@@ -0,0 +1,62 @@
+{
+ "data": [
+ {
+ "values": [
+ 20,
+ 19,
+ 18,
+ 17,
+ 16,
+ 15,
+ 14,
+ 13,
+ 12,
+ 11,
+ 10
+ ],
+ "marker": {
+ "line": {
+ "color": "#fff",
+ "width": 3
+ }
+ },
+ "type": "funnelarea"
+ }
+ ],
+ "layout": {
+ "height": 400,
+ "width": 500,
+ "paper_bgcolor": "#ddd",
+ "showlegend": false,
+ "shapes": [
+ {
+ "type": "rect",
+ "xref": "paper",
+ "yref": "paper",
+ "x0": 0,
+ "y0": 0.4,
+ "x1": 1,
+ "y1": 0.6,
+ "fillcolor": "rgba(128, 0, 0, 0.5)",
+ "line": {
+ "width": 0
+ },
+ "layer": "below"
+ },
+ {
+ "type": "rect",
+ "xref": "paper",
+ "yref": "paper",
+ "x0": 0.4,
+ "y0": 0,
+ "x1": 0.6,
+ "y1": 1,
+ "fillcolor": "rgba(0, 0, 128, 0.5)",
+ "line": {
+ "width": 0
+ },
+ "layer": "above"
+ }
+ ]
+ }
+}
diff --git a/test/image/mocks/funnelarea_title_multiple.json b/test/image/mocks/funnelarea_title_multiple.json
new file mode 100644
index 00000000000..85797c08ece
--- /dev/null
+++ b/test/image/mocks/funnelarea_title_multiple.json
@@ -0,0 +1,143 @@
+{
+ "data": [
+ {
+ "values": [
+ 83100,
+ 38600,
+ 15400,
+ 11100,
+ 1740,
+ 77
+ ],
+ "labels": [
+ "Palladium",
+ "Platinum",
+ "Ruthenium",
+ "Rhodium",
+ "Iridium",
+ "Osmium"
+ ],
+ "type": "funnelarea",
+ "name": "Year 2013",
+ "title": "Year
2013",
+ "titleposition": "top left",
+ "domain": {
+ "x": [
+ 0,
+ 0.5
+ ],
+ "y": [
+ 0.51,
+ 1
+ ]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ },
+ {
+ "values": [
+ 92900,
+ 45800,
+ 11100,
+ 11000,
+ 1960,
+ 322
+ ],
+ "labels": [
+ "Palladium",
+ "Platinum",
+ "Rhodium",
+ "Ruthenium",
+ "Iridium",
+ "Osmium"
+ ],
+ "type": "funnelarea",
+ "name": "Year 2014",
+ "title": "Year
2014",
+ "titleposition": "top right",
+ "domain": {
+ "x": [
+ 0.51,
+ 1
+ ],
+ "y": [
+ 0.51,
+ 1
+ ]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ },
+ {
+ "values": [
+ 85300,
+ 42700,
+ 10600,
+ 8230,
+ 1010,
+ 8
+ ],
+ "labels": [
+ "Palladium",
+ "Platinum",
+ "Rhodium",
+ "Ruthenium",
+ "Iridium",
+ "Osmium"
+ ],
+ "type": "funnelarea",
+ "name": "Year 2015",
+ "title": "Year
2015",
+ "domain": {
+ "x": [
+ 0,
+ 0.5
+ ],
+ "y": [
+ 0,
+ 0.5
+ ]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ },
+ {
+ "values": [
+ 80400,
+ 42300,
+ 10700,
+ 8410,
+ 1300,
+ 27
+ ],
+ "labels": [
+ "Palladium",
+ "Platinum",
+ "Rhodium",
+ "Ruthenium",
+ "Iridium",
+ "Osmium"
+ ],
+ "type": "funnelarea",
+ "name": "Year 2016",
+ "title": "Year
2016",
+ "domain": {
+ "x": [
+ 0.51,
+ 1
+ ],
+ "y": [
+ 0,
+ 0.5
+ ]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ }
+ ],
+ "layout": {
+ "title": "U.S. Imports for Platinum-group Metals",
+ "height": 400,
+ "width": 500
+ }
+}
diff --git a/test/image/mocks/funnelarea_with_other_traces.json b/test/image/mocks/funnelarea_with_other_traces.json
new file mode 100644
index 00000000000..42bf299f4dd
--- /dev/null
+++ b/test/image/mocks/funnelarea_with_other_traces.json
@@ -0,0 +1,82 @@
+{
+ "data": [
+ {
+ "name": "pie",
+ "type": "pie",
+ "labels": ["A", "B", "C", "D", "E", "F"],
+ "values": [6.000006, 5.00005, 4.0004, 3.003, 2.02, 1.1],
+ "text": [true, "", "0", 0, 1, null],
+ "textinfo": "label+text+value+percent",
+ "domain": {
+ "x": [0.52, 1],
+ "y": [0.52, 1]
+ }
+ },
+ {
+ "name": "sunburst",
+ "type": "sunburst",
+ "parents": ["", "A", "B", "C", "D", "E"],
+ "labels": ["A", "B", "C", "D", "E", "F"],
+ "values": [6.000006, 5.00005, 4.0004, 3.003, 2.02, 1.1],
+ "text": [true, "", "0", 0, 1, null],
+ "textinfo": "label+text+value",
+ "domain": {
+ "x": [0.52, 1],
+ "y": [0, 0.48]
+ }
+ },
+ {
+ "name": "funnel-1",
+ "type": "funnel",
+ "y": ["A", "B", "C", "D", "E", "F"],
+ "x": [6.000006, 5.00005, 4.0004, 3.003, 2.02, 1.1],
+ "text": [true, "", "0", 0, 1, null],
+ "textinfo": "label+text+value+percent total",
+ "domain": {
+ "x": [0, 0.48],
+ "y": [0, 0.48]
+ }
+ },
+ {
+ "name": "funnel-2",
+ "type": "funnel",
+ "y": ["A", "B", "C", "D", "E", "F"],
+ "x": [1.6, 1.5, 1.4, 1.3, 1.2, 1.1],
+ "domain": {
+ "x": [0, 0.48],
+ "y": [0, 0.48]
+ }
+ },
+ {
+ "name": "funnelarea",
+ "type": "funnelarea",
+ "labels": ["A", "B", "C", "D", "E", "F"],
+ "values": [6.000006, 5.00005, 4.0004, 3.003, 2.02, 1.1],
+ "text": [true, "", "0", 0, 1, null],
+ "textinfo": "label+text+value+percent",
+ "domain": {
+ "x": [0, 0.48],
+ "y": [0.52, 1]
+ }
+ }
+ ],
+ "layout": {
+ "hiddenlabels": ["B", "E"],
+ "width": 800,
+ "height": 800,
+ "hovermode": "closest",
+ "dragmode": "pan",
+ "xaxis": {
+ "domain": [
+ 0,
+ 0.48
+ ]
+ },
+ "yaxis": {
+ "domain": [
+ 0,
+ 0.48
+ ]
+ }
+ }
+}
diff --git a/test/jasmine/assets/mock_lists.js b/test/jasmine/assets/mock_lists.js
index 24011ff51c3..7d962d594d5 100644
--- a/test/jasmine/assets/mock_lists.js
+++ b/test/jasmine/assets/mock_lists.js
@@ -16,6 +16,7 @@ var svgMockList = [
['bar_and_histogram', require('@mocks/bar_and_histogram.json')],
['waterfall', require('@mocks/waterfall_profit-loss_2018vs2019_rectangle.json')],
['funnel', require('@mocks/funnel_horizontal_group_basic.json')],
+ ['funnelarea', require('@mocks/funnelarea_title_multiple.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/calcdata_test.js b/test/jasmine/tests/calcdata_test.js
index f26b1481ce7..181d7394eec 100644
--- a/test/jasmine/tests/calcdata_test.js
+++ b/test/jasmine/tests/calcdata_test.js
@@ -4,6 +4,7 @@ var BADNUM = require('@src/constants/numerical').BADNUM;
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var failTest = require('../assets/fail_test');
+var delay = require('../assets/delay');
var Lib = require('@src/lib');
describe('calculated data and points', function() {
@@ -985,6 +986,7 @@ describe('calculated data and points', function() {
expect(gd._fullLayout[trace.type === 'splom' ? 'xaxis' : axName]._categories).toEqual(finalOrder, 'wrong order');
}
})
+ .then(delay(100))
.catch(failTest)
.then(done);
}
diff --git a/test/jasmine/tests/funnelarea_test.js b/test/jasmine/tests/funnelarea_test.js
new file mode 100644
index 00000000000..81b6e50f492
--- /dev/null
+++ b/test/jasmine/tests/funnelarea_test.js
@@ -0,0 +1,1700 @@
+var Plotly = require('@lib/index');
+var Lib = require('@src/lib');
+
+var d3 = require('d3');
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+var failTest = require('../assets/fail_test');
+var click = require('../assets/click');
+var getClientPosition = require('../assets/get_client_position');
+var mouseEvent = require('../assets/mouse_event');
+var supplyAllDefaults = require('../assets/supply_defaults');
+var rgb = require('../../../src/components/color').rgb;
+
+var customAssertions = require('../assets/custom_assertions');
+var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle;
+var assertHoverLabelContent = customAssertions.assertHoverLabelContent;
+
+var SLICES_SELECTOR = '.slice path';
+var SLICES_TEXT_SELECTOR = '.funnelarealayer text.slicetext';
+var LEGEND_ENTRIES_SELECTOR = '.legendpoints path';
+
+function convexPolygonArea(points) {
+ var s1 = 0;
+ var s2 = 0;
+ var n = points.length;
+ for(var i = 0; i < n; i++) {
+ var k = (i + 1) % n;
+ var x0 = points[i][0];
+ var y0 = points[i][1];
+ var x1 = points[k][0];
+ var y1 = points[k][1];
+ s1 += x0 * y1;
+ s2 += x1 * y0;
+ }
+ return 0.5 * Math.abs(s1 - s2);
+}
+
+describe('Funnelarea defaults', function() {
+ function _supply(trace, layout) {
+ var gd = {
+ data: [trace],
+ layout: layout || {}
+ };
+
+ supplyAllDefaults(gd);
+
+ return gd._fullData[0];
+ }
+
+ it('finds the minimum length of labels & values', function() {
+ var out = _supply({type: 'funnelarea', labels: ['A', 'B'], values: [1, 2, 3]});
+ expect(out._length).toBe(2);
+
+ out = _supply({type: 'funnelarea', labels: ['A', 'B', 'C'], values: [1, 2]});
+ expect(out._length).toBe(2);
+ });
+
+ it('allows labels or values to be missing but not both', function() {
+ var out = _supply({type: 'funnelarea', values: [1, 2]});
+ expect(out.visible).toBe(true);
+ expect(out._length).toBe(2);
+ expect(out.label0).toBe(0);
+ expect(out.dlabel).toBe(1);
+
+ out = _supply({type: 'funnelarea', labels: ['A', 'B']});
+ expect(out.visible).toBe(true);
+ expect(out._length).toBe(2);
+
+ out = _supply({type: 'funnelarea'});
+ expect(out.visible).toBe(false);
+ });
+
+ it('is marked invisible if either labels or values is empty', function() {
+ var out = _supply({type: 'funnelarea', labels: [], values: [1, 2]});
+ expect(out.visible).toBe(false);
+
+ out = _supply({type: 'funnelarea', labels: ['A', 'B'], values: []});
+ expect(out.visible).toBe(false);
+ });
+
+ it('does not apply layout.font.color to insidetextfont.color (it\'ll be contrasting instead)', function() {
+ var out = _supply({type: 'funnelarea', values: [1, 2]}, {font: {color: 'blue'}});
+ expect(out.insidetextfont.color).toBe(undefined);
+ });
+
+ it('does apply textfont.color to insidetextfont.color if not set', function() {
+ var out = _supply({type: 'funnelarea', values: [1, 2], textfont: {color: 'blue'}}, {font: {color: 'red'}});
+ expect(out.insidetextfont.color).toBe('blue');
+ });
+});
+
+describe('Funnelarea traces', function() {
+ 'use strict';
+
+ var DARK = '#444';
+ var LIGHT = '#fff';
+
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+
+ afterEach(destroyGraphDiv);
+
+ it('should separate colors and opacities', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [1, 2, 3, 4, 5],
+ type: 'funnelarea',
+ marker: {
+ line: {width: 3, color: 'rgba(100,100,100,0.7)'},
+ colors: [
+ 'rgba(0,0,0,0.2)',
+ 'rgba(255,0,0,0.3)',
+ 'rgba(0,255,0,0.4)',
+ 'rgba(0,0,255,0.5)',
+ 'rgba(255,255,0,0.6)'
+ ]
+ }
+ }], {height: 300, width: 400}).then(function() {
+ var colors = [
+ 'rgb(0,0,0)',
+ 'rgb(255,0,0)',
+ 'rgb(0,255,0)',
+ 'rgb(0,0,255)',
+ 'rgb(255,255,0)'
+ ];
+ var opacities = [0.2, 0.3, 0.4, 0.5, 0.6];
+
+ function checkPath(d, i) {
+ // strip spaces (ie 'rgb(0, 0, 0)') so we're not dependent on browser specifics
+ expect(this.style.fill.replace(/\s/g, '')).toBe(colors[i]);
+ expect(this.style.fillOpacity).toBe(String(opacities[i]));
+ expect(this.style.stroke.replace(/\s/g, '')).toBe('rgb(100,100,100)');
+ expect(this.style.strokeOpacity).toBe('0.7');
+ }
+ var slices = d3.selectAll(SLICES_SELECTOR);
+ slices.each(checkPath);
+ expect(slices.size()).toBe(5);
+
+ var legendEntries = d3.selectAll(LEGEND_ENTRIES_SELECTOR);
+ legendEntries.each(checkPath);
+ expect(legendEntries.size()).toBe(5);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('can sum values or count labels', function(done) {
+ Plotly.newPlot(gd, [{
+ labels: ['a', 'b', 'c', 'a', 'b', 'a'],
+ values: [1, 2, 3, 4, 5, 6],
+ type: 'funnelarea',
+ domain: {x: [0, 0.45]}
+ }, {
+ labels: ['d', 'e', 'f', 'd', 'e', 'd'],
+ type: 'funnelarea',
+ domain: {x: [0.55, 1]}
+ }])
+ .then(function() {
+ var expected = [
+ [['a', 11], ['b', 7], ['c', 3]],
+ [['d', 3], ['e', 2], ['f', 1]]
+ ];
+ for(var i = 0; i < 2; i++) {
+ for(var j = 0; j < 3; j++) {
+ expect(gd.calcdata[i][j].label).toBe(expected[i][j][0], i + ',' + j);
+ expect(gd.calcdata[i][j].v).toBe(expected[i][j][1], i + ',' + j);
+ }
+ }
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ function _checkSliceColors(colors) {
+ return function() {
+ d3.select(gd).selectAll(SLICES_SELECTOR).each(function(d, i) {
+ expect(this.style.fill.replace(/(\s|rgb\(|\))/g, '')).toBe(colors[i], i);
+ });
+ };
+ }
+
+ function _checkFontColors(expFontColors) {
+ return function() {
+ d3.selectAll(SLICES_TEXT_SELECTOR).each(function(d, i) {
+ expect(this.style.fill).toBe(rgb(expFontColors[i]), 'fill color of ' + i);
+ });
+ };
+ }
+
+ function _checkFontFamilies(expFontFamilies) {
+ return function() {
+ d3.selectAll(SLICES_TEXT_SELECTOR).each(function(d, i) {
+ expect(this.style.fontFamily).toBe(expFontFamilies[i], 'fontFamily of ' + i);
+ });
+ };
+ }
+
+ function _checkFontSizes(expFontSizes) {
+ return function() {
+ d3.selectAll(SLICES_TEXT_SELECTOR).each(function(d, i) {
+ expect(this.style.fontSize).toBe(expFontSizes[i] + 'px', 'fontSize of ' + i);
+ });
+ };
+ }
+
+ it('propagate explicit colors to the same labels in earlier OR later traces', function(done) {
+ var data1 = [
+ {type: 'funnelarea', values: [3, 2], marker: {colors: ['red', 'black']}, domain: {x: [0.5, 1]}},
+ {type: 'funnelarea', values: [2, 5], domain: {x: [0, 0.5]}}
+ ];
+ var data2 = Lib.extendDeep([], [data1[1], data1[0]]);
+
+ Plotly.newPlot(gd, data1)
+ .then(_checkSliceColors(['255,0,0', '0,0,0', '255,0,0', '0,0,0']))
+ .then(function() {
+ return Plotly.newPlot(gd, data2);
+ })
+ .then(_checkSliceColors(['255,0,0', '0,0,0', '255,0,0', '0,0,0']))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('can use a separate funnelarea colorway and disable extended colors', function(done) {
+ Plotly.newPlot(gd, [{type: 'funnelarea', values: [7, 6, 5, 4, 3, 2, 1]}], {colorway: ['#777', '#F00']})
+ .then(_checkSliceColors(['119,119,119', '255,0,0', '170,170,170', '255,102,102', '68,68,68', '153,0,0', '119,119,119']))
+ .then(function() {
+ return Plotly.relayout(gd, {extendfunnelareacolors: false});
+ })
+ .then(_checkSliceColors(['119,119,119', '255,0,0', '119,119,119', '255,0,0', '119,119,119', '255,0,0', '119,119,119']))
+ .then(function() {
+ return Plotly.relayout(gd, {funnelareacolorway: ['#FF0', '#0F0', '#00F']});
+ })
+ .then(_checkSliceColors(['255,255,0', '0,255,0', '0,0,255', '255,255,0', '0,255,0', '0,0,255', '255,255,0']))
+ .then(function() {
+ return Plotly.relayout(gd, {extendfunnelareacolors: null});
+ })
+ .then(_checkSliceColors(['255,255,0', '0,255,0', '0,0,255', '255,255,102', '102,255,102', '102,102,255', '153,153,0']))
+ .catch(failTest)
+ .then(done);
+ });
+
+ function _verifyTitle(checkLeft, checkRight, checkTop, checkBottom, checkMiddleX) {
+ return function() {
+ var title = d3.selectAll('.titletext text');
+ expect(title.size()).toBe(1);
+ var titleBox = d3.select('g.titletext').node().getBoundingClientRect();
+ var funnelareaBox = d3.select('g.trace').node().getBoundingClientRect();
+ // check that margins agree. we leave an error margin of 2.
+ if(checkLeft) expect(Math.abs(titleBox.left - funnelareaBox.left)).toBeLessThan(2);
+ if(checkRight) expect(Math.abs(titleBox.right - funnelareaBox.right)).toBeLessThan(2);
+ if(checkTop) expect(Math.abs(titleBox.top - funnelareaBox.top)).toBeLessThan(2);
+ if(checkBottom) expect(Math.abs(titleBox.bottom - funnelareaBox.bottom)).toBeLessThan(2);
+ if(checkMiddleX) {
+ expect(Math.abs(titleBox.left + titleBox.right - funnelareaBox.left - funnelareaBox.right))
+ .toBeLessThan(2);
+ }
+ };
+ }
+
+ it('shows title top center if titleposition is undefined', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [2, 2, 2, 2],
+ title: 'Test
Title',
+ titlefont: {
+ size: 12
+ },
+ type: 'funnelarea',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top center', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [1, 1, 1, 1, 2],
+ title: 'Test
Title',
+ titleposition: 'top center',
+ titlefont: {
+ size: 12
+ },
+ type: 'funnelarea',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top left', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [3, 2, 1],
+ title: 'Test
Title',
+ titleposition: 'top left',
+ titlefont: {
+ size: 12
+ },
+ type: 'funnelarea',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(true, false, true, false, false))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top right', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [4, 5, 6, 5],
+ title: 'Test
Title',
+ titleposition: 'top right',
+ titlefont: {
+ size: 12
+ },
+ type: 'funnelarea',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, true, true, false, false))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('correctly positions large title', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [1, 3, 4, 1, 2],
+ title: 'Test
Title',
+ titleposition: 'top center',
+ titlefont: {
+ size: 60
+ },
+ type: 'funnelarea',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('support separate stroke width values per slice', function(done) {
+ var data = [
+ {
+ values: [20, 26, 55],
+ labels: ['Residential', 'Non-Residential', 'Utility'],
+ type: 'funnelarea',
+ marker: {
+ colors: ['rebeccapurple', 'purple', 'mediumpurple'],
+ line: {
+ width: [3, 0, 0]
+ }
+ }
+ }
+ ];
+ var layout = {
+ showlegend: true
+ };
+
+ Plotly.plot(gd, data, layout)
+ .then(function() {
+ var expWidths = ['3', '0', '0'];
+
+ d3.selectAll(SLICES_SELECTOR).each(function(d, i) {
+ expect(this.style.strokeWidth).toBe(expWidths[d.pointNumber], 'sector #' + i);
+ });
+ d3.selectAll(LEGEND_ENTRIES_SELECTOR).each(function(d, i) {
+ expect(this.style.strokeWidth).toBe(expWidths[d[0].i], 'item #' + i);
+ });
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ [
+ {fontAttr: 'textfont', textposition: 'inside'},
+ {fontAttr: 'insidetextfont', textposition: 'inside'}
+ ].forEach(function(spec) {
+ var desc = 'allow to specify ' + spec.fontAttr +
+ ' properties per individual slice (textposition ' + spec.textposition + ')';
+ it(desc, function(done) {
+ var data = {
+ values: [3, 2, 1],
+ type: 'funnelarea',
+ textposition: spec.textposition
+ };
+ data[spec.fontAttr] = {
+ color: ['red', 'green', 'blue'],
+ family: ['Arial', 'Gravitas', 'Roboto'],
+ size: [12, 20, 16]
+ };
+
+ Plotly.plot(gd, [data])
+ .then(_checkFontColors(['red', 'green', 'blue']))
+ .then(_checkFontFamilies(['Arial', 'Gravitas', 'Roboto']))
+ .then(_checkFontSizes([12, 20, 16]))
+ .catch(failTest)
+ .then(done);
+ });
+ });
+
+ var insideTextTestsTrace = {
+ values: [6, 5, 4, 3, 2, 1],
+ type: 'funnelarea',
+ marker: {
+ colors: ['#ee1', '#eee', '#333', '#9467bd', '#dda', '#922'],
+ }
+ };
+
+ it('should use inside text colors contrasting to explicitly set slice colors by default', function(done) {
+ Plotly.plot(gd, [insideTextTestsTrace])
+ .then(_checkFontColors([DARK, DARK, LIGHT, LIGHT, DARK, LIGHT]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should use inside text colors contrasting to standard slice colors by default', function(done) {
+ var noMarkerTrace = Lib.extendFlat({}, insideTextTestsTrace);
+ delete noMarkerTrace.marker;
+
+ Plotly.plot(gd, [noMarkerTrace])
+ .then(_checkFontColors([LIGHT, DARK, LIGHT, LIGHT, LIGHT, LIGHT]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should use textfont.color for inside text instead of the contrasting default', function(done) {
+ var data = Lib.extendFlat({}, insideTextTestsTrace, {textfont: {color: 'red'}});
+ Plotly.plot(gd, [data])
+ .then(_checkFontColors(Lib.repeat('red', 6)))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should use matching color from textfont.color array for inside text, contrasting otherwise', function(done) {
+ var data = Lib.extendFlat({}, insideTextTestsTrace, {textfont: {color: ['red', 'blue']}});
+ Plotly.plot(gd, [data])
+ .then(_checkFontColors(['red', 'blue', LIGHT, LIGHT, DARK, LIGHT]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should not use layout.font.color for inside text, but a contrasting color instead', function(done) {
+ Plotly.plot(gd, [insideTextTestsTrace], {font: {color: 'green'}})
+ .then(_checkFontColors([DARK, DARK, LIGHT, LIGHT, DARK, LIGHT]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should use matching color from insidetextfont.color array instead of the contrasting default', function(done) {
+ var data = Lib.extendFlat({}, insideTextTestsTrace, {textfont: {color: ['orange', 'purple']}});
+ Plotly.plot(gd, [data])
+ .then(_checkFontColors(['orange', 'purple', LIGHT, LIGHT, DARK, LIGHT]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ [
+ {fontAttr: 'insidetextfont', textposition: 'inside'}
+ ].forEach(function(spec) {
+ it('should fall back to textfont scalar values if ' + spec.fontAttr + ' value ' +
+ 'arrays don\'t cover all slices', function(done) {
+ var data = Lib.extendFlat({}, insideTextTestsTrace, {
+ textposition: spec.textposition,
+ textfont: {color: 'orange', family: 'Gravitas', size: 12}
+ });
+ data[spec.fontAttr] = {color: ['blue', 'yellow'], family: ['Arial', 'Arial'], size: [24, 34]};
+
+ Plotly.plot(gd, [data])
+ .then(_checkFontColors(['blue', 'yellow', 'orange', 'orange', 'orange', 'orange']))
+ .then(_checkFontFamilies(['Arial', 'Arial', 'Gravitas', 'Gravitas', 'Gravitas', 'Gravitas']))
+ .then(_checkFontSizes([24, 34, 12, 12, 12, 12]))
+ .catch(failTest)
+ .then(done);
+ });
+ });
+
+ it('should fall back to textfont array values and layout.font scalar (except color)' +
+ ' values for inside text', function(done) {
+ var layout = {font: {color: 'orange', family: 'serif', size: 16}};
+ var data = Lib.extendFlat({}, insideTextTestsTrace, {
+ textfont: {
+ color: ['blue', 'blue'], family: ['Arial', 'Arial'], size: [18, 18]
+ },
+ insidetextfont: {
+ color: ['purple'], family: ['Roboto'], size: [24]
+ }
+ });
+
+ Plotly.plot(gd, [data], layout)
+ .then(_checkFontColors(['purple', 'blue', LIGHT, LIGHT, DARK, LIGHT]))
+ .then(_checkFontFamilies(['Roboto', 'Arial', 'serif', 'serif', 'serif', 'serif']))
+ .then(_checkFontSizes([24, 18, 16, 16, 16, 16]))
+ .catch(failTest)
+ .then(done);
+ });
+
+ [
+ {fontAttr: 'textfont'},
+ {fontAttr: 'insidetextfont'}
+ ].forEach(function(spec) {
+ it('should fall back to layout.font scalar values for inside text (except color) if ' + spec.fontAttr + ' value ' +
+ 'arrays don\'t cover all slices', function(done) {
+ var layout = {font: {color: 'orange', family: 'serif', size: 16}};
+ var data = Lib.extendFlat({}, insideTextTestsTrace);
+ data.textposition = 'inside';
+ data[spec.fontAttr] = {color: ['blue', 'yellow'], family: ['Arial', 'Arial'], size: [24, 34]};
+
+ Plotly.plot(gd, [data], layout)
+ .then(_checkFontColors(['blue', 'yellow', LIGHT, LIGHT, DARK, LIGHT]))
+ .then(_checkFontFamilies(['Arial', 'Arial', 'serif', 'serif', 'serif', 'serif']))
+ .then(_checkFontSizes([24, 34, 16, 16, 16, 16]))
+ .catch(failTest)
+ .then(done);
+ });
+ });
+
+ function _assertTitle(msg, expText, expColor) {
+ var title = d3.select('.titletext > text');
+ expect(title.text()).toBe(expText, msg + ' text');
+ expect(title.node().style.fill).toBe(expColor, msg + ' color');
+ }
+
+ it('show a user-defined title with a custom position and font', function(done) {
+ Plotly.plot(gd, [{
+ type: 'funnelarea',
+ values: [1, 2, 3],
+ title: {
+ text: 'yo',
+ font: {color: 'blue'},
+ position: 'top left'
+ }
+ }])
+ .then(function() {
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to restyle title', function(done) {
+ Plotly.plot(gd, [{
+ type: 'funnelarea',
+ values: [1, 2, 3],
+ title: {
+ text: 'yo',
+ font: {color: 'blue'},
+ position: 'top left'
+ }
+ }])
+ .then(function() {
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+
+ return Plotly.restyle(gd, {
+ 'title.text': 'oy',
+ 'title.font.color': 'red',
+ 'title.position': 'top right'
+ });
+ })
+ .then(function() {
+ _assertTitle('base', 'oy', 'rgb(255, 0, 0)');
+ _verifyTitle(false, true, true, false, false);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to restyle title despite using the deprecated attributes', function(done) {
+ Plotly.plot(gd, [{
+ type: 'funnelarea',
+ values: [1, 2, 3],
+ title: 'yo',
+ titlefont: {color: 'blue'},
+ titleposition: 'top left'
+ }])
+ .then(function() {
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+
+ return Plotly.restyle(gd, {
+ 'title': 'oy',
+ 'titlefont.color': 'red',
+ 'titleposition': 'top right'
+ });
+ })
+ .then(function() {
+ _assertTitle('base', 'oy', 'rgb(255, 0, 0)');
+ _verifyTitle(false, true, true, false, false);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to react with new text colors', function(done) {
+ Plotly.plot(gd, [{
+ type: 'funnelarea',
+ values: [1, 2, 3],
+ text: ['A', 'B', 'C'],
+ textposition: 'inside'
+ }])
+ .then(_checkFontColors(['rgb(255, 255, 255)', 'rgb(68, 68, 68)', 'rgb(255, 255, 255)']))
+ .then(function() {
+ gd.data[0].insidetextfont = {color: 'red'};
+ return Plotly.react(gd, gd.data);
+ })
+ .then(_checkFontColors(['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(_checkFontColors(['rgb(0, 0, 255)', 'rgb(0, 0, 255)', 'rgb(0, 0, 255)']))
+ .then(function() {
+ gd.data[0].textposition = 'none';
+ return Plotly.react(gd, gd.data);
+ })
+ .then(_checkFontColors(['rgb(0, 0, 255)', 'rgb(0, 0, 255)', 'rgb(0, 0, 255)']))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to toggle visibility', function(done) {
+ var mock = Lib.extendDeep({}, require('@mocks/funnelarea_title_multiple.json'));
+
+ function _assert(msg, exp) {
+ return function() {
+ var layer = d3.select(gd).select('.funnelarealayer');
+ expect(layer.selectAll('.trace').size()).toBe(exp, msg);
+ };
+ }
+
+ Plotly.plot(gd, mock)
+ .then(_assert('base', 4))
+ .then(function() { return Plotly.restyle(gd, 'visible', false); })
+ .then(_assert('both visible:false', 0))
+ .then(function() { return Plotly.restyle(gd, 'visible', true); })
+ .then(_assert('back to visible:true', 4))
+ .catch(failTest)
+ .then(done);
+ });
+});
+
+describe('funnelarea hovering', function() {
+ var mock = require('@mocks/funnelarea_simple.json');
+
+ describe('with hoverinfo set to none', function() {
+ var mockCopy = Lib.extendDeep({}, mock);
+ var gd;
+
+ mockCopy.data[0].hoverinfo = 'none';
+
+ beforeEach(function(done) {
+ gd = createGraphDiv();
+
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(done);
+ });
+
+ afterEach(destroyGraphDiv);
+
+ it('should fire hover event when moving from one slice to another', function(done) {
+ var count = 0;
+ var hoverData = [];
+
+ gd.on('plotly_hover', function(data) {
+ count++;
+ hoverData.push(data);
+ });
+
+ mouseEvent('mouseover', 200, 100);
+ setTimeout(function() {
+ mouseEvent('mouseover', 200, 200);
+ expect(count).toEqual(2);
+ expect(hoverData[0]).not.toEqual(hoverData[1]);
+ done();
+ }, 100);
+ });
+
+ it('should fire unhover event when the mouse moves off the graph', function(done) {
+ var count = 0;
+ var unhoverData = [];
+
+ gd.on('plotly_unhover', function(data) {
+ count++;
+ unhoverData.push(data);
+ });
+
+ mouseEvent('mouseover', 200, 100);
+ mouseEvent('mouseout', 200, 100);
+ setTimeout(function() {
+ mouseEvent('mouseover', 200, 200);
+ mouseEvent('mouseout', 200, 200);
+ expect(count).toEqual(2);
+ expect(unhoverData[0]).not.toEqual(unhoverData[1]);
+ done();
+ }, 100);
+ });
+ });
+
+ describe('event data', function() {
+ var mockCopy = Lib.extendDeep({}, mock);
+ var width = mockCopy.layout.width;
+ var height = mockCopy.layout.height;
+ var gd;
+
+ beforeEach(function(done) {
+ gd = createGraphDiv();
+
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(done);
+ });
+
+ afterEach(destroyGraphDiv);
+
+ it('should contain the correct fields', function() {
+ var hoverData;
+ var unhoverData;
+
+ gd.on('plotly_hover', function(data) {
+ hoverData = data;
+ });
+
+ gd.on('plotly_unhover', function(data) {
+ unhoverData = data;
+ });
+
+ mouseEvent('mouseover', width / 2 - 7, height / 2 + 50);
+ mouseEvent('mouseout', width / 2 - 7, height / 2 + 50);
+
+ expect(hoverData.points.length).toEqual(1);
+ expect(unhoverData.points.length).toEqual(1);
+
+ var fields = [
+ 'curveNumber', 'pointNumber', 'pointNumbers',
+ 'data', 'fullData',
+ 'label', 'color', 'value',
+ 'percent', 'text'
+ ];
+
+ expect(Object.keys(hoverData.points[0]).sort()).toEqual(fields.sort());
+ expect(hoverData.points[0].pointNumber).toEqual(3);
+
+ expect(Object.keys(unhoverData.points[0]).sort()).toEqual(fields.sort());
+ expect(unhoverData.points[0].pointNumber).toEqual(3);
+ });
+
+ it('should fire hover event when moving from one slice to another', function(done) {
+ var count = 0;
+ var hoverData = [];
+
+ gd.on('plotly_hover', function(data) {
+ count++;
+ hoverData.push(data);
+ });
+
+ mouseEvent('mouseover', 200, 100);
+ setTimeout(function() {
+ mouseEvent('mouseover', 200, 200);
+ expect(count).toEqual(2);
+ expect(hoverData[0]).not.toEqual(hoverData[1]);
+ done();
+ }, 100);
+ });
+
+ it('should fire unhover event when the mouse moves off the graph', function(done) {
+ var count = 0;
+ var unhoverData = [];
+
+ gd.on('plotly_unhover', function(data) {
+ count++;
+ unhoverData.push(data);
+ });
+
+ mouseEvent('mouseover', 200, 100);
+ mouseEvent('mouseout', 200, 100);
+ setTimeout(function() {
+ mouseEvent('mouseover', 200, 200);
+ mouseEvent('mouseout', 200, 200);
+ expect(count).toEqual(2);
+ expect(unhoverData[0]).not.toEqual(unhoverData[1]);
+ done();
+ }, 100);
+ });
+ });
+
+ describe('labels', function() {
+ var gd, mockCopy;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ mockCopy = Lib.extendDeep({}, mock);
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function _hover() {
+ mouseEvent('mouseover', 200, 125);
+ Lib.clearThrottle();
+ }
+
+ function _hover2() {
+ mouseEvent('mouseover', 200, 225);
+ Lib.clearThrottle();
+ }
+
+ function assertLabel(content, style, msg) {
+ assertHoverLabelContent({nums: content}, msg);
+
+ if(style) {
+ assertHoverLabelStyle(d3.select('.hovertext'), {
+ bgcolor: style[0],
+ bordercolor: style[1],
+ fontSize: style[2],
+ fontFamily: style[3],
+ fontColor: style[4]
+ }, msg);
+ }
+ }
+
+ it('should show the default selected values', function(done) {
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', '5', '33.3%'].join('\n'),
+ ['rgb(31, 119, 180)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'],
+ 'initial'
+ );
+
+ return Plotly.restyle(gd, 'text', [['E', 'D', 'C', 'B', 'A']]);
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', 'E', '5', '33.3%'].join('\n'),
+ null,
+ 'added text'
+ );
+
+ return Plotly.restyle(gd, 'hovertext', [[
+ 'Eggplant', 'Dragon Fruit', 'Clementine', 'Banana', 'Apple'
+ ]]);
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', 'Eggplant', '5', '33.3%'].join('\n'),
+ null,
+ 'added hovertext'
+ );
+
+ return Plotly.restyle(gd, 'hovertext', 'SUP');
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', 'SUP', '5', '33.3%'].join('\n'),
+ null,
+ 'constant hovertext'
+ );
+
+ return Plotly.restyle(gd, {
+ 'hoverlabel.bgcolor': [['red', 'green', 'blue', 'yellow', 'red']],
+ 'hoverlabel.bordercolor': 'yellow',
+ 'hoverlabel.font.size': [[15, 20, 30, 20, 15]],
+ 'hoverlabel.font.family': 'Roboto',
+ 'hoverlabel.font.color': 'blue'
+ });
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', 'SUP', '5', '33.3%'].join('\n'),
+ ['rgb(255, 0, 0)', 'rgb(255, 255, 0)', 15, 'Roboto', 'rgb(0, 0, 255)'],
+ 'new styles'
+ );
+
+ return Plotly.restyle(gd, 'hoverinfo', [['label+percent', null, null, null, null]]);
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(['0', '33.3%'].join('\n'), null, 'new hoverinfo');
+
+ return Plotly.restyle(gd, 'hoverinfo', [['dont+know+what+im-doing', null, null, null, null]]);
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', 'SUP', '5', '33.3%'].join('\n'),
+ null,
+ 'garbage hoverinfo'
+ );
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should show the correct separators for values', function(done) {
+ mockCopy.layout.separators = '@|';
+ mockCopy.data[0].values[0] = 12345678.912;
+ mockCopy.data[0].values[1] = 10000;
+
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(_hover)
+ .then(function() {
+ assertLabel('0\n12|345|678@91\n99@9%');
+ })
+ .then(done);
+ });
+
+ it('should show falsy zero text', function(done) {
+ Plotly.plot(gd, {
+ data: [{
+ type: 'funnelarea',
+ labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
+ values: [7, 6, 5, 4, 3, 2, 1],
+ text: [null, '', '0', 0, 1, true, false],
+ textinfo: 'label+text+value'
+ }],
+ layout: {
+ width: 400,
+ height: 400
+ }
+ })
+ .then(_hover2)
+ .then(function() {
+ assertLabel('D\n0\n4\n14.3%');
+ })
+ .then(done);
+ });
+
+ it('should use hovertemplate if specified', function(done) {
+ mockCopy.data[0].name = '';
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0', '5', '33.3%'].join('\n'),
+ ['rgb(31, 119, 180)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'],
+ 'initial'
+ );
+
+ return Plotly.restyle(gd, 'hovertemplate', '%{value}');
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['5'].join('\n'),
+ null,
+ 'hovertemplate %{value}'
+ );
+
+ return Plotly.restyle(gd, {
+ 'text': [['E', 'D', 'C', 'B', 'A']],
+ 'hovertemplate': '%{text}'
+ });
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['E'].join('\n'),
+ null,
+ 'hovertemplate %{text}'
+ );
+
+ return Plotly.restyle(gd, 'hovertemplate', '%{percent}');
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['33.3%'].join('\n'),
+ null,
+ 'hovertemplate %{percent}'
+ );
+
+ return Plotly.restyle(gd, 'hovertemplate', '%{label}');
+ })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['0'].join('\n'),
+ null,
+ 'hovertemplate %{label}'
+ );
+ })
+ .then(function() { return Plotly.restyle(gd, 'hovertemplate', [['ht 5 %{percent:0.2%}', '', '', '', '']]); })
+ .then(_hover)
+ .then(function() {
+ assertLabel(
+ ['ht 5 33.33%'].join('\n'),
+ null,
+ 'hovertemplate arrayOK'
+ );
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should honor *hoverlabel.namelength*', function(done) {
+ mockCopy.data[0].name = 'loooooooooooooooooooooooong';
+ mockCopy.data[0].hoverinfo = 'all';
+
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(_hover)
+ .then(function() {
+ assertHoverLabelContent({nums: '0\n5\n33.3%', name: 'looooooooooo...'}, 'base');
+ })
+ .then(function() { return Plotly.restyle(gd, 'hoverlabel.namelength', 2); })
+ .then(_hover)
+ .then(function() {
+ assertHoverLabelContent({nums: '0\n5\n33.3%', name: 'lo'}, 'base');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+});
+
+
+describe('Test event data of interactions on a funnelarea plot:', function() {
+ var mock = require('@mocks/funnelarea_simple.json');
+
+ var mockCopy, gd;
+
+ var blankPos = [10, 10];
+ var pointPos;
+
+ beforeAll(function(done) {
+ gd = createGraphDiv();
+ mockCopy = Lib.extendDeep({}, mock);
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
+ pointPos = getClientPosition('g.slicetext');
+ destroyGraphDiv();
+ done();
+ });
+ });
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ mockCopy = Lib.extendDeep({}, mock);
+ Lib.extendFlat(mockCopy.data[0], {
+ ids: ['marge', 'homer', 'bart', 'lisa', 'maggie'],
+ customdata: [{1: 2}, {3: 4}, {5: 6}, {7: 8}, {9: 10}]
+ });
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function checkEventData(data) {
+ var point = data.points[0];
+
+ expect(point.curveNumber).toBe(0);
+ expect(point.pointNumber).toBe(0);
+ expect(point.pointNumbers).toEqual([0]);
+ expect(point.data).toBe(gd.data[0]);
+ expect(point.fullData).toBe(gd._fullData[0]);
+ expect(point.label).toBe('0');
+ expect(point.value).toBe(5);
+ expect(point.color).toBe('#1f77b4');
+ expect(point.id).toEqual(['marge']);
+ expect(point.customdata).toEqual([{1: 2}]);
+
+ // no need for backward compat i/v
+ expect('i' in point).toBe(false);
+ expect('v' in point).toBe(false);
+
+ var evt = data.event;
+ expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
+ expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
+ }
+
+ describe('click events', function() {
+ var futureData;
+
+ beforeEach(function(done) {
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
+
+ gd.on('plotly_click', function(data) {
+ futureData = data;
+ });
+ });
+
+ it('should not be trigged when not on data points', function() {
+ click(blankPos[0], blankPos[1]);
+ expect(futureData).toBe(undefined);
+ });
+
+ it('should contain the correct fields', function() {
+ click(pointPos[0], pointPos[1]);
+ expect(futureData.points.length).toEqual(1);
+
+ checkEventData(futureData);
+ });
+
+ it('should not contain pointNumber if aggregating', function() {
+ var values = gd.data[0].values;
+ var labels = [];
+ for(var i = 0; i < values.length; i++) labels.push(i);
+ Plotly.restyle(gd, {
+ labels: [labels.concat(labels)],
+ values: [values.concat(values)]
+ });
+
+ click(pointPos[0], pointPos[1]);
+ expect(futureData.points.length).toEqual(1);
+
+ expect(futureData.points[0].pointNumber).toBeUndefined();
+ expect(futureData.points[0].i).toBeUndefined();
+ expect(futureData.points[0].pointNumbers).toEqual([0, 5]);
+ });
+ });
+
+ describe('modified click events', function() {
+ var clickOpts = {
+ altKey: true,
+ ctrlKey: true,
+ metaKey: true,
+ shiftKey: true
+ };
+ var futureData;
+
+ beforeEach(function(done) {
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
+
+ gd.on('plotly_click', function(data) {
+ futureData = data;
+ });
+ });
+
+ it('should not be trigged when not on data points', function() {
+ click(blankPos[0], blankPos[1], clickOpts);
+ expect(futureData).toBe(undefined);
+ });
+
+ it('does not respond to right-click', function() {
+ click(pointPos[0], pointPos[1], clickOpts);
+ expect(futureData).toBe(undefined);
+
+ // TODO: 'should contain the correct fields'
+ // This test passed previously, but only because assets/click
+ // incorrectly generated a click event for right click. It never
+ // worked in reality.
+ // expect(futureData.points.length).toEqual(1);
+
+ // checkEventData(futureData);
+
+ // var evt = futureData.event;
+ // Object.getOwnPropertyNames(clickOpts).forEach(function(opt) {
+ // expect(evt[opt]).toEqual(clickOpts[opt], 'event.' + opt);
+ // });
+ });
+ });
+
+ describe('hover events', function() {
+ var futureData;
+
+ beforeEach(function(done) {
+ futureData = undefined;
+ Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(done);
+
+ gd.on('plotly_hover', function(data) {
+ futureData = data;
+ });
+ });
+
+ it('should contain the correct fields', function() {
+ mouseEvent('mouseover', pointPos[0], pointPos[1]);
+
+ checkEventData(futureData);
+ });
+
+ it('should not emit a hover if you\'re dragging', function() {
+ gd._dragging = true;
+ mouseEvent('mouseover', pointPos[0], pointPos[1]);
+ expect(futureData).toBeUndefined();
+ });
+
+ it('should not emit a hover if hover is disabled', function() {
+ Plotly.relayout(gd, 'hovermode', false);
+ mouseEvent('mouseover', pointPos[0], pointPos[1]);
+ expect(futureData).toBeUndefined();
+ });
+ });
+
+ describe('unhover events', function() {
+ var futureData;
+
+ beforeEach(function(done) {
+ futureData = undefined;
+ Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(done);
+
+ gd.on('plotly_unhover', function(data) {
+ futureData = data;
+ });
+ });
+
+ it('should contain the correct fields', function() {
+ mouseEvent('mouseover', pointPos[0], pointPos[1]);
+ mouseEvent('mouseout', pointPos[0], pointPos[1]);
+
+ checkEventData(futureData);
+ });
+
+ it('should not emit an unhover if you didn\'t first hover', function() {
+ mouseEvent('mouseout', pointPos[0], pointPos[1]);
+ expect(futureData).toBeUndefined();
+ });
+ });
+});
+
+describe('funnelarea relayout', function() {
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+
+ afterEach(destroyGraphDiv);
+
+ it('will update colors when colorway is updated', function(done) {
+ var originalColors = [
+ 'rgb(255,0,0)',
+ 'rgb(0,255,0)',
+ 'rgb(0,0,255)',
+ ];
+
+ var relayoutColors = [
+ 'rgb(255,255,0)',
+ 'rgb(0,255,255)',
+ 'rgb(255,0,255)',
+ ];
+
+ function checkRelayoutColor(d, i) {
+ expect(this.style.fill.replace(/\s/g, '')).toBe(relayoutColors[i]);
+ }
+
+ Plotly.newPlot(gd, [{
+ labels: ['a', 'b', 'c', 'a', 'b', 'a'],
+ type: 'funnelarea'
+ }], {
+ colorway: originalColors
+ })
+ .then(function() {
+ return Plotly.relayout(gd, 'colorway', relayoutColors);
+ })
+ .then(function() {
+ var slices = d3.selectAll(SLICES_SELECTOR);
+ slices.each(checkRelayoutColor);
+ })
+ .then(done);
+ });
+});
+
+describe('Test funnelarea interactions edge cases:', function() {
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+
+ afterEach(destroyGraphDiv);
+
+ function _mouseEvent(type, v) {
+ return function() {
+ var el = d3.select(gd).select('.slice:nth-child(' + v + ')').node();
+ mouseEvent(type, 0, 0, {element: el});
+ };
+ }
+
+ function hover(v) {
+ return _mouseEvent('mouseover', v);
+ }
+
+ function unhover(v) {
+ return _mouseEvent('mouseout', v);
+ }
+
+ it('should keep tracking hover labels and hover events after *calc* edits', function(done) {
+ var mock = Lib.extendFlat({}, require('@mocks/funnelarea_simple.json'));
+ var hoverCnt = 0;
+ var unhoverCnt = 0;
+
+ // see https://github.com/plotly/plotly.js/issues/3618
+
+ function _assert(msg, exp) {
+ expect(hoverCnt).toBe(exp.hoverCnt, msg + ' - hover cnt');
+ expect(unhoverCnt).toBe(exp.unhoverCnt, msg + ' - unhover cnt');
+
+ var label = d3.select(gd).select('g.hovertext');
+ expect(label.size()).toBe(exp.hoverLabel, msg + ' - hover label cnt');
+
+ hoverCnt = 0;
+ unhoverCnt = 0;
+ }
+
+ Plotly.plot(gd, mock)
+ .then(function() {
+ gd.on('plotly_hover', function() {
+ hoverCnt++;
+ // N.B. trigger a 'calc' edit
+ Plotly.restyle(gd, 'textinfo', 'percent');
+ });
+ gd.on('plotly_unhover', function() {
+ unhoverCnt++;
+ // N.B. trigger a 'calc' edit
+ Plotly.restyle(gd, 'textinfo', null);
+ });
+ })
+ .then(hover(1))
+ .then(function() {
+ _assert('after hovering on first sector', {
+ hoverCnt: 1,
+ unhoverCnt: 0,
+ hoverLabel: 1
+ });
+ })
+ .then(unhover(1))
+ .then(function() {
+ _assert('after un-hovering from first sector', {
+ hoverCnt: 0,
+ unhoverCnt: 1,
+ hoverLabel: 0
+ });
+ })
+ .then(hover(2))
+ .then(function() {
+ _assert('after hovering onto second sector', {
+ hoverCnt: 1,
+ unhoverCnt: 0,
+ hoverLabel: 1
+ });
+ })
+ .catch(failTest)
+ .then(done);
+ });
+});
+
+describe('Test funnelarea calculated areas', function() {
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+
+ afterEach(destroyGraphDiv);
+
+ function _checkCalculatedAreaRatios(expRatios) {
+ return function() {
+ var i;
+ var areas = [];
+ var totalArea = 0;
+ for(i = 0; i < 4; i++) {
+ var cdi = gd.calcdata[0][i];
+
+ var area = convexPolygonArea([cdi.TR, cdi.TL, cdi.BL, cdi.BR]);
+ areas.push(area);
+ totalArea += area;
+ }
+
+ var ratios = [];
+ for(i = 0; i < areas.length; i++) {
+ ratios[i] = areas[i] / totalArea;
+ }
+
+ expect(ratios).toBeCloseToArray(expRatios);
+ };
+ }
+
+ [
+ {aspectratio: 0.25, baseratio: 0},
+ {aspectratio: 0.25, baseratio: 0.25},
+ {aspectratio: 0.25, baseratio: 0.5},
+ {aspectratio: 0.25, baseratio: 0.75},
+ {aspectratio: 0.25, baseratio: 1},
+ {aspectratio: 0.5, baseratio: 0},
+ {aspectratio: 0.5, baseratio: 0.25},
+ {aspectratio: 0.5, baseratio: 0.5},
+ {aspectratio: 0.5, baseratio: 0.75},
+ {aspectratio: 0.5, baseratio: 1},
+ {aspectratio: 1, baseratio: 0},
+ {aspectratio: 1, baseratio: 0.25},
+ {aspectratio: 1, baseratio: 0.5},
+ {aspectratio: 1, baseratio: 0.75},
+ {aspectratio: 1, baseratio: 1},
+ {aspectratio: 2, baseratio: 0},
+ {aspectratio: 2, baseratio: 0.25},
+ {aspectratio: 2, baseratio: 0.5},
+ {aspectratio: 2, baseratio: 0.75},
+ {aspectratio: 2, baseratio: 1},
+ {aspectratio: 4, baseratio: 0},
+ {aspectratio: 4, baseratio: 0.25},
+ {aspectratio: 4, baseratio: 0.5},
+ {aspectratio: 4, baseratio: 0.75},
+ {aspectratio: 4, baseratio: 1}
+ ].forEach(function(spec) {
+ var desc = 'calculate correct area with ' +
+ '(aspectratio ' + spec.aspectratio + ') and ' +
+ '(baseratio ' + spec.baseratio + ')';
+
+ it(desc, function(done) {
+ var data = [{
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ }];
+
+ Plotly.plot(gd, data)
+ .then(_checkCalculatedAreaRatios([0.4, 0.3, 0.2, 0.1]))
+ .catch(failTest)
+ .then(done);
+ });
+ });
+});
+
+describe('Test funnelarea calculated areas with scalegroup', function() {
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+
+ afterEach(destroyGraphDiv);
+
+ function _checkCalculatedAreaRatios(expRatios) {
+ return function() {
+ var i, k;
+ var areas = [[], [], [], []];
+ var totalArea = [0, 0, 0, 0];
+ for(k = 0; k < 4; k++) {
+ for(i = 0; i < 4; i++) {
+ var cdi = gd.calcdata[k][i];
+
+ var area = convexPolygonArea([cdi.TR, cdi.TL, cdi.BL, cdi.BR]);
+ areas[k].push(area);
+ totalArea[k] += area;
+ }
+ }
+
+ for(k = 0; k < 4; k++) {
+ var ratios = [];
+ for(i = 0; i < 4; i++) {
+ ratios[i] = areas[k][i] / totalArea[k];
+ }
+ expect(ratios).toBeCloseToArray(expRatios);
+ }
+
+ for(i = 0; i < 4; i++) {
+ expect(areas[0][i] / areas[1][i]).toBeCloseTo(1);
+ expect(areas[0][i] / areas[2][i]).toBeCloseTo(10);
+ expect(areas[0][i] / areas[3][i]).toBeCloseTo(1);
+ }
+ };
+ }
+
+ [
+ {aspectratio: 0.25, baseratio: 0},
+ {aspectratio: 0.25, baseratio: 0.25},
+ {aspectratio: 0.25, baseratio: 0.5},
+ {aspectratio: 0.25, baseratio: 0.75},
+ {aspectratio: 0.25, baseratio: 1},
+ {aspectratio: 0.5, baseratio: 0},
+ {aspectratio: 0.5, baseratio: 0.25},
+ {aspectratio: 0.5, baseratio: 0.5},
+ {aspectratio: 0.5, baseratio: 0.75},
+ {aspectratio: 0.5, baseratio: 1},
+ {aspectratio: 2, baseratio: 0},
+ {aspectratio: 2, baseratio: 0.25},
+ {aspectratio: 2, baseratio: 0.5},
+ {aspectratio: 2, baseratio: 0.75},
+ {aspectratio: 2, baseratio: 1},
+ {aspectratio: 4, baseratio: 0},
+ {aspectratio: 4, baseratio: 0.25},
+ {aspectratio: 4, baseratio: 0.5},
+ {aspectratio: 4, baseratio: 0.75},
+ {aspectratio: 4, baseratio: 1}
+ ].forEach(function(spec) {
+ var desc = 'calculate correct area with ' +
+ '(aspectratio ' + spec.aspectratio + ') and ' +
+ '(baseratio ' + spec.baseratio + ')';
+
+ it(desc, function(done) {
+ var data = [{
+ scalegroup: 'x',
+ values: [40, 30, 20, 10],
+ type: 'funnelarea'
+ },
+ {
+ scalegroup: 'x',
+ values: [40, 30, 20, 10],
+ type: 'funnelarea',
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio,
+ },
+ {
+ scalegroup: 'x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ },
+ {
+ scalegroup: '10x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ }];
+
+ Plotly.plot(gd, data)
+ .then(_checkCalculatedAreaRatios([0.4, 0.3, 0.2, 0.1]))
+ .catch(failTest)
+ .then(done);
+ });
+ });
+});
+
+describe('Test funnelarea calculated areas with scalegroup on various domain ratios', function() {
+ var gd;
+
+ beforeEach(function() { gd = createGraphDiv(); });
+
+ afterEach(destroyGraphDiv);
+
+ function _checkCalculatedAreaRatios(expRatios) {
+ return function() {
+ var i, k;
+ var areas = [[], [], [], [], [], [], [], [], [], []];
+ var totalArea = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ for(k = 0; k < 10; k++) {
+ for(i = 0; i < 4; i++) {
+ var cdi = gd.calcdata[k][i];
+
+ var area = convexPolygonArea([cdi.TR, cdi.TL, cdi.BL, cdi.BR]);
+ areas[k].push(area);
+ totalArea[k] += area;
+ }
+ }
+
+ for(k = 0; k < 10; k++) {
+ var ratios = [];
+ for(i = 0; i < 4; i++) {
+ ratios[i] = areas[k][i] / totalArea[k];
+ }
+ expect(ratios).toBeCloseToArray(expRatios);
+ }
+
+ for(i = 0; i < 4; i++) {
+ expect(areas[0][i] / areas[1][i]).toBeCloseTo(1);
+ expect(areas[0][i] / areas[2][i]).toBeCloseTo(1);
+ expect(areas[0][i] / areas[3][i]).toBeCloseTo(1);
+ expect(areas[0][i] / areas[4][i]).toBeCloseTo(10);
+ expect(areas[0][i] / areas[5][i]).toBeCloseTo(10);
+ expect(areas[0][i] / areas[6][i]).toBeCloseTo(10);
+ expect(areas[0][i] / areas[7][i]).toBeCloseTo(1);
+ expect(areas[0][i] / areas[8][i]).toBeCloseTo(1);
+ expect(areas[0][i] / areas[9][i]).toBeCloseTo(1);
+ }
+ };
+ }
+
+ [
+ {aspectratio: 0.25, baseratio: 0},
+ {aspectratio: 0.25, baseratio: 0.25},
+ {aspectratio: 0.25, baseratio: 0.5},
+ {aspectratio: 0.25, baseratio: 0.75},
+ {aspectratio: 0.25, baseratio: 1},
+ {aspectratio: 0.5, baseratio: 0},
+ {aspectratio: 0.5, baseratio: 0.25},
+ {aspectratio: 0.5, baseratio: 0.5},
+ {aspectratio: 0.5, baseratio: 0.75},
+ {aspectratio: 0.5, baseratio: 1},
+ {aspectratio: 1, baseratio: 0},
+ {aspectratio: 1, baseratio: 0.25},
+ {aspectratio: 1, baseratio: 0.5},
+ {aspectratio: 1, baseratio: 0.75},
+ {aspectratio: 1, baseratio: 1},
+ {aspectratio: 2, baseratio: 0},
+ {aspectratio: 2, baseratio: 0.25},
+ {aspectratio: 2, baseratio: 0.5},
+ {aspectratio: 2, baseratio: 0.75},
+ {aspectratio: 2, baseratio: 1},
+ {aspectratio: 4, baseratio: 0},
+ {aspectratio: 4, baseratio: 0.25},
+ {aspectratio: 4, baseratio: 0.5},
+ {aspectratio: 4, baseratio: 0.75},
+ {aspectratio: 4, baseratio: 1}
+ ].forEach(function(spec) {
+ var desc = 'calculate correct area with ' +
+ '(aspectratio ' + spec.aspectratio + ') and ' +
+ '(baseratio ' + spec.baseratio + ')';
+
+ it(desc, function(done) {
+ var data = [{
+ scalegroup: 'x',
+ values: [40, 30, 20, 10],
+ type: 'funnelarea',
+ domain: {
+ x: [0.5, 1],
+ y: [0.5, 1]
+ }
+ },
+ {
+ scalegroup: 'x',
+ values: [40, 30, 20, 10],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.25],
+ y: [0, 0.5]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio,
+ },
+ {
+ scalegroup: 'x',
+ values: [40, 30, 20, 10],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.5],
+ y: [0, 0.25]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio,
+ },
+ {
+ scalegroup: 'x',
+ values: [40, 30, 20, 10],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.5],
+ y: [0, 0.5]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio,
+ },
+ {
+ scalegroup: 'x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.25],
+ y: [0, 0.5]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ },
+ {
+ scalegroup: 'x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.5],
+ y: [0, 0.25]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ },
+ {
+ scalegroup: 'x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.5],
+ y: [0, 0.5]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ },
+ {
+ scalegroup: '10x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.25],
+ y: [0, 0.5]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ },
+ {
+ scalegroup: '10x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.5],
+ y: [0, 0.25]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ },
+ {
+ scalegroup: '10x',
+ values: [4, 3, 2, 1],
+ type: 'funnelarea',
+ domain: {
+ x: [0, 0.5],
+ y: [0, 0.5]
+ },
+ aspectratio: spec.aspectratio,
+ baseratio: spec.baseratio
+ }];
+
+ Plotly.plot(gd, data)
+ .then(_checkCalculatedAreaRatios([0.4, 0.3, 0.2, 0.1]))
+ .catch(failTest)
+ .then(done);
+ });
+ });
+});
diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js
index 7274a07de1f..4b6f84c8ebc 100644
--- a/test/jasmine/tests/pie_test.js
+++ b/test/jasmine/tests/pie_test.js
@@ -428,6 +428,30 @@ describe('Pie traces', function() {
.then(done);
});
+ it('should be able to restyle title position', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [3, 2, 1],
+ title: 'Test
Title',
+ titleposition: 'top left',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(true, false, true, false, false))
+ .then(function() { return Plotly.restyle(gd, 'titleposition', 'top right'); })
+ .then(_verifyTitle(false, true, true, false, false))
+ .then(function() { return Plotly.restyle(gd, 'titleposition', 'bottom left'); })
+ .then(_verifyTitle(true, false, false, true, false))
+ .then(function() { return Plotly.restyle(gd, 'titleposition', 'bottom center'); })
+ .then(_verifyTitle(false, false, false, true, true))
+ .then(function() { return Plotly.restyle(gd, 'titleposition', 'bottom right'); })
+ .then(_verifyTitle(false, true, false, true, false))
+ .catch(failTest)
+ .then(done);
+ });
+
it('does not intersect pulled slices', function(done) {
Plotly.newPlot(gd, [{
values: [2, 2, 2, 2],
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 7011ba296f7..a57be677418 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -1895,7 +1895,7 @@ describe('Test select box and lasso per trace:', function() {
.then(done);
});
- it('@noCI @flaky should work on scattermapbox traces', function(done) {
+ it('@noCI should work on scattermapbox traces', function(done) {
var assertPoints = makeAssertPoints(['lon', 'lat']);
var assertRanges = makeAssertRanges('mapbox');
var assertLassoPoints = makeAssertLassoPoints('mapbox');
@@ -2208,7 +2208,7 @@ describe('Test select box and lasso per trace:', function() {
.then(done);
}, LONG_TIMEOUT_INTERVAL);
- it('@noCI should work for waterfall traces', function(done) {
+ it('@flaky should work for waterfall traces', function(done) {
var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']);
var assertSelectedPoints = makeAssertSelectedPoints();
var assertRanges = makeAssertRanges();
@@ -2233,8 +2233,8 @@ describe('Test select box and lasso per trace:', function() {
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']
+ [288.8086, 57.7617, 288.8086, 519.8555, 404.3321],
+ [4.33870, 6.7580, 9.1774, 6.75806, 5.54838]
]);
},
null, LASSOEVENTS, 'waterfall lasso'
@@ -2255,8 +2255,8 @@ describe('Test select box and lasso per trace:', function() {
0: [5, 6]
});
assertRanges([
- [173.91304347826087, 289.8550724637681],
- ['Net revenue', 'Personnel expenses']
+ [173.28519, 288.8086],
+ [4.3387, 6.7580]
]);
},
null, BOXEVENTS, 'waterfall select'
@@ -2266,7 +2266,7 @@ describe('Test select box and lasso per trace:', function() {
.then(done);
});
- it('@noCI should work for funnel traces', function(done) {
+ it('@flaky should work for funnel traces', function(done) {
var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']);
var assertSelectedPoints = makeAssertSelectedPoints();
var assertRanges = makeAssertRanges();
@@ -2291,8 +2291,8 @@ describe('Test select box and lasso per trace:', function() {
1: [1, 2]
});
assertLassoPoints([
- [-154.56790123456787, -1700.2469, -154.5679, 1391.1111, 618.2716],
- ['Pull requests', 'Author: etpinard', 'Label: bug', 'Author: etpinard', 'Author: etpinard']
+ [-161.6974, -1701.6728, -161.6974, 1378.2779, 608.2902],
+ [1.1129, 1.9193, 2.7258, 1.9193, 1.5161]
]);
},
null, LASSOEVENTS, 'funnel lasso'
@@ -2315,8 +2315,8 @@ describe('Test select box and lasso per trace:', function() {
1: [1, 2]
});
assertRanges([
- [-927.4074, 618.2716],
- ['Pull requests', 'Label: bug']
+ [-931.6851, 608.2902],
+ [1.1129, 2.7258]
]);
},
null, BOXEVENTS, 'funnel select'