diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 7872f2ad6be..443f882d95c 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -266,43 +266,76 @@ function makePointPath(symbolNumber, r) { var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0}; var VERTGRADIENT = {x1: 0, x2: 0, y1: 1, y2: 0}; +var stopFormatter = d3.format('~.1f'); +var gradientInfo = { + radial: {node: 'radialGradient'}, + radialreversed: {node: 'radialGradient', reversed: true}, + horizontal: {node: 'linearGradient', attrs: HORZGRADIENT}, + horizontalreversed: {node: 'linearGradient', attrs: HORZGRADIENT, reversed: true}, + vertical: {node: 'linearGradient', attrs: VERTGRADIENT}, + verticalreversed: {node: 'linearGradient', attrs: VERTGRADIENT, reversed: true} +}; + +/** + * gradient: create and apply a gradient fill + * + * @param {object} sel: d3 selection to apply this gradient to + * You can use `selection.call(Drawing.gradient, ...)` + * @param {DOM element} gd: the graph div `sel` is part of + * @param {string} gradientID: a unique (within this plot) identifier + * for this gradient, so that we don't create unnecessary definitions + * @param {string} type: 'radial', 'horizontal', or 'vertical', optionally with + * 'reversed' at the end. Normally radial goes center to edge, + * horizontal goes right to left, and vertical goes bottom to top + * @param {array} colorscale: as in attribute values, [[fraction, color], ...] + * @param {string} prop: the property to apply to, 'fill' or 'stroke' + */ +drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) { + var len = colorscale.length; + var info = gradientInfo[type]; + var colorStops = new Array(len); + for(var i = 0; i < len; i++) { + if(info.reversed) { + colorStops[len - 1 - i] = [stopFormatter((1 - colorscale[i][0]) * 100), colorscale[i][1]]; + } + else { + colorStops[i] = [stopFormatter(colorscale[i][0] * 100), colorscale[i][1]]; + } + } + + var fullID = 'g' + gd._fullLayout._uid + '-' + gradientID; -drawing.gradient = function(sel, gd, gradientID, type, color1, color2) { var gradient = gd._fullLayout._defs.select('.gradients') - .selectAll('#' + gradientID) - .data([type + color1 + color2], Lib.identity); + .selectAll('#' + fullID) + .data([type + colorStops.join(';')], Lib.identity); gradient.exit().remove(); gradient.enter() - .append(type === 'radial' ? 'radialGradient' : 'linearGradient') + .append(info.node) .each(function() { var el = d3.select(this); - if(type === 'horizontal') el.attr(HORZGRADIENT); - else if(type === 'vertical') el.attr(VERTGRADIENT); - - el.attr('id', gradientID); - - var tc1 = tinycolor(color1); - var tc2 = tinycolor(color2); - - el.append('stop').attr({ - offset: '0%', - 'stop-color': Color.tinyRGB(tc2), - 'stop-opacity': tc2.getAlpha() - }); - - el.append('stop').attr({ - offset: '100%', - 'stop-color': Color.tinyRGB(tc1), - 'stop-opacity': tc1.getAlpha() + if(info.attrs) el.attr(info.attrs); + + el.attr('id', fullID); + + var stops = el.selectAll('stop') + .data(colorStops); + stops.exit().remove(); + stops.enter().append('stop'); + + stops.each(function(d) { + var tc = tinycolor(d[1]); + d3.select(this).attr({ + offset: d[0] + '%', + 'stop-color': Color.tinyRGB(tc), + 'stop-opacity': tc.getAlpha() + }); }); }); - sel.style({ - fill: 'url(#' + gradientID + ')', - 'fill-opacity': null - }); + sel.style(prop, 'url(#' + fullID + ')') + .style(prop + '-opacity', null); }; /* @@ -420,21 +453,29 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) { if(gradientType) perPointGradient = true; else gradientType = markerGradient && markerGradient.type; + // for legend - arrays will propagate through here, but we don't need + // to treat it as per-point. + if(Array.isArray(gradientType)) { + gradientType = gradientType[0]; + if(!gradientInfo[gradientType]) gradientType = 0; + } + if(gradientType && gradientType !== 'none') { var gradientColor = d.mgc; if(gradientColor) perPointGradient = true; else gradientColor = markerGradient.color; - var gradientID = 'g' + gd._fullLayout._uid + '-' + trace.uid; + var gradientID = trace.uid; if(perPointGradient) gradientID += '-' + d.i; - sel.call(drawing.gradient, gd, gradientID, gradientType, fillColor, gradientColor); + drawing.gradient(sel, gd, gradientID, gradientType, + [[0, gradientColor], [1, fillColor]], 'fill'); } else { - sel.call(Color.fill, fillColor); + Color.fill(sel, fillColor); } if(lineWidth) { - sel.call(Color.stroke, lineColor); + Color.stroke(sel, lineColor); } } }; diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index a27d0c9c6a9..c82d175951f 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -21,7 +21,8 @@ var helpers = require('./helpers'); module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { var containerIn = layoutIn.legend || {}; - var visibleTraces = 0; + var legendTraceCount = 0; + var legendReallyHasATrace = false; var defaultOrder = 'normal'; var defaultX, defaultY, defaultXAnchor, defaultYAnchor; @@ -29,10 +30,24 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; - if(helpers.legendGetsTrace(trace)) { - visibleTraces++; - // always show the legend by default if there's a pie - if(Registry.traceIs(trace, 'pie')) visibleTraces++; + if(!trace.visible) continue; + + // Note that we explicitly count any trace that is either shown or + // *would* be shown by default, toward the two traces you need to + // ensure the legend is shown by default, because this can still help + // disambiguate. + if(trace.showlegend || trace._dfltShowLegend) { + legendTraceCount++; + if(trace.showlegend) { + legendReallyHasATrace = true; + // Always show the legend by default if there's a pie, + // or if there's only one trace but it's explicitly shown + if(Registry.traceIs(trace, 'pie') || + trace._input.showlegend === true + ) { + legendTraceCount++; + } + } } if((Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') || @@ -48,7 +63,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { } var showLegend = Lib.coerce(layoutIn, layoutOut, - basePlotLayoutAttributes, 'showlegend', visibleTraces > 1); + basePlotLayoutAttributes, 'showlegend', + legendReallyHasATrace && legendTraceCount > 1); if(showLegend === false) return; diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js index 943075b41b2..c7b3562dc2c 100644 --- a/src/components/legend/get_legend_data.js +++ b/src/components/legend/get_legend_data.js @@ -41,12 +41,12 @@ module.exports = function getLegendData(calcdata, opts) { // build an { legendgroup: [cd0, cd0], ... } object for(i = 0; i < calcdata.length; i++) { - var cd = calcdata[i], - cd0 = cd[0], - trace = cd0.trace, - lgroup = trace.legendgroup; + var cd = calcdata[i]; + var cd0 = cd[0]; + var trace = cd0.trace; + var lgroup = trace.legendgroup; - if(!helpers.legendGetsTrace(trace) || !trace.showlegend) continue; + if(!trace.visible || !trace.showlegend) continue; if(Registry.traceIs(trace, 'pie')) { if(!slicesShown[lgroup]) slicesShown[lgroup] = {}; diff --git a/src/components/legend/helpers.js b/src/components/legend/helpers.js index d249e681fb1..1c01f45bc3f 100644 --- a/src/components/legend/helpers.js +++ b/src/components/legend/helpers.js @@ -9,16 +9,6 @@ 'use strict'; -exports.legendGetsTrace = function legendGetsTrace(trace) { - // traceIs(trace, 'showLegend') is not sufficient anymore, due to contour(carpet)? - // which are legend-eligible only if type: constraint. Otherwise, showlegend gets deleted. - - // Note that we explicitly include showlegend: false, so a trace that *could* be - // in the legend but is not shown still counts toward the two traces you need to - // ensure the legend is shown by default, because this can still help disambiguate. - return trace.visible && (trace.showlegend !== undefined); -}; - exports.isGrouped = function isGrouped(legendLayout) { return (legendLayout.traceorder || '').indexOf('grouped') !== -1; }; diff --git a/src/components/legend/style.js b/src/components/legend/style.js index 0a1f1948451..fd68381fcdd 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -61,25 +61,78 @@ module.exports = function style(s, gd) { var showFill = trace.visible && trace.fill && trace.fill !== 'none'; var showLine = subTypes.hasLines(trace); var contours = trace.contours; + var showGradientLine = false; + var showGradientFill = false; - if(contours && contours.type === 'constraint') { - showLine = contours.showlines; - showFill = contours._operation !== '='; + if(contours) { + var coloring = contours.coloring; + + if(coloring === 'lines') { + showGradientLine = true; + } + else { + showLine = coloring === 'none' || coloring === 'heatmap' || + contours.showlines; + } + + if(contours.type === 'constraint') { + showFill = contours._operation !== '='; + } + else if(coloring === 'fill' || coloring === 'heatmap') { + showGradientFill = true; + } } - var fill = d3.select(this).select('.legendfill').selectAll('path') - .data(showFill ? [d] : []); + // with fill and no markers or text, move the line and fill up a bit + // so it's more centered + var markersOrText = subTypes.hasMarkers(trace) || subTypes.hasText(trace); + var anyFill = showFill || showGradientFill; + var anyLine = showLine || showGradientLine; + var pathStart = (markersOrText || !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') + .data(showFill || showGradientFill ? [d] : []); fill.enter().append('path').classed('js-fill', true); fill.exit().remove(); - fill.attr('d', 'M5,0h30v6h-30z') - .call(Drawing.fillGroupStyle); + fill.attr('d', pathStart + 'h30v6h-30z') + .call(showFill ? Drawing.fillGroupStyle : fillGradient); - var line = d3.select(this).select('.legendlines').selectAll('path') - .data(showLine ? [d] : []); - line.enter().append('path').classed('js-line', true) - .attr('d', 'M5,0h30'); + var line = this3.select('.legendlines').selectAll('path') + .data(showLine || showGradientLine ? [d] : []); + line.enter().append('path').classed('js-line', true); line.exit().remove(); - line.call(Drawing.lineGroupStyle); + + // this is ugly... but you can't apply a gradient to a perfectly + // horizontal or vertical line. Presumably because then + // the system doesn't know how to scale vertical variation, even + // 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 ? 'l30,0.0001' : 'h30')) + .call(showLine ? Drawing.lineGroupStyle : lineGradient); + + function fillGradient(s) { + if(s.size()) { + var gradientID = 'legendfill-' + trace.uid; + Drawing.gradient(s, gd, gradientID, 'horizontalreversed', + trace.colorscale, 'fill'); + } + } + + function lineGradient(s) { + if(s.size()) { + var gradientID = 'legendline-' + trace.uid; + Drawing.lineGroupStyle(s); + Drawing.gradient(s, gd, gradientID, 'horizontalreversed', + trace.colorscale, 'stroke'); + } + } + } function stylePoints(d) { diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 4b320e18d6b..9e422717d4c 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -173,7 +173,13 @@ module.exports = { valType: 'boolean', role: 'info', editType: 'legend', - description: 'Determines whether or not a legend is drawn.' + description: [ + 'Determines whether or not a legend is drawn.', + 'Default is `true` if there is a trace to show and any of these:', + 'a) Two or more traces would by default be shown in the legend.', + 'b) One pie trace is shown in the legend.', + 'c) One trace is explicitly given with `showlegend: true`.' + ].join(' ') }, colorway: { valType: 'colorlist', diff --git a/src/plots/plots.js b/src/plots/plots.js index 861493eea07..908289fa72c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1144,9 +1144,13 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac coerce('ids'); if(Registry.traceIs(traceOut, 'showLegend')) { + traceOut._dfltShowLegend = true; coerce('showlegend'); coerce('legendgroup'); } + else { + traceOut._dfltShowLegend = false; + } Registry.getComponentMethod( 'fx', diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index f995de309f5..396302d1bf7 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -37,9 +37,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var isConstraint = (coerce('contours.type') === 'constraint'); coerce('connectgaps', Lib.isArray1D(traceOut.z)); - // trace-level showlegend has already been set, but is only allowed if this is a constraint - if(!isConstraint) delete traceOut.showlegend; - if(isConstraint) { handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor); } diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index 857f9e6dfad..e0e11f7fb34 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -16,7 +16,6 @@ var Drawing = require('../../components/drawing'); var svgTextUtils = require('../../lib/svg_text_utils'); var Axes = require('../../plots/cartesian/axes'); var setConvert = require('../../plots/cartesian/set_convert'); -var getUidsFromCalcData = require('../../plots/get_data').getUidsFromCalcData; var heatmapPlot = require('../heatmap/plot'); var makeCrossings = require('./make_crossings'); @@ -28,32 +27,40 @@ var constants = require('./constants'); var costConstants = constants.LABELOPTIMIZER; exports.plot = function plot(gd, plotinfo, cdcontours, contourLayer) { - var uidLookup = getUidsFromCalcData(cdcontours); + plotWrapper(gd, plotinfo, cdcontours, contourLayer, plotOne); +}; - contourLayer.selectAll('g.contour').each(function(d) { - if(!uidLookup[d.trace.uid]) { - d3.select(this).remove(); - } - }); +function plotWrapper(gd, plotinfo, cdcontours, contourLayer, plotOneFn) { + var contours = contourLayer.selectAll('g.contour') + .data( + cdcontours.map(function(d) { return d[0]; }), + function(cd) { return cd.trace.uid; } + ); - for(var i = 0; i < cdcontours.length; i++) { - plotOne(gd, plotinfo, cdcontours[i], contourLayer); - } -}; + contours.exit().remove(); + + contours.enter().append('g') + .classed('contour', true); -function plotOne(gd, plotinfo, cd, contourLayer) { - var trace = cd[0].trace; - var x = cd[0].x; - var y = cd[0].y; + contours.each(function(cd) { + plotOneFn(gd, plotinfo, cd, d3.select(this)); + }) + .order(); +} +exports.plotWrapper = plotWrapper; + +function plotOne(gd, plotinfo, cd, plotGroup) { + var trace = cd.trace; + var x = cd.x; + var y = cd.y; var contours = trace.contours; - var id = 'contour' + trace.uid; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var fullLayout = gd._fullLayout; - var pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); + var pathinfo = emptyPathinfo(contours, plotinfo, cd); // use a heatmap to fill - draw it behind the lines - var heatmapColoringLayer = Lib.ensureSingle(contourLayer, 'g', 'heatmapcoloring'); + var heatmapColoringLayer = Lib.ensureSingle(plotGroup, 'g', 'heatmapcoloring'); var cdheatmaps = []; if(contours.coloring === 'heatmap') { if(trace.zauto && (trace.autocontour === false)) { @@ -62,7 +69,7 @@ function plotOne(gd, plotinfo, cd, contourLayer) { trace._input.zmax = trace.zmax = trace.zmin + pathinfo.length * contours.size; } - cdheatmaps = [cd]; + cdheatmaps = [[cd]]; } heatmapPlot(gd, plotinfo, cdheatmaps, heatmapColoringLayer); @@ -87,27 +94,12 @@ function plotOne(gd, plotinfo, cd, contourLayer) { } // draw everything - var plotGroup = exports.makeContourGroup(contourLayer, cd, id); makeBackground(plotGroup, perimeter, contours); makeFills(plotGroup, fillPathinfo, perimeter, contours); - makeLinesAndLabels(plotGroup, pathinfo, gd, cd[0], contours, perimeter); - clipGaps(plotGroup, plotinfo, fullLayout._clips, cd[0], perimeter); + makeLinesAndLabels(plotGroup, pathinfo, gd, cd, contours, perimeter); + clipGaps(plotGroup, plotinfo, fullLayout._clips, cd, perimeter); } -exports.makeContourGroup = function(layer, cd, id) { - var plotgroup = layer - .selectAll('g.contour.' + id) - .data(cd); - - plotgroup.enter().append('g') - .classed('contour', true) - .classed(id, true); - - plotgroup.exit().remove(); - - return plotgroup; -}; - function makeBackground(plotgroup, perimeter, contours) { var bggroup = Lib.ensureSingle(plotgroup, 'g', 'contourbg'); diff --git a/src/traces/contour/style_defaults.js b/src/traces/contour/style_defaults.js index 256b4a55586..ca940b82a8d 100644 --- a/src/traces/contour/style_defaults.js +++ b/src/traces/contour/style_defaults.js @@ -27,6 +27,11 @@ module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout, } if(coloring !== 'none') { + // plots/plots always coerces showlegend to true, but in this case + // we default to false and (by default) show a colorbar instead + if(traceIn.showlegend !== true) traceOut.showlegend = false; + traceOut._dfltShowLegend = false; + colorscaleDefaults( traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} ); diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index b0cc3f19817..892965c626c 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -55,9 +55,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); var isConstraint = (coerce('contours.type') === 'constraint'); - // trace-level showlegend has already been set, but is only allowed if this is a constraint - if(!isConstraint) delete traceOut.showlegend; - if(isConstraint) { handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor, {hasHover: false}); } else { diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 16689efd848..6b1a9eb2784 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -13,7 +13,6 @@ var map1dArray = require('../carpet/map_1d_array'); var makepath = require('../carpet/makepath'); var Drawing = require('../../components/drawing'); var Lib = require('../../lib'); -var getUidsFromCalcData = require('../../plots/get_data').getUidsFromCalcData; var makeCrossings = require('../contour/make_crossings'); var findAllPaths = require('../contour/find_all_paths'); @@ -27,35 +26,23 @@ var lookupCarpet = require('../carpet/lookup_carpetid'); var closeBoundaries = require('../contour/close_boundaries'); module.exports = function plot(gd, plotinfo, cdcontours, contourcarpetLayer) { - var uidLookup = getUidsFromCalcData(cdcontours); - - contourcarpetLayer.selectAll('g.contour').each(function(d) { - if(!uidLookup[d.trace.uid]) { - d3.select(this).remove(); - } - }); - - for(var i = 0; i < cdcontours.length; i++) { - plotOne(gd, plotinfo, cdcontours[i], contourcarpetLayer); - } + contourPlot.plotWrapper(gd, plotinfo, cdcontours, contourcarpetLayer, plotOne); }; -function plotOne(gd, plotinfo, cd, contourcarpetLayer) { - var trace = cd[0].trace; +function plotOne(gd, plotinfo, cd, plotGroup) { + var trace = cd.trace; var carpet = trace._carpetTrace = lookupCarpet(gd, trace); var carpetcd = gd.calcdata[carpet.index][0]; if(!carpet.visible || carpet.visible === 'legendonly') return; - var a = cd[0].a; - var b = cd[0].b; + var a = cd.a; + var b = cd.b; var contours = trace.contours; - var uid = trace.uid; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - var id = 'contour' + uid; - var pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); + var pathinfo = emptyPathinfo(contours, plotinfo, cd); var isConstraint = contours.type === 'constraint'; var operation = contours._operation; var coloring = isConstraint ? (operation === '=' ? 'lines' : 'fill') : contours.coloring; @@ -98,7 +85,6 @@ function plotOne(gd, plotinfo, cd, contourcarpetLayer) { mapPathinfo(pathinfo, ab2p); // draw everything - var plotGroup = contourPlot.makeContourGroup(contourcarpetLayer, cd, id); // Compute the boundary path var seg, xp, yp, i; @@ -124,7 +110,7 @@ function plotOne(gd, plotinfo, cd, contourcarpetLayer) { makeFills(trace, plotGroup, xa, ya, fillPathinfo, perimeter, ab2p, carpet, carpetcd, coloring, boundaryPath); // Draw contour lines: - makeLinesAndLabels(plotGroup, pathinfo, gd, cd[0], contours, plotinfo, carpet); + makeLinesAndLabels(plotGroup, pathinfo, gd, cd, contours, plotinfo, carpet); // Clip the boundary of the plot Drawing.setClipUrl(plotGroup, carpet._clipPathId); diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index ccd98cc8bd7..c16206dff5e 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -23,7 +23,7 @@ Histogram2dContour.hoverPoints = require('../contour/hover'); Histogram2dContour.moduleType = 'trace'; Histogram2dContour.name = 'histogram2dcontour'; Histogram2dContour.basePlotModule = require('../../plots/cartesian'); -Histogram2dContour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'histogram']; +Histogram2dContour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'histogram', 'showLegend']; Histogram2dContour.meta = { hrName: 'histogram_2d_contour', description: [ diff --git a/test/image/baselines/15.png b/test/image/baselines/15.png index ef3f11f5404..7d6fd5517dd 100644 Binary files a/test/image/baselines/15.png and b/test/image/baselines/15.png differ diff --git a/test/image/baselines/5.png b/test/image/baselines/5.png index 9c2719bf9bb..65248fd0fee 100644 Binary files a/test/image/baselines/5.png and b/test/image/baselines/5.png differ diff --git a/test/image/baselines/airfoil.png b/test/image/baselines/airfoil.png index 562ffa99d90..0e15244a66d 100644 Binary files a/test/image/baselines/airfoil.png and b/test/image/baselines/airfoil.png differ diff --git a/test/image/baselines/cheater.png b/test/image/baselines/cheater.png index aa6517788ec..adf96eaf62f 100644 Binary files a/test/image/baselines/cheater.png and b/test/image/baselines/cheater.png differ diff --git a/test/image/baselines/cheater_constraints.png b/test/image/baselines/cheater_constraints.png index c79c53351c8..5a105d4fb53 100644 Binary files a/test/image/baselines/cheater_constraints.png and b/test/image/baselines/cheater_constraints.png differ diff --git a/test/image/baselines/cheater_fully_filled.png b/test/image/baselines/cheater_fully_filled.png index 0c2cedc4919..fcb32c3ed8e 100644 Binary files a/test/image/baselines/cheater_fully_filled.png and b/test/image/baselines/cheater_fully_filled.png differ diff --git a/test/image/baselines/cheater_smooth.png b/test/image/baselines/cheater_smooth.png index 7d4b479bf24..bac7ff8e2ea 100644 Binary files a/test/image/baselines/cheater_smooth.png and b/test/image/baselines/cheater_smooth.png differ diff --git a/test/image/baselines/connectgaps_2d.png b/test/image/baselines/connectgaps_2d.png index 80bdf117b11..fb5e18862be 100644 Binary files a/test/image/baselines/connectgaps_2d.png and b/test/image/baselines/connectgaps_2d.png differ diff --git a/test/image/baselines/contour_constraints.png b/test/image/baselines/contour_constraints.png index e0924427939..4bc2f1f41d8 100644 Binary files a/test/image/baselines/contour_constraints.png and b/test/image/baselines/contour_constraints.png differ diff --git a/test/image/baselines/contour_heatmap_coloring.png b/test/image/baselines/contour_heatmap_coloring.png index 0e2a7370fcb..7bbafb31c21 100644 Binary files a/test/image/baselines/contour_heatmap_coloring.png and b/test/image/baselines/contour_heatmap_coloring.png differ diff --git a/test/image/baselines/contour_legend.png b/test/image/baselines/contour_legend.png new file mode 100644 index 00000000000..34fb9616813 Binary files /dev/null and b/test/image/baselines/contour_legend.png differ diff --git a/test/image/baselines/error_bar_layers.png b/test/image/baselines/error_bar_layers.png index eac2ee07de6..26fb35e986d 100644 Binary files a/test/image/baselines/error_bar_layers.png and b/test/image/baselines/error_bar_layers.png differ diff --git a/test/image/baselines/range_slider.png b/test/image/baselines/range_slider.png index 29e44cb026a..0708cf613b2 100644 Binary files a/test/image/baselines/range_slider.png and b/test/image/baselines/range_slider.png differ diff --git a/test/image/baselines/range_slider_initial_expanded.png b/test/image/baselines/range_slider_initial_expanded.png index cd4639e40b9..1abce867322 100644 Binary files a/test/image/baselines/range_slider_initial_expanded.png and b/test/image/baselines/range_slider_initial_expanded.png differ diff --git a/test/image/baselines/range_slider_initial_valid.png b/test/image/baselines/range_slider_initial_valid.png index ae79d9e748a..3b6cdedb466 100644 Binary files a/test/image/baselines/range_slider_initial_valid.png and b/test/image/baselines/range_slider_initial_valid.png differ diff --git a/test/image/baselines/yaxis-over-yaxis2.png b/test/image/baselines/yaxis-over-yaxis2.png index b13b35cb913..03efae11d59 100644 Binary files a/test/image/baselines/yaxis-over-yaxis2.png and b/test/image/baselines/yaxis-over-yaxis2.png differ diff --git a/test/image/mocks/airfoil.json b/test/image/mocks/airfoil.json index e06a97c466e..9c8822ed939 100644 --- a/test/image/mocks/airfoil.json +++ b/test/image/mocks/airfoil.json @@ -31,7 +31,8 @@ "l":40, "t":80 }, - "width":900 + "width":900, + "paper_bgcolor": "#ccc" }, "data":[ { @@ -221,12 +222,14 @@ "contours":{ "start":-1, "size":0.025, - "end":1.000, - "showlines":false + "end":1.000 }, "line":{ - "smoothing":0 + "width": 1, + "dash": "dot", + "color": "rgba(0,0,0,0.2)" }, + "showlegend": true, "z":[ [ 0.361, 0.300, 0.246, 0.209, 0.182, 0.162, 0.145, 0.132, 0.121, 0.111, 0.103, 0.096, 0.090, 0.085, 0.080, 0.075, 0.072, 0.068, 0.065, 0.062, 0.059, 0.057, 0.055, 0.053, 0.051, 0.049, 0.047, 0.046, 0.044, 0.043, 0.042 ], [ 0.261, 0.234, 0.199, 0.170, 0.147, 0.129, 0.115, 0.103, 0.093, 0.085, 0.078, 0.072, 0.066, 0.062, 0.058, 0.054, 0.051, 0.048, 0.045, 0.043, 0.041, 0.039, 0.037, 0.036, 0.034, 0.033, 0.031, 0.030, 0.029, 0.028, 0.027 ], @@ -472,7 +475,8 @@ "type":"contourcarpet", "line":{ "color":"rgba(0, 0, 0, 0.5)", - "smoothing":1 + "smoothing":1, + "width": 1 }, "contours":{ "size":0.250, diff --git a/test/image/mocks/contour_legend.json b/test/image/mocks/contour_legend.json new file mode 100644 index 00000000000..f42af42164f --- /dev/null +++ b/test/image/mocks/contour_legend.json @@ -0,0 +1,57 @@ +{ + "data":[{ + "type": "contour", + "contours":{"coloring":"fills", "showlabels": true}, + "z":[[1, 2, 1], [2, 4, 1], [3, 4, 4]], + "colorscale": "Greys", + "showscale": false, + "showlegend": true, + "name": "fills" + }, { + "type": "contour", + "contours":{"coloring":"lines", "showlabels": true}, + "z":[[1, 2, 3], [4, 5, 6], [7, 8, 9]], + "showscale": false, + "showlegend": true, + "line": {"width": 2, "dash": "dot"}, + "name": "lines" + }, { + "type": "contour", + "contours":{"coloring":"none", "showlabels": true}, + "z":[[1, 4, 7], [2, 5, 8], [3, 6, 9]], + "line": {"color": "#0f0"}, + "name": "none" + }, { + "type": "contour", + "contours": {"type": "constraint", "operation": "[]", "value": [3, 7], "showlabels": true}, + "z": [[3, 2, 1], [6, 5, 4], [9, 8, 7]], + "line": {"color": "#f88"}, + "name": "constraint" + }, { + "type": "contour", + "contours": {"coloring": "heatmap"}, + "line": {"width": 3, "color": "#cf0", "dash": "dashdot"}, + "z": [[1, 2], [3, 4]], + "showscale": false, + "showlegend": true, + "y": [2, 3], + "name": "heatmap" + }, { + "type": "histogram2dcontour", + "contours": {"coloring": "heatmap"}, + "xbins": {"start": 0.5, "end": 2.5, "size": 1}, + "ybins": {"start": 1.5, "end": 3.5, "size": 1}, + "line": {"color": "#f00", "dash": "dot"}, + "showscale": false, + "showlegend": true, + "colorscale": "Viridis", + "x": [1.1, 1.1, 1.1, 1.8, 1.8, 1.1, 1.8, 1.8, 1.8, 1.8], + "y": [2.2, 2.8, 2.8, 2.2, 2.2, 2.8, 2.8, 2.8, 2.8, 2.8], + "name": "histogram" + }], + "layout":{ + "autosize":false, + "height":400, + "width":500 + } +} diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js index 2f4ca27302e..e288128f303 100644 --- a/test/jasmine/assets/custom_assertions.js +++ b/test/jasmine/assets/custom_assertions.js @@ -247,3 +247,66 @@ exports.assertPlotSize = function(opts, msg) { if(widthLessThan) expect(actualWidth).toBeLessThan(widthLessThan - 1, 'widthLessThan' + msgPlus); if(heightLessThan) expect(actualHeight).toBeLessThan(heightLessThan - 1, 'heightLessThan' + msgPlus); }; + +/** + * Ordering test - since SVG layering is purely dependent on ordering in the + * node tree, this tells you if the items are layered correctly. + * Note that we only take the first matching node for each selector, and it's + * not necessary that the nodes be siblings or at the same level of nesting. + * + * @param {string} selectorBehind: css selector for the node that should be behind + * @param {string} selectorInFront: css selector for the node that should be in front + * @param {string} msg: context for debugging + */ +exports.assertNodeOrder = function(selectorBehind, selectorInFront, msg) { + var nodeBehind = document.querySelector(selectorBehind); + var nodeInFront = document.querySelector(selectorInFront); + if(!nodeBehind) { + fail(selectorBehind + ' not found (' + msg + ')'); + } + else if(!nodeInFront) { + fail(selectorInFront + ' not found (' + msg + ')'); + } + else { + var parentsBehind = getParents(nodeBehind); + var parentsInFront = getParents(nodeInFront); + + var commonParent = null; + var siblingBehind = null; + var siblingInFront = null; + for(var i = 0; i < parentsBehind.length; i++) { + if(parentsBehind[i] === parentsInFront[i]) { + commonParent = parentsBehind[i]; + } + else { + siblingBehind = parentsBehind[i]; + siblingInFront = parentsInFront[i]; + break; + } + } + var allSiblings = collectionToArray(commonParent.children); + var behindIndex = allSiblings.indexOf(siblingBehind); + var frontIndex = allSiblings.indexOf(siblingInFront); + + // sanity check - if these fail there's just something wrong in this routine + expect(behindIndex).toBeGreaterThan(-1, 'error in assertNodeOrder: ' + msg); + expect(frontIndex).toBeGreaterThan(-1, 'error in assertNodeOrder: ' + msg); + + // the real test + expect(frontIndex).toBeGreaterThan(behindIndex, + '"' + selectorBehind + '" is not behind "' + selectorInFront + '": ' + msg); + } +}; + +function getParents(node) { + var parent = node.parentNode; + if(parent) return getParents(parent).concat(node); + return [node]; +} + +function collectionToArray(collection) { + var len = collection.length; + var a = new Array(len); + for(var i = 0; i < len; i++) a[i] = collection[i]; + return a; +} diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js index db602897759..3e784c9772b 100644 --- a/test/jasmine/tests/carpet_test.js +++ b/test/jasmine/tests/carpet_test.js @@ -9,7 +9,7 @@ var smoothFill = require('@src/traces/carpet/smooth_fill_array'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var assertHoverLabelContent = require('../assets/custom_assertions').assertHoverLabelContent; @@ -463,7 +463,7 @@ describe('Test carpet interactions:', function() { expect(countCarpets()).toEqual(0); expect(countContourTraces()).toEqual(0); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -494,7 +494,7 @@ describe('Test carpet interactions:', function() { expect(countCarpets()).toEqual(0); expect(countContourTraces()).toEqual(0); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -508,7 +508,7 @@ describe('Test carpet interactions:', function() { .then(function() { return Plotly.relayout(gd, 'yaxis.range', [7, 8]); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -534,7 +534,7 @@ describe('Test carpet interactions:', function() { .then(function() { _assert(3); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -592,7 +592,7 @@ describe('scattercarpet array attributes', function() { expect(pt.mlc).toBe(mlc[i]); } }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -646,3 +646,52 @@ describe('scattercarpet hover labels', function() { .then(done); }); }); + +describe('contourcarpet plotting & editing', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + it('keeps the correct ordering after hide and show', function(done) { + function getIndices() { + var out = []; + d3.selectAll('.contour').each(function(d) { out.push(d.trace.index); }); + return out; + } + + Plotly.newPlot(gd, [{ + type: 'carpet', + a: [1, 1, 1, 3, 3, 3, 5, 5, 5], + b: [1, 2, 3, 1, 2, 3, 1, 2, 3], + y: [1, 2, 3, 2, 3, 4, 3, 4, 5], + cheaterslope: 2 + }, { + type: 'contourcarpet', + a: [1, 1, 1, 3, 3, 3, 5, 5, 5], + b: [1, 2, 3, 1, 2, 3, 1, 2, 3], + z: [1, 2, 3, 4, 5, 6, 7, 8, 9] + }, { + type: 'contourcarpet', + a: [1, 1, 1, 3, 3, 3, 5, 5, 5], + b: [1, 2, 3, 1, 2, 3, 1, 2, 3], + z: [1, 4, 7, 2, 5, 8, 3, 6, 9], + contours: {coloring: 'lines'} + }]) + .then(function() { + expect(getIndices()).toEqual([1, 2]); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + expect(getIndices()).toEqual([2]); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + expect(getIndices()).toEqual([1, 2]); + }) + .catch(failTest) + .then(done); + }); +}); diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 4e34458328f..7f19e18c7d4 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -1,3 +1,5 @@ +var d3 = require('d3'); + var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); @@ -6,10 +8,12 @@ var Contour = require('@src/traces/contour'); var makeColorMap = require('@src/traces/contour/make_color_map'); var colorScales = require('@src/components/colorscale/scales'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var checkTicks = require('../assets/custom_assertions').checkTicks; +var customAssertions = require('../assets/custom_assertions'); +var checkTicks = customAssertions.checkTicks; +var assertNodeOrder = customAssertions.assertNodeOrder; var supplyAllDefaults = require('../assets/supply_defaults'); @@ -386,7 +390,7 @@ describe('contour plotting and editing', function() { checkTicks('y', ['Jan 102016', 'Jan 24', 'Feb 7', 'Feb 21'], 'date y #2'); expect(gd._fullLayout.yaxis.type).toBe('date'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -405,30 +409,17 @@ describe('contour plotting and editing', function() { .then(function() { expect(gd.querySelector('.contourlabels text').textContent).toBe('0.41'); }) - .catch(fail) + .catch(failTest) .then(done); }); it('should always draw heatmap coloring layer below contour lines', function(done) { - var cnt = 0; - - function _assert(exp) { - var msg = ' index in (call #' + cnt + ')'; - var contourLayer = gd.querySelector('.xy > .plot > .contourlayer'); - var hmIndex = -1; - var contoursIndex = -1; - - for(var i in contourLayer.children) { - var child = contourLayer.children[i]; - if(child.querySelector) { - if(child.querySelector('.hm')) hmIndex = +i; - else if(child.querySelector('.contourlevel')) contoursIndex = +i; - } - } - - expect(hmIndex).toBe(exp.hmIndex, 'heatmap' + msg); - expect(contoursIndex).toBe(exp.contoursIndex, 'contours' + msg); - cnt++; + function _assertNoHeatmap(msg) { + msg = ' (' + msg + ')'; + // all we care about here *really* is that there are contour levels + // *somewhere* on the plot, and there is no heatmap anywhere. + expect(gd.querySelector('.hm')).toBe(null, 'heatmap exists' + msg); + expect(gd.querySelector('.contourlevel')).not.toBe(null, 'missing contours' + msg); } Plotly.newPlot(gd, [{ @@ -437,26 +428,17 @@ describe('contour plotting and editing', function() { contours: {coloring: 'heatmap'} }]) .then(function() { - _assert({ - hmIndex: 0, - contoursIndex: 1 - }); + assertNodeOrder('.hm', '.contourlevel', 'initial heatmap coloring'); return Plotly.restyle(gd, 'contours.coloring', 'lines'); }) .then(function() { - _assert({ - hmIndex: -1, - contoursIndex: 1 - }); + _assertNoHeatmap('line coloring'); return Plotly.restyle(gd, 'contours.coloring', 'heatmap'); }) .then(function() { - _assert({ - hmIndex: 0, - contoursIndex: 1 - }); + assertNodeOrder('.hm', '.contourlevel', 'back to heatmap coloring'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -498,7 +480,37 @@ describe('contour plotting and editing', function() { expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2.5]]); expect(gd.calcdata[0][0].zmask).toEqual([[1, 1], [0, 1], [1, 0]]); }) - .catch(fail) + .catch(failTest) + .then(done); + }); + + it('keeps the correct ordering after hide and show', function(done) { + function getIndices() { + var out = []; + d3.selectAll('.contour').each(function(d) { out.push(d.trace.index); }); + return out; + } + + Plotly.newPlot(gd, [{ + type: 'contour', + z: [[1, 2], [3, 4]] + }, { + type: 'contour', + z: [[2, 1], [4, 3]], + contours: {coloring: 'lines'} + }]) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([1]); + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + }) + .catch(failTest) .then(done); }); }); diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 686f7b24da6..930fa8cd236 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -33,12 +33,81 @@ describe('legend defaults', function() { }; }); + function allShown(fullData) { + 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() { + fullData = allShown([ + {type: 'scatter'}, + {type: 'scatter', visible: false}, // ignored + {type: 'contour', _dfltShowLegend: false, showlegend: false} // hidden by default + ]); + + supplyLayoutDefaults({}, layoutOut, fullData); + expect(layoutOut.showlegend).toBe(false); + }); + + 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 + ]); + + supplyLayoutDefaults({}, layoutOut, fullData); + expect(layoutOut.showlegend).toBe(true); + }); + + it('hides if no items are actually shown', function() { + fullData = allShown([ + {type: 'scatter', showlegend: false}, + {type: 'scatter', showlegend: false} + ]); + + supplyLayoutDefaults({}, layoutOut, fullData); + expect(layoutOut.showlegend).toBe(false); + }); + + 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} + ]); + + 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}} + ]); + + supplyLayoutDefaults({}, layoutOut, fullData); + expect(layoutOut.showlegend).toBe(true); + }); + it('should default traceorder to reversed for stack bar charts', function() { - fullData = [ - { type: 'bar' }, - { type: 'bar' }, - { type: 'scatter' } - ]; + fullData = allShown([ + {type: 'bar', visible: 'legendonly'}, + {type: 'bar', visible: 'legendonly'}, + {type: 'scatter'} + ]); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.legend.traceorder).toEqual('normal'); @@ -50,20 +119,20 @@ describe('legend defaults', function() { }); it('should default traceorder to reversed for filled tonext scatter charts', function() { - fullData = [ - { type: 'scatter' }, - { type: 'scatter', fill: 'tonexty' } - ]; + 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 = [ - { type: 'scatter', legendgroup: 'group' }, - { type: 'scatter'} - ]; + fullData = allShown([ + {type: 'scatter', legendgroup: 'group'}, + {type: 'scatter'} + ]); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.legend.traceorder).toEqual('grouped'); @@ -74,6 +143,27 @@ describe('legend defaults', function() { expect(layoutOut.legend.traceorder).toEqual('grouped+reversed'); }); + 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'} + ]); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.traceorder).toEqual('normal'); + }); + it('should default orientation to vertical', function() { supplyLayoutDefaults(layoutIn, layoutOut, []); expect(layoutOut.legend.orientation).toEqual('v'); @@ -382,24 +472,6 @@ describe('legend getLegendData', function() { describe('legend helpers:', function() { 'use strict'; - describe('legendGetsTraces', function() { - var legendGetsTrace = helpers.legendGetsTrace; - - it('should return true when trace is visible and supports legend', function() { - expect(legendGetsTrace({ visible: true, showlegend: true })).toBe(true); - expect(legendGetsTrace({ visible: false, showlegend: true })).toBe(false); - expect(legendGetsTrace({ visible: 'legendonly', showlegend: true })).toBe(true); - - expect(legendGetsTrace({ visible: true, showlegend: false })).toBe(true); - expect(legendGetsTrace({ visible: false, showlegend: false })).toBe(false); - expect(legendGetsTrace({ visible: 'legendonly', showlegend: false })).toBe(true); - - expect(legendGetsTrace({ visible: true })).toBe(false); - expect(legendGetsTrace({ visible: false })).toBe(false); - expect(legendGetsTrace({ visible: 'legendonly' })).toBe(false); - }); - }); - describe('isGrouped', function() { var isGrouped = helpers.isGrouped;