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'