diff --git a/devtools/test_dashboard/index.html b/devtools/test_dashboard/index.html index d52115330d7..f553c983514 100644 --- a/devtools/test_dashboard/index.html +++ b/devtools/test_dashboard/index.html @@ -21,7 +21,6 @@ - diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 8e6d5c71251..05ffc99a260 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -180,8 +180,8 @@ exports.lsInner = function(gd) { Drawing.setClipUrl(plotinfo.plot, plotClipId); - for(i = 0; i < cartesianConstants.layers.length; i++) { - var layer = cartesianConstants.layers[i]; + for(i = 0; i < cartesianConstants.traceLayerClasses.length; i++) { + var layer = cartesianConstants.traceLayerClasses[i]; if(layer !== 'scatterlayer') { plotinfo.plot.selectAll('g.' + layer).call(Drawing.setClipUrl, layerClipId); } diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index f502bcee5f5..80c6f48af50 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -51,19 +51,24 @@ module.exports = { DFLTRANGEX: [-1, 6], DFLTRANGEY: [-1, 4], - // Layers to keep plot types in the right order. + // Layers to keep trace types in the right order. // from back to front: // 1. heatmaps, 2D histos and contour maps // 2. bars / 1D histos // 3. errorbars for bars and scatter // 4. scatter // 5. box plots - layers: [ + traceLayerClasses: [ 'imagelayer', 'maplayer', 'barlayer', 'carpetlayer', 'boxlayer', 'scatterlayer' - ] + ], + + layerValue2layerClass: { + 'above traces': 'above', + 'below traces': 'below' + } }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index fa93d51e5b4..c1e2b4cda46 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -743,7 +743,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { var plotDx = xa2._offset - clipDx / xScaleFactor2, plotDy = ya2._offset - clipDy / yScaleFactor2; - fullLayout._defs.selectAll('#' + subplot.clipId) + fullLayout._defs.select('#' + subplot.clipId + '> rect') .call(Drawing.setTranslate, clipDx, clipDy) .call(Drawing.setScale, xScaleFactor2, yScaleFactor2); diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 7548a608251..1b7ae929c9b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -296,6 +296,8 @@ function makeSubplotData(gd) { function makeSubplotLayer(plotinfo) { var plotgroup = plotinfo.plotgroup; var id = plotinfo.id; + var xLayer = constants.layerValue2layerClass[plotinfo.xaxis.layer]; + var yLayer = constants.layerValue2layerClass[plotinfo.yaxis.layer]; if(!plotinfo.mainplot) { var backLayer = joinLayer(plotgroup, 'g', 'layer-subplot'); @@ -308,19 +310,36 @@ function makeSubplotLayer(plotinfo) { plotinfo.zerolinelayer = joinLayer(plotgroup, 'g', 'zerolinelayer'); plotinfo.overzero = joinLayer(plotgroup, 'g', 'overzero'); + joinLayer(plotgroup, 'path', 'xlines-below'); + joinLayer(plotgroup, 'path', 'ylines-below'); + plotinfo.overlinesBelow = joinLayer(plotgroup, 'g', 'overlines-below'); + + joinLayer(plotgroup, 'g', 'xaxislayer-below'); + joinLayer(plotgroup, 'g', 'yaxislayer-below'); + plotinfo.overaxesBelow = joinLayer(plotgroup, 'g', 'overaxes-below'); + plotinfo.plot = joinLayer(plotgroup, 'g', 'plot'); plotinfo.overplot = joinLayer(plotgroup, 'g', 'overplot'); - plotinfo.xlines = joinLayer(plotgroup, 'path', 'xlines'); - plotinfo.ylines = joinLayer(plotgroup, 'path', 'ylines'); - plotinfo.overlines = joinLayer(plotgroup, 'g', 'overlines'); + joinLayer(plotgroup, 'path', 'xlines-above'); + joinLayer(plotgroup, 'path', 'ylines-above'); + plotinfo.overlinesAbove = joinLayer(plotgroup, 'g', 'overlines-above'); + + joinLayer(plotgroup, 'g', 'xaxislayer-above'); + joinLayer(plotgroup, 'g', 'yaxislayer-above'); + plotinfo.overaxesAbove = joinLayer(plotgroup, 'g', 'overaxes-above'); - plotinfo.xaxislayer = joinLayer(plotgroup, 'g', 'xaxislayer'); - plotinfo.yaxislayer = joinLayer(plotgroup, 'g', 'yaxislayer'); - plotinfo.overaxes = joinLayer(plotgroup, 'g', 'overaxes'); + // set refs to correct layers as determined by 'axis.layer' + plotinfo.xlines = plotgroup.select('.xlines-' + xLayer); + plotinfo.ylines = plotgroup.select('.ylines-' + yLayer); + plotinfo.xaxislayer = plotgroup.select('.xaxislayer-' + xLayer); + plotinfo.yaxislayer = plotgroup.select('.yaxislayer-' + yLayer); } else { var mainplotinfo = plotinfo.mainplotinfo; + var mainplotgroup = mainplotinfo.plotgroup; + var xId = id + '-x'; + var yId = id + '-y'; // now make the components of overlaid subplots // overlays don't have backgrounds, and append all @@ -330,17 +349,29 @@ function makeSubplotLayer(plotinfo) { plotinfo.gridlayer = joinLayer(mainplotinfo.overgrid, 'g', id); plotinfo.zerolinelayer = joinLayer(mainplotinfo.overzero, 'g', id); + joinLayer(mainplotinfo.overlinesBelow, 'path', xId); + joinLayer(mainplotinfo.overlinesBelow, 'path', yId); + joinLayer(mainplotinfo.overaxesBelow, 'g', xId); + joinLayer(mainplotinfo.overaxesBelow, 'g', yId); + plotinfo.plot = joinLayer(mainplotinfo.overplot, 'g', id); - plotinfo.xlines = joinLayer(mainplotinfo.overlines, 'path', id + '-x'); - plotinfo.ylines = joinLayer(mainplotinfo.overlines, 'path', id + '-y'); - plotinfo.xaxislayer = joinLayer(mainplotinfo.overaxes, 'g', id + '-x'); - plotinfo.yaxislayer = joinLayer(mainplotinfo.overaxes, 'g', id + '-y'); + + joinLayer(mainplotinfo.overlinesAbove, 'path', xId); + joinLayer(mainplotinfo.overlinesAbove, 'path', yId); + joinLayer(mainplotinfo.overaxesAbove, 'g', xId); + joinLayer(mainplotinfo.overaxesAbove, 'g', yId); + + // set refs to correct layers as determined by 'abovetraces' + plotinfo.xlines = mainplotgroup.select('.overlines-' + xLayer).select('.' + xId); + plotinfo.ylines = mainplotgroup.select('.overlines-' + yLayer).select('.' + yId); + plotinfo.xaxislayer = mainplotgroup.select('.overaxes-' + xLayer).select('.' + xId); + plotinfo.yaxislayer = mainplotgroup.select('.overaxes-' + yLayer).select('.' + yId); } // common attributes for all subplots, overlays or not - for(var i = 0; i < constants.layers.length; i++) { - joinLayer(plotinfo.plot, 'g', constants.layers[i]); + for(var i = 0; i < constants.traceLayerClasses.length; i++) { + joinLayer(plotinfo.plot, 'g', constants.traceLayerClasses[i]); } plotinfo.xlines diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index ef75a5e91c5..90855d7452a 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -573,6 +573,20 @@ module.exports = { 'If *false*, this axis does not overlay any same-letter axes.' ].join(' ') }, + layer: { + valType: 'enumerated', + values: ['above traces', 'below traces'], + dflt: 'above traces', + role: 'info', + description: [ + 'Sets the layer on which this axis is displayed.', + 'If *above traces*, this axis is displayed above all the subplot\'s traces', + 'If *below traces*, this axis is displayed below all the subplot\'s traces,', + 'but above the grid lines.', + 'Useful when used together with scatter-like traces with `cliponaxis`', + 'set to *false* to show markers and/or text nodes above this axis.' + ].join(' ') + }, domain: { valType: 'info_array', role: 'info', diff --git a/src/plots/cartesian/position_defaults.js b/src/plots/cartesian/position_defaults.js index 94ea5ed4e73..ba797fbe038 100644 --- a/src/plots/cartesian/position_defaults.js +++ b/src/plots/cartesian/position_defaults.js @@ -59,5 +59,7 @@ module.exports = function handlePositionDefaults(containerIn, containerOut, coer Lib.noneOrAll(containerIn.domain, containerOut.domain, [0, 1]); } + coerce('layer'); + return containerOut; }; diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js index 37557d6cc5f..2994b07efe9 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -137,7 +137,7 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo var xa2 = subplot.xaxis; var ya2 = subplot.yaxis; - fullLayout._defs.selectAll('#' + subplot.clipId) + fullLayout._defs.select('#' + subplot.clipId + '> rect') .call(Drawing.setTranslate, 0, 0) .call(Drawing.setScale, 1, 1); @@ -221,7 +221,7 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo var plotDx = xa2._offset - fracDx, plotDy = ya2._offset - fracDy; - fullLayout._defs.selectAll('#' + subplot.clipId) + fullLayout._defs.select('#' + subplot.clipId + '> rect') .call(Drawing.setTranslate, clipDx, clipDy) .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); diff --git a/src/plots/plots.js b/src/plots/plots.js index 1a95bad876b..ab54d91ec7b 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -638,9 +638,11 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa var ids = Plotly.Axes.getSubplots(mockGd); for(var i = 0; i < ids.length; i++) { - var id = ids[i], - oldSubplot = oldSubplots[id], - plotinfo; + var id = ids[i]; + var oldSubplot = oldSubplots[id]; + var xaxis = Plotly.Axes.getFromId(mockGd, id, 'x'); + var yaxis = Plotly.Axes.getFromId(mockGd, id, 'y'); + var plotinfo; if(oldSubplot) { plotinfo = newSubplots[id] = oldSubplot; @@ -648,14 +650,23 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa if(plotinfo._scene2d) { plotinfo._scene2d.updateRefs(newFullLayout); } - } - else { + + if(plotinfo.xaxis.layer !== xaxis.layer) { + plotinfo.xlines.attr('d', null); + plotinfo.xaxislayer.selectAll('*').remove(); + } + + if(plotinfo.yaxis.layer !== yaxis.layer) { + plotinfo.ylines.attr('d', null); + plotinfo.yaxislayer.selectAll('*').remove(); + } + } else { plotinfo = newSubplots[id] = {}; plotinfo.id = id; } - plotinfo.xaxis = Plotly.Axes.getFromId(mockGd, id, 'x'); - plotinfo.yaxis = Plotly.Axes.getFromId(mockGd, id, 'y'); + plotinfo.xaxis = xaxis; + plotinfo.yaxis = yaxis; // By default, we clip at the subplot level, // but if one trace on a given subplot has *cliponaxis* set to false, diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 0d4431f7a1a..c5840d5e9ec 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -292,7 +292,7 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { _this.plotContainer.selectAll('.scatterlayer,.maplayer') .attr('transform', plotTransform); - _this.clipDefRelative.attr('transform', null); + _this.clipDefRelative.select('path').attr('transform', null); // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle @@ -619,7 +619,7 @@ proto.initInteractions = function() { .attr('transform', plotTransform); var plotTransform2 = 'translate(' + -dx + ',' + -dy + ')'; - _this.clipDefRelative.attr('transform', plotTransform2); + _this.clipDefRelative.select('path').attr('transform', plotTransform2); // move the ticks _this.aaxis.range = [mins.a, _this.sum - mins.b - mins.c]; diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 2076f1012b7..f48092247b3 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -186,7 +186,9 @@ module.exports = { editType: 'doplot', description: [ 'Determines whether or not markers and text nodes', - 'are clipped about the subplot axes.' + 'are clipped about the subplot axes.', + 'To show markers and text nodes above axis lines and tick labels,', + 'make sure to set `xaxis.layer` and `yaxis.layer` to *below traces*.' ].join(' ') }, diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 2155cc03e96..eec2d39e05b 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -404,8 +404,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition var trace = d[0].trace, s = d3.select(this), showMarkers = subTypes.hasMarkers(trace), - showText = subTypes.hasText(trace), - hasClipOnAxisFalse = trace.cliponaxis === false; + showText = subTypes.hasText(trace); var keyFunc = getKeyFunc(trace), markerFilter = hideFilter, @@ -450,7 +449,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition if(hasNode) { Drawing.singlePointStyle(d, sel, trace, markerScale, lineScale, gd); - if(hasClipOnAxisFalse) { + if(plotinfo.layerClipId) { Drawing.hideOutsideRangePoint(d, sel, xa, ya); } @@ -486,7 +485,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition hasNode = Drawing.translatePoint(d, sel, xa, ya); if(hasNode) { - if(hasClipOnAxisFalse) { + if(plotinfo.layerClipId) { Drawing.hideOutsideRangePoint(d, g, xa, ya); } } else { @@ -525,6 +524,13 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition .each(makePoints); join.exit().remove(); + + // lastly, clip points groups of `cliponaxis !== false` traces + // on `plotinfo._hasClipOnAxisFalse === true` subplots + join.each(function(d) { + var hasClipOnAxisFalse = d[0].trace.cliponaxis === false; + Drawing.setClipUrl(d3.select(this), hasClipOnAxisFalse ? null : plotinfo.layerClipId); + }); } function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) { diff --git a/tasks/cibundle.js b/tasks/cibundle.js index cd5ff768a4c..6f6e13d90f0 100644 --- a/tasks/cibundle.js +++ b/tasks/cibundle.js @@ -16,6 +16,7 @@ var _bundle = require('./util/browserify_wrapper'); _bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyBuild, { standalone: 'Plotly', pathToMinBundle: constants.pathToPlotlyDistMin, + debug: true }); // Browserify the geo assets diff --git a/tasks/util/browserify_wrapper.js b/tasks/util/browserify_wrapper.js index 0e13fa01874..7050706eb93 100644 --- a/tasks/util/browserify_wrapper.js +++ b/tasks/util/browserify_wrapper.js @@ -7,6 +7,7 @@ var UglifyJS = require('uglify-js'); var constants = require('./constants'); var compressAttributes = require('./compress_attributes'); var patchMinified = require('./patch_minified'); +var strictD3 = require('./strict_d3'); /** Convenience browserify wrapper * @@ -32,16 +33,20 @@ module.exports = function _bundle(pathToIndex, pathToBundle, opts) { opts = opts || {}; // do we output a minified file? - var pathToMinBundle = opts.pathToMinBundle, - outputMinified = !!pathToMinBundle && !opts.debug; + var pathToMinBundle = opts.pathToMinBundle; + var outputMinified = !!pathToMinBundle; var browserifyOpts = {}; browserifyOpts.standalone = opts.standalone; browserifyOpts.debug = opts.debug; browserifyOpts.transform = outputMinified ? [compressAttributes] : []; - var b = browserify(pathToIndex, browserifyOpts), - bundleWriteStream = fs.createWriteStream(pathToBundle); + if(opts.debug) { + browserifyOpts.transform.push(strictD3); + } + + var b = browserify(pathToIndex, browserifyOpts); + var bundleWriteStream = fs.createWriteStream(pathToBundle); bundleWriteStream.on('finish', function() { logger(pathToBundle); diff --git a/tasks/util/constants.js b/tasks/util/constants.js index 9c8b1aaf828..7cffc50cfe0 100644 --- a/tasks/util/constants.js +++ b/tasks/util/constants.js @@ -58,6 +58,7 @@ module.exports = { pathToTestDashboardBundle: path.join(pathToBuild, 'test_dashboard-bundle.js'), pathToImageViewerBundle: path.join(pathToBuild, 'image_viewer-bundle.js'), + pathToImageTest: pathToImageTest, pathToTestImageMocks: path.join(pathToImageTest, 'mocks/'), pathToTestImageBaselines: path.join(pathToImageTest, 'baselines/'), pathToTestImages: path.join(pathToBuild, 'test_images/'), diff --git a/tasks/util/strict_d3.js b/tasks/util/strict_d3.js new file mode 100644 index 00000000000..1e51ab8912b --- /dev/null +++ b/tasks/util/strict_d3.js @@ -0,0 +1,26 @@ +var path = require('path'); +var transformTools = require('browserify-transform-tools'); +var constants = require('./constants'); + +var pathToStrictD3Module = path.join( + constants.pathToImageTest, + 'strict-d3.js' +); + +/** + * Transform `require('d3')` expressions to `require(/path/to/strict-d3.js)` + */ + +module.exports = transformTools.makeRequireTransform('requireTransform', + { evaluateArguments: true, jsFilesOnly: true }, + function(args, opts, cb) { + var pathIn = args[0]; + var pathOut; + + if(pathIn === 'd3' && opts.file !== pathToStrictD3Module) { + pathOut = 'require(\'' + pathToStrictD3Module + '\')'; + } + + if(pathOut) return cb(null, pathOut); + else return cb(); + }); diff --git a/tasks/util/watchified_bundle.js b/tasks/util/watchified_bundle.js index e2ab22068ee..864716dca6f 100644 --- a/tasks/util/watchified_bundle.js +++ b/tasks/util/watchified_bundle.js @@ -7,6 +7,7 @@ var prettySize = require('prettysize'); var constants = require('./constants'); var common = require('./common'); var compressAttributes = require('./compress_attributes'); +var strictD3 = require('./strict_d3'); /** * Make a plotly.js browserify bundle function watched by watchify. @@ -22,7 +23,7 @@ module.exports = function makeWatchifiedBundle(onFirstBundleCallback) { var b = browserify(constants.pathToPlotlyIndex, { debug: true, standalone: 'Plotly', - transform: [compressAttributes], + transform: [strictD3, compressAttributes], cache: {}, packageCache: {}, plugin: [watchify] diff --git a/test/image/baselines/cliponaxis_false.png b/test/image/baselines/cliponaxis_false.png index aaa7ecb5d2d..43f2dce8423 100644 Binary files a/test/image/baselines/cliponaxis_false.png and b/test/image/baselines/cliponaxis_false.png differ diff --git a/test/image/index.html b/test/image/index.html index 9907839a5d5..efa5c6d2edc 100644 --- a/test/image/index.html +++ b/test/image/index.html @@ -5,7 +5,6 @@ - diff --git a/test/image/mocks/cliponaxis_false.json b/test/image/mocks/cliponaxis_false.json index 1053fc82087..22b38fd91c0 100644 --- a/test/image/mocks/cliponaxis_false.json +++ b/test/image/mocks/cliponaxis_false.json @@ -21,7 +21,7 @@ "opacity": 1 }, "text": "not
clipped", - "textposition": ["left", "left", "bottom", "right", "right", "top"], + "textposition": ["left", "left", "bottom", "bottom left", "top left", "top"], "error_y": { "array": [0.1, 0.2, 0.3, 0.1, 0.3, 0.2], "arrayminus": [0.1, 0.2, 0.1, 0.1, 0.1, 0.2] @@ -32,23 +32,82 @@ }, "cliponaxis": false + }, + { + "mode": "markers", + "x": [1, 1, 2, 3, 3, 2], + "y": [2, 1, 1, 1, 2, 2], + "marker": { + "size": [15, 20, 40, 25, 50, 40], + "opacity": 1 + }, + "cliponaxis": false, + "xaxis": "x2", + "yaxis": "y2" + }, + { + "mode": "markers+text", + "x": [1, 1.5, 2.5, 3, 2.5, 1.5], + "y": [1.5, 1, 1, 1.5, 2, 2], + "marker": { + "size": 30 + }, + "text": "clipped
should not see this!", + "textposition": ["left", "bottom", "bottom", "right", "top", "top"], + "cliponaxis": true } ], "layout": { "xaxis": { + "domain": [0, 0.48], "range": [1, 3], "showline": true, "linewidth": 2, - "mirror": "all" + "mirror": "all", + "layer": "below traces" }, "yaxis": { "range": [1, 2], "showline": true, "linewidth": 2, - "mirror": "all" + "mirror": "all", + "layer": "below traces" + }, + "xaxis2": { + "anchor": "y2", + "range": [1, 3], + "domain": [0.52, 1], + "showline": true, + "linewidth": 2, + "mirror": "all", + "layer": "above traces" + }, + "yaxis2": { + "anchor": "x2", + "range": [1, 2], + "showline": true, + "linewidth": 2, + "mirror": "all", + "side": "right", + "layer": "above traces" }, + "annotations": [{ + "showarrow": false, + "xref": "paper", "yref": "paper", + "x": 0.24, "y": 0.5, + "xanchor": "center", + "text": "axis layer
below traces" + }, { + "showarrow": false, + "xref": "paper", "yref": "paper", + "x": 0.74, "y": 0.5, + "xanchor": "center", + "text": "axis layer
above traces" + }], "showlegend": false, "dragmode": "pan", - "hovermode": "closest" + "hovermode": "closest", + "height": 500, + "width": 800 } } diff --git a/test/image/strict-d3.js b/test/image/strict-d3.js index d54336e3dfb..fec9f4cbec0 100644 --- a/test/image/strict-d3.js +++ b/test/image/strict-d3.js @@ -2,77 +2,69 @@ * strict-d3: wrap selection.style to prohibit specific incorrect style values * that are known to cause problems in IE (at least IE9) */ +'use strict'; -/* global Plotly:false */ -(function() { - 'use strict'; +var d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); - var selProto = Plotly.d3.selection.prototype; +var selProto = d3.selection.prototype; +var originalSelAttr = selProto.attr; +var originalSelStyle = selProto.style; - var originalSelStyle = selProto.style; +selProto.attr = function() { + var sel = this; + var obj = arguments[0]; - selProto.style = function() { - var sel = this, - obj = arguments[0]; - - if(sel.size()) { - if(typeof obj === 'string') { - checkVal(sel, obj, arguments[1]); - } - else { - Object.keys(obj).forEach(function(key) { checkVal(sel, key, obj[key]); }); - } + if(sel.size()) { + if(typeof obj === 'string') { + checkAttrVal(sel, obj, arguments[1]); + } else { + Object.keys(obj).forEach(function(key) { checkAttrVal(sel, key, obj[key]); }); } + } - return originalSelStyle.apply(sel, arguments); - }; + return originalSelAttr.apply(sel, arguments); +}; - function checkVal(sel, key, val) { - if(typeof val === 'string') { - // in case of multipart styles (stroke-dasharray, margins, etc) - // test each part separately - val.split(/[, ]/g).forEach(function(valPart) { - var pxSplit = valPart.length - 2; - if(valPart.substr(pxSplit) === 'px' && !isNumeric(valPart.substr(0, pxSplit))) { - throw new Error('d3 selection.style called with value: ' + val); - } - }); - } +selProto.style = function() { + var sel = this; + var obj = arguments[0]; - // Microsoft browsers incl. "Edge" don't support CSS transform on SVG elements - if(key === 'transform' && sel.node() instanceof SVGElement) { - throw new Error('d3 selection.style called on an SVG element with key: ' + key); + if(sel.size()) { + if(typeof obj === 'string') { + checkStyleVal(sel, obj, arguments[1]); + } else { + Object.keys(obj).forEach(function(key) { checkStyleVal(sel, key, obj[key]); }); } } - // below ripped from fast-isnumeric so I don't need to build this file + return originalSelStyle.apply(sel, arguments); +}; - function allBlankCharCodes(str) { - var l = str.length, - a; - for(var i = 0; i < l; i++) { - a = str.charCodeAt(i); - if((a < 9 || a > 13) && (a !== 32) && (a !== 133) && (a !== 160) && - (a !== 5760) && (a !== 6158) && (a < 8192 || a > 8205) && - (a !== 8232) && (a !== 8233) && (a !== 8239) && (a !== 8287) && - (a !== 8288) && (a !== 12288) && (a !== 65279)) { - return false; - } - } - return true; +function checkAttrVal(sel, key) { + // setting the transform attribute on a does not + // work in Chrome, IE and Edge + if(sel.node().nodeName === 'clipPath' && key === 'transform') { + throw new Error('d3 selection.attr called with key \'transform\' on a clipPath node'); } +} - function isNumeric(n) { - var type = typeof n; - if(type === 'string') { - var original = n; - n = +n; - // whitespace strings cast to zero - filter them out - if(n === 0 && allBlankCharCodes(original)) return false; - } - else if(type !== 'number') return false; +function checkStyleVal(sel, key, val) { + if(typeof val === 'string') { + // in case of multipart styles (stroke-dasharray, margins, etc) + // test each part separately + val.split(/[, ]/g).forEach(function(valPart) { + var pxSplit = valPart.length - 2; + if(valPart.substr(pxSplit) === 'px' && !isNumeric(valPart.substr(0, pxSplit))) { + throw new Error('d3 selection.style called with value: ' + val); + } + }); + } - return n - n < 1; + // Microsoft browsers incl. "Edge" don't support CSS transform on SVG elements + if(key === 'transform' && sel.node() instanceof SVGElement) { + throw new Error('d3 selection.style called on an SVG element with key: ' + key); } +} -})(); +module.exports = d3; diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index 8f8af91fbf7..43408f66367 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -95,6 +95,7 @@ if(isFullSuite) { } var pathToShortcutPath = path.join(__dirname, '..', '..', 'tasks', 'util', 'shortcut_paths.js'); +var pathToStrictD3 = path.join(__dirname, '..', '..', 'tasks', 'util', 'strict_d3.js'); var pathToMain = path.join(__dirname, '..', '..', 'lib', 'index.js'); var pathToJQuery = path.join(__dirname, 'assets', 'jquery-1.8.3.min.js'); var pathToIE9mock = path.join(__dirname, 'assets', 'ie9_mock.js'); @@ -193,7 +194,7 @@ func.defaultConfig = { }, browserify: { - transform: [pathToShortcutPath], + transform: [pathToStrictD3, pathToShortcutPath], extensions: ['.js'], watch: !argv.nowatch, debug: true diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index f700e378ed6..324d3789f87 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -403,4 +403,79 @@ describe('subplot creation / deletion:', function() { .catch(failTest) .then(done); }); + + it('should clear obsolete content out of axis layers when relayout\'ing *layer*', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/overlaying-axis-lines.json')); + + function assertPathDatum(sel, expected, msg) { + expect(sel.attr('d') === null ? false : true).toBe(expected, msg); + } + + function assertChildrenCnt(sel, expected, msg) { + expect(sel.selectAll('*').size()).toBe(expected, msg); + } + + function _assert(xBelow, yBelow, xAbove, yAbove) { + var g = d3.select('.subplot.xy'); + + assertPathDatum(g.select('.xlines-below'), xBelow[0], 'xlines below'); + assertChildrenCnt(g.select('.xaxislayer-below'), xBelow[1], 'xaxislayer below'); + + assertPathDatum(g.select('.ylines-below'), yBelow[0], 'ylines below'); + assertChildrenCnt(g.select('.yaxislayer-below'), yBelow[1], 'yaxislayer below'); + + assertPathDatum(g.select('.xlines-above'), xAbove[0], 'xlines above'); + assertChildrenCnt(g.select('.xaxislayer-above'), xAbove[1], 'xaxislayer above'); + + assertPathDatum(g.select('.ylines-above'), yAbove[0], 'ylines above'); + assertChildrenCnt(g.select('.yaxislayer-above'), yAbove[1], 'yaxislayer above'); + } + + Plotly.plot(gd, fig).then(function() { + _assert( + [false, 0], + [false, 0], + [true, 10], + [true, 10] + ); + return Plotly.relayout(gd, 'xaxis.layer', 'below traces'); + }) + .then(function() { + _assert( + [true, 10], + [false, 0], + [false, 0], + [true, 10] + ); + return Plotly.relayout(gd, 'yaxis.layer', 'below traces'); + }) + .then(function() { + _assert( + [true, 10], + [true, 10], + [false, 0], + [false, 0] + ); + return Plotly.relayout(gd, { 'xaxis.layer': null, 'yaxis.layer': null }); + }) + .then(function() { + _assert( + [false, 0], + [false, 0], + [true, 10], + [true, 10] + ); + return Plotly.relayout(gd, { 'xaxis.layer': 'below traces', 'yaxis.layer': 'below traces' }); + }) + .then(function() { + _assert( + [true, 10], + [true, 10], + [false, 0], + [false, 0] + ); + }) + .catch(failTest) + .then(done); + }); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index b333989aeae..b95b6482db0 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -1,5 +1,6 @@ var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); @@ -38,13 +39,18 @@ describe('Test Plots', function() { var oldFullLayout = { _plots: { xy: { plot: {} } }, - xaxis: { c2p: function() {} }, - yaxis: { _m: 20 }, + xaxis: { c2p: function() {}, layer: 'above traces' }, + yaxis: { _m: 20, layer: 'above traces' }, scene: { _scene: {} }, annotations: [{ _min: 10, }, { _max: 20 }], someFunc: function() {} }; + Lib.extendFlat(oldFullLayout._plots.xy, { + xaxis: oldFullLayout.xaxis, + yaxis: oldFullLayout.yaxis + }); + var newData = [{ type: 'scatter3d', z: [1, 2, 3, 4] diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 7dfd8572452..ec42e45a2e4 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -675,7 +675,7 @@ describe('scatter hoverPoints', function() { }); }); -describe('Test scatter *clipnaxis*', function() { +describe('Test scatter *clipnaxis*:', function() { afterEach(destroyGraphDiv); it('should show/hide point/text/errorbars in clipped and non-clipped layers', function(done) { @@ -684,7 +684,7 @@ describe('Test scatter *clipnaxis*', function() { var xRange0 = fig.layout.xaxis.range.slice(); var yRange0 = fig.layout.yaxis.range.slice(); - // only show *cliponaxis: false* trace + // only show 1 *cliponaxis: false* trace fig.data = [fig.data[2]]; // add lines @@ -805,5 +805,4 @@ describe('Test scatter *clipnaxis*', function() { .catch(fail) .then(done); }); - });