diff --git a/draftlogs/6918_add.md b/draftlogs/6918_add.md new file mode 100644 index 00000000000..2af28d6f804 --- /dev/null +++ b/draftlogs/6918_add.md @@ -0,0 +1 @@ +- Add a new attribute `zorder` to SVG based Cartesian traces (not to WebGL traces). Traces with higher `zorder` values are drawn in front of traces with lower `zorder` values. This feature was anonymously sponsored: thank you to our sponsor! \ No newline at end of file diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index e57c5572d01..ea58db52bad 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -130,6 +130,10 @@ exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { var calcdata = gd.calcdata; var i; + // Traces is a list of trace indices to (re)plot. If it's not provided, + // then it's a complete replot so we create a new list and add all trace indices + // which are in calcdata. + if(!Array.isArray(traces)) { // If traces is not provided, then it's a complete replot and missing // traces are removed @@ -137,14 +141,16 @@ exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { for(i = 0; i < calcdata.length; i++) traces.push(i); } + // For each subplot for(i = 0; i < subplots.length; i++) { var subplot = subplots[i]; var subplotInfo = fullLayout._plots[subplot]; - // Get all calcdata for this subplot: + // Get all calcdata (traces) for this subplot: var cdSubplot = []; var pcd; + // For each trace for(var j = 0; j < calcdata.length; j++) { var cd = calcdata[j]; var trace = cd[0].trace; @@ -178,7 +184,7 @@ exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { pcd = cd; } } - + // Plot the traces for this subplot plotOne(gd, subplotInfo, cdSubplot, transitionOpts, makeOnCompleteCallback); } }; @@ -189,41 +195,60 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback var modules = fullLayout._modules; var _module, cdModuleAndOthers, cdModule; + // Separate traces by zorder and plot each zorder group separately + // TODO: Performance + var traceZorderGroups = {}; + for(var t = 0; t < cdSubplot.length; t++) { + var trace = cdSubplot[t][0].trace; + var zi = trace.zorder || 0; + if(!traceZorderGroups[zi]) traceZorderGroups[zi] = []; + traceZorderGroups[zi].push(cdSubplot[t]); + } + var layerData = []; var zoomScaleQueryParts = []; - for(var i = 0; i < modules.length; i++) { - _module = modules[i]; - var name = _module.name; - var categories = Registry.modules[name].categories; - - if(categories.svg) { - var className = (_module.layerName || name + 'layer'); - var plotMethod = _module.plot; - - // plot all visible traces of this type on this subplot at once - cdModuleAndOthers = getModuleCalcData(cdSubplot, plotMethod); - cdModule = cdModuleAndOthers[0]; - // don't need to search the found traces again - in fact we need to NOT - // so that if two modules share the same plotter we don't double-plot - cdSubplot = cdModuleAndOthers[1]; - - if(cdModule.length) { - layerData.push({ - i: traceLayerClasses.indexOf(className), - className: className, - plotMethod: plotMethod, - cdModule: cdModule - }); - } + // Plot each zorder group in ascending order + var zindices = Object.keys(traceZorderGroups) + .map(Number) + .sort(Lib.sorterAsc); + for(var z = 0; z < zindices.length; z++) { + var zorder = zindices[z]; + // For each "module" (trace type) + for(var i = 0; i < modules.length; i++) { + _module = modules[i]; + var name = _module.name; + var categories = Registry.modules[name].categories; + + if(categories.svg) { + var className = (_module.layerName || name + 'layer') + (z ? Number(z) + 1 : ''); + var plotMethod = _module.plot; + + // plot all visible traces of this type on this subplot at once + cdModuleAndOthers = getModuleCalcData(cdSubplot, plotMethod, zorder); + cdModule = cdModuleAndOthers[0]; + // don't need to search the found traces again - in fact we need to NOT + // so that if two modules share the same plotter we don't double-plot + cdSubplot = cdModuleAndOthers[1]; + + if(cdModule.length) { + layerData.push({ + i: traceLayerClasses.indexOf(className), + zorder: z, + className: className, + plotMethod: plotMethod, + cdModule: cdModule + }); + } - if(categories.zoomScale) { - zoomScaleQueryParts.push('.' + className); + if(categories.zoomScale) { + zoomScaleQueryParts.push('.' + className); + } } } } - - layerData.sort(function(a, b) { return a.i - b.i; }); + // Sort the layers primarily by z, then by i + layerData.sort(function(a, b) { return (a.zorder || 0) - (b.zorder || 0) || a.i - b.i; }); var layers = plotinfo.plot.selectAll('g.mlayer') .data(layerData, function(d) { return d.className; }); @@ -434,7 +459,6 @@ function makeSubplotData(gd) { } subplotData[i] = d; } - return subplotData; } diff --git a/src/plots/get_data.js b/src/plots/get_data.js index 18e19a97edf..6096580e5a1 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -39,10 +39,10 @@ exports.getSubplotCalcData = function(calcData, type, subplotId) { * @param {array} calcdata: as in gd.calcdata * @param {object|string|fn} arg1: * the plotting module, or its name, or its plot method - * + * @param {int} arg2: (optional) zorder to filter on * @return {array[array]} [foundCalcdata, remainingCalcdata] */ -exports.getModuleCalcData = function(calcdata, arg1) { +exports.getModuleCalcData = function(calcdata, arg1, arg2) { var moduleCalcData = []; var remainingCalcData = []; @@ -57,10 +57,12 @@ exports.getModuleCalcData = function(calcdata, arg1) { if(!plotMethod) { return [moduleCalcData, calcdata]; } + var zorder = arg2; for(var i = 0; i < calcdata.length; i++) { var cd = calcdata[i]; var trace = cd[0].trace; + var filterByZ = (trace.zorder !== undefined); // N.B. // - 'legendonly' traces do not make it past here // - skip over 'visible' traces that got trimmed completely during calc transforms @@ -70,7 +72,7 @@ exports.getModuleCalcData = function(calcdata, arg1) { // would suggest), but by 'module plot method' so that if some traces // share the same module plot method (e.g. bar and histogram), we // only call it one! - if(trace._module && trace._module.plot === plotMethod) { + if(trace._module && trace._module.plot === plotMethod && (!filterByZ || trace.zorder === zorder)) { moduleCalcData.push(cd); } else { remainingCalcData.push(cd); diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index 092d9fe52bf..c5eead1b3d2 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -226,6 +226,7 @@ module.exports = { textfont: scatterAttrs.unselected.textfont, editType: 'style' }, + zorder: scatterAttrs.zorder, _deprecated: { bardir: { diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index bfa15379cba..b969e30b1ee 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -29,6 +29,8 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { coerce('xhoverformat'); coerce('yhoverformat'); + coerce('zorder'); + coerce('orientation', (traceOut.x && !traceOut.y) ? 'h' : 'v'); coerce('base'); coerce('offset'); diff --git a/src/traces/bar/style.js b/src/traces/bar/style.js index df30b7c3850..9b3f9750f28 100644 --- a/src/traces/bar/style.js +++ b/src/traces/bar/style.js @@ -14,7 +14,7 @@ var attributeOutsideTextFont = attributes.outsidetextfont; var helpers = require('./helpers'); function style(gd) { - var s = d3.select(gd).selectAll('g.barlayer').selectAll('g.trace'); + var s = d3.select(gd).selectAll('g[class^="barlayer"]').selectAll('g.trace'); resizeText(gd, s, 'bar'); var barcount = s.size(); diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 63b0def141a..6acfb11b7db 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -451,5 +451,6 @@ module.exports = { 'Do the hover effects highlight individual boxes ', 'or sample points or both?' ].join(' ') - } + }, + zorder: scatterAttrs.zorder }; diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 892d4401bd0..0e9a5a71591 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -67,6 +67,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { if(notched) coerce('notchwidth'); handlePointsDefaults(traceIn, traceOut, coerce, {prefix: 'box'}); + coerce('zorder'); } function handleSampleDefaults(traceIn, traceOut, coerce, layout) { diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js index e114d59307b..4cfde82c30c 100644 --- a/src/traces/candlestick/attributes.js +++ b/src/traces/candlestick/attributes.js @@ -53,4 +53,5 @@ module.exports = { whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }), hoverlabel: OHLCattrs.hoverlabel, + zorder: boxAttrs.zorder }; diff --git a/src/traces/candlestick/defaults.js b/src/traces/candlestick/defaults.js index 43ca40f8809..e8e44b4ed5b 100644 --- a/src/traces/candlestick/defaults.js +++ b/src/traces/candlestick/defaults.js @@ -31,6 +31,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('whiskerwidth'); layout._requestRangeslider[traceOut.xaxis] = true; + coerce('zorder'); }; function handleDirection(traceIn, traceOut, coerce, direction) { diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js index 3d947048121..e6ff6442a33 100644 --- a/src/traces/carpet/attributes.js +++ b/src/traces/carpet/attributes.js @@ -8,6 +8,9 @@ var carpetFont = fontAttrs({ editType: 'calc', description: 'The default font used for axis & tick labels on this carpet' }); + +var zorder = require('../scatter/attributes').zorder; + // TODO: inherit from global font carpetFont.family.dflt = '"Open Sans", verdana, arial, sans-serif'; carpetFont.size.dflt = 12; @@ -112,5 +115,6 @@ module.exports = { 'Individual pieces can override this.' ].join(' ') }, - transforms: undefined + transforms: undefined, + zorder: zorder }; diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index bbadc66cde7..4f3f634f39d 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -45,4 +45,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, dfltColor, fullLayou if(traceOut._cheater) { coerce('cheaterslope'); } + coerce('zorder'); }; diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js index eb16d5e49c0..3562a98c641 100644 --- a/src/traces/contour/attributes.js +++ b/src/traces/contour/attributes.js @@ -265,7 +265,8 @@ module.exports = extendFlat({ ].join(' ') }), editType: 'plot' - } + }, + zorder: scatterAttrs.zorder }, colorScaleAttrs('', { cLetter: 'z', diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index c1ebd9be8e9..0d45eca75e6 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -51,4 +51,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout ) { handleHeatmapLabelDefaults(coerce, layout); } + coerce('zorder'); }; diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index bfe5f4e6db7..d938f5554d7 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -70,6 +70,7 @@ module.exports = extendFlat({ editType: 'plot' }, + zorder: contourAttrs.zorder, transforms: undefined }, diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 4a83c3558ae..a6548befe21 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -55,4 +55,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut._defaultColor = defaultColor; traceOut._length = null; } + coerce('zorder'); }; diff --git a/src/traces/funnel/attributes.js b/src/traces/funnel/attributes.js index 501738f9cca..86a52007b13 100644 --- a/src/traces/funnel/attributes.js +++ b/src/traces/funnel/attributes.js @@ -109,7 +109,8 @@ module.exports = { }, offsetgroup: barAttrs.offsetgroup, - alignmentgroup: barAttrs.alignmentgroup + alignmentgroup: barAttrs.alignmentgroup, + zorder: barAttrs.zorder }; function funnelMarker() { diff --git a/src/traces/funnel/defaults.js b/src/traces/funnel/defaults.js index 80f9745fac9..b8bd941f68e 100644 --- a/src/traces/funnel/defaults.js +++ b/src/traces/funnel/defaults.js @@ -61,6 +61,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { coerce('connector.line.dash'); } } + coerce('zorder'); } function defaultFillColor(markerColor) { diff --git a/src/traces/funnel/style.js b/src/traces/funnel/style.js index 2a393fd15a6..8a782427196 100644 --- a/src/traces/funnel/style.js +++ b/src/traces/funnel/style.js @@ -10,7 +10,7 @@ var resizeText = require('../bar/uniform_text').resizeText; var styleTextPoints = barStyle.styleTextPoints; function style(gd, cd, sel) { - var s = sel ? sel : d3.select(gd).selectAll('g.funnellayer').selectAll('g.trace'); + var s = sel ? sel : d3.select(gd).selectAll('g[class^="funnellayer"]').selectAll('g.trace'); resizeText(gd, s, 'funnel'); s.style('opacity', function(d) { return d[0].trace.opacity; }); diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js index d2faa7d1649..4802521a090 100644 --- a/src/traces/heatmap/attributes.js +++ b/src/traces/heatmap/attributes.js @@ -132,7 +132,8 @@ module.exports = extendFlat({ description: 'Sets the text font.' }), - showlegend: extendFlat({}, baseAttrs.showlegend, {dflt: false}) + showlegend: extendFlat({}, baseAttrs.showlegend, {dflt: false}), + zorder: scatterAttrs.zorder }, { transforms: undefined }, diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index 5e5acc5111a..7387b005623 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -36,4 +36,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('connectgaps', Lib.isArray1D(traceOut.z) && (traceOut.zsmooth !== false)); colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}); + coerce('zorder'); }; diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 2eba375a3bb..f442de30fbb 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -248,5 +248,7 @@ module.exports = { _deprecated: { bardir: barAttrs._deprecated.bardir - } + }, + + zorder: barAttrs.zorder }; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 78842de9f8e..e381b0d7841 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -74,4 +74,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var errorBarsSupplyDefaults = Registry.getComponentMethod('errorbars', 'supplyDefaults'); errorBarsSupplyDefaults(traceIn, traceOut, lineColor || Color.defaultLine, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, lineColor || Color.defaultLine, {axis: 'x', inherit: 'y'}); + + coerce('zorder'); }; diff --git a/src/traces/image/attributes.js b/src/traces/image/attributes.js index b17768e6575..8b33b5ebec1 100644 --- a/src/traces/image/attributes.js +++ b/src/traces/image/attributes.js @@ -1,6 +1,7 @@ 'use strict'; var baseAttrs = require('../../plots/attributes'); +var zorder = require('../scatter/attributes').zorder; var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; var colormodel = require('./constants').colormodel; @@ -133,5 +134,6 @@ module.exports = extendFlat({ keys: ['z', 'color', 'colormodel'] }), + zorder: zorder, transforms: undefined }); diff --git a/src/traces/image/defaults.js b/src/traces/image/defaults.js index 5d19c45ea0e..24131a669be 100644 --- a/src/traces/image/defaults.js +++ b/src/traces/image/defaults.js @@ -45,4 +45,6 @@ module.exports = function supplyDefaults(traceIn, traceOut) { coerce('hovertemplate'); traceOut._length = null; + + coerce('zorder'); }; diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index faf59ab3308..e5305791d4f 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -133,4 +133,6 @@ module.exports = { ].join(' ') } }), + + zorder: scatterAttrs.zorder }; diff --git a/src/traces/ohlc/defaults.js b/src/traces/ohlc/defaults.js index 9c0dc35086d..2557ecea8ee 100644 --- a/src/traces/ohlc/defaults.js +++ b/src/traces/ohlc/defaults.js @@ -31,6 +31,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('tickwidth'); layout._requestRangeslider[traceOut.xaxis] = true; + + coerce('zorder'); }; function handleDirection(traceIn, traceOut, coerce, direction) { diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 7241fd5784d..84844d8fc60 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -686,4 +686,14 @@ module.exports = { arrayOk: true, description: 'Sets the text font.' }), + zorder: { + valType: 'integer', + dflt: 0, + editType: 'plot', + description: [ + 'Sets the layer on which this trace is displayed, relative to', + 'other SVG traces on the same subplot. SVG traces with higher `zorder`', + 'appear in front of those with lower `zorder`.' + ].join(' ') + } }; diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 31cae6f285c..de63b81e49e 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -30,6 +30,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('xhoverformat'); coerce('yhoverformat'); + coerce('zorder'); + var stackGroupOpts = handleStackDefaults(traceIn, traceOut, layout, coerce); if( layout.scattermode === 'group' && diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index af0e7d406bf..33cad08035b 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -118,5 +118,6 @@ module.exports = { flags: ['a', 'b', 'text', 'name'] }), hoveron: scatterAttrs.hoveron, - hovertemplate: hovertemplateAttrs() + hovertemplate: hovertemplateAttrs(), + zorder: scatterAttrs.zorder }; diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index 0ca56773e37..f1234ea192f 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -75,5 +75,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var hoverOn = coerce('hoveron', dfltHoverOn.join('+') || 'points'); if(hoverOn !== 'fills') coerce('hovertemplate'); + coerce('zorder'); Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js index 18d1358ee13..ca1eecd2500 100644 --- a/src/traces/violin/attributes.js +++ b/src/traces/violin/attributes.js @@ -254,5 +254,6 @@ module.exports = { 'Do the hover effects highlight individual violins', 'or sample points or the kernel density estimate or any combination of them?' ].join(' ') - } + }, + zorder: boxAttrs.zorder }; diff --git a/src/traces/violin/defaults.js b/src/traces/violin/defaults.js index 1224fdd9bc8..fd60511bf9b 100644 --- a/src/traces/violin/defaults.js +++ b/src/traces/violin/defaults.js @@ -50,4 +50,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(!meanLineVisible) traceOut.meanline = {visible: false}; coerce('quartilemethod'); + coerce('zorder'); }; diff --git a/src/traces/waterfall/attributes.js b/src/traces/waterfall/attributes.js index cadd27a18ab..afd5e9f29ad 100644 --- a/src/traces/waterfall/attributes.js +++ b/src/traces/waterfall/attributes.js @@ -151,5 +151,6 @@ module.exports = { }, offsetgroup: barAttrs.offsetgroup, - alignmentgroup: barAttrs.alignmentgroup + alignmentgroup: barAttrs.alignmentgroup, + zorder: barAttrs.zorder }; diff --git a/src/traces/waterfall/defaults.js b/src/traces/waterfall/defaults.js index e5b07290ab6..067d8aded90 100644 --- a/src/traces/waterfall/defaults.js +++ b/src/traces/waterfall/defaults.js @@ -76,6 +76,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { coerce('connector.line.dash'); } } + coerce('zorder'); } function crossTraceDefaults(fullData, fullLayout) { diff --git a/src/traces/waterfall/style.js b/src/traces/waterfall/style.js index 7a0cd005ac4..e46183d394b 100644 --- a/src/traces/waterfall/style.js +++ b/src/traces/waterfall/style.js @@ -10,7 +10,7 @@ var resizeText = require('../bar/uniform_text').resizeText; var styleTextPoints = barStyle.styleTextPoints; function style(gd, cd, sel) { - var s = sel ? sel : d3.select(gd).selectAll('g.waterfalllayer').selectAll('g.trace'); + var s = sel ? sel : d3.select(gd).selectAll('g[class^="waterfalllayer"]').selectAll('g.trace'); resizeText(gd, s, 'waterfall'); s.style('opacity', function(d) { return d[0].trace.opacity; }); diff --git a/test/image/baselines/funnel_axis_with_other_traces.png b/test/image/baselines/funnel_axis_with_other_traces.png index 72a00cc4660..5b57abd3ede 100644 Binary files a/test/image/baselines/funnel_axis_with_other_traces.png and b/test/image/baselines/funnel_axis_with_other_traces.png differ diff --git a/test/image/baselines/zz-zorder-contour.png b/test/image/baselines/zz-zorder-contour.png new file mode 100644 index 00000000000..889288c6dcf Binary files /dev/null and b/test/image/baselines/zz-zorder-contour.png differ diff --git a/test/image/baselines/zz-zorder-heatmap.png b/test/image/baselines/zz-zorder-heatmap.png new file mode 100644 index 00000000000..201879d8305 Binary files /dev/null and b/test/image/baselines/zz-zorder-heatmap.png differ diff --git a/test/image/baselines/zz-zorder-scatter-image.png b/test/image/baselines/zz-zorder-scatter-image.png new file mode 100644 index 00000000000..c2995298f75 Binary files /dev/null and b/test/image/baselines/zz-zorder-scatter-image.png differ diff --git a/test/image/baselines/zz-zorder_basic.png b/test/image/baselines/zz-zorder_basic.png new file mode 100644 index 00000000000..7bc63f50483 Binary files /dev/null and b/test/image/baselines/zz-zorder_basic.png differ diff --git a/test/image/baselines/zz-zorder_funnel_carpet_waterfall.png b/test/image/baselines/zz-zorder_funnel_carpet_waterfall.png new file mode 100644 index 00000000000..b0c546c6039 Binary files /dev/null and b/test/image/baselines/zz-zorder_funnel_carpet_waterfall.png differ diff --git a/test/image/baselines/zz-zorder_violin_box.png b/test/image/baselines/zz-zorder_violin_box.png new file mode 100644 index 00000000000..d709d192f31 Binary files /dev/null and b/test/image/baselines/zz-zorder_violin_box.png differ diff --git a/test/image/mocks/funnel_axis_with_other_traces.json b/test/image/mocks/funnel_axis_with_other_traces.json index 88ab0b35c66..f1819c681a4 100644 --- a/test/image/mocks/funnel_axis_with_other_traces.json +++ b/test/image/mocks/funnel_axis_with_other_traces.json @@ -21,7 +21,8 @@ "type": "histogram", "x": [ 1, 1, 1, 2, 2, 3 - ] + ], + "zorder": -10 }, { "type": "funnel", @@ -40,7 +41,8 @@ 3 ], "xaxis": "x2", - "yaxis": "y2" + "yaxis": "y2", + "zorder": 10 }, { "type": "bar", diff --git a/test/image/mocks/zz-zorder-contour.json b/test/image/mocks/zz-zorder-contour.json new file mode 100644 index 00000000000..b629db1270b --- /dev/null +++ b/test/image/mocks/zz-zorder-contour.json @@ -0,0 +1,22 @@ +{ +"data": [ + { + "type": "scatter", + "name": "scatter", + "y": [-1,0,1,2,3], + "x": [-1,0,1,2,3], + "zorder": 1 + }, { + "type": "contour", + "z": [ + [1, 2, 3], + [2, 0, 0], + [3, 0, 3] + ], + "zorder": 10 + } +], + "layout": { + "width": 400, "height": 400 + } +} diff --git a/test/image/mocks/zz-zorder-heatmap.json b/test/image/mocks/zz-zorder-heatmap.json new file mode 100644 index 00000000000..87e4ec32d58 --- /dev/null +++ b/test/image/mocks/zz-zorder-heatmap.json @@ -0,0 +1,25 @@ +{ +"data": [ + { + "type": "scatter", + "y": [-0.5,0,1,2,3.5], + "x": [-0.5,0,1,2,3.5] + }, { + "x": [ 3, 2, 1, 0 ], + "y": [ 3, 2, 1, 0 ], + "z": [ + [ 98, 97, 96 ], + [ 88, 87, 86 ], + [ 78, 77, 76 ] + ], + "type": "heatmap", + "showscale": false, + "xgap": 5, + "ygap": 5, + "zorder": 10 + } +], + "layout": { + "width": 400, "height": 400 + } +} diff --git a/test/image/mocks/zz-zorder-scatter-image.json b/test/image/mocks/zz-zorder-scatter-image.json new file mode 100644 index 00000000000..3e1deb02a15 --- /dev/null +++ b/test/image/mocks/zz-zorder-scatter-image.json @@ -0,0 +1,24 @@ +{ +"data": [ + { + "type": "scatter", + "y": ["E", "F", "G", "H"], + "x": ["A", "B", "C", "D"] + }, + { + "type": "image", + "zmax": [1, 1, 1], + "x0": "B", + "y0": "F", + "z": [ + [[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 0]], + [[0, 1, 0], [0, null, 1], [1, 0, 0], [0, 0, 0]], + [[1, 0, 0], [1, 0, 0], [0, 0, 1], [0, 0, 0]] + ], + "zorder":10 + } +], + "layout": { + "width": 400, "height": 400 + } +} diff --git a/test/image/mocks/zz-zorder_basic.json b/test/image/mocks/zz-zorder_basic.json new file mode 100644 index 00000000000..bd46aefef80 --- /dev/null +++ b/test/image/mocks/zz-zorder_basic.json @@ -0,0 +1,68 @@ +{ + "data": [ + { + "x": [2, 3, 4, 5], + "y": [4, 3, 9, 5], + "name": "blue", + "type": "scatter", + "marker": {"size": 20}, + "line": {"width": 6}, + "zorder": 5 + }, + { + "x": [2, 3, 4, 5, 6], + "y": [2, 5, 4, 6, 4], + "name": "orange", + "type": "scatter", + "marker": {"size": 20}, + "line": {"width": 6}, + "zorder": 4 + }, + { + "x": [3, 4, 5, 6, 7], + "y": [1, 8, 6, 8, 5], + "name": "green", + "type": "scatter", + "marker": {"size": 20}, + "line": {"width": 6} + }, + { + "x": [4, 5, 6, 7], + "y": [2, 8, 2, 7], + "name": "red", + "type": "scatter", + "marker": {"size": 20}, + "line": {"width": 6}, + "zorder": 300 + }, + { + "x": [2, 3, 4, 5, 6, 7], + "y": [4, 2, 6, 1, 6, 9], + "name": "purple", + "type": "bar", + "zorder": 6 + }, + { + "x": [2, 3, 4, 5, 6, 7], + "y": [5, 4, 10, 10, 6, 9], + "name": "brown", + "type": "bar", + "zorder": -10 + }, + { + "x": [2, 3, 4, 5, 6, 7], + "y": [5, 3, 8, 8, 6, 9], + "name": "pink", + "type": "bar", + "zorder": -5 + } + ], + "layout": { + "xaxis": { + "title": { + "text": "zorder stacking" + } + }, + "barmode": "overlay" + } +} diff --git a/test/image/mocks/zz-zorder_funnel_carpet_waterfall.json b/test/image/mocks/zz-zorder_funnel_carpet_waterfall.json new file mode 100644 index 00000000000..0ad58bb8a00 --- /dev/null +++ b/test/image/mocks/zz-zorder_funnel_carpet_waterfall.json @@ -0,0 +1,73 @@ +{ + "data": [ + { + "x": [-3, -2, 4], + "y": [2, 8, 2], + "name": "scatter zorder=2", + "type": "scatter", + "marker": {"size": 20}, + "line": {"width": 6}, + "zorder": 2 + }, + { + "x": [-3, -2, 2, 3], + "y": [2, 5, -3, -1], + "name": "waterfall zorder=3", + "type": "waterfall", + "zorder": 3 + }, + { + "width": 0.7, + "y": [4, 5, 6], + "x": [7, 5, 7], + "opacity": 1, + "type": "funnel", + "name": "funnel zorder=4", + "zorder": 4 + }, + { + "type": "scattercarpet", + "a": [4, 4.5, 5, 6], + "b": [2.5, 2.5, 2.5, 2.5], + "line": { + "shape": "spline", + "smoothing": 1, + "color": "orange" + }, + "mode": "lines+markers", + "marker": { + "size": 15, + "symbol": "arrow", + "angleref": "previous" + }, + "carpet": "c0", + "name": "scattercarpet zorder=3", + "zorder": 3 + }, + { + "type": "carpet", + "a": [4, 4, 5, 5, 5, 6, 6], + "b": [2, 3, 2, 1, 3, 1, 2], + "y": [3, 4, 5, 7, 8, 6, 8], + "aaxis": { + "smoothing": 1, + "minorgridcount": 9 + }, + "baxis": { + "smoothing": 1, + "minorgridcount": 9 + }, + "color": "red", + "zorder": 10, + "carpet": "c0", + "name": "carpet zorder=10" + } + ], + "layout": { + "xaxis": { + "title": { + "text": "zorder stacking" + } + } + } +} diff --git a/test/image/mocks/zz-zorder_violin_box.json b/test/image/mocks/zz-zorder_violin_box.json new file mode 100644 index 00000000000..b3cf51e11cf --- /dev/null +++ b/test/image/mocks/zz-zorder_violin_box.json @@ -0,0 +1,31 @@ +{ + "data": [ + { + "type": "violin", + "name": "violin", + "fillcolor": "yellow", + "x": [ + 0.1, 0.3, 0.1, 0.9, 0.6, 0.6, + 0.9, 1, 0.3, 0.6, 0.8, 0.5 + ] + }, + { + "type": "box", + "name": "box no zorder", + "y": [-1, 1] + }, + { + "type": "box", + "name": "box below", + "y": [-1, 1], + "zorder": -100 + } + ], + "layout": { + "xaxis": { + "title": { + "text": "zorder stacking" + } + } + } +} diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 5688c193bb4..1f9524e7a6f 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -2547,3 +2547,182 @@ describe('Cartesian plots with css transforms', function() { }); }); }); + +describe('Cartesian taces with zorder', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + var data0 = [ + {x: [1, 2], y: [1, 1], type: 'scatter', marker: {size: 10}, zorder: 10}, + {x: [1, 2], y: [1, 2], type: 'scatter', marker: {size: 30}}, + {x: [1, 2], y: [1, 3], type: 'scatter', marker: {size: 20}, zorder: 5} + ]; + + var data1 = [ + {x: [1, 2], y: [1, 1], type: 'scatter', marker: {size: 10}}, + {x: [1, 2], y: [1, 2], type: 'scatter', marker: {size: 30}, zorder: -5}, + {x: [1, 2], y: [1, 3], type: 'scatter', marker: {size: 20}, zorder: 10}, + ]; + + var barData = [ + {x: [1, 2], y: [2, 4], type: 'bar'}, + {x: [1, 2], y: [4, 2], type: 'bar', zorder: -10} + ]; + + function fig(data) { + return { + data: data, + layout: { + width: 400, height: 400 + } + }; + } + + function assertZIndices(data, expectedData) { + for(var i = 0; i < data.length; i++) { + var zorder = expectedData[i].zorder ? expectedData[i].zorder : 0; + expect(data[i].zorder).toEqual(zorder); + } + } + + function assertZIndicesSorted(data) { + var prevZIndex; + expect(data.length).toBeGreaterThan(0); + for(var i = 0; i < data.length; i++) { + var currentZIndex = data[i].__data__.zorder; + if(prevZIndex !== undefined) { + expect(currentZIndex).toBeGreaterThanOrEqual(prevZIndex); + } + prevZIndex = currentZIndex; + } + } + + it('should be able to update and remove layers for scatter traces in respect to zorder', function(done) { + Plotly.newPlot(gd, fig(data0)) + .then(function() { + var data = gd._fullData; + assertZIndices(data, data0); + }) + .then(function() { + return Plotly.react(gd, fig(data1)); + }) + .then(function() { + var data = gd._fullData; + assertZIndices(data, data1); + }) + .then(function() { + return Plotly.react(gd, fig(barData)); + }) + .then(function() { + var data = gd._fullData; + assertZIndices(data, barData); + var scatterTraces = d3SelectAll('g[class^="scatterlayer"]')[0]; + expect(scatterTraces.length).toBe(0); + var barTraces = d3SelectAll('g[class^="barlayer"]')[0]; + expect(barTraces.length).toBe(2); + }) + .then(function() { + return Plotly.react(gd, fig(barData.concat(data0))); + }) + .then(function() { + var data = gd._fullData; + assertZIndices(data, barData.concat(data0)); + var scatterTraces = d3SelectAll('g[class^="scatterlayer"]')[0]; + expect(scatterTraces.length).toBe(3); + var barTraces = d3SelectAll('g[class^="barlayer"]')[0]; + expect(barTraces.length).toBe(2); + }) + .then(done, done.fail); + }); + + it('should display traces in ascending order', function(done) { + Plotly.newPlot(gd, fig(data0)) + .then(function() { + var tracesData = d3SelectAll('g[class^="scatterlayer"]'); + assertZIndicesSorted(tracesData[0]); + }) + .then(function() { + return Plotly.react(gd, fig(data1)); + }) + .then(function() { + var tracesData = d3SelectAll('g[class^="scatterlayer"]'); + assertZIndicesSorted(tracesData[0]); + }) + .then(done, done.fail); + }); + + it('should display traces in ascending zorder order after restyle', function(done) { + Plotly.newPlot(gd, fig(data0)) + .then(function() { + var tracesData = d3SelectAll('g[class^="scatterlayer"]'); + var data = gd._fullData; + assertZIndices(data, data0); + assertZIndicesSorted(tracesData[0]); + }) + .then(function() { + return Plotly.restyle(gd, 'marker.size', 20); + }) + .then(function() { + var tracesData = d3SelectAll('g[class^="scatterlayer"]'); + var data = gd._fullData; + assertZIndices(data, data0); + assertZIndicesSorted(tracesData[0]); + }) + .then(function() { + return Plotly.react(gd, fig(data1)); + }) + .then(function() { + var tracesData = d3SelectAll('g[class^="scatterlayer"]'); + var data = gd._fullData; + assertZIndices(data, data1); + assertZIndicesSorted(tracesData[0]); + }) + .then(function() { + return Plotly.restyle(gd, 'marker.size', 20); + }) + .then(function() { + var tracesData = d3SelectAll('g[class^="scatterlayer"]'); + var data = gd._fullData; + assertZIndices(data, data1); + assertZIndicesSorted(tracesData[0]); + }) + .then(done, done.fail); + }); + + ['bar', 'waterfall', 'funnel'].forEach(function(traceType) { + it('should display ' + traceType + ' traces in ascending order', function(done) { + var _Data = [ + {x: [1, 2], y: [2, 4], type: traceType}, + {x: [1, 2], y: [4, 2], type: traceType, zorder: -10} + ]; + var _Class = 'g[class^="' + traceType + 'layer"]'; + Plotly.newPlot(gd, fig(_Data)) + .then(function() { + var data = gd._fullData; + assertZIndices(data, _Data); + var tracesData = d3SelectAll(_Class); + assertZIndicesSorted(tracesData[0]); + }) + .then(function() { + return Plotly.restyle(gd, 'barmode', 'overlay'); + }) + .then(function() { + var tracesData = d3SelectAll(_Class); + assertZIndicesSorted(tracesData[0]); + }) + .then(function() { + return Plotly.restyle(gd, 'barmode', 'stack'); + }) + .then(function() { + var tracesData = d3SelectAll(_Class); + assertZIndicesSorted(tracesData[0]); + }) + .then(done, done.fail); + }); + }); +}); diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index c685eaf32fd..d8984a719c6 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -144,6 +144,21 @@ var BOXEVENTS = [1, 2, 1]; // assumes 5 points in the lasso path var LASSOEVENTS = [4, 2, 1]; +var mockZorder = { + data: [ + {x: [1, 2], y: [1, 1], type: 'scatter', zorder: 10, marker: {size: 50}}, + {x: [1, 2], y: [1, 2], type: 'scatter', marker: {size: 50}}, + {x: [1, 2], y: [1, 3], type: 'scatter', zorder: 5, marker: {size: 50}} + ], + layout: { + width: 400, + height: 400, + clickmode: 'event+select', + dragmode: 'select', + hovermode: 'closest' + } +}; + describe('Click-to-select', function() { var mock14Pts = { 1: { x: 134, y: 116 }, @@ -264,6 +279,84 @@ describe('Click-to-select', function() { .then(done, done.fail); }); + it('selects a single data point when being clicked on trace with zorder', function(done) { + _newPlot(gd, mockZorder.data, mockZorder.layout) + .then(function() { + return _immediateClickPt({ x: 270, y: 150 }); + }) + .then(function() { + assertSelectedPoints([], [], [1]); + return _clickPt({ x: 270, y: 200 }); + }) + .then(function() { + assertSelectedPoints([], [1], []); + return _clickPt({ x: 270, y: 250 }); + }) + .then(done, done.fail); + }); + + it('should only select top most zorder trace if overlapping position on single click', function(done) { + _newPlot(gd, mockZorder.data, mockZorder.layout) + .then(function() { + return _immediateClickPt({ x: 130, y: 250 }); + }) + .then(function() { + assertSelectedPoints([], [1], []); + }) + .then(done, done.fail); + }); + + it('should lasso select all overlapping points regardless of zorder', function(done) { + mockZorder.layout.dragmode = 'lasso'; + _newPlot(gd, mockZorder.data, mockZorder.layout) + .then(function() { + drag([[200, 200], [200, 300], [100, 300], [100, 200], [200, 200]]); + }) + .then(function() { + expect(gd.data[0].selectedpoints).toEqual([0]); + expect(gd.data[1].selectedpoints).toEqual([0]); + expect(gd.data[2].selectedpoints).toEqual([0]); + }) + .then(function() { + return doubleClick(200, 200); // Clear selection + }) + .then(function() { + drag([[200, 100], [200, 300], [300, 300], [300, 100], [200, 100]]); + }) + .then(function() { + expect(gd.data[0].selectedpoints).toEqual([1]); + expect(gd.data[1].selectedpoints).toEqual([1]); + expect(gd.data[2].selectedpoints).toEqual([1]); + }) + .then(done, done.fail); + }); + + it('should box select all overlapping points regardless of zorder', function(done) { + mockZorder.layout.dragmode = 'select'; + _newPlot(gd, mockZorder.data, mockZorder.layout) + .then(function() { + drag([[200, 200], [100, 300]]); + }) + .then(function() { + expect(gd.data[0].selectedpoints).toEqual([0]); + expect(gd.data[1].selectedpoints).toEqual([0]); + expect(gd.data[2].selectedpoints).toEqual([0]); + }) + .then(function() { + return doubleClick(200, 200); // Clear selection + }) + .then(function() { + drag([[200, 100], [300, 300]]); + }) + .then(function() { + expect(gd.data[0].selectedpoints).toEqual([1]); + expect(gd.data[1].selectedpoints).toEqual([1]); + expect(gd.data[2].selectedpoints).toEqual([1]); + }) + .then(done, done.fail); + }); + + it('cleanly clears and starts selections although add/subtract mode on', function(done) { plotMock14() .then(function() { diff --git a/test/plot-schema.json b/test/plot-schema.json index 9b3a8816729..a0e3a7d1282 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -14841,6 +14841,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -17514,6 +17520,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -18062,6 +18074,12 @@ "dflt": "", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -19463,6 +19481,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -23960,6 +23984,12 @@ }, "valType": "number" }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" + }, "zsrc": { "description": "Sets the source reference on Chart Studio Cloud for `z`.", "editType": "none", @@ -25009,6 +25039,12 @@ }, "valType": "number" }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" + }, "zsrc": { "description": "Sets the source reference on Chart Studio Cloud for `z`.", "editType": "none", @@ -27394,6 +27430,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -29386,6 +29428,12 @@ }, "valType": "number" }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" + }, "zsmooth": { "description": "Picks a smoothing algorithm use to smooth `z` data.", "dflt": false, @@ -32125,6 +32173,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -36519,6 +36573,12 @@ ], "valType": "info_array" }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" + }, "zsmooth": { "description": "Picks a smoothing algorithm used to smooth `z` data. This only applies for image traces that use the `source` attribute.", "dflt": false, @@ -40477,6 +40537,12 @@ "dflt": "", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -47221,6 +47287,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -51448,6 +51520,12 @@ "dflt": "y", "editType": "calc+clearAxisTypes", "valType": "subplotid" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -73396,6 +73474,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [ @@ -75584,6 +75668,12 @@ "description": "Sets the source reference on Chart Studio Cloud for `y`.", "editType": "none", "valType": "string" + }, + "zorder": { + "description": "Sets the layer on which this trace is displayed, relative to other SVG traces on the same subplot. SVG traces with higher `zorder` appear in front of those with lower `zorder`.", + "dflt": 0, + "editType": "plot", + "valType": "integer" } }, "categories": [