diff --git a/draftlogs/7673_add.md b/draftlogs/7673_add.md new file mode 100644 index 00000000000..04d803b98c6 --- /dev/null +++ b/draftlogs/7673_add.md @@ -0,0 +1 @@ + - Add support for dashed marker lines in scatter plots [[#7673](https://github.com/plotly/plotly.js/pull/7673)], with thanks to @chrimaho for the contribution! diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 38e8686d102..861df3131a5 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -965,6 +965,8 @@ drawing.singlePointStyle = function (d, sel, trace, fns, gd, pt) { } } + const lineDash = d.mld || (markerLine || {}).dash; + if (lineDash) drawing.dashLine(sel, lineDash, lineWidth); if (d.om) { // open markers can't have zero linewidth, default to 1px, // and use fill color as stroke color diff --git a/src/components/legend/style.js b/src/components/legend/style.js index 849271d3962..d2b3350c0c8 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -23,17 +23,17 @@ var MAX_MARKER_LINE_WIDTH = 5; module.exports = function style(s, gd, legend) { var fullLayout = gd._fullLayout; - if(!legend) legend = fullLayout.legend; + if (!legend) legend = fullLayout.legend; var constantItemSizing = legend.itemsizing === 'constant'; var itemWidth = legend.itemwidth; var centerPos = (itemWidth + constants.itemGap * 2) / 2; var centerTransform = strTranslate(centerPos, 0); - var boundLineWidth = function(mlw, cont, max, cst) { + var boundLineWidth = function (mlw, cont, max, cst) { var v; - if(mlw + 1) { + if (mlw + 1) { v = mlw; - } else if(cont && cont.width > 0) { + } else if (cont && cont.width > 0) { v = cont.width; } else { return 0; @@ -41,7 +41,7 @@ module.exports = function style(s, gd, legend) { return constantItemSizing ? cst : Math.min(v, max); }; - s.each(function(d) { + s.each(function (d) { var traceGroup = d3.select(this); var layers = Lib.ensureSingle(traceGroup, 'g', 'layers'); @@ -51,49 +51,37 @@ module.exports = function style(s, gd, legend) { var valign = legend.valign; var lineHeight = d[0].lineHeight; var height = d[0].height; - if((valign === 'middle' && indentation === 0) || !lineHeight || !height) { + if ((valign === 'middle' && indentation === 0) || !lineHeight || !height) { layers.attr('transform', null); } else { - var factor = {top: 1, bottom: -1}[valign]; - var markerOffsetY = (factor * (0.5 * (lineHeight - height + 3))) || 0; + var factor = { top: 1, bottom: -1 }[valign]; + var markerOffsetY = factor * (0.5 * (lineHeight - height + 3)) || 0; var markerOffsetX = legend.indentation; layers.attr('transform', strTranslate(markerOffsetX, markerOffsetY)); } - var fill = layers - .selectAll('g.legendfill') - .data([d]); - fill.enter().append('g') - .classed('legendfill', true); - - var line = layers - .selectAll('g.legendlines') - .data([d]); - line.enter().append('g') - .classed('legendlines', true); - - var symbol = layers - .selectAll('g.legendsymbols') - .data([d]); - symbol.enter().append('g') - .classed('legendsymbols', true); - - symbol.selectAll('g.legendpoints') - .data([d]) - .enter().append('g') - .classed('legendpoints', true); + var fill = layers.selectAll('g.legendfill').data([d]); + fill.enter().append('g').classed('legendfill', true); + + var line = layers.selectAll('g.legendlines').data([d]); + line.enter().append('g').classed('legendlines', true); + + var symbol = layers.selectAll('g.legendsymbols').data([d]); + symbol.enter().append('g').classed('legendsymbols', true); + + symbol.selectAll('g.legendpoints').data([d]).enter().append('g').classed('legendpoints', true); }) - .each(styleSpatial) - .each(styleWaterfalls) - .each(styleFunnels) - .each(styleBars) - .each(styleBoxes) - .each(styleFunnelareas) - .each(stylePies) - .each(styleLines) - .each(stylePoints) - .each(styleCandles) - .each(styleOHLC); + .each(styleSpatial) + .each(styleWaterfalls) + .each(styleFunnels) + .each(styleBars) + .each(styleBoxes) + .each(styleFunnelareas) + .each(stylePies) + .each(styleLines) + .each(stylePoints) + .each(styleCandles) + .each(styleOHLC); function styleLines(d) { var styleGuide = getStyleGuide(d); @@ -112,53 +100,56 @@ module.exports = function style(s, gd, legend) { var colorscale = cOpts.colorscale; var reversescale = cOpts.reversescale; - var fillStyle = function(s) { - if(s.size()) { - if(showFill) { + var fillStyle = function (s) { + if (s.size()) { + if (showFill) { Drawing.fillGroupStyle(s, gd, true); } else { var gradientID = 'legendfill-' + trace.uid; - Drawing.gradient(s, gd, gradientID, - getGradientDirection(reversescale), - colorscale, 'fill'); + Drawing.gradient(s, gd, gradientID, getGradientDirection(reversescale), colorscale, 'fill'); } } }; - var lineGradient = function(s) { - if(s.size()) { + var lineGradient = function (s) { + if (s.size()) { var gradientID = 'legendline-' + trace.uid; Drawing.lineGroupStyle(s); - Drawing.gradient(s, gd, gradientID, - getGradientDirection(reversescale), - colorscale, 'stroke'); + Drawing.gradient(s, gd, gradientID, getGradientDirection(reversescale), colorscale, 'stroke'); } }; // with fill and no markers or text, move the line and fill up a bit // so it's more centered - var pathStart = (subTypes.hasMarkers(trace) || !anyFill) ? 'M5,0' : - // with a line leave it slightly below center, to leave room for the - // line thickness and because the line is usually more prominent - anyLine ? 'M5,-2' : 'M5,-3'; + var pathStart = + subTypes.hasMarkers(trace) || !anyFill + ? 'M5,0' + : // with a line leave it slightly below center, to leave room for the + // line thickness and because the line is usually more prominent + anyLine + ? 'M5,-2' + : 'M5,-3'; var this3 = d3.select(this); - var fill = this3.select('.legendfill').selectAll('path') + var fill = this3 + .select('.legendfill') + .selectAll('path') .data(showFill || showGradientFill ? [d] : []); fill.enter().append('path').classed('js-fill', true); fill.exit().remove(); - fill.attr('d', pathStart + 'h' + itemWidth + 'v6h-' + itemWidth + 'z') - .call(fillStyle); + fill.attr('d', pathStart + 'h' + itemWidth + 'v6h-' + itemWidth + 'z').call(fillStyle); - if(showLine || showGradientLine) { + if (showLine || showGradientLine) { var lw = boundLineWidth(undefined, trace.line, MAX_LINE_WIDTH, CST_LINE_WIDTH); - tMod = Lib.minExtend(trace, {line: {width: lw}}); - dMod = [Lib.minExtend(d0, {trace: tMod})]; + tMod = Lib.minExtend(trace, { line: { width: lw } }); + dMod = [Lib.minExtend(d0, { trace: tMod })]; } - var line = this3.select('.legendlines').selectAll('path') + var line = this3 + .select('.legendlines') + .selectAll('path') .data(showLine || showGradientLine ? [dMod] : []); line.enter().append('path').classed('js-line', true); line.exit().remove(); @@ -169,8 +160,9 @@ module.exports = function style(s, gd, legend) { // though there *is* no vertical variation in this case. // so add an invisibly small angle to the line // This issue (and workaround) exist across (Mac) Chrome, FF, and Safari - line.attr('d', pathStart + (showGradientLine ? 'l' + itemWidth + ',0.0001' : 'h' + itemWidth)) - .call(showLine ? Drawing.lineGroupStyle : lineGradient); + line.attr('d', pathStart + (showGradientLine ? 'l' + itemWidth + ',0.0001' : 'h' + itemWidth)).call( + showLine ? Drawing.lineGroupStyle : lineGradient + ); } function stylePoints(d) { @@ -190,37 +182,38 @@ module.exports = function style(s, gd, legend) { function boundVal(attrIn, arrayToValFn, bounds, cst) { var valIn = Lib.nestedProperty(trace, attrIn).get(); - var valToBound = (Lib.isArrayOrTypedArray(valIn) && arrayToValFn) ? - arrayToValFn(valIn) : - valIn; + var valToBound = Lib.isArrayOrTypedArray(valIn) && arrayToValFn ? arrayToValFn(valIn) : valIn; - if(constantItemSizing && valToBound && cst !== undefined) { + if (constantItemSizing && valToBound && cst !== undefined) { valToBound = cst; } - if(bounds) { - if(valToBound < bounds[0]) return bounds[0]; - else if(valToBound > bounds[1]) return bounds[1]; + if (bounds) { + if (valToBound < bounds[0]) return bounds[0]; + else if (valToBound > bounds[1]) return bounds[1]; } return valToBound; } function pickFirst(array) { - if(d0._distinct && d0.index && array[d0.index]) return array[d0.index]; + if (d0._distinct && d0.index && array[d0.index]) return array[d0.index]; return array[0]; } // constrain text, markers, etc so they'll fit on the legend - if(showMarker || showText || showLine) { + if (showMarker || showText || showLine) { var dEdit = {}; var tEdit = {}; - if(showMarker) { + if (showMarker) { dEdit.mc = boundVal('marker.color', pickFirst); dEdit.mx = boundVal('marker.symbol', pickFirst); dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]); dEdit.mlc = boundVal('marker.line.color', pickFirst); dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5], CST_MARKER_LINE_WIDTH); + // TODO: Remove this check in next major version + // Use 'solid' for shapes to match existing behavior + dEdit.mld = trace._isShape ? 'solid' : boundVal('marker.line.dash', pickFirst); tEdit.marker = { sizeref: 1, sizemin: 1, @@ -232,13 +225,13 @@ module.exports = function style(s, gd, legend) { tEdit.marker.size = ms; } - if(showLine) { + if (showLine) { tEdit.line = { width: boundVal('line.width', pickFirst, [0, 10], CST_LINE_WIDTH) }; } - if(showText) { + if (showText) { dEdit.tx = 'Aa'; dEdit.tp = boundVal('textposition', pickFirst); dEdit.ts = 10; @@ -264,24 +257,18 @@ module.exports = function style(s, gd, legend) { var ptgroup = d3.select(this).select('g.legendpoints'); - var pts = ptgroup.selectAll('path.scatterpts') - .data(showMarker ? dMod : []); + var pts = ptgroup.selectAll('path.scatterpts').data(showMarker ? dMod : []); // make sure marker is on the bottom, in case it enters after text - pts.enter().insert('path', ':first-child') - .classed('scatterpts', true) - .attr('transform', centerTransform); + pts.enter().insert('path', ':first-child').classed('scatterpts', true).attr('transform', centerTransform); pts.exit().remove(); pts.call(Drawing.pointStyle, tMod, gd); // 'mrc' is set in pointStyle and used in textPointStyle: // constrain it here - if(showMarker) dMod[0].mrc = 3; + if (showMarker) dMod[0].mrc = 3; - var txt = ptgroup.selectAll('g.pointtext') - .data(showText ? dMod : []); - txt.enter() - .append('g').classed('pointtext', true) - .append('text').attr('transform', centerTransform); + var txt = ptgroup.selectAll('g.pointtext').data(showText ? dMod : []); + txt.enter().append('g').classed('pointtext', true).append('text').attr('transform', centerTransform); txt.exit().remove(); txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd); } @@ -290,7 +277,7 @@ module.exports = function style(s, gd, legend) { var trace = d[0].trace; var isWaterfall = trace.type === 'waterfall'; - if(d[0]._distinct && isWaterfall) { + if (d[0]._distinct && isWaterfall) { var cont = d[0].trace[d[0].dir].marker; d[0].mc = cont.color; d[0].mlw = cont.line.width; @@ -299,21 +286,28 @@ module.exports = function style(s, gd, legend) { } var ptsData = []; - if(trace.visible && isWaterfall) { - ptsData = d[0].hasTotals ? - [['increasing', 'M-6,-6V6H0Z'], ['totals', 'M6,6H0L-6,-6H-0Z'], ['decreasing', 'M6,6V-6H0Z']] : - [['increasing', 'M-6,-6V6H6Z'], ['decreasing', 'M6,6V-6H-6Z']]; + if (trace.visible && isWaterfall) { + ptsData = d[0].hasTotals + ? [ + ['increasing', 'M-6,-6V6H0Z'], + ['totals', 'M6,6H0L-6,-6H-0Z'], + ['decreasing', 'M6,6V-6H0Z'] + ] + : [ + ['increasing', 'M-6,-6V6H6Z'], + ['decreasing', 'M6,6V-6H-6Z'] + ]; } - var pts = d3.select(this).select('g.legendpoints') - .selectAll('path.legendwaterfall') - .data(ptsData); - pts.enter().append('path').classed('legendwaterfall', true) + var pts = d3.select(this).select('g.legendpoints').selectAll('path.legendwaterfall').data(ptsData); + pts.enter() + .append('path') + .classed('legendwaterfall', true) .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); - pts.each(function(dd) { + pts.each(function (dd) { var pt = d3.select(this); var cont = trace[dd[0]].marker; var lw = boundLineWidth(undefined, cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); @@ -322,7 +316,7 @@ module.exports = function style(s, gd, legend) { .style('stroke-width', lw + 'px') .call(Color.fill, cont.color); - if(lw) { + if (lw) { pt.call(Color.stroke, cont.line.color); } }); @@ -342,22 +336,27 @@ module.exports = function style(s, gd, legend) { var markerLine = marker.line || {}; // If bar has rounded corners, round corners of legend icon - var pathStr = marker.cornerradius ? - 'M6,3a3,3,0,0,1-3,3H-3a3,3,0,0,1-3-3V-3a3,3,0,0,1,3-3H3a3,3,0,0,1,3,3Z' : // Square with rounded corners - 'M6,6H-6V-6H6Z'; // Normal square + var pathStr = marker.cornerradius + ? 'M6,3a3,3,0,0,1-3,3H-3a3,3,0,0,1-3-3V-3a3,3,0,0,1,3-3H3a3,3,0,0,1,3,3Z' + : // Square with rounded corners + 'M6,6H-6V-6H6Z'; // Normal square - var isVisible = (!desiredType) ? Registry.traceIs(trace, 'bar') : - (trace.visible && trace.type === desiredType); + var isVisible = !desiredType ? Registry.traceIs(trace, 'bar') : trace.visible && trace.type === desiredType; - var barpath = d3.select(lThis).select('g.legendpoints') + var barpath = d3 + .select(lThis) + .select('g.legendpoints') .selectAll('path.legend' + desiredType) .data(isVisible ? [d] : []); - barpath.enter().append('path').classed('legend' + desiredType, true) + barpath + .enter() + .append('path') + .classed('legend' + desiredType, true) .attr('d', pathStr) .attr('transform', centerTransform); barpath.exit().remove(); - barpath.each(function(d) { + barpath.each(function (d) { var p = d3.select(this); var d0 = d[0]; var w = boundLineWidth(d0.mlw, marker.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); @@ -365,23 +364,21 @@ module.exports = function style(s, gd, legend) { p.style('stroke-width', w + 'px'); var mcc = d0.mcc; - if(!legend._inHover && 'mc' in d0) { + if (!legend._inHover && 'mc' in d0) { // not in unified hover but // for legend use the color in the middle of scale var cOpts = extractOpts(marker); var mid = cOpts.mid; - if(mid === undefined) mid = (cOpts.max + cOpts.min) / 2; + if (mid === undefined) mid = (cOpts.max + cOpts.min) / 2; mcc = Drawing.tryColorscale(marker, '')(mid); } var fillColor = mcc || d0.mc || marker.color; var markerPattern = marker.pattern; var pAttr = Drawing.getPatternAttr; - var patternShape = markerPattern && ( - pAttr(markerPattern.shape, 0, '') || pAttr(markerPattern.path, 0, '') - ); + var patternShape = markerPattern && (pAttr(markerPattern.shape, 0, '') || pAttr(markerPattern.path, 0, '')); - if(patternShape) { + if (patternShape) { var patternBGColor = pAttr(markerPattern.bgcolor, 0, null); var patternFGColor = pAttr(markerPattern.fgcolor, 0, null); var patternFGOpacity = markerPattern.fgopacity; @@ -389,36 +386,50 @@ module.exports = function style(s, gd, legend) { var patternSolidity = dimAttr(markerPattern.solidity, 0.5, 1); var patternID = 'legend-' + trace.uid; p.call( - Drawing.pattern, 'legend', gd, patternID, - patternShape, patternSize, patternSolidity, - mcc, markerPattern.fillmode, - patternBGColor, patternFGColor, patternFGOpacity + Drawing.pattern, + 'legend', + gd, + patternID, + patternShape, + patternSize, + patternSolidity, + mcc, + markerPattern.fillmode, + patternBGColor, + patternFGColor, + patternFGOpacity ); } else { p.call(Color.fill, fillColor); } - if(w) Color.stroke(p, d0.mlc || markerLine.color); + if (w) Color.stroke(p, d0.mlc || markerLine.color); }); } function styleBoxes(d) { var trace = d[0].trace; - var pts = d3.select(this).select('g.legendpoints') + var pts = d3 + .select(this) + .select('g.legendpoints') .selectAll('path.legendbox') .data(trace.visible && Registry.traceIs(trace, 'box-violin') ? [d] : []); - pts.enter().append('path').classed('legendbox', true) + pts.enter() + .append('path') + .classed('legendbox', true) // if we want the median bar, prepend M6,0H-6 .attr('d', 'M6,6H-6V-6H6Z') .attr('transform', centerTransform); pts.exit().remove(); - pts.each(function() { + pts.each(function () { var p = d3.select(this); - if((trace.boxpoints === 'all' || trace.points === 'all') && - Color.opacity(trace.fillcolor) === 0 && Color.opacity((trace.line || {}).color) === 0 + if ( + (trace.boxpoints === 'all' || trace.points === 'all') && + Color.opacity(trace.fillcolor) === 0 && + Color.opacity((trace.line || {}).color) === 0 ) { var tMod = Lib.minExtend(trace, { marker: { @@ -432,10 +443,9 @@ module.exports = function style(s, gd, legend) { } else { var w = boundLineWidth(undefined, trace.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); - p.style('stroke-width', w + 'px') - .call(Color.fill, trace.fillcolor); + p.style('stroke-width', w + 'px').call(Color.fill, trace.fillcolor); - if(w) Color.stroke(p, trace.line.color); + if (w) Color.stroke(p, trace.line.color); } }); } @@ -443,54 +453,60 @@ module.exports = function style(s, gd, legend) { function styleCandles(d) { var trace = d[0].trace; - var pts = d3.select(this).select('g.legendpoints') + var pts = d3 + .select(this) + .select('g.legendpoints') .selectAll('path.legendcandle') .data(trace.visible && trace.type === 'candlestick' ? [d, d] : []); - pts.enter().append('path').classed('legendcandle', true) - .attr('d', function(_, i) { - if(i) return 'M-15,0H-8M-8,6V-6H8Z'; // increasing + pts.enter() + .append('path') + .classed('legendcandle', true) + .attr('d', function (_, i) { + if (i) return 'M-15,0H-8M-8,6V-6H8Z'; // increasing return 'M15,0H8M8,-6V6H-8Z'; // decreasing }) .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); - pts.each(function(_, i) { + pts.each(function (_, i) { var p = d3.select(this); var cont = trace[i ? 'increasing' : 'decreasing']; var w = boundLineWidth(undefined, cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); - p.style('stroke-width', w + 'px') - .call(Color.fill, cont.fillcolor); + p.style('stroke-width', w + 'px').call(Color.fill, cont.fillcolor); - if(w) Color.stroke(p, cont.line.color); + if (w) Color.stroke(p, cont.line.color); }); } function styleOHLC(d) { var trace = d[0].trace; - var pts = d3.select(this).select('g.legendpoints') + var pts = d3 + .select(this) + .select('g.legendpoints') .selectAll('path.legendohlc') .data(trace.visible && trace.type === 'ohlc' ? [d, d] : []); - pts.enter().append('path').classed('legendohlc', true) - .attr('d', function(_, i) { - if(i) return 'M-15,0H0M-8,-6V0'; // increasing + pts.enter() + .append('path') + .classed('legendohlc', true) + .attr('d', function (_, i) { + if (i) return 'M-15,0H0M-8,-6V0'; // increasing return 'M15,0H0M8,6V0'; // decreasing }) .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); - pts.each(function(_, i) { + pts.each(function (_, i) { var p = d3.select(this); var cont = trace[i ? 'increasing' : 'decreasing']; var w = boundLineWidth(undefined, cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); - p.style('fill', 'none') - .call(Drawing.dashLine, cont.line.dash, w); + p.style('fill', 'none').call(Drawing.dashLine, cont.line.dash, w); - if(w) Color.stroke(p, cont.line.color); + if (w) Color.stroke(p, cont.line.color); }); } @@ -506,59 +522,66 @@ module.exports = function style(s, gd, legend) { var d0 = d[0]; var trace = d0.trace; - var isVisible = (!desiredType) ? Registry.traceIs(trace, desiredType) : - (trace.visible && trace.type === desiredType); + var isVisible = !desiredType + ? Registry.traceIs(trace, desiredType) + : trace.visible && trace.type === desiredType; - var pts = d3.select(lThis).select('g.legendpoints') + var pts = d3 + .select(lThis) + .select('g.legendpoints') .selectAll('path.legend' + desiredType) .data(isVisible ? [d] : []); - pts.enter().append('path').classed('legend' + desiredType, true) + pts.enter() + .append('path') + .classed('legend' + desiredType, true) .attr('d', 'M6,6H-6V-6H6Z') .attr('transform', centerTransform); pts.exit().remove(); - if(pts.size()) { + if (pts.size()) { var cont = trace.marker || {}; - var lw = boundLineWidth(pieCastOption(cont.line.width, d0.pts), cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); + var lw = boundLineWidth( + pieCastOption(cont.line.width, d0.pts), + cont.line, + MAX_MARKER_LINE_WIDTH, + CST_MARKER_LINE_WIDTH + ); var opt = 'pieLike'; - var tMod = Lib.minExtend(trace, {marker: {line: {width: lw}}}, opt); - var d0Mod = Lib.minExtend(d0, {trace: tMod}, opt); + var tMod = Lib.minExtend(trace, { marker: { line: { width: lw } } }, opt); + var d0Mod = Lib.minExtend(d0, { trace: tMod }, opt); stylePie(pts, d0Mod, tMod, gd); } } - function styleSpatial(d) { // i.e. maninly traces having z and colorscale + function styleSpatial(d) { + // i.e. maninly traces having z and colorscale var trace = d[0].trace; var useGradient; var ptsData = []; - if(trace.visible) { - switch(trace.type) { - case 'histogram2d' : - case 'heatmap' : + if (trace.visible) { + switch (trace.type) { + case 'histogram2d': + case 'heatmap': ptsData = [ ['M-15,-2V4H15V-2Z'] // similar to contour ]; useGradient = true; break; - case 'choropleth' : - case 'choroplethmapbox' : - case 'choroplethmap' : - ptsData = [ - ['M-6,-6V6H6V-6Z'] - ]; + case 'choropleth': + case 'choroplethmapbox': + case 'choroplethmap': + ptsData = [['M-6,-6V6H6V-6Z']]; useGradient = true; break; - case 'densitymapbox' : - case 'densitymap' : - ptsData = [ - ['M-6,0 a6,6 0 1,0 12,0 a 6,6 0 1,0 -12,0'] - ]; + case 'densitymapbox': + case 'densitymap': + ptsData = [['M-6,0 a6,6 0 1,0 12,0 a 6,6 0 1,0 -12,0']]; useGradient = 'radial'; break; - case 'cone' : + case 'cone': ptsData = [ ['M-6,2 A2,2 0 0,0 -6,6 V6L6,4Z'], ['M-6,-6 A2,2 0 0,0 -6,-2 L6,-4Z'], @@ -566,7 +589,7 @@ module.exports = function style(s, gd, legend) { ]; useGradient = false; break; - case 'streamtube' : + case 'streamtube': ptsData = [ ['M-6,2 A2,2 0 0,0 -6,6 H6 A2,2 0 0,1 6,2 Z'], ['M-6,-6 A2,2 0 0,0 -6,-2 H6 A2,2 0 0,1 6,-6 Z'], @@ -574,79 +597,76 @@ module.exports = function style(s, gd, legend) { ]; useGradient = false; break; - case 'surface' : + case 'surface': ptsData = [ ['M-6,-6 A2,3 0 0,0 -6,0 H6 A2,3 0 0,1 6,-6 Z'], ['M-6,1 A2,3 0 0,1 -6,6 H6 A2,3 0 0,0 6,0 Z'] ]; useGradient = true; break; - case 'mesh3d' : - ptsData = [ - ['M-6,6H0L-6,-6Z'], - ['M6,6H0L6,-6Z'], - ['M-6,-6H6L0,6Z'] - ]; + case 'mesh3d': + ptsData = [['M-6,6H0L-6,-6Z'], ['M6,6H0L6,-6Z'], ['M-6,-6H6L0,6Z']]; useGradient = false; break; - case 'volume' : - ptsData = [ - ['M-6,6H0L-6,-6Z'], - ['M6,6H0L6,-6Z'], - ['M-6,-6H6L0,6Z'] - ]; + case 'volume': + ptsData = [['M-6,6H0L-6,-6Z'], ['M6,6H0L6,-6Z'], ['M-6,-6H6L0,6Z']]; useGradient = true; break; case 'isosurface': - ptsData = [ - ['M-6,6H0L-6,-6Z'], - ['M6,6H0L6,-6Z'], - ['M-6,-6 A12,24 0 0,0 6,-6 L0,6Z'] - ]; + ptsData = [['M-6,6H0L-6,-6Z'], ['M6,6H0L6,-6Z'], ['M-6,-6 A12,24 0 0,0 6,-6 L0,6Z']]; useGradient = false; break; } } - var pts = d3.select(this).select('g.legendpoints') - .selectAll('path.legend3dandfriends') - .data(ptsData); - pts.enter().append('path').classed('legend3dandfriends', true) + var pts = d3.select(this).select('g.legendpoints').selectAll('path.legend3dandfriends').data(ptsData); + pts.enter() + .append('path') + .classed('legend3dandfriends', true) .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); - pts.each(function(dd, i) { + pts.each(function (dd, i) { var pt = d3.select(this); var cOpts = extractOpts(trace); var colorscale = cOpts.colorscale; var reversescale = cOpts.reversescale; - var fillGradient = function(s) { - if(s.size()) { + var fillGradient = function (s) { + if (s.size()) { var gradientID = 'legendfill-' + trace.uid; - Drawing.gradient(s, gd, gradientID, + Drawing.gradient( + s, + gd, + gradientID, getGradientDirection(reversescale, useGradient === 'radial'), - colorscale, 'fill'); + colorscale, + 'fill' + ); } }; var fillColor; - if(!colorscale) { + if (!colorscale) { var color = trace.vertexcolor || trace.facecolor || trace.color; - fillColor = Lib.isArrayOrTypedArray(color) ? (color[i] || color[0]) : color; + fillColor = Lib.isArrayOrTypedArray(color) ? color[i] || color[0] : color; } else { - if(!useGradient) { + if (!useGradient) { var len = colorscale.length; fillColor = - i === 0 ? colorscale[reversescale ? len - 1 : 0][1] : // minimum - i === 1 ? colorscale[reversescale ? 0 : len - 1][1] : // maximum - colorscale[Math.floor((len - 1) / 2)][1]; // middle + i === 0 + ? colorscale[reversescale ? len - 1 : 0][1] + : // minimum + i === 1 + ? colorscale[reversescale ? 0 : len - 1][1] + : // maximum + colorscale[Math.floor((len - 1) / 2)][1]; // middle } } pt.attr('d', dd[0]); - if(fillColor) { + if (fillColor) { pt.call(Color.fill, fillColor); } else { pt.call(fillGradient); @@ -670,18 +690,18 @@ function getStyleGuide(d) { var showGradientLine = false; var showGradientFill = false; - if(contours) { + if (contours) { var coloring = contours.coloring; - if(coloring === 'lines') { + if (coloring === 'lines') { showGradientLine = true; } else { showLine = coloring === 'none' || coloring === 'heatmap' || contours.showlines; } - if(contours.type === 'constraint') { + if (contours.type === 'constraint') { showFill = contours._operation !== '='; - } else if(coloring === 'fill' || coloring === 'heatmap') { + } else if (coloring === 'fill' || coloring === 'heatmap') { showGradientFill = true; } } @@ -692,12 +712,12 @@ function getStyleGuide(d) { showGradientLine: showGradientLine, showGradientFill: showGradientFill, anyLine: showLine || showGradientLine, - anyFill: showFill || showGradientFill, + anyFill: showFill || showGradientFill }; } function dimAttr(v, dflt, max) { - if(v && Lib.isArrayOrTypedArray(v)) return dflt; - if(v > max) return max; + if (v && Lib.isArrayOrTypedArray(v)) return dflt; + if (v > max) return max; return v; } diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index efa5332498c..07eac057140 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -38,6 +38,7 @@ module.exports = function arraysToCalcdata(cd, trace) { if(marker.line) { Lib.mergeArray(markerLine.color, cd, 'mlc'); Lib.mergeArrayCastPositive(markerLine.width, cd, 'mlw'); + Lib.mergeArray(markerLine.dash, cd, 'mld'); } var markerGradient = marker.gradient; diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 18231a889b7..255496750a0 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -555,6 +555,9 @@ module.exports = { anim: true, description: 'Sets the width (in px) of the lines bounding the marker points.' }, + dash: extendFlat({}, dash, { + arrayOk: true + }), editType: 'calc' }, colorScaleAttrs('marker.line', { anim: true }) diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js index c4358730534..b7edca3e6dd 100644 --- a/src/traces/scatter/marker_defaults.js +++ b/src/traces/scatter/marker_defaults.js @@ -12,70 +12,65 @@ var subTypes = require('./subtypes'); * gradient: caller supports gradients * noSelect: caller does not support selected/unselected attribute containers */ -module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) { +module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts = {}) { var isBubble = subTypes.isBubble(traceIn); var lineColor = (traceIn.line || {}).color; var defaultMLC; - opts = opts || {}; - // marker.color inherit from line.color (even if line.color is an array) - if(lineColor) defaultColor = lineColor; + if (lineColor) defaultColor = lineColor; coerce('marker.symbol'); coerce('marker.opacity', isBubble ? 0.7 : 1); coerce('marker.size'); - if(!opts.noAngle) { + if (!opts.noAngle) { coerce('marker.angle'); - if(!opts.noAngleRef) { - coerce('marker.angleref'); - } - - if(!opts.noStandOff) { - coerce('marker.standoff'); - } + if (!opts.noAngleRef) coerce('marker.angleref'); + if (!opts.noStandOff) coerce('marker.standoff'); } coerce('marker.color', defaultColor); - if(hasColorscale(traceIn, 'marker')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'}); + if (hasColorscale(traceIn, 'marker')) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { prefix: 'marker.', cLetter: 'c' }); } - if(!opts.noSelect) { + if (!opts.noSelect) { coerce('selected.marker.color'); coerce('unselected.marker.color'); coerce('selected.marker.size'); coerce('unselected.marker.size'); } - if(!opts.noLine) { + if (!opts.noLine) { // if there's a line with a different color than the marker, use // that line color as the default marker line color // (except when it's an array) // mostly this is for transparent markers to behave nicely - if(lineColor && !Array.isArray(lineColor) && (traceOut.marker.color !== lineColor)) { + if (lineColor && !Array.isArray(lineColor) && traceOut.marker.color !== lineColor) { defaultMLC = lineColor; - } else if(isBubble) defaultMLC = Color.background; - else defaultMLC = Color.defaultLine; + } else if (isBubble) { + defaultMLC = Color.background; + } else { + defaultMLC = Color.defaultLine; + } coerce('marker.line.color', defaultMLC); - if(hasColorscale(traceIn, 'marker.line')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'}); + if (hasColorscale(traceIn, 'marker.line')) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { prefix: 'marker.line.', cLetter: 'c' }); } coerce('marker.line.width', isBubble ? 1 : 0); + if (!opts.noLineDash) coerce('marker.line.dash'); } - if(isBubble) { + if (isBubble) { coerce('marker.sizeref'); coerce('marker.sizemin'); coerce('marker.sizemode'); } - if(opts.gradient) { + if (opts.gradient) { var gradientType = coerce('marker.gradient.type'); - if(gradientType !== 'none') { - coerce('marker.gradient.color'); - } + if (gradientType !== 'none') coerce('marker.gradient.color'); } }; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index e99182fef69..eb366090612 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -32,7 +32,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode'); if (subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noSelect: true, noAngle: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngle: true, + noLineDash: true, + noSelect: true + }); } if (subTypes.hasLines(traceOut)) { diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index 68caaade24a..ded94305d8a 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -97,6 +97,7 @@ module.exports = { line: extendFlat( { width: scatterMarkerLineAttrs.width, + dash: scatterMarkerLineAttrs.dash, editType: 'calc' }, colorScaleAttrs('marker.line') diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 0f8cede9834..f2fb2639cf8 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -139,7 +139,8 @@ module.exports = overrideAll( colorbar: scatterMarkerAttrs.colorbar, line: extendFlat( { - width: scatterMarkerLineAttrs.width + width: scatterMarkerLineAttrs.width, + dash: scatterMarkerLineAttrs.dash }, colorAttributes('marker.line') ), diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index efaecbb5e63..47f1dbd7e10 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -41,7 +41,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode', defaultMode); if (subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noAngleRef: true, noStandOff: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngleRef: true, + noLineDash: true, + noStandOff: true + }); coerce('marker.line.width', isOpen || isBubble ? 1 : 0); } diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js index 7f095c4ea71..c540adc2acd 100644 --- a/src/traces/scatterpolargl/defaults.js +++ b/src/traces/scatterpolargl/defaults.js @@ -33,7 +33,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if (subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noAngleRef: true, noStandOff: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngleRef: true, + noLineDash: true, + noStandOff: true + }); } if (subTypes.hasLines(traceOut)) { diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index 5954d376f6f..c51c1abdfbb 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -126,6 +126,7 @@ module.exports = { line: extendFlat( { width: scatterMarkerLineAttrs.width, + dash: scatterMarkerLineAttrs.dash, editType: 'calc' }, colorScaleAttrs('marker.line') diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index d3c4154b9ba..a7ee35aedbe 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -37,7 +37,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('xhoverformat'); coerce('yhoverformat'); - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noAngleRef: true, noStandOff: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngleRef: true, + noLineDash: true, + noStandOff: true + }); var isOpen = isOpenSymbol(traceOut.marker.symbol); var isBubble = subTypes.isBubble(traceOut); diff --git a/test/image/baselines/zz-scatter_marker_line_dash.png b/test/image/baselines/zz-scatter_marker_line_dash.png new file mode 100644 index 00000000000..e00765926e1 Binary files /dev/null and b/test/image/baselines/zz-scatter_marker_line_dash.png differ diff --git a/test/image/mocks/zz-scatter_marker_line_dash.json b/test/image/mocks/zz-scatter_marker_line_dash.json new file mode 100644 index 00000000000..44ffeef3f01 --- /dev/null +++ b/test/image/mocks/zz-scatter_marker_line_dash.json @@ -0,0 +1,82 @@ +{ + "data": [ + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [1, 1, 1, 1, 1, 1], + "marker": { + "size": 30, + "color": "rgba(255,0,0,0.2)", + "line": { + "color": "black", + "width": 3, + "dash": ["solid", "dot", "dash", "longdash", "dashdot", "longdashdot"] + } + }, + "name": "Array of dashes" + }, + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [2, 2, 2, 2, 2, 2], + "marker": { + "size": 30, + "color": "rgba(255,0,0,0.2)", + "line": { + "color": "red", + "width": 4, + "dash": "dash" + } + }, + "name": "Single dash" + }, + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [3, 3, 3, 3, 3, 3], + "marker": { + "size": 30, + "symbol": "square", + "color": "rgba(255,0,0,0.2)", + "line": { + "color": "blue", + "width": 2, + "dash": "dot" + } + }, + "name": "Dot with squares" + }, + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [4, 4, 4, 4, 4, 4], + "marker": { + "size": 30, + "symbol": "circle-open", + "color": "green", + "line": { + "width": 2, + "dash": "dash" + } + }, + "name": "Open markers with dash" + } + ], + "layout": { + "title": { + "text": "Scatter Marker Line Dash Support" + }, + "xaxis": { + "range": [0, 7] + }, + "yaxis": { + "range": [0, 5] + }, + "width": 600, + "height": 400 + } +} diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 9fc21aabb47..b07985a1e62 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -21,14 +21,14 @@ var mock = require('../../image/mocks/legend_horizontal.json'); var Drawing = require('../../../src/components/drawing'); -describe('legend defaults', function() { +describe('legend defaults', function () { 'use strict'; var supplyLayoutDefaults = Legend.supplyLayoutDefaults; var layoutIn, layoutOut, fullData; - beforeEach(function() { + beforeEach(function () { layoutIn = { showlegend: true }; @@ -39,17 +39,20 @@ describe('legend defaults', function() { }); function allShown(fullData) { - return fullData.map(function(trace) { - return Lib.extendDeep({ - visible: true, - showlegend: true, - _dfltShowLegend: true, - _input: {} - }, trace); + return fullData.map(function (trace) { + return Lib.extendDeep( + { + visible: true, + showlegend: true, + _dfltShowLegend: true, + _input: {} + }, + trace + ); }); } - it('hides by default if there is only one legend item by default', function() { + it('hides by default if there is only one legend item by default', function () { fullData = allShown([ { type: 'scatter' }, { type: 'scatter', visible: false }, // ignored @@ -60,7 +63,7 @@ describe('legend defaults', function() { expect(layoutOut.showlegend).toBe(false); }); - it('shows if there are two legend items by default but only one is shown', function() { + it('shows if there are two legend items by default but only one is shown', function () { fullData = allShown([ { type: 'scatter' }, { type: 'scatter', showlegend: false } // not shown but still triggers legend @@ -70,7 +73,7 @@ describe('legend defaults', function() { expect(layoutOut.showlegend).toBe(true); }); - it('hides if no items are actually shown', function() { + it('hides if no items are actually shown', function () { fullData = allShown([ { type: 'scatter', showlegend: false }, { type: 'scatter', showlegend: false } @@ -80,34 +83,28 @@ describe('legend defaults', function() { expect(layoutOut.showlegend).toBe(false); }); - it('shows with one visible pie', function() { - fullData = allShown([ - { type: 'pie' } - ]); + it('shows with one visible pie', function () { + fullData = allShown([{ type: 'pie' }]); supplyLayoutDefaults({}, layoutOut, fullData); expect(layoutOut.showlegend).toBe(true); }); - it('does not show with a hidden pie', function() { - fullData = allShown([ - { type: 'pie', showlegend: false } - ]); + it('does not show with a hidden pie', function () { + fullData = allShown([{ type: 'pie', showlegend: false }]); supplyLayoutDefaults({}, layoutOut, fullData); expect(layoutOut.showlegend).toBe(false); }); - it('shows if even a default hidden single item is explicitly shown', function() { - fullData = allShown([ - { type: 'contour', _dfltShowLegend: false, _input: { showlegend: true } } - ]); + it('shows if even a default hidden single item is explicitly shown', function () { + fullData = allShown([{ type: 'contour', _dfltShowLegend: false, _input: { showlegend: true } }]); supplyLayoutDefaults({}, layoutOut, fullData); expect(layoutOut.showlegend).toBe(true); }); - it('should default traceorder to reversed for stack bar charts', function() { + it('should default traceorder to reversed for stack bar charts', function () { fullData = allShown([ { type: 'bar', visible: 'legendonly' }, { type: 'bar', visible: 'legendonly' }, @@ -123,7 +120,7 @@ describe('legend defaults', function() { expect(layoutOut.legend.traceorder).toEqual('reversed'); }); - it('should default traceorder to reversed for stack bar charts | multi-legend case', function() { + it('should default traceorder to reversed for stack bar charts | multi-legend case', function () { fullData = allShown([ { type: 'scatter' }, { legend: 'legend2', type: 'bar', visible: 'legendonly' }, @@ -148,21 +145,15 @@ describe('legend defaults', function() { expect(layoutOut.legend3.traceorder).toEqual('normal'); }); - it('should default traceorder to reversed for filled tonext scatter charts', function() { - fullData = allShown([ - { type: 'scatter' }, - { type: 'scatter', fill: 'tonexty' } - ]); + it('should default traceorder to reversed for filled tonext scatter charts', function () { + fullData = allShown([{ type: 'scatter' }, { type: 'scatter', fill: 'tonexty' }]); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.legend.traceorder).toEqual('reversed'); }); - it('should default traceorder to grouped when a group is present', function() { - fullData = allShown([ - { type: 'scatter', legendgroup: 'group' }, - { type: 'scatter' } - ]); + it('should default traceorder to grouped when a group is present', function () { + fullData = allShown([{ type: 'scatter', legendgroup: 'group' }, { type: 'scatter' }]); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.legend.traceorder).toEqual('grouped'); @@ -173,7 +164,7 @@ describe('legend defaults', function() { expect(layoutOut.legend.traceorder).toEqual('grouped+reversed'); }); - it('should default traceorder to grouped when a group is present | multi-legend case', function() { + it('should default traceorder to grouped when a group is present | multi-legend case', function () { fullData = allShown([ { type: 'scatter' }, { legend: 'legend2', type: 'scatter', legendgroup: 'group' }, @@ -197,34 +188,27 @@ describe('legend defaults', function() { expect(layoutOut.legend3.traceorder).toEqual('normal'); }); - it('does not consider invisible traces for traceorder default', function() { - fullData = allShown([ - { type: 'bar', visible: false }, - { type: 'bar', visible: false }, - { type: 'scatter' } - ]); + it('does not consider invisible traces for traceorder default', function () { + fullData = allShown([{ type: 'bar', visible: false }, { type: 'bar', visible: false }, { type: 'scatter' }]); layoutOut.barmode = 'stack'; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.legend.traceorder).toEqual('normal'); - fullData = allShown([ - { type: 'scatter', legendgroup: 'group', visible: false }, - { type: 'scatter' } - ]); + fullData = allShown([{ type: 'scatter', legendgroup: 'group', visible: false }, { type: 'scatter' }]); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.legend.traceorder).toEqual('normal'); }); - it('does not consider invisible traces for traceorder default | multi-legend case', function() { + it('does not consider invisible traces for traceorder default | multi-legend case', function () { fullData = allShown([ { type: 'scatter' }, { legend: 'legend2', type: 'bar', visible: false }, { legend: 'legend2', type: 'bar', visible: false }, { legend: 'legend2', type: 'scatter' }, - { legend: 'legend3', type: 'scatter' }, + { legend: 'legend3', type: 'scatter' } ]); layoutOut.legend2 = {}; @@ -250,66 +234,78 @@ describe('legend defaults', function() { expect(layoutOut.legend3.traceorder).toEqual('normal'); }); - it('should default orientation to vertical', function() { + it('should default orientation to vertical', function () { supplyLayoutDefaults(layoutIn, layoutOut, []); expect(layoutOut.legend.orientation).toEqual('v'); }); - it('should not coerce `title.font` and `title.side` if the `title.text` is blank', function() { - var layoutWithTitle = Lib.extendDeep({ - legend: { - title: { - text: '' + it('should not coerce `title.font` and `title.side` if the `title.text` is blank', function () { + var layoutWithTitle = Lib.extendDeep( + { + legend: { + title: { + text: '' + } } - } - }, layoutIn); + }, + layoutIn + ); supplyLayoutDefaults(layoutWithTitle, layoutOut, []); expect(layoutOut.legend.title.font).toEqual(undefined); expect(layoutOut.legend.title.side).toEqual(undefined); }); - it('should default `title.side` to *top* for vertical legends', function() { - var layoutWithTitle = Lib.extendDeep({ - legend: { - title: { - text: 'Legend Title' + it('should default `title.side` to *top* for vertical legends', function () { + var layoutWithTitle = Lib.extendDeep( + { + legend: { + title: { + text: 'Legend Title' + } } - } - }, layoutIn); + }, + layoutIn + ); supplyLayoutDefaults(layoutWithTitle, layoutOut, []); expect(layoutOut.legend.title.side).toEqual('top'); }); - it('should default `title.side` to *left* for horizontal legends', function() { - var layoutWithTitle = Lib.extendDeep({ - legend: { - orientation: 'h', - title: { - text: 'Legend Title' + it('should default `title.side` to *left* for horizontal legends', function () { + var layoutWithTitle = Lib.extendDeep( + { + legend: { + orientation: 'h', + title: { + text: 'Legend Title' + } } - } - }, layoutIn); + }, + layoutIn + ); supplyLayoutDefaults(layoutWithTitle, layoutOut, []); expect(layoutOut.legend.title.side).toEqual('left'); }); - describe('for horizontal legends', function() { + describe('for horizontal legends', function () { var layoutInForHorizontalLegends; - beforeEach(function() { - layoutInForHorizontalLegends = Lib.extendDeep({ - legend: { - orientation: 'h' - }, - xaxis: { - rangeslider: { - visible: false + beforeEach(function () { + layoutInForHorizontalLegends = Lib.extendDeep( + { + legend: { + orientation: 'h' + }, + xaxis: { + rangeslider: { + visible: false + } } - } - }, layoutIn); + }, + layoutIn + ); }); - it('should default position to bottom left', function() { + it('should default position to bottom left', function () { supplyLayoutDefaults(layoutInForHorizontalLegends, layoutOut, []); expect(layoutOut.legend.x).toEqual(0); expect(layoutOut.legend.xanchor).toEqual('left'); @@ -317,7 +313,7 @@ describe('legend defaults', function() { expect(layoutOut.legend.yanchor).toEqual('top'); }); - it('should default position to top left if a range slider present', function() { + it('should default position to top left if a range slider present', function () { var mockLayoutIn = Lib.extendDeep({}, layoutInForHorizontalLegends); mockLayoutIn.xaxis.rangeslider.visible = true; @@ -330,114 +326,46 @@ describe('legend defaults', function() { }); }); -describe('legend getLegendData user-defined legendrank', function() { +describe('legend getLegendData user-defined legendrank', function () { 'use strict'; var calcdata, opts, legendData, expected; - it('should group legendgroup traces', function() { + it('should group legendgroup traces', function () { calcdata = [ - [{ - trace: { - legendrank: 3, - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - } - }], - [{ - trace: { - legendrank: 2, - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - legendrank: 1, - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - } - }] - ]; - opts = { - traceorder: 'grouped' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ [ - [{ - _preSort: 1, trace: { - legendrank: 1, - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - } - }], - [{ - _groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { + { + trace: { legendrank: 3, type: 'scatter', visible: true, legendgroup: 'group', showlegend: true } - }] + } ], [ - [{ - _groupMinRank: 2, _preGroupSort: 1, _preSort: 0, trace: { + { + trace: { legendrank: 2, type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true } - }] - ] - ]; - - expect(legendData).toEqual(expected); - expect(opts._lgroupsLength).toEqual(2); - }); - - it('should collapse when data has only one group', function() { - calcdata = [ - [{ - trace: { - legendrank: 3, - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true } - }], - [{ - trace: { - legendrank: 2, - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - legendrank: 1, - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true + ], + [ + { + trace: { + legendrank: 1, + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }] + ] ]; opts = { traceorder: 'grouped' @@ -447,33 +375,137 @@ describe('legend getLegendData user-defined legendrank', function() { expected = [ [ - [{ - _preSort: 2, trace: { - legendrank: 1, + [ + { + _preSort: 1, + trace: { + legendrank: 1, + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } + } + ], + [ + { + _groupMinRank: 1, + _preGroupSort: 0, + _preSort: 0, + trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } + } + ] + ], + [ + [ + { + _groupMinRank: 2, + _preGroupSort: 1, + _preSort: 0, + trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } + } + ] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(2); + }); + + it('should collapse when data has only one group', function () { + calcdata = [ + [ + { + trace: { + legendrank: 3, type: 'scatter', visible: true, legendgroup: '', showlegend: true } - }], - [{ - _preSort: 1, trace: { + } + ], + [ + { + trace: { legendrank: 2, type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true } - }], - [{ - _groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { - legendrank: 3, + } + ], + [ + { + trace: { + legendrank: 1, type: 'scatter', visible: true, legendgroup: '', showlegend: true } - }] + } + ] + ]; + opts = { + traceorder: 'grouped' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + _preSort: 2, + trace: { + legendrank: 1, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _preSort: 1, + trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _groupMinRank: 1, + _preGroupSort: 0, + _preSort: 0, + trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } + } + ] ] ]; @@ -481,34 +513,40 @@ describe('legend getLegendData user-defined legendrank', function() { expect(opts._lgroupsLength).toEqual(1); }); - it('should return empty array when legend data has no traces', function() { + it('should return empty array when legend data has no traces', function () { calcdata = [ - [{ - trace: { - legendrank: 3, - type: 'histogram', - visible: true, - legendgroup: '', - showlegend: false + [ + { + trace: { + legendrank: 3, + type: 'histogram', + visible: true, + legendgroup: '', + showlegend: false + } } - }], - [{ - trace: { - legendrank: 2, - type: 'box', - visible: 'legendonly', - legendgroup: '', - showlegend: false + ], + [ + { + trace: { + legendrank: 2, + type: 'box', + visible: 'legendonly', + legendgroup: '', + showlegend: false + } } - }], - [{ - trace: { - legendrank: 1, - type: 'heatmap', - visible: true, - legendgroup: '' + ], + [ + { + trace: { + legendrank: 1, + type: 'heatmap', + visible: true, + legendgroup: '' + } } - }] + ] ]; opts = { _main: true, @@ -519,71 +557,88 @@ describe('legend getLegendData user-defined legendrank', function() { expect(legendData).toEqual([]); }); - it('should reverse the order when legend.traceorder is set', function() { + it('should reverse the order when legend.traceorder is set', function () { calcdata = [ - [{ - trace: { - legendrank: 3, - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - legendrank: 2, - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - legendrank: 1, - type: 'box', - visible: true, - legendgroup: '', - showlegend: true - } - }] - ]; - opts = { - traceorder: 'reversed' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ [ - [{ - _groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { + { + trace: { legendrank: 3, type: 'scatter', visible: true, legendgroup: '', showlegend: true } - }], - [{ - _preSort: 1, trace: { + } + ], + [ + { + trace: { legendrank: 2, type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true } - }], - [{ - _preSort: 2, trace: { + } + ], + [ + { + trace: { legendrank: 1, type: 'box', visible: true, legendgroup: '', showlegend: true } - }] + } + ] + ]; + opts = { + traceorder: 'reversed' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + _groupMinRank: 1, + _preGroupSort: 0, + _preSort: 0, + trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _preSort: 1, + trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _preSort: 2, + trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: '', + showlegend: true + } + } + ] ] ]; @@ -591,35 +646,41 @@ describe('legend getLegendData user-defined legendrank', function() { expect(opts._lgroupsLength).toEqual(1); }); - it('should reverse the trace order within groups when reversed+grouped', function() { + it('should reverse the trace order within groups when reversed+grouped', function () { calcdata = [ - [{ - trace: { - legendrank: 3, - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + [ + { + trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }], - [{ - trace: { - legendrank: 2, - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true + ], + [ + { + trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } } - }], - [{ - trace: { - legendrank: 1, - type: 'box', - visible: true, - legendgroup: 'group', - showlegend: true + ], + [ + { + trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }] + ] ]; opts = { traceorder: 'reversed+grouped' @@ -629,35 +690,48 @@ describe('legend getLegendData user-defined legendrank', function() { expected = [ [ - [{ - _groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { - legendrank: 3, - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + [ + { + _groupMinRank: 1, + _preGroupSort: 0, + _preSort: 0, + trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }], - [{ - _preSort: 1, trace: { - legendrank: 1, - type: 'box', - visible: true, - legendgroup: 'group', - showlegend: true + ], + [ + { + _preSort: 1, + trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }] + ] ], [ - [{ - _groupMinRank: 2, _preGroupSort: 1, _preSort: 0, trace: { - legendrank: 2, - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true + [ + { + _groupMinRank: 2, + _preGroupSort: 1, + _preSort: 0, + trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } } - }] + ] ] ]; @@ -666,37 +740,43 @@ describe('legend getLegendData user-defined legendrank', function() { }); }); -describe('legend getLegendData default legendrank', function() { +describe('legend getLegendData default legendrank', function () { 'use strict'; var calcdata, opts, legendData, expected; - it('should group legendgroup traces', function() { + it('should group legendgroup traces', function () { calcdata = [ - [{ - trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + [ + { + trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }], - [{ - trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true + ], + [ + { + trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } } - }], - [{ - trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + ], + [ + { + trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }] + ] ]; opts = { traceorder: 'grouped' @@ -706,32 +786,45 @@ describe('legend getLegendData default legendrank', function() { expected = [ [ - [{ - _groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + [ + { + _groupMinRank: Infinity, + _preGroupSort: 0, + _preSort: 0, + trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }], - [{ - _preSort: 1, trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + ], + [ + { + _preSort: 1, + trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }] + ] ], [ - [{ - _groupMinRank: Infinity, _preGroupSort: 1, _preSort: 0, trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true + [ + { + _groupMinRank: Infinity, + _preGroupSort: 1, + _preSort: 0, + trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } } - }] + ] ] ]; @@ -739,65 +832,82 @@ describe('legend getLegendData default legendrank', function() { expect(opts._lgroupsLength).toEqual(2); }); - it('should collapse when data has only one group', function() { + it('should collapse when data has only one group', function () { calcdata = [ - [{ - trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - } - }] - ]; - opts = { - traceorder: 'grouped' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ [ - [{ - _groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { + { + trace: { type: 'scatter', visible: true, legendgroup: '', showlegend: true } - }], - [{ - _preSort: 1, trace: { + } + ], + [ + { + trace: { type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true } - }], - [{ - _preSort: 2, trace: { + } + ], + [ + { + trace: { type: 'scatter', visible: true, legendgroup: '', showlegend: true } - }] + } + ] + ]; + opts = { + traceorder: 'grouped' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + _groupMinRank: Infinity, + _preGroupSort: 0, + _preSort: 0, + trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _preSort: 1, + trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _preSort: 2, + trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } + } + ] ] ]; @@ -805,31 +915,37 @@ describe('legend getLegendData default legendrank', function() { expect(opts._lgroupsLength).toEqual(1); }); - it('should return empty array when legend data has no traces', function() { + it('should return empty array when legend data has no traces', function () { calcdata = [ - [{ - trace: { - type: 'histogram', - visible: true, - legendgroup: '', - showlegend: false + [ + { + trace: { + type: 'histogram', + visible: true, + legendgroup: '', + showlegend: false + } } - }], - [{ - trace: { - type: 'box', - visible: 'legendonly', - legendgroup: '', - showlegend: false + ], + [ + { + trace: { + type: 'box', + visible: 'legendonly', + legendgroup: '', + showlegend: false + } } - }], - [{ - trace: { - type: 'heatmap', - visible: true, - legendgroup: '' + ], + [ + { + trace: { + type: 'heatmap', + visible: true, + legendgroup: '' + } } - }] + ] ]; opts = { _main: true, @@ -840,32 +956,38 @@ describe('legend getLegendData default legendrank', function() { expect(legendData).toEqual([]); }); - it('should reverse the order when legend.traceorder is set', function() { + it('should reverse the order when legend.traceorder is set', function () { calcdata = [ - [{ - trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true + [ + { + trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } } - }], - [{ - trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true + ], + [ + { + trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } } - }], - [{ - trace: { - type: 'box', - visible: true, - legendgroup: '', - showlegend: true + ], + [ + { + trace: { + type: 'box', + visible: true, + legendgroup: '', + showlegend: true + } } - }] + ] ]; opts = { traceorder: 'reversed' @@ -873,65 +995,82 @@ describe('legend getLegendData default legendrank', function() { legendData = getLegendData(calcdata, opts); - expected = [ + expected = [ + [ + [ + { + _preSort: 2, + trace: { + type: 'box', + visible: true, + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _preSort: 1, + trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } + } + ], + [ + { + _groupMinRank: Infinity, + _preGroupSort: 0, + _preSort: 0, + trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + } + } + ] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(1); + }); + + it('should reverse the trace order within groups when reversed+grouped', function () { + calcdata = [ [ - [{ - _preSort: 2, trace: { - type: 'box', + { + trace: { + type: 'scatter', visible: true, - legendgroup: '', + legendgroup: 'group', showlegend: true } - }], - [{ - _preSort: 1, trace: { + } + ], + [ + { + trace: { type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true } - }], - [{ - _groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { - type: 'scatter', + } + ], + [ + { + trace: { + type: 'box', visible: true, - legendgroup: '', + legendgroup: 'group', showlegend: true } - }] - ] - ]; - - expect(legendData).toEqual(expected); - expect(opts._lgroupsLength).toEqual(1); - }); - - it('should reverse the trace order within groups when reversed+grouped', function() { - calcdata = [ - [{ - trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true } - }], - [{ - trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - } - }], - [{ - trace: { - type: 'box', - visible: true, - legendgroup: 'group', - showlegend: true - } - }] + ] ]; opts = { traceorder: 'reversed+grouped' @@ -941,32 +1080,45 @@ describe('legend getLegendData default legendrank', function() { expected = [ [ - [{ - _preSort: 1, trace: { - type: 'box', - visible: true, - legendgroup: 'group', - showlegend: true + [ + { + _preSort: 1, + trace: { + type: 'box', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }], - [{ - _groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true + ], + [ + { + _groupMinRank: Infinity, + _preGroupSort: 0, + _preSort: 0, + trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + } } - }] + ] ], [ - [{ - _groupMinRank: Infinity, _preGroupSort: 1, _preSort: 0, trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true + [ + { + _groupMinRank: Infinity, + _preGroupSort: 1, + _preSort: 0, + trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + } } - }] + ] ] ]; @@ -975,13 +1127,13 @@ describe('legend getLegendData default legendrank', function() { }); }); -describe('legend helpers:', function() { +describe('legend helpers:', function () { 'use strict'; - describe('isGrouped', function() { + describe('isGrouped', function () { var isGrouped = helpers.isGrouped; - it('should return true when trace is visible and supports legend', function() { + it('should return true when trace is visible and supports legend', function () { expect(isGrouped({ traceorder: 'normal' })).toBe(false); expect(isGrouped({ traceorder: 'grouped' })).toBe(true); expect(isGrouped({ traceorder: 'reversed+grouped' })).toBe(true); @@ -990,10 +1142,10 @@ describe('legend helpers:', function() { }); }); - describe('isReversed', function() { + describe('isReversed', function () { var isReversed = helpers.isReversed; - it('should return true when trace is visible and supports legend', function() { + it('should return true when trace is visible and supports legend', function () { expect(isReversed({ traceorder: 'normal' })).toBe(false); expect(isReversed({ traceorder: 'grouped' })).toBe(false); expect(isReversed({ traceorder: 'reversed+grouped' })).toBe(true); @@ -1003,105 +1155,101 @@ describe('legend helpers:', function() { }); }); -describe('legend anchor utils:', function() { +describe('legend anchor utils:', function () { 'use strict'; - describe('isRightAnchor', function() { + describe('isRightAnchor', function () { var isRightAnchor = Lib.isRightAnchor; var threshold = 2 / 3; - it('should return true when \'xanchor\' is set to \'right\'', function() { + it("should return true when 'xanchor' is set to 'right'", function () { expect(isRightAnchor({ xanchor: 'left' })).toBe(false); expect(isRightAnchor({ xanchor: 'center' })).toBe(false); expect(isRightAnchor({ xanchor: 'right' })).toBe(true); }); - it('should return true when \'xanchor\' is set to \'auto\' and \'x\' >= 2/3', function() { + it("should return true when 'xanchor' is set to 'auto' and 'x' >= 2/3", function () { var opts = { xanchor: 'auto' }; - [0, 0.4, 0.7, 1].forEach(function(v) { + [0, 0.4, 0.7, 1].forEach(function (v) { opts.x = v; - expect(isRightAnchor(opts)) - .toBe(v > threshold, 'case ' + v); + expect(isRightAnchor(opts)).toBe(v > threshold, 'case ' + v); }); }); }); - describe('isCenterAnchor', function() { + describe('isCenterAnchor', function () { var isCenterAnchor = Lib.isCenterAnchor; var threshold0 = 1 / 3; var threshold1 = 2 / 3; - it('should return true when \'xanchor\' is set to \'center\'', function() { + it("should return true when 'xanchor' is set to 'center'", function () { expect(isCenterAnchor({ xanchor: 'left' })).toBe(false); expect(isCenterAnchor({ xanchor: 'center' })).toBe(true); expect(isCenterAnchor({ xanchor: 'right' })).toBe(false); }); - it('should return true when \'xanchor\' is set to \'auto\' and 1/3 < \'x\' < 2/3', function() { + it("should return true when 'xanchor' is set to 'auto' and 1/3 < 'x' < 2/3", function () { var opts = { xanchor: 'auto' }; - [0, 0.4, 0.7, 1].forEach(function(v) { + [0, 0.4, 0.7, 1].forEach(function (v) { opts.x = v; - expect(isCenterAnchor(opts)) - .toBe(v > threshold0 && v < threshold1, 'case ' + v); + expect(isCenterAnchor(opts)).toBe(v > threshold0 && v < threshold1, 'case ' + v); }); }); }); - describe('isBottomAnchor', function() { + describe('isBottomAnchor', function () { var isBottomAnchor = Lib.isBottomAnchor; var threshold = 1 / 3; - it('should return true when \'yanchor\' is set to \'right\'', function() { + it("should return true when 'yanchor' is set to 'right'", function () { expect(isBottomAnchor({ yanchor: 'top' })).toBe(false); expect(isBottomAnchor({ yanchor: 'middle' })).toBe(false); expect(isBottomAnchor({ yanchor: 'bottom' })).toBe(true); }); - it('should return true when \'yanchor\' is set to \'auto\' and \'y\' <= 1/3', function() { + it("should return true when 'yanchor' is set to 'auto' and 'y' <= 1/3", function () { var opts = { yanchor: 'auto' }; - [0, 0.4, 0.7, 1].forEach(function(v) { + [0, 0.4, 0.7, 1].forEach(function (v) { opts.y = v; - expect(isBottomAnchor(opts)) - .toBe(v < threshold, 'case ' + v); + expect(isBottomAnchor(opts)).toBe(v < threshold, 'case ' + v); }); }); }); - describe('isMiddleAnchor', function() { + describe('isMiddleAnchor', function () { var isMiddleAnchor = Lib.isMiddleAnchor; var threshold0 = 1 / 3; var threshold1 = 2 / 3; - it('should return true when \'yanchor\' is set to \'center\'', function() { + it("should return true when 'yanchor' is set to 'center'", function () { expect(isMiddleAnchor({ yanchor: 'top' })).toBe(false); expect(isMiddleAnchor({ yanchor: 'middle' })).toBe(true); expect(isMiddleAnchor({ yanchor: 'bottom' })).toBe(false); }); - it('should return true when \'yanchor\' is set to \'auto\' and 1/3 < \'y\' < 2/3', function() { + it("should return true when 'yanchor' is set to 'auto' and 1/3 < 'y' < 2/3", function () { var opts = { yanchor: 'auto' }; - [0, 0.4, 0.7, 1].forEach(function(v) { + [0, 0.4, 0.7, 1].forEach(function (v) { opts.y = v; - expect(isMiddleAnchor(opts)) - .toBe(v > threshold0 && v < threshold1, 'case ' + v); + expect(isMiddleAnchor(opts)).toBe(v > threshold0 && v < threshold1, 'case ' + v); }); }); }); }); -describe('legend relayout update', function() { +describe('legend relayout update', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - it('should hide and show the legend', function(done) { + it('should hide and show the legend', function (done) { var mockCopy = Lib.extendDeep({}, require('../../image/mocks/0.json'), { layout: { legend: { x: 1.1, xanchor: 'left' }, @@ -1111,30 +1259,30 @@ describe('legend relayout update', function() { }); Plotly.newPlot(gd, mockCopy.data, mockCopy.layout) - .then(function() { + .then(function () { expect(d3SelectAll('g.legend').size()).toBe(1); // check that the margins changed assertPlotSize({ widthLessThan: 400 }); return Plotly.relayout(gd, { showlegend: false }); }) - .then(function() { + .then(function () { expect(d3SelectAll('g.legend').size()).toBe(0); assertPlotSize({ width: 400 }); return Plotly.relayout(gd, { showlegend: true }); }) - .then(function() { + .then(function () { expect(d3SelectAll('g.legend').size()).toBe(1); assertPlotSize({ widthLessThan: 400 }); return Plotly.relayout(gd, { 'legend.x': 0.7 }); }) - .then(function() { + .then(function () { expect(d3SelectAll('g.legend').size()).toBe(1); assertPlotSize({ width: 400 }); }) .then(done, done.fail); }); - it('should update border styling', function(done) { + it('should update border styling', function (done) { var mockCopy = Lib.extendDeep({}, require('../../image/mocks/0.json')); function assertLegendStyle(bgColor, borderColor, borderWidth) { @@ -1145,52 +1293,57 @@ describe('legend relayout update', function() { expect(node.style.strokeWidth).toEqual(borderWidth + 'px'); } - Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(function() { - assertLegendStyle('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 1); + Plotly.newPlot(gd, mockCopy.data, mockCopy.layout) + .then(function () { + assertLegendStyle('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 1); - return Plotly.relayout(gd, { - 'legend.bordercolor': 'red', - 'legend.bgcolor': 'blue' - }); - }).then(function() { - assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 1); + return Plotly.relayout(gd, { + 'legend.bordercolor': 'red', + 'legend.bgcolor': 'blue' + }); + }) + .then(function () { + assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 1); - return Plotly.relayout(gd, 'legend.borderwidth', 10); - }).then(function() { - assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 10); + return Plotly.relayout(gd, 'legend.borderwidth', 10); + }) + .then(function () { + assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 10); - return Plotly.relayout(gd, 'legend.bgcolor', null); - }).then(function() { - assertLegendStyle('rgb(255, 255, 255)', 'rgb(255, 0, 0)', 10); + return Plotly.relayout(gd, 'legend.bgcolor', null); + }) + .then(function () { + assertLegendStyle('rgb(255, 255, 255)', 'rgb(255, 0, 0)', 10); - return Plotly.relayout(gd, 'paper_bgcolor', 'blue'); - }).then(function() { - assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 10); - }) + return Plotly.relayout(gd, 'paper_bgcolor', 'blue'); + }) + .then(function () { + assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 10); + }) .then(done, done.fail); }); - describe('should update legend valign', function() { + describe('should update legend valign', function () { function markerOffsetY() { var translate = Drawing.getTranslate(d3Select('.legend .traces .layers')); return translate.y; } - it('it should translate markers', function(done) { + it('it should translate markers', function (done) { var mockCopy = Lib.extendDeep({}, require('../../image/mocks/legend_valign_top.json')); var top, middle, bottom; Plotly.newPlot(gd, mockCopy.data, mockCopy.layout) - .then(function() { + .then(function () { top = markerOffsetY(); return Plotly.relayout(gd, 'legend.valign', 'middle'); }) - .then(function() { + .then(function () { middle = markerOffsetY(); expect(middle).toBeGreaterThan(top); return Plotly.relayout(gd, 'legend.valign', 'bottom'); }) - .then(function() { + .then(function () { bottom = markerOffsetY(); expect(bottom).toBeGreaterThan(middle); }) @@ -1198,30 +1351,30 @@ describe('legend relayout update', function() { }); }); - describe('with legendgroup', function() { - it('changes the margin size to fit tracegroupgap', function(done) { + describe('with legendgroup', function () { + it('changes the margin size to fit tracegroupgap', function (done) { var mockCopy = Lib.extendDeep({}, require('../../image/mocks/legendgroup_horizontal_wrapping.json')); Plotly.newPlot(gd, mockCopy) - .then(function() { + .then(function () { expect(gd._fullLayout._size.b).toBe(113); return Plotly.relayout(gd, 'legend.tracegroupgap', 70); }) - .then(function() { + .then(function () { expect(gd._fullLayout._size.b).toBe(167); return Plotly.relayout(gd, 'legend.tracegroupgap', 10); }) - .then(function() { + .then(function () { expect(gd._fullLayout._size.b).toBe(113); }) .then(done, done.fail); }); }); - it('should make legend fit in graph viewport', function(done) { + it('should make legend fit in graph viewport', function (done) { var fig = Lib.extendDeep({}, require('../../image/mocks/legend_negative_x.json')); function _assert(msg, xy, wh) { - return function() { + return function () { var fullLayout = gd._fullLayout; var legend3 = d3Select('g.legend'); var bg3 = legend3.select('rect.bg'); @@ -1239,12 +1392,14 @@ describe('legend relayout update', function() { Plotly.newPlot(gd, fig) .then(_assert('base', [5, 4.4], [512, 29])) - .then(function() { return Plotly.relayout(gd, 'legend.x', 0.8); }) + .then(function () { + return Plotly.relayout(gd, 'legend.x', 0.8); + }) .then(_assert('after relayout almost to right edge', [188, 4.4], [512, 29])) .then(done, done.fail); }); - it('should fit in graph viewport when changing legend.title.side', function(done) { + it('should fit in graph viewport when changing legend.title.side', function (done) { var fig = Lib.extendDeep({}, require('../../image/mocks/0.json')); fig.layout.legend = { title: { @@ -1253,7 +1408,7 @@ describe('legend relayout update', function() { }; function _assert(msg, xy, wh) { - return function() { + return function () { var fullLayout = gd._fullLayout; var legend3 = d3Select('g.legend'); var bg3 = legend3.select('rect.bg'); @@ -1272,19 +1427,25 @@ describe('legend relayout update', function() { Plotly.newPlot(gd, fig) .then(_assert('base', [667.72, 60], [120, 83])) - .then(function() { return Plotly.relayout(gd, 'legend.title.side', 'left'); }) + .then(function () { + return Plotly.relayout(gd, 'legend.title.side', 'left'); + }) .then(_assert('after relayout to *left*', [607.54, 60], [180, 67])) - .then(function() { return Plotly.relayout(gd, 'legend.title.side', 'top'); }) + .then(function () { + return Plotly.relayout(gd, 'legend.title.side', 'top'); + }) .then(_assert('after relayout to *top*', [667.72, 60], [120, 83])) .then(done, done.fail); }); - it('should be able to clear legend title using react', function(done) { - var data = [{ - type: 'scatter', - x: [0, 1], - y: [1, 0] - }]; + it('should be able to clear legend title using react', function (done) { + var data = [ + { + type: 'scatter', + x: [0, 1], + y: [1, 0] + } + ]; Plotly.newPlot(gd, { data: data, @@ -1297,10 +1458,10 @@ describe('legend relayout update', function() { } } }) - .then(function() { + .then(function () { expect(d3SelectAll('.legendtitletext')[0].length).toBe(1); }) - .then(function() { + .then(function () { return Plotly.react(gd, { data: data, layout: { @@ -1308,13 +1469,13 @@ describe('legend relayout update', function() { } }); }) - .then(function() { + .then(function () { expect(d3SelectAll('.legendtitletext')[0].length).toBe(0); }) .then(done, done.fail); }); - it('should clear an empty legend & add legend using react', function(done) { + it('should clear an empty legend & add legend using react', function (done) { var fig1 = { data: [{ y: [1, 2] }], layout: { showlegend: true } @@ -1326,92 +1487,100 @@ describe('legend relayout update', function() { }; Plotly.newPlot(gd, fig1) - .then(function() { + .then(function () { expect(d3SelectAll('.legend')[0].length).toBe(1); }) - .then(function() { + .then(function () { return Plotly.react(gd, fig2); }) - .then(function() { + .then(function () { expect(d3SelectAll('.legend')[0].length).toBe(0); }) - .then(function() { + .then(function () { return Plotly.react(gd, fig1); }) - .then(function() { + .then(function () { expect(d3SelectAll('.legend')[0].length).toBe(1); }) .then(done, done.fail); }); - it('should be able to add & clear multiple legends using react', function(done) { + it('should be able to add & clear multiple legends using react', function (done) { var fig1 = { - data: [{ - y: [1, 2, 3] - }] + data: [ + { + y: [1, 2, 3] + } + ] }; var fig2 = { - data: [{ - y: [1, 2, 3] - }, { - y: [3, 1, 2], - legend: 'legend2' - }], + data: [ + { + y: [1, 2, 3] + }, + { + y: [3, 1, 2], + legend: 'legend2' + } + ], layout: { legend2: { y: 0.5 } } }; Plotly.newPlot(gd, fig1) - .then(function() { + .then(function () { expect(d3SelectAll('.legend2')[0].length).toBe(0); }) - .then(function() { + .then(function () { return Plotly.react(gd, fig2); }) - .then(function() { + .then(function () { expect(d3SelectAll('.legend2')[0].length).toBe(1); }) - .then(function() { + .then(function () { return Plotly.react(gd, fig1); }) - .then(function() { + .then(function () { expect(d3SelectAll('.legend2')[0].length).toBe(0); }) .then(done, done.fail); }); }); -describe('legend orientation change:', function() { +describe('legend orientation change:', function () { 'use strict'; afterEach(destroyGraphDiv); - it('should update plot background', function(done) { + it('should update plot background', function (done) { var mock = require('../../image/mocks/legend_horizontal_autowrap.json'); var gd = createGraphDiv(); var initialLegendBGColor; - Plotly.newPlot(gd, mock.data, mock.layout).then(function() { - initialLegendBGColor = gd._fullLayout.legend.bgcolor; - return Plotly.relayout(gd, 'legend.bgcolor', '#000000'); - }).then(function() { - expect(gd._fullLayout.legend.bgcolor).toBe('#000000'); - return Plotly.relayout(gd, 'legend.bgcolor', initialLegendBGColor); - }).then(function() { - expect(gd._fullLayout.legend.bgcolor).toBe(initialLegendBGColor); - }) + Plotly.newPlot(gd, mock.data, mock.layout) + .then(function () { + initialLegendBGColor = gd._fullLayout.legend.bgcolor; + return Plotly.relayout(gd, 'legend.bgcolor', '#000000'); + }) + .then(function () { + expect(gd._fullLayout.legend.bgcolor).toBe('#000000'); + return Plotly.relayout(gd, 'legend.bgcolor', initialLegendBGColor); + }) + .then(function () { + expect(gd._fullLayout.legend.bgcolor).toBe(initialLegendBGColor); + }) .then(done, done.fail); }); }); -describe('legend restyle update', function() { +describe('legend restyle update', function () { 'use strict'; afterEach(destroyGraphDiv); - it('should update trace toggle background rectangle', function(done) { + it('should update trace toggle background rectangle', function (done) { var mock = require('../../image/mocks/0.json'); var mockCopy = Lib.extendDeep({}, mock); var gd = createGraphDiv(); @@ -1428,7 +1597,7 @@ describe('legend restyle update', function() { function assertTraceToggleRect() { var nodes = d3SelectAll('rect.legendtoggle'); - nodes.each(function() { + nodes.each(function () { var node = d3Select(this); expect(node.attr('x')).toEqual('0'); @@ -1440,36 +1609,39 @@ describe('legend restyle update', function() { }); } - Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(countLegendItems()).toEqual(1); - assertTraceToggleRect(); + Plotly.newPlot(gd, mockCopy.data, mockCopy.layout) + .then(function () { + expect(countLegendItems()).toEqual(1); + assertTraceToggleRect(); - return Plotly.restyle(gd, 'visible', [true, false, false]); - }).then(function() { - expect(countLegendItems()).toEqual(0); + return Plotly.restyle(gd, 'visible', [true, false, false]); + }) + .then(function () { + expect(countLegendItems()).toEqual(0); - return Plotly.restyle(gd, 'showlegend', [true, false, false]); - }).then(function() { - expect(countLegendItems()).toEqual(1); - assertTraceToggleRect(); - }) + return Plotly.restyle(gd, 'showlegend', [true, false, false]); + }) + .then(function () { + expect(countLegendItems()).toEqual(1); + assertTraceToggleRect(); + }) .then(done, done.fail); }); }); -describe('legend interaction', function() { +describe('legend interaction', function () { 'use strict'; - describe('pie chart', function() { + describe('pie chart', function () { var mockCopy, gd, legendItems, legendItem, legendLabels, legendLabel; var testEntry = 2; - beforeAll(function(done) { + beforeAll(function (done) { var mock = require('../../image/mocks/pie_simple.json'); mockCopy = Lib.extendDeep({}, mock); gd = createGraphDiv(); - Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(function() { + Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(function () { legendItems = d3SelectAll('rect.legendtoggle')[0]; legendLabels = d3SelectAll('text.legendtext')[0]; legendItem = legendItems[testEntry]; @@ -1480,53 +1652,53 @@ describe('legend interaction', function() { afterAll(destroyGraphDiv); - describe('single click', function() { - it('should hide slice', function(done) { + describe('single click', function () { + it('should hide slice', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { + setTimeout(function () { expect(gd._fullLayout.hiddenlabels.length).toBe(1); expect(gd._fullLayout.hiddenlabels[0]).toBe(legendLabel); done(); }, DBLCLICKDELAY + 20); }); - it('should fade legend item', function() { + it('should fade legend item', function () { expect(+legendItem.parentNode.style.opacity).toBeLessThan(1); }); - it('should unhide slice', function(done) { + it('should unhide slice', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { + setTimeout(function () { expect(gd._fullLayout.hiddenlabels.length).toBe(0); done(); }, DBLCLICKDELAY + 20); }); - it('should unfade legend item', function() { + it('should unfade legend item', function () { expect(+legendItem.parentNode.style.opacity).toBe(1); }); }); - describe('double click', function() { - it('should hide other slices', function(done) { + describe('double click', function () { + it('should hide other slices', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { - expect(gd._fullLayout.hiddenlabels.length).toBe((legendItems.length - 1)); + setTimeout(function () { + expect(gd._fullLayout.hiddenlabels.length).toBe(legendItems.length - 1); expect(gd._fullLayout.hiddenlabels.indexOf(legendLabel)).toBe(-1); done(); }, 20); }); - it('should fade other legend items', function() { + it('should fade other legend items', function () { var legendItemi; - for(var i = 0; i < legendItems.length; i++) { + for (var i = 0; i < legendItems.length; i++) { legendItemi = legendItems[i]; - if(i === testEntry) { + if (i === testEntry) { expect(+legendItemi.parentNode.style.opacity).toBe(1); } else { expect(+legendItemi.parentNode.style.opacity).toBeLessThan(1); @@ -1534,20 +1706,20 @@ describe('legend interaction', function() { } }); - it('should unhide all slices', function(done) { + it('should unhide all slices', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { + setTimeout(function () { expect(gd._fullLayout.hiddenlabels.length).toBe(0); done(); }, 20); }); - it('should unfade legend items', function() { + it('should unfade legend items', function () { var legendItemi; - for(var i = 0; i < legendItems.length; i++) { + for (var i = 0; i < legendItems.length; i++) { legendItemi = legendItems[i]; expect(+legendItemi.parentNode.style.opacity).toBe(1); } @@ -1555,16 +1727,16 @@ describe('legend interaction', function() { }); }); - describe('non-pie chart', function() { + describe('non-pie chart', function () { var mockCopy, gd, legendItems, legendItem; var testEntry = 2; - beforeAll(function(done) { + beforeAll(function (done) { var mock = require('../../image/mocks/29.json'); mockCopy = Lib.extendDeep({}, mock); gd = createGraphDiv(); - Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(function() { + Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(function () { legendItems = d3SelectAll('rect.legendtoggle')[0]; legendItem = legendItems[testEntry]; done(); @@ -1573,43 +1745,43 @@ describe('legend interaction', function() { afterAll(destroyGraphDiv); - describe('single click', function() { - it('should hide series', function(done) { + describe('single click', function () { + it('should hide series', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { + setTimeout(function () { expect(gd.data[2].visible).toBe('legendonly'); done(); }, DBLCLICKDELAY + 20); }); - it('should fade legend item', function() { + it('should fade legend item', function () { expect(+legendItem.parentNode.style.opacity).toBeLessThan(1); }); - it('should unhide series', function(done) { + it('should unhide series', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { + setTimeout(function () { expect(gd.data[2].visible).toBe(true); done(); }, DBLCLICKDELAY + 20); }); - it('should unfade legend item', function() { + it('should unfade legend item', function () { expect(+legendItem.parentNode.style.opacity).toBe(1); }); }); - describe('double click', function() { - it('should hide series', function(done) { + describe('double click', function () { + it('should hide series', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { - for(var i = 0; i < legendItems.length; i++) { - if(i === testEntry) { + setTimeout(function () { + for (var i = 0; i < legendItems.length; i++) { + if (i === testEntry) { expect(gd.data[i].visible).toBe(true); } else { expect(gd.data[i].visible).toBe('legendonly'); @@ -1619,11 +1791,11 @@ describe('legend interaction', function() { }, 20); }); - it('should fade legend item', function() { + it('should fade legend item', function () { var legendItemi; - for(var i = 0; i < legendItems.length; i++) { + for (var i = 0; i < legendItems.length; i++) { legendItemi = legendItems[i]; - if(i === testEntry) { + if (i === testEntry) { expect(+legendItemi.parentNode.style.opacity).toBe(1); } else { expect(+legendItemi.parentNode.style.opacity).toBeLessThan(1); @@ -1631,22 +1803,22 @@ describe('legend interaction', function() { } }); - it('should unhide series', function(done) { + it('should unhide series', function (done) { legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); legendItem.dispatchEvent(new MouseEvent('mousedown')); legendItem.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(function() { - for(var i = 0; i < legendItems.length; i++) { + setTimeout(function () { + for (var i = 0; i < legendItems.length; i++) { expect(gd.data[i].visible).toBe(true); } done(); }, 20); }); - it('should unfade legend items', function() { + it('should unfade legend items', function () { var legendItemi; - for(var i = 0; i < legendItems.length; i++) { + for (var i = 0; i < legendItems.length; i++) { legendItemi = legendItems[i]; expect(+legendItemi.parentNode.style.opacity).toBe(1); } @@ -1654,13 +1826,13 @@ describe('legend interaction', function() { }); }); - describe('carpet plots', function() { + describe('carpet plots', function () { afterAll(destroyGraphDiv); function _click(index) { - return function() { + return function () { var item = d3SelectAll('rect.legendtoggle')[0][index || 0]; - return new Promise(function(resolve) { + return new Promise(function (resolve) { item.dispatchEvent(new MouseEvent('mousedown')); item.dispatchEvent(new MouseEvent('mouseup')); setTimeout(resolve, DBLCLICKDELAY + 20); @@ -1669,9 +1841,9 @@ describe('legend interaction', function() { } function _dblclick(index) { - return function() { + return function () { var item = d3SelectAll('rect.legendtoggle')[0][index || 0]; - return new Promise(function(resolve) { + return new Promise(function (resolve) { item.dispatchEvent(new MouseEvent('mousedown')); item.dispatchEvent(new MouseEvent('mouseup')); item.dispatchEvent(new MouseEvent('mousedown')); @@ -1682,55 +1854,94 @@ describe('legend interaction', function() { } function assertVisible(gd, expectation) { - var actual = gd._fullData.map(function(trace) { return trace.visible; }); + var actual = gd._fullData.map(function (trace) { + return trace.visible; + }); expect(actual).toEqual(expectation); } - it('should ignore carpet traces when toggling', function(done) { + it('should ignore carpet traces when toggling', function (done) { var _mock = Lib.extendDeep({}, require('../../image/mocks/cheater.json')); var gd = createGraphDiv(); - Plotly.newPlot(gd, _mock).then(function() { - assertVisible(gd, [true, true, true, true]); - }) + Plotly.newPlot(gd, _mock) + .then(function () { + assertVisible(gd, [true, true, true, true]); + }) .then(_click(0)) - .then(function() { + .then(function () { assertVisible(gd, [true, 'legendonly', true, true]); }) .then(_click(0)) - .then(function() { + .then(function () { assertVisible(gd, [true, true, true, true]); }) .then(_dblclick(0)) - .then(function() { + .then(function () { assertVisible(gd, [true, true, 'legendonly', 'legendonly']); }) .then(_dblclick(0)) - .then(function() { + .then(function () { assertVisible(gd, [true, true, true, true]); }) .then(done, done.fail); }); }); - describe('editable mode interactions for shape legends', function() { + describe('editable mode interactions for shape legends', function () { var gd; var mock = { data: [], layout: { shapes: [ - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.1, y0: 0.2, x1: 0.2, y1: 0.1 }, - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.3, y0: 0.4, x1: 0.4, y1: 0.3 }, - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.5, y0: 0.6, x1: 0.6, y1: 0.5 }, - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.7, y0: 0.8, x1: 0.8, y1: 0.7 }, + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.1, + y0: 0.2, + x1: 0.2, + y1: 0.1 + }, + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.3, + y0: 0.4, + x1: 0.4, + y1: 0.3 + }, + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.5, + y0: 0.6, + x1: 0.6, + y1: 0.5 + }, + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.7, + y0: 0.8, + x1: 0.8, + y1: 0.7 + }, { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.9, y0: 1.0, x1: 1.0, y1: 0.9 } ] }, config: { editable: true } }; - beforeEach(function(done) { + beforeEach(function (done) { gd = createGraphDiv(); Plotly.newPlot(gd, Lib.extendDeep({}, mock)).then(done); }); @@ -1740,44 +1951,49 @@ describe('legend interaction', function() { function _setValue(index, str) { var item = d3SelectAll('text.legendtext')[0][index || 0]; item.dispatchEvent(new MouseEvent('click')); - return delay(20)().then(function() { - var input = d3Select('.plugin-editable.editable'); - input.text(str); - input.node().dispatchEvent(new KeyboardEvent('blur')); - }).then(delay(20)); + return delay(20)() + .then(function () { + var input = d3Select('.plugin-editable.editable'); + input.text(str); + input.node().dispatchEvent(new KeyboardEvent('blur')); + }) + .then(delay(20)); } function assertLabels(expected) { var labels = []; - d3SelectAll('text.legendtext').each(function() { + d3SelectAll('text.legendtext').each(function () { labels.push(this.textContent); }); expect(labels).toEqual(expected); } - it('sets and unsets shape group names', function(done) { + it('sets and unsets shape group names', function (done) { assertLabels(['shape 0', 'shape 1', 'shape 2', 'shape 3', 'shape 4']); // Set the name of the first shape: - _setValue(0, 'foo').then(function() { - expect(gd.layout.shapes[0].name).toEqual('foo'); - // labels shorter than half the longest get padded with spaces to match the longest length - assertLabels(['foo ', 'shape 1', 'shape 2', 'shape 3', 'shape 4']); - - // Set the name of the third legend item: - return _setValue(3, 'barbar'); - }).then(function() { - expect(gd.layout.shapes[3].name).toEqual('barbar'); - assertLabels(['foo ', 'shape 1', 'shape 2', 'barbar', 'shape 4']); - - return _setValue(2, 'asdf'); - }).then(done, done.fail); + _setValue(0, 'foo') + .then(function () { + expect(gd.layout.shapes[0].name).toEqual('foo'); + // labels shorter than half the longest get padded with spaces to match the longest length + assertLabels(['foo ', 'shape 1', 'shape 2', 'shape 3', 'shape 4']); + + // Set the name of the third legend item: + return _setValue(3, 'barbar'); + }) + .then(function () { + expect(gd.layout.shapes[3].name).toEqual('barbar'); + assertLabels(['foo ', 'shape 1', 'shape 2', 'barbar', 'shape 4']); + + return _setValue(2, 'asdf'); + }) + .then(done, done.fail); }); }); - describe('staticPlot', function() { + describe('staticPlot', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); @@ -1795,44 +2011,37 @@ describe('legend interaction', function() { } function assertToggled(toggled) { - return function() { + return function () { var container = d3Select('g.traces').node(); expect(container).not.toEqual(null); expect(container.style.opacity).toBe(toggled ? '0.5' : '1'); }; } - it('should prevent toggling if set', function(done) { + it('should prevent toggling if set', function (done) { var data = [{ x: [0, 1], y: [0, 1], type: 'scatter' }]; var layout = { showlegend: true }; var config = { staticPlot: true }; - Plotly.newPlot(gd, data, layout, config) - .then(toggleTrace) - .then(assertToggled(false)) - .then(done, done.fail); + Plotly.newPlot(gd, data, layout, config).then(toggleTrace).then(assertToggled(false)).then(done, done.fail); }); }); - describe('visible toggle', function() { + describe('visible toggle', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - var data = [ - { y: [1, 2, 1] }, - { y: [2, 1, 2] }, - { y: [2, 3, 4] } - ]; + var data = [{ y: [1, 2, 1] }, { y: [2, 1, 2] }, { y: [2, 3, 4] }]; // we need to click on the drag cover to truly test this, function clickAt(p) { - return function() { - return new Promise(function(resolve) { + return function () { + return new Promise(function (resolve) { var el = d3Select('g.legend').node(); var opts = { element: el }; mouseEvent('mousedown', p[0], p[1], opts); @@ -1843,35 +2052,59 @@ describe('legend interaction', function() { } function assertVisible(expectation) { - return function() { - var actual = gd._fullData.map(function(t) { return t.visible; }); + return function () { + var actual = gd._fullData.map(function (t) { + return t.visible; + }); expect(actual).toEqual(expectation); }; } - var specs = [{ - orientation: 'h', - edits: { legendPosition: true }, - clickPos: [[118, 469], [212, 469], [295, 469]] - }, { - orientation: 'h', - edits: { legendPosition: true, legendText: true }, - clickPos: [[118, 469], [212, 469], [295, 469]] - }, { - orientation: 'v', - edits: { legendPosition: true }, - clickPos: [[430, 114], [430, 131], [430, 153]] - }, { - orientation: 'v', - edits: { legendPosition: true, legendText: true }, - clickPos: [[430, 114], [430, 131], [430, 153]] - }]; - - specs.forEach(function(s) { + var specs = [ + { + orientation: 'h', + edits: { legendPosition: true }, + clickPos: [ + [118, 469], + [212, 469], + [295, 469] + ] + }, + { + orientation: 'h', + edits: { legendPosition: true, legendText: true }, + clickPos: [ + [118, 469], + [212, 469], + [295, 469] + ] + }, + { + orientation: 'v', + edits: { legendPosition: true }, + clickPos: [ + [430, 114], + [430, 131], + [430, 153] + ] + }, + { + orientation: 'v', + edits: { legendPosition: true, legendText: true }, + clickPos: [ + [430, 114], + [430, 131], + [430, 153] + ] + } + ]; + + specs.forEach(function (s) { var msg = s.orientation + ' - ' + JSON.stringify(s.edits); - it('should find correct bounding box (case ' + msg + ')', function(done) { - Plotly.newPlot(gd, + it('should find correct bounding box (case ' + msg + ')', function (done) { + Plotly.newPlot( + gd, Lib.extendDeep([], data), { legend: { orientation: s.orientation }, width: 500, height: 500 }, { edits: s.edits } @@ -1888,20 +2121,20 @@ describe('legend interaction', function() { }); }); - describe('legend visibility interactions', function() { + describe('legend visibility interactions', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); function click(index, clicks) { - return function() { - return new Promise(function(resolve) { + return function () { + return new Promise(function (resolve) { var item = d3SelectAll('rect.legendtoggle')[0][index || 0]; - for(var i = 0; i < (clicks || 1); i++) { + for (var i = 0; i < (clicks || 1); i++) { item.dispatchEvent(new MouseEvent('mousedown')); item.dispatchEvent(new MouseEvent('mouseup')); } @@ -1911,25 +2144,27 @@ describe('legend interaction', function() { } function extractVisibilities(data) { - return data.map(function(trace) { return trace.visible; }); + return data.map(function (trace) { + return trace.visible; + }); } function assertVisible(expectation) { - return function() { + return function () { var actual = extractVisibilities(gd._fullData); expect(actual).toEqual(expectation); }; } function assertVisibleShapes(expectation) { - return function() { + return function () { var actual = extractVisibilities(gd._fullLayout.shapes); expect(actual).toEqual(expectation); }; } - describe('for regular traces', function() { - beforeEach(function(done) { + describe('for regular traces', function () { + beforeEach(function (done) { Plotly.newPlot(gd, [ { x: [1, 2], y: [0, 1], visible: false }, { x: [1, 2], y: [1, 2], visible: 'legendonly' }, @@ -1937,7 +2172,7 @@ describe('legend interaction', function() { ]).then(done); }); - it('clicking once toggles legendonly -> true', function(done) { + it('clicking once toggles legendonly -> true', function (done) { Promise.resolve() .then(assertVisible([false, 'legendonly', true])) .then(click(0)) @@ -1945,7 +2180,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('clicking once toggles true -> legendonly', function(done) { + it('clicking once toggles true -> legendonly', function (done) { Promise.resolve() .then(assertVisible([false, 'legendonly', true])) .then(click(1)) @@ -1953,7 +2188,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('double-clicking isolates a visible trace ', function(done) { + it('double-clicking isolates a visible trace ', function (done) { Promise.resolve() .then(click(0)) .then(assertVisible([false, true, true])) @@ -1962,7 +2197,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('double-clicking an isolated trace shows all non-hidden traces', function(done) { + it('double-clicking an isolated trace shows all non-hidden traces', function (done) { Promise.resolve() .then(click(0, 2)) .then(assertVisible([false, true, true])) @@ -1970,30 +2205,34 @@ describe('legend interaction', function() { }); }); - describe('for regular traces in different legends', function() { - beforeEach(function(done) { - Plotly.newPlot(gd, [ - { x: [1, 2], y: [0, 1], visible: false }, - { x: [1, 2], y: [1, 2], visible: 'legendonly' }, - { x: [1, 2], y: [2, 3] }, - { x: [1, 2], y: [0, 1], yaxis: 'y2', legend: 'legend2', visible: false }, - { x: [1, 2], y: [1, 2], yaxis: 'y2', legend: 'legend2', visible: 'legendonly' }, - { x: [1, 2], y: [2, 3], yaxis: 'y2', legend: 'legend2' } - ], { - yaxis: { - domain: [0.55, 1] - }, - yaxis2: { - anchor: 'x', - domain: [0, 0.45] - }, - legend2: { - y: 0.5 + describe('for regular traces in different legends', function () { + beforeEach(function (done) { + Plotly.newPlot( + gd, + [ + { x: [1, 2], y: [0, 1], visible: false }, + { x: [1, 2], y: [1, 2], visible: 'legendonly' }, + { x: [1, 2], y: [2, 3] }, + { x: [1, 2], y: [0, 1], yaxis: 'y2', legend: 'legend2', visible: false }, + { x: [1, 2], y: [1, 2], yaxis: 'y2', legend: 'legend2', visible: 'legendonly' }, + { x: [1, 2], y: [2, 3], yaxis: 'y2', legend: 'legend2' } + ], + { + yaxis: { + domain: [0.55, 1] + }, + yaxis2: { + anchor: 'x', + domain: [0, 0.45] + }, + legend2: { + y: 0.5 + } } - }).then(done); + ).then(done); }); - it('clicking once toggles legendonly -> true', function(done) { + it('clicking once toggles legendonly -> true', function (done) { Promise.resolve() .then(assertVisible([false, 'legendonly', true, false, 'legendonly', true])) .then(click(0)) @@ -2001,7 +2240,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('clicking once toggles true -> legendonly', function(done) { + it('clicking once toggles true -> legendonly', function (done) { Promise.resolve() .then(assertVisible([false, 'legendonly', true, false, 'legendonly', true])) .then(click(1)) @@ -2009,7 +2248,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('double-clicking isolates a visible trace ', function(done) { + it('double-clicking isolates a visible trace ', function (done) { Promise.resolve() .then(click(0)) .then(assertVisible([false, true, true, false, 'legendonly', true])) @@ -2018,7 +2257,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('double-clicking an isolated trace shows all non-hidden traces', function(done) { + it('double-clicking an isolated trace shows all non-hidden traces', function (done) { Promise.resolve() .then(click(0, 2)) .then(assertVisible([false, true, true, false, 'legendonly', true])) @@ -2034,18 +2273,47 @@ describe('legend interaction', function() { }); }); - describe('click shape legends', function() { - beforeEach(function(done) { + describe('click shape legends', function () { + beforeEach(function (done) { Plotly.newPlot(gd, [], { shapes: [ - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.1, y0: 0.2, x1: 0.2, y1: 0.1, visible: false }, - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.3, y0: 0.4, x1: 0.4, y1: 0.3, visible: 'legendonly' }, - { showlegend: true, type: 'line', xref: 'paper', yref: 'paper', x0: 0.5, y0: 0.6, x1: 0.6, y1: 0.5 } + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.1, + y0: 0.2, + x1: 0.2, + y1: 0.1, + visible: false + }, + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.3, + y0: 0.4, + x1: 0.4, + y1: 0.3, + visible: 'legendonly' + }, + { + showlegend: true, + type: 'line', + xref: 'paper', + yref: 'paper', + x0: 0.5, + y0: 0.6, + x1: 0.6, + y1: 0.5 + } ] }).then(done); }); - it('clicking once toggles legendonly -> true', function(done) { + it('clicking once toggles legendonly -> true', function (done) { Promise.resolve() .then(assertVisibleShapes([false, 'legendonly', true])) .then(click(0)) @@ -2053,7 +2321,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('clicking once toggles true -> legendonly', function(done) { + it('clicking once toggles true -> legendonly', function (done) { Promise.resolve() .then(assertVisibleShapes([false, 'legendonly', true])) .then(click(1)) @@ -2061,7 +2329,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('double-clicking isolates a visible shape', function(done) { + it('double-clicking isolates a visible shape', function (done) { Promise.resolve() .then(click(0)) .then(assertVisibleShapes([false, true, true])) @@ -2070,7 +2338,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('double-clicking an isolated trace shows all non-hidden shapes', function(done) { + it('double-clicking an isolated trace shows all non-hidden shapes', function (done) { Promise.resolve() .then(click(0, 2)) .then(assertVisibleShapes([false, true, true])) @@ -2078,27 +2346,32 @@ describe('legend interaction', function() { }); }); - describe('legendgroup visibility', function() { - beforeEach(function(done) { - Plotly.newPlot(gd, [{ - x: [1, 2], - y: [3, 4], - visible: false - }, { - x: [1, 2, 3, 4], - y: [0, 1, 2, 3], - legendgroup: 'foo' - }, { - x: [1, 2, 3, 4], - y: [1, 3, 2, 4], - }, { - x: [1, 2, 3, 4], - y: [1, 3, 2, 4], - legendgroup: 'foo' - }]).then(done); + describe('legendgroup visibility', function () { + beforeEach(function (done) { + Plotly.newPlot(gd, [ + { + x: [1, 2], + y: [3, 4], + visible: false + }, + { + x: [1, 2, 3, 4], + y: [0, 1, 2, 3], + legendgroup: 'foo' + }, + { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4] + }, + { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + legendgroup: 'foo' + } + ]).then(done); }); - it('toggles the visibility of legendgroups as a whole', function(done) { + it('toggles the visibility of legendgroups as a whole', function (done) { Promise.resolve() .then(click(1)) .then(assertVisible([false, 'legendonly', true, 'legendonly'])) @@ -2107,7 +2380,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('isolates legendgroups as a whole', function(done) { + it('isolates legendgroups as a whole', function (done) { Promise.resolve() .then(click(1, 2)) .then(assertVisible([false, true, 'legendonly', true])) @@ -2117,31 +2390,40 @@ describe('legend interaction', function() { }); }); - describe('legendgroup visibility case of groupclick: "toggleitem"', function() { - beforeEach(function(done) { - Plotly.newPlot(gd, [{ - x: [1, 2], - y: [3, 4], - visible: false - }, { - x: [1, 2, 3, 4], - y: [0, 1, 2, 3], - legendgroup: 'foo' - }, { - x: [1, 2, 3, 4], - y: [1, 3, 2, 4], - }, { - x: [1, 2, 3, 4], - y: [1, 3, 2, 4], - legendgroup: 'foo' - }], { - legend: { - groupclick: 'toggleitem' + describe('legendgroup visibility case of groupclick: "toggleitem"', function () { + beforeEach(function (done) { + Plotly.newPlot( + gd, + [ + { + x: [1, 2], + y: [3, 4], + visible: false + }, + { + x: [1, 2, 3, 4], + y: [0, 1, 2, 3], + legendgroup: 'foo' + }, + { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4] + }, + { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + legendgroup: 'foo' + } + ], + { + legend: { + groupclick: 'toggleitem' + } } - }).then(done); + ).then(done); }); - it('toggles visibilities', function(done) { + it('toggles visibilities', function (done) { Promise.resolve() .then(assertVisible([false, true, true, true])) .then(click(0)) @@ -2160,17 +2442,23 @@ describe('legend interaction', function() { }); }); - describe('legend visibility with *showlegend:false* traces', function() { - beforeEach(function(done) { + describe('legend visibility with *showlegend:false* traces', function () { + beforeEach(function (done) { Plotly.newPlot(gd, [ { y: [1, 2, 3] }, { y: [2, 3, 1] }, - { type: 'heatmap', z: [[1, 2], [3, 4]], showscale: false } - ]) - .then(done); + { + type: 'heatmap', + z: [ + [1, 2], + [3, 4] + ], + showscale: false + } + ]).then(done); }); - it('isolate trace in legend, ignore trace that is not in legend', function(done) { + it('isolate trace in legend, ignore trace that is not in legend', function (done) { Promise.resolve() .then(click(0, 2)) .then(assertVisible([true, 'legendonly', true])) @@ -2179,7 +2467,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('isolate trace in legend, ignore trace that is not in legend (2)', function(done) { + it('isolate trace in legend, ignore trace that is not in legend (2)', function (done) { Promise.resolve() .then(click(1, 2)) .then(assertVisible(['legendonly', true, true])) @@ -2188,7 +2476,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('isolate trace in legend AND trace in associated legendgroup', function(done) { + it('isolate trace in legend AND trace in associated legendgroup', function (done) { Plotly.restyle(gd, 'legendgroup', ['group', '', 'group']) .then(click(0, 2)) .then(assertVisible([true, 'legendonly', true])) @@ -2197,7 +2485,7 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('isolate trace in legend, hide trace not in legend that has set legendgroup', function(done) { + it('isolate trace in legend, hide trace not in legend that has set legendgroup', function (done) { Plotly.restyle(gd, 'legendgroup', ['group', '', 'group']) .then(click(1, 2)) .then(assertVisible(['legendonly', true, 'legendonly'])) @@ -2207,116 +2495,119 @@ describe('legend interaction', function() { }); }); - describe('custom legend click/doubleclick handlers', function() { + describe('custom legend click/doubleclick handlers', function () { var fig, to; - beforeEach(function() { + beforeEach(function () { fig = Lib.extendDeep({}, require('../../image/mocks/0.json')); }); - afterEach(function() { + afterEach(function () { clearTimeout(to); }); function setupFail() { - to = setTimeout(function() { + to = setTimeout(function () { fail('did not trigger plotly_legendclick'); }, 2 * DBLCLICKDELAY); } - it('should call custom click handler before default handler', function(done) { - Plotly.newPlot(gd, fig).then(function() { - var gotCalled = false; + it('should call custom click handler before default handler', function (done) { + Plotly.newPlot(gd, fig) + .then(function () { + var gotCalled = false; - gd.on('plotly_legendclick', function(d) { - gotCalled = true; - expect(extractVisibilities(d.fullData)).toEqual([true, true, true]); - expect(extractVisibilities(gd._fullData)).toEqual([true, true, true]); - }); - gd.on('plotly_restyle', function() { - expect(extractVisibilities(gd._fullData)).toEqual([true, 'legendonly', true]); - if(gotCalled) done(); - }); - setupFail(); - }) + gd.on('plotly_legendclick', function (d) { + gotCalled = true; + expect(extractVisibilities(d.fullData)).toEqual([true, true, true]); + expect(extractVisibilities(gd._fullData)).toEqual([true, true, true]); + }); + gd.on('plotly_restyle', function () { + expect(extractVisibilities(gd._fullData)).toEqual([true, 'legendonly', true]); + if (gotCalled) done(); + }); + setupFail(); + }) .then(click(1, 1)) .catch(failTest); }); - it('should call custom doubleclick handler before default handler', function(done) { - Plotly.newPlot(gd, fig).then(function() { - var gotCalled = false; + it('should call custom doubleclick handler before default handler', function (done) { + Plotly.newPlot(gd, fig) + .then(function () { + var gotCalled = false; - gd.on('plotly_legenddoubleclick', function(d) { - gotCalled = true; - expect(extractVisibilities(d.fullData)).toEqual([true, true, true]); - expect(extractVisibilities(gd._fullData)).toEqual([true, true, true]); - }); - gd.on('plotly_restyle', function() { - expect(extractVisibilities(gd._fullData)).toEqual(['legendonly', true, 'legendonly']); - if(gotCalled) done(); - }); - setupFail(); - }) + gd.on('plotly_legenddoubleclick', function (d) { + gotCalled = true; + expect(extractVisibilities(d.fullData)).toEqual([true, true, true]); + expect(extractVisibilities(gd._fullData)).toEqual([true, true, true]); + }); + gd.on('plotly_restyle', function () { + expect(extractVisibilities(gd._fullData)).toEqual(['legendonly', true, 'legendonly']); + if (gotCalled) done(); + }); + setupFail(); + }) .then(click(1, 2)) .catch(failTest); }); - it('should not call default click handler if custom handler return *false*', function(done) { - Plotly.newPlot(gd, fig).then(function() { - gd.on('plotly_legendclick', function(d) { - Plotly.relayout(gd, 'title', 'just clicked on trace #' + d.curveNumber); - return false; - }); - gd.on('plotly_relayout', function(d) { - expect(typeof d).toBe('object'); - expect(d.title).toBe('just clicked on trace #2'); - done(); - }); - gd.on('plotly_restyle', function() { - fail('should not have triggered plotly_restyle'); - }); - setupFail(); - }) + it('should not call default click handler if custom handler return *false*', function (done) { + Plotly.newPlot(gd, fig) + .then(function () { + gd.on('plotly_legendclick', function (d) { + Plotly.relayout(gd, 'title', 'just clicked on trace #' + d.curveNumber); + return false; + }); + gd.on('plotly_relayout', function (d) { + expect(typeof d).toBe('object'); + expect(d.title).toBe('just clicked on trace #2'); + done(); + }); + gd.on('plotly_restyle', function () { + fail('should not have triggered plotly_restyle'); + }); + setupFail(); + }) .then(click(2, 1)) .catch(failTest); }); - it('should not call default doubleclick handle if custom handler return *false*', function(done) { - Plotly.newPlot(gd, fig).then(function() { - gd.on('plotly_legenddoubleclick', function(d) { - Plotly.relayout(gd, 'title', 'just double clicked on trace #' + d.curveNumber); - return false; - }); - gd.on('plotly_relayout', function(d) { - expect(typeof d).toBe('object'); - expect(d.title).toBe('just double clicked on trace #0'); - done(); - }); - gd.on('plotly_restyle', function() { - fail('should not have triggered plotly_restyle'); - }); - setupFail(); - }) + it('should not call default doubleclick handle if custom handler return *false*', function (done) { + Plotly.newPlot(gd, fig) + .then(function () { + gd.on('plotly_legenddoubleclick', function (d) { + Plotly.relayout(gd, 'title', 'just double clicked on trace #' + d.curveNumber); + return false; + }); + gd.on('plotly_relayout', function (d) { + expect(typeof d).toBe('object'); + expect(d.title).toBe('just double clicked on trace #0'); + done(); + }); + gd.on('plotly_restyle', function () { + fail('should not have triggered plotly_restyle'); + }); + setupFail(); + }) .then(click(0, 2)) .catch(failTest); }); }); - describe('legend click/doubleclick event data', function() { + describe('legend click/doubleclick event data', function () { function _assert(act, exp) { - for(var k in exp) { - if(k === 'event' || k === 'node') { + for (var k in exp) { + if (k === 'event' || k === 'node') { expect(act[k]).toBeDefined(); - } else if(k === 'group') { + } else if (k === 'group') { expect(act[k]).toEqual(exp[k]); } else { expect(act[k]).toBe(exp[k], 'key ' + k); } } - expect(Object.keys(act).length) - .toBe(Object.keys(exp).length, '# of keys'); + expect(Object.keys(act).length).toBe(Object.keys(exp).length, '# of keys'); } function clickAndCheck(clickArg, exp) { @@ -2336,21 +2627,21 @@ describe('legend interaction', function() { 2: 'plotly_legenddoubleclick' }[clickArg[1]]; - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { var hasBeenCalled = false; - var to = setTimeout(function() { + var to = setTimeout(function () { reject('did not trigger ' + evtName); }, 2 * DBLCLICKDELAY); function done() { - if(hasBeenCalled) { + if (hasBeenCalled) { clearTimeout(to); resolve(); } } - gd.once(evtName, function(d) { + gd.once(evtName, function (d) { hasBeenCalled = true; _assert(d, exp); }); @@ -2362,20 +2653,26 @@ describe('legend interaction', function() { }); } - it('should have correct keys (base case)', function(done) { - Plotly.newPlot(gd, [{ - x: [1, 2, 3, 4, 5], - y: [1, 2, 1, 2, 3] - }], { - showlegend: true - }) - .then(function() { + it('should have correct keys (base case)', function (done) { + Plotly.newPlot( + gd, + [ + { + x: [1, 2, 3, 4, 5], + y: [1, 2, 1, 2, 3] + } + ], + { + showlegend: true + } + ) + .then(function () { return clickAndCheck([0, 1], { curveNumber: 0, expandedIndex: 0 }); }) - .then(function() { + .then(function () { return clickAndCheck([0, 2], { curveNumber: 0, expandedIndex: 0 @@ -2384,20 +2681,22 @@ describe('legend interaction', function() { .then(done, done.fail); }); - it('should have correct keys (pie case)', function(done) { - Plotly.newPlot(gd, [{ - type: 'pie', - labels: ['A', 'B', 'C', 'D'], - values: [1, 2, 1, 3] - }]) - .then(function() { + it('should have correct keys (pie case)', function (done) { + Plotly.newPlot(gd, [ + { + type: 'pie', + labels: ['A', 'B', 'C', 'D'], + values: [1, 2, 1, 3] + } + ]) + .then(function () { return clickAndCheck([0, 1], { curveNumber: 0, expandedIndex: 0, label: 'D' }); }) - .then(function() { + .then(function () { return clickAndCheck([2, 2], { curveNumber: 0, expandedIndex: 0, @@ -2408,159 +2707,196 @@ describe('legend interaction', function() { }); }); - describe('should honor *itemclick* and *itemdoubleclick* settings', function() { + describe('should honor *itemclick* and *itemdoubleclick* settings', function () { var _assert; function run() { return Promise.resolve() - .then(click(0, 1)).then(_assert(['legendonly', true, true])) - .then(click(0, 1)).then(_assert([true, true, true])) - .then(click(0, 2)).then(_assert([true, 'legendonly', 'legendonly'])) - .then(click(0, 2)).then(_assert([true, true, true])) - .then(function() { + .then(click(0, 1)) + .then(_assert(['legendonly', true, true])) + .then(click(0, 1)) + .then(_assert([true, true, true])) + .then(click(0, 2)) + .then(_assert([true, 'legendonly', 'legendonly'])) + .then(click(0, 2)) + .then(_assert([true, true, true])) + .then(function () { return Plotly.relayout(gd, { 'legend.itemclick': false, 'legend.itemdoubleclick': false }); }) .then(delay(100)) - .then(click(0, 1)).then(_assert([true, true, true])) - .then(click(0, 2)).then(_assert([true, true, true])) - .then(function() { + .then(click(0, 1)) + .then(_assert([true, true, true])) + .then(click(0, 2)) + .then(_assert([true, true, true])) + .then(function () { return Plotly.relayout(gd, { 'legend.itemclick': 'toggleothers', 'legend.itemdoubleclick': 'toggle' }); }) .then(delay(100)) - .then(click(0, 1)).then(_assert([true, 'legendonly', 'legendonly'])) - .then(click(0, 1)).then(_assert([true, true, true])) - .then(click(0, 2)).then(_assert(['legendonly', true, true])) - .then(click(0, 2)).then(_assert([true, true, true])); + .then(click(0, 1)) + .then(_assert([true, 'legendonly', 'legendonly'])) + .then(click(0, 1)) + .then(_assert([true, true, true])) + .then(click(0, 2)) + .then(_assert(['legendonly', true, true])) + .then(click(0, 2)) + .then(_assert([true, true, true])); } - it('- regular trace case', function(done) { - _assert = assertVisible; + it( + '- regular trace case', + function (done) { + _assert = assertVisible; - Plotly.newPlot(gd, [ - { y: [1, 2, 1] }, - { y: [2, 1, 2] }, - { y: [3, 5, 0] } - ]) - .then(run) - .then(done, done.fail); - }, 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL); - - it('- pie case', function(done) { - _assert = function(_exp) { - return function() { - var exp = []; - if(_exp[0] === 'legendonly') exp.push('C'); - if(_exp[1] === 'legendonly') exp.push('B'); - if(_exp[2] === 'legendonly') exp.push('A'); - expect(gd._fullLayout.hiddenlabels || []).toEqual(exp); + Plotly.newPlot(gd, [{ y: [1, 2, 1] }, { y: [2, 1, 2] }, { y: [3, 5, 0] }]) + .then(run) + .then(done, done.fail); + }, + 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL + ); + + it( + '- pie case', + function (done) { + _assert = function (_exp) { + return function () { + var exp = []; + if (_exp[0] === 'legendonly') exp.push('C'); + if (_exp[1] === 'legendonly') exp.push('B'); + if (_exp[2] === 'legendonly') exp.push('A'); + expect(gd._fullLayout.hiddenlabels || []).toEqual(exp); + }; }; - }; - Plotly.newPlot(gd, [{ - type: 'pie', - labels: ['A', 'B', 'C'], - values: [1, 2, 3] - }]) - .then(run) - .then(done, done.fail); - }, 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL); + Plotly.newPlot(gd, [ + { + type: 'pie', + labels: ['A', 'B', 'C'], + values: [1, 2, 3] + } + ]) + .then(run) + .then(done, done.fail); + }, + 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL + ); }); - describe('should honor *itemclick* and *itemdoubleclick* settings | case of pie in multiple legends', function() { + describe('should honor *itemclick* and *itemdoubleclick* settings | case of pie in multiple legends', function () { var _assert; function run() { return Promise.resolve() - .then(click(0, 1)).then(_assert(['legendonly', true, true, true, true, true])) - .then(click(0, 1)).then(_assert([true, true, true, true, true, true])) - .then(click(0, 2)).then(_assert([true, 'legendonly', 'legendonly', true, true, true])) - .then(click(0, 2)).then(_assert([true, true, true, true, true, true])) - .then(function() { + .then(click(0, 1)) + .then(_assert(['legendonly', true, true, true, true, true])) + .then(click(0, 1)) + .then(_assert([true, true, true, true, true, true])) + .then(click(0, 2)) + .then(_assert([true, 'legendonly', 'legendonly', true, true, true])) + .then(click(0, 2)) + .then(_assert([true, true, true, true, true, true])) + .then(function () { return Plotly.relayout(gd, { 'legend.itemclick': false, 'legend.itemdoubleclick': false }); }) .then(delay(100)) - .then(click(0, 1)).then(_assert([true, true, true, true, true, true])) - .then(click(0, 2)).then(_assert([true, true, true, true, true, true])) - .then(function() { + .then(click(0, 1)) + .then(_assert([true, true, true, true, true, true])) + .then(click(0, 2)) + .then(_assert([true, true, true, true, true, true])) + .then(function () { return Plotly.relayout(gd, { 'legend.itemclick': 'toggleothers', 'legend.itemdoubleclick': 'toggle' }); }) .then(delay(100)) - .then(click(0, 1)).then(_assert([true, 'legendonly', 'legendonly', true, true, true])) - .then(click(0, 1)).then(_assert([true, true, true, true, true, true])) - .then(click(0, 2)).then(_assert(['legendonly', true, true, true, true, true])) - .then(click(0, 2)).then(_assert([true, true, true, true, true, true])); + .then(click(0, 1)) + .then(_assert([true, 'legendonly', 'legendonly', true, true, true])) + .then(click(0, 1)) + .then(_assert([true, true, true, true, true, true])) + .then(click(0, 2)) + .then(_assert(['legendonly', true, true, true, true, true])) + .then(click(0, 2)) + .then(_assert([true, true, true, true, true, true])); } - _assert = function(_exp) { - return function() { + _assert = function (_exp) { + return function () { var exp = []; - if(_exp[0] === 'legendonly') exp.push('F'); - if(_exp[1] === 'legendonly') exp.push('E'); - if(_exp[2] === 'legendonly') exp.push('D'); - if(_exp[3] === 'legendonly') exp.push('C'); - if(_exp[4] === 'legendonly') exp.push('B'); - if(_exp[5] === 'legendonly') exp.push('A'); + if (_exp[0] === 'legendonly') exp.push('F'); + if (_exp[1] === 'legendonly') exp.push('E'); + if (_exp[2] === 'legendonly') exp.push('D'); + if (_exp[3] === 'legendonly') exp.push('C'); + if (_exp[4] === 'legendonly') exp.push('B'); + if (_exp[5] === 'legendonly') exp.push('A'); expect(gd._fullLayout.hiddenlabels || []).toEqual(exp); }; }; - it('- pie case | multiple legends', function(done) { - Plotly.newPlot(gd, [{ - legend: 'legend2', - type: 'pie', - labels: ['A', 'B', 'C'], - values: [1, 2, 3], - domain: { - y: [0, 0.45] - } - }, { - type: 'pie', - labels: ['D', 'E', 'F'], - values: [1, 2, 3], - domain: { - y: [0.55, 1] - } - }], { - legend2: { - y: 0.35 - }, - width: 500, - height: 500 - }) - .then(run) - .then(done, done.fail); - }, 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL); + it( + '- pie case | multiple legends', + function (done) { + Plotly.newPlot( + gd, + [ + { + legend: 'legend2', + type: 'pie', + labels: ['A', 'B', 'C'], + values: [1, 2, 3], + domain: { + y: [0, 0.45] + } + }, + { + type: 'pie', + labels: ['D', 'E', 'F'], + values: [1, 2, 3], + domain: { + y: [0.55, 1] + } + } + ], + { + legend2: { + y: 0.35 + }, + width: 500, + height: 500 + } + ) + .then(run) + .then(done, done.fail); + }, + 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL + ); }); }); }); -describe('legend DOM', function() { +describe('legend DOM', function () { 'use strict'; afterEach(destroyGraphDiv); - it('draws `legendtoggle` last to make sure it is unobstructed', function(done) { + it('draws `legendtoggle` last to make sure it is unobstructed', function (done) { var gd = createGraphDiv(); Plotly.newPlot(gd, mock) - .then(function() { + .then(function () { // Find legend in figure var legend = document.getElementsByClassName('legend')[0]; // For each legend item var legendItems = legend.getElementsByClassName('traces'); - Array.prototype.slice.call(legendItems).forEach(function(legendItem) { + Array.prototype.slice.call(legendItems).forEach(function (legendItem) { // Check that the last element is our `legendtoggle` var lastEl = legendItem.children[legendItem.children.length - 1]; expect(lastEl.getAttribute('class')).toBe('legendtoggle'); @@ -2570,116 +2906,136 @@ describe('legend DOM', function() { }); }); -describe('legend with custom doubleClickDelay', function() { +describe('legend with custom doubleClickDelay', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); function click(index) { - return function() { + return function () { var item = d3SelectAll('rect.legendtoggle')[0][index]; item.dispatchEvent(new MouseEvent('mousedown')); item.dispatchEvent(new MouseEvent('mouseup')); }; } - it('should differentiate clicks and double-clicks according *doubleClickDelay* config', function(done) { - var tLong = 1.5 * DBLCLICKDELAY; - var tShort = 0.75 * DBLCLICKDELAY; - - var clickCnt = 0; - var dblClickCnt = 0; - var newPlot = function(fig) { - return Plotly.newPlot(gd, fig).then(function() { - gd.on('plotly_legendclick', function() { clickCnt++; }); - gd.on('plotly_legenddoubleclick', function() { dblClickCnt++; }); - }); - }; - - function _assert(msg, _clickCnt, _dblClickCnt) { - return function() { - expect(clickCnt).toBe(_clickCnt, msg + '| clickCnt'); - expect(dblClickCnt).toBe(_dblClickCnt, msg + '| dblClickCnt'); - clickCnt = 0; - dblClickCnt = 0; + it( + 'should differentiate clicks and double-clicks according *doubleClickDelay* config', + function (done) { + var tLong = 1.5 * DBLCLICKDELAY; + var tShort = 0.75 * DBLCLICKDELAY; + + var clickCnt = 0; + var dblClickCnt = 0; + var newPlot = function (fig) { + return Plotly.newPlot(gd, fig).then(function () { + gd.on('plotly_legendclick', function () { + clickCnt++; + }); + gd.on('plotly_legenddoubleclick', function () { + dblClickCnt++; + }); + }); }; - } - newPlot({ - data: [ - { y: [1, 2, 1] }, - { y: [2, 1, 2] } - ], - layout: {}, - config: { - doubleClickDelay: tLong + function _assert(msg, _clickCnt, _dblClickCnt) { + return function () { + expect(clickCnt).toBe(_clickCnt, msg + '| clickCnt'); + expect(dblClickCnt).toBe(_dblClickCnt, msg + '| dblClickCnt'); + clickCnt = 0; + dblClickCnt = 0; + }; } - }) - .then(click(0)).then(delay(tLong / 2)) - .then(_assert('[long] after click + (t/2) delay', 1, 0)) - .then(delay(tLong + 10)) - .then(click(0)).then(delay(DBLCLICKDELAY + 1)).then(click(0)) - .then(_assert('[long] after click + (DBLCLICKDELAY+1) delay + click', 2, 1)) - .then(delay(tLong + 10)) - .then(click(0)).then(delay(1.1 * tLong)).then(click(0)) - .then(_assert('[long] after click + (1.1*t) delay + click', 2, 0)) - .then(delay(tLong + 10)) - .then(function() { - return newPlot({ - data: gd.data, - layout: gd.layout, - config: { doubleClickDelay: tShort } - }); - }) - .then(click(0)).then(delay(tShort / 2)) - .then(_assert('[short] after click + (t/2) delay', 1, 0)) - .then(delay(tShort + 10)) - .then(click(0)).then(delay(DBLCLICKDELAY + 1)).then(click(0)) - .then(_assert('[short] after click + (DBLCLICKDELAY+1) delay + click', 2, 0)) - .then(delay(tShort + 10)) - .then(click(0)).then(delay(1.1 * tShort)).then(click(0)) - .then(_assert('[short] after click + (1.1*t) delay + click', 2, 0)) - .then(done, done.fail); - }, 3 * jasmine.DEFAULT_TIMEOUT_INTERVAL); - - it('custom plotly_legenddoubleclick handler should fire even when plotly_legendclick has been cancelled', function(done) { - var tShort = 0.75 * DBLCLICKDELAY; - var dblClickCnt = 0; - var newPlot = function(fig) { - return Plotly.newPlot(gd, fig).then(function() { - gd.on('plotly_legendclick', function() { return false; }); - gd.on('plotly_legenddoubleclick', function() { dblClickCnt++; }); - }); - }; - function _assert(msg, _dblClickCnt) { - return function() { - expect(dblClickCnt).toBe(_dblClickCnt, msg + '| dblClickCnt'); - dblClickCnt = 0; + newPlot({ + data: [{ y: [1, 2, 1] }, { y: [2, 1, 2] }], + layout: {}, + config: { + doubleClickDelay: tLong + } + }) + .then(click(0)) + .then(delay(tLong / 2)) + .then(_assert('[long] after click + (t/2) delay', 1, 0)) + .then(delay(tLong + 10)) + .then(click(0)) + .then(delay(DBLCLICKDELAY + 1)) + .then(click(0)) + .then(_assert('[long] after click + (DBLCLICKDELAY+1) delay + click', 2, 1)) + .then(delay(tLong + 10)) + .then(click(0)) + .then(delay(1.1 * tLong)) + .then(click(0)) + .then(_assert('[long] after click + (1.1*t) delay + click', 2, 0)) + .then(delay(tLong + 10)) + .then(function () { + return newPlot({ + data: gd.data, + layout: gd.layout, + config: { doubleClickDelay: tShort } + }); + }) + .then(click(0)) + .then(delay(tShort / 2)) + .then(_assert('[short] after click + (t/2) delay', 1, 0)) + .then(delay(tShort + 10)) + .then(click(0)) + .then(delay(DBLCLICKDELAY + 1)) + .then(click(0)) + .then(_assert('[short] after click + (DBLCLICKDELAY+1) delay + click', 2, 0)) + .then(delay(tShort + 10)) + .then(click(0)) + .then(delay(1.1 * tShort)) + .then(click(0)) + .then(_assert('[short] after click + (1.1*t) delay + click', 2, 0)) + .then(done, done.fail); + }, + 3 * jasmine.DEFAULT_TIMEOUT_INTERVAL + ); + + it( + 'custom plotly_legenddoubleclick handler should fire even when plotly_legendclick has been cancelled', + function (done) { + var tShort = 0.75 * DBLCLICKDELAY; + var dblClickCnt = 0; + var newPlot = function (fig) { + return Plotly.newPlot(gd, fig).then(function () { + gd.on('plotly_legendclick', function () { + return false; + }); + gd.on('plotly_legenddoubleclick', function () { + dblClickCnt++; + }); + }); }; - } - newPlot({ - data: [ - { y: [1, 2, 1] }, - { y: [2, 1, 2] } - ], - layout: {}, - config: {} - }) - .then(click(0)) - .then(delay(tShort)) - .then(click(0)) - .then(_assert('Double click increases count', 1)) - .then(done); - }, 3 * jasmine.DEFAULT_TIMEOUT_INTERVAL); + function _assert(msg, _dblClickCnt) { + return function () { + expect(dblClickCnt).toBe(_dblClickCnt, msg + '| dblClickCnt'); + dblClickCnt = 0; + }; + } + + newPlot({ + data: [{ y: [1, 2, 1] }, { y: [2, 1, 2] }], + layout: {}, + config: {} + }) + .then(click(0)) + .then(delay(tShort)) + .then(click(0)) + .then(_assert('Double click increases count', 1)) + .then(done); + }, + 3 * jasmine.DEFAULT_TIMEOUT_INTERVAL + ); }); -describe('legend with custom legendwidth', function() { +describe('legend with custom legendwidth', function () { var gd; var data = [ @@ -2694,7 +3050,7 @@ describe('legend with custom legendwidth', function() { } }; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); @@ -2703,40 +3059,44 @@ describe('legend with custom legendwidth', function() { function assertLegendTextWidth(variants) { var nodes = d3SelectAll('rect.legendtoggle'); var index = 0; - nodes.each(function() { + nodes.each(function () { var node = d3Select(this); var w = +node.attr('width'); - if(variants[index]) expect(w).toEqual(variants[index]); + if (variants[index]) expect(w).toEqual(variants[index]); index += 1; }); } - it('should change width when trace has legendwidth', function(done) { + it('should change width when trace has legendwidth', function (done) { var extendedData = Lib.extendDeep([], data); - extendedData.forEach(function(trace, index) { + extendedData.forEach(function (trace, index) { trace.legendwidth = (index + 1) * 50; }); var textGap = 30 + constants.itemGap * 2 + constants.itemGap / 2; - Plotly.newPlot(gd, { data: extendedData, layout: layout }).then(function() { - assertLegendTextWidth([50 + textGap, 100 + textGap, 150 + textGap]); - }).then(done); + Plotly.newPlot(gd, { data: extendedData, layout: layout }) + .then(function () { + assertLegendTextWidth([50 + textGap, 100 + textGap, 150 + textGap]); + }) + .then(done); }); - it('should change width when legend has entrywidth', function(done) { + it('should change width when legend has entrywidth', function (done) { var extendedLayout = Lib.extendDeep({}, layout); var width = 50; extendedLayout.legend.entrywidth = width; var textGap = 30 + constants.itemGap * 2 + constants.itemGap / 2; - Plotly.newPlot(gd, { data: data, layout: extendedLayout }).then(function() { - assertLegendTextWidth([width + textGap, width + textGap, width + textGap]); - }).then(done); + Plotly.newPlot(gd, { data: data, layout: extendedLayout }) + .then(function () { + assertLegendTextWidth([width + textGap, width + textGap, width + textGap]); + }) + .then(done); }); - it('should change group width when trace has legendwidth', function(done) { + it('should change group width when trace has legendwidth', function (done) { var extendedLayout = Lib.extendDeep([], layout); extendedLayout.legend.traceorder = 'grouped'; @@ -2747,22 +3107,26 @@ describe('legend with custom legendwidth', function() { var textGap = 30 + constants.itemGap * 2 + constants.itemGap / 2; - Plotly.newPlot(gd, { data: extendedData, layout: extendedLayout }).then(function() { - assertLegendTextWidth([100 + textGap, 100 + textGap, undefined]); - }).then(done); + Plotly.newPlot(gd, { data: extendedData, layout: extendedLayout }) + .then(function () { + assertLegendTextWidth([100 + textGap, 100 + textGap, undefined]); + }) + .then(done); }); - it('should change width when legend has entrywidth and entrywidthmode is fraction', function(done) { + it('should change width when legend has entrywidth and entrywidthmode is fraction', function (done) { var extendedLayout = Lib.extendDeep({}, layout); extendedLayout.legend.entrywidthmode = 'fraction'; extendedLayout.legend.entrywidth = 0.3; - Plotly.newPlot(gd, { data: data, layout: extendedLayout }).then(function() { - assertLegendTextWidth([162, 162, 162]); - }).then(done); + Plotly.newPlot(gd, { data: data, layout: extendedLayout }) + .then(function () { + assertLegendTextWidth([162, 162, 162]); + }) + .then(done); }); - it('should not hide other legends when one legend is set to visible=false (issue #7559)', function(done) { + it('should not hide other legends when one legend is set to visible=false (issue #7559)', function (done) { var data = [ { legend: 'legend', name: 'Left legend entry', x: ['A', 'B'], y: [15, 20] }, { legend: 'legend2', name: 'Right legend entry', x: ['A', 'B'], y: [15, 25] }, @@ -2776,21 +3140,75 @@ describe('legend with custom legendwidth', function() { legend3: { visible: false, title: { text: 'Third legend' }, x: 0.25, y: 1.2 } }; - Plotly.newPlot(gd, data, layout).then(function() { - // legend and legend2 should be visible - var legend1 = document.querySelector('.legend'); - var legend2 = document.querySelector('.legend2'); - var legend3 = document.querySelector('.legend3'); - - expect(legend1).toBeTruthy(); - expect(legend2).toBeTruthy(); - expect(legend3).toBeFalsy(); // legend3 should not exist in DOM - - // Verify legends have correct data - expect(gd._fullLayout.legend).toBeDefined(); - expect(gd._fullLayout.legend.visible).toBe(true); - expect(gd._fullLayout.legend2).toBeDefined(); - expect(gd._fullLayout.legend2.visible).toBe(true); - }).then(done, done.fail); + Plotly.newPlot(gd, data, layout) + .then(function () { + // legend and legend2 should be visible + var legend1 = document.querySelector('.legend'); + var legend2 = document.querySelector('.legend2'); + var legend3 = document.querySelector('.legend3'); + + expect(legend1).toBeTruthy(); + expect(legend2).toBeTruthy(); + expect(legend3).toBeFalsy(); // legend3 should not exist in DOM + + // Verify legends have correct data + expect(gd._fullLayout.legend).toBeDefined(); + expect(gd._fullLayout.legend.visible).toBe(true); + expect(gd._fullLayout.legend2).toBeDefined(); + expect(gd._fullLayout.legend2.visible).toBe(true); + }) + .then(done, done.fail); + }); + + // TODO: Update this test when releasing next major version which will remove the exception for shape traces + it('should apply marker.line.dash to scatter traces but use solid for shape traces', (done) => { + const data = [ + { + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + size: 10, + line: { + width: 2, + dash: 'dot', + color: 'blue' + } + } + } + ]; + + const layout = { + showlegend: true, + shapes: [ + { + showlegend: true, + type: 'circle', + xref: 'paper', + yref: 'paper', + x0: 0.1, + y0: 0.1, + x1: 0.2, + y1: 0.2, + line: { + width: 2, + dash: 'dot', + color: 'red' + }, + fillcolor: 'rgba(255, 0, 0, 0.3)' + } + ] + }; + + Plotly.newPlot(gd, data, layout) + .then(function () { + const legendItems = gd.querySelectorAll('.legendpoints path.scatterpts'); + + expect(legendItems.length).toBe(2); + expect(legendItems[0].style.strokeDasharray).not.toBe(''); + expect(legendItems[1].style.strokeDasharray).toBe(''); + }) + .then(done, done.fail); }); }); diff --git a/test/jasmine/tests/scatter_marker_line_dash_test.js b/test/jasmine/tests/scatter_marker_line_dash_test.js new file mode 100644 index 00000000000..7e5e2976f58 --- /dev/null +++ b/test/jasmine/tests/scatter_marker_line_dash_test.js @@ -0,0 +1,135 @@ +var Plotly = require('../../../lib/index'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('Test scatter marker line dash:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it('should support marker line dash', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + size: 20, + line: { + color: 'red', + width: 2, + dash: 'dash' + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers.length).toBe(3); + + markers.forEach(function(node) { + // In plotly.js, dash is applied via stroke-dasharray + expect(node.style.strokeDasharray).not.toBe(''); + }); + }) + .then(done, done.fail); + }); + + it('should support array marker line dash', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + size: 20, + line: { + color: 'red', + width: 2, + dash: ['solid', 'dot', 'dash'] + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers.length).toBe(3); + + // 'solid' should have no dasharray or 'none' (represented as empty string in node.style.strokeDasharray) + // 'dot' and 'dash' should have numerical dasharrays + expect(markers[0].style.strokeDasharray).toBe(''); + expect(markers[1].style.strokeDasharray).not.toBe(''); + expect(markers[2].style.strokeDasharray).not.toBe(''); + }) + .then(done, done.fail); + }); + + it('should show marker line dash in the legend', function(done) { + Plotly.newPlot( + gd, + [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + line: { + color: 'red', + width: 2, + dash: 'dash' + } + } + }], + { showlegend: true } + ) + .then(function () { + var legendPoints = gd.querySelectorAll('.legendpoints path.scatterpts'); + expect(legendPoints.length).toBe(1); + expect(legendPoints[0].style.strokeDasharray).not.toBe(''); + }) + .then(done, done.fail); + }); + + it('should update marker line dash via restyle', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + line: { + color: 'red', + width: 2, + dash: 'solid' + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers[0].style.strokeDasharray).toBe(''); + + return Plotly.restyle(gd, {'marker.line.dash': 'dot'}); + }).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers[0].style.strokeDasharray).not.toBe(''); + }) + .then(done, done.fail); + }); + it('should support marker line dash on open markers', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + symbol: 'circle-open', + line: { + color: 'red', + width: 2, + dash: 'dash' + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers.length).toBe(3); + + markers.forEach(function(node) { + expect(node.style.strokeDasharray).not.toBe(''); + }); + }) + .then(done, done.fail); + });}); diff --git a/test/plot-schema.json b/test/plot-schema.json index 211da680a56..04e324e6977 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -60613,6 +60613,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -65817,6 +65837,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -68123,6 +68163,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "calc", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -75980,6 +76040,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -80482,6 +80562,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -82800,6 +82900,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.",