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/components/drawing/index.js b/src/components/drawing/index.js index e234c5d2a68..344cfeebe75 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -68,9 +68,8 @@ drawing.setRect = function(s, x, y, w, h) { * false if selection could not get translated */ drawing.translatePoint = function(d, sel, xa, ya) { - // put xp and yp into d if pixel scaling is already done - var x = d.xp || xa.c2p(d.x), - y = d.yp || ya.c2p(d.y); + var x = xa.c2p(d.x); + var y = ya.c2p(d.y); if(isNumeric(x) && isNumeric(y) && sel.node()) { // for multiline text this works better @@ -86,10 +85,28 @@ drawing.translatePoint = function(d, sel, xa, ya) { return true; }; -drawing.translatePoints = function(s, xa, ya, trace) { +drawing.translatePoints = function(s, xa, ya) { s.each(function(d) { var sel = d3.select(this); - drawing.translatePoint(d, sel, xa, ya, trace); + drawing.translatePoint(d, sel, xa, ya); + }); +}; + +drawing.hideOutsideRangePoint = function(d, sel, xa, ya) { + sel.attr( + 'display', + xa.isPtWithinRange(d) && ya.isPtWithinRange(d) ? null : 'none' + ); +}; + +drawing.hideOutsideRangePoints = function(points, subplot) { + if(!subplot._hasClipOnAxisFalse) return; + + var xa = subplot.xaxis; + var ya = subplot.yaxis; + + points.each(function(d) { + drawing.hideOutsideRangePoint(d, d3.select(this), xa, ya); }); }; diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js index 84bc05504bf..227fe4a99ca 100644 --- a/src/components/errorbars/plot.js +++ b/src/components/errorbars/plot.js @@ -12,13 +12,14 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); +var Drawing = require('../drawing'); var subTypes = require('../../traces/scatter/subtypes'); module.exports = function plot(traces, plotinfo, transitionOpts) { var isNew; - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; var hasAnimation = transitionOpts && transitionOpts.duration > 0; @@ -60,6 +61,8 @@ module.exports = function plot(traces, plotinfo, transitionOpts) { .style('opacity', 1); } + Drawing.setClipUrl(errorbars, plotinfo.layerClipId); + errorbars.each(function(d) { var errorbar = d3.select(this); var coords = errorCoords(d, xa, ya); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 64d1663910d..05ffc99a260 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -20,6 +20,7 @@ var Drawing = require('../components/drawing'); var Titles = require('../components/titles'); var ModeBar = require('../components/modebar'); var initInteractions = require('../plots/cartesian/graph_interact'); +var cartesianConstants = require('../plots/cartesian/constants'); exports.layoutStyles = function(gd) { return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); @@ -164,9 +165,31 @@ exports.lsInner = function(gd) { 'height': ya._length }); - plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset); - plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); + + var plotClipId; + var layerClipId; + + if(plotinfo._hasClipOnAxisFalse) { + plotClipId = null; + layerClipId = plotinfo.clipId; + } else { + plotClipId = plotinfo.clipId; + layerClipId = null; + } + + Drawing.setClipUrl(plotinfo.plot, plotClipId); + + 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); + } + } + + // stash layer clipId value (null or same as clipId) + // to DRY up Drawing.setClipUrl calls downstream + plotinfo.layerClipId = layerClipId; var xlw = Drawing.crispRound(gd, xa.linewidth, 1), ylw = Drawing.crispRound(gd, ya.linewidth, 1), diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index d72f7c2fd7b..80c6f48af50 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -49,5 +49,26 @@ module.exports = { // last resort axis ranges for x and y axes if we have no data DFLTRANGEX: [-1, 6], - DFLTRANGEY: [-1, 4] + DFLTRANGEY: [-1, 4], + + // 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 + 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 603e88d2f78..c1e2b4cda46 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -743,23 +743,26 @@ 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); + var scatterPoints = subplot.plot.select('.scatterlayer').selectAll('.points'); + subplot.plot .call(Drawing.setTranslate, plotDx, plotDy) - .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2) - - // This is specifically directed at scatter traces, applying an inverse - // scale to individual points to counteract the scale of the trace - // as a whole: - .select('.scatterlayer').selectAll('.points').selectAll('.point') - .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); - - subplot.plot.select('.scatterlayer') - .selectAll('.points').selectAll('.textpoint') - .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); + .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2); + + // This is specifically directed at scatter traces, applying an inverse + // scale to individual points to counteract the scale of the trace + // as a whole: + scatterPoints.selectAll('.point') + .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2) + .call(Drawing.hideOutsideRangePoints, subplot); + + scatterPoints.selectAll('.textpoint') + .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2) + .call(Drawing.hideOutsideRangePoints, subplot); } } diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 8d86648e4be..1b7ae929c9b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -294,24 +294,10 @@ function makeSubplotData(gd) { } function makeSubplotLayer(plotinfo) { - var plotgroup = plotinfo.plotgroup, - id = plotinfo.id; - - // Layers to keep plot 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 - function joinPlotLayers(parent) { - joinLayer(parent, 'g', 'imagelayer'); - joinLayer(parent, 'g', 'maplayer'); - joinLayer(parent, 'g', 'barlayer'); - joinLayer(parent, 'g', 'carpetlayer'); - joinLayer(parent, 'g', 'boxlayer'); - joinLayer(parent, 'g', 'scatterlayer'); - } + 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'); @@ -324,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'); - plotinfo.xaxislayer = joinLayer(plotgroup, 'g', 'xaxislayer'); - plotinfo.yaxislayer = joinLayer(plotgroup, 'g', 'yaxislayer'); - plotinfo.overaxes = joinLayer(plotgroup, 'g', 'overaxes'); + joinLayer(plotgroup, 'g', 'xaxislayer-above'); + joinLayer(plotgroup, 'g', 'yaxislayer-above'); + plotinfo.overaxesAbove = joinLayer(plotgroup, 'g', 'overaxes-above'); + + // 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 @@ -346,15 +349,30 @@ 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 - plotinfo.plot.call(joinPlotLayers); + + for(var i = 0; i < constants.traceLayerClasses.length; i++) { + joinLayer(plotinfo.plot, 'g', constants.traceLayerClasses[i]); + } plotinfo.xlines .style('fill', 'none') 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/set_convert.js b/src/plots/cartesian/set_convert.js index 5b885f80503..c72492a4d0e 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -58,6 +58,8 @@ function fromLog(v) { module.exports = function setConvert(ax, fullLayout) { fullLayout = fullLayout || {}; + var axLetter = (ax._id || 'x').charAt(0); + // clipMult: how many axis lengths past the edge do we render? // for panning, 1-2 would suffice, but for zooming more is nice. // also, clipping can affect the direction of lines off the edge... @@ -277,7 +279,6 @@ module.exports = function setConvert(ax, fullLayout) { ax.cleanRange = function(rangeAttr) { if(!rangeAttr) rangeAttr = 'range'; var range = Lib.nestedProperty(ax, rangeAttr).get(), - axLetter = (ax._id || 'x').charAt(0), i, dflt; if(ax.type === 'date') dflt = Lib.dfltRange(ax.calendar); @@ -341,8 +342,7 @@ module.exports = function setConvert(ax, fullLayout) { // set scaling to pixels ax.setScale = function(usePrivateRange) { - var gs = fullLayout._size, - axLetter = ax._id.charAt(0); + var gs = fullLayout._size; // TODO cleaner way to handle this case if(!ax._categories) ax._categories = []; @@ -435,6 +435,18 @@ module.exports = function setConvert(ax, fullLayout) { ); }; + if(axLetter === 'x') { + ax.isPtWithinRange = function(d) { + var x = d.x; + return x >= ax.range[0] && x <= ax.range[1]; + }; + } else { + ax.isPtWithinRange = function(d) { + var y = d.y; + return y >= ax.range[0] && y <= ax.range[1]; + }; + } + // for autoranging: arrays of objects: // {val: axis value, pad: pixel padding} // on the low and high sides diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js index 44344a35132..2994b07efe9 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -137,22 +137,26 @@ 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); subplot.plot .call(Drawing.setTranslate, xa2._offset, ya2._offset) - .call(Drawing.setScale, 1, 1) + .call(Drawing.setScale, 1, 1); - // This is specifically directed at scatter traces, applying an inverse - // scale to individual points to counteract the scale of the trace - // as a whole: - .select('.scatterlayer').selectAll('.points').selectAll('.point') - .call(Drawing.setPointGroupScale, 1, 1); + var scatterPoints = subplot.plot.select('.scatterlayer').selectAll('.points'); + + // This is specifically directed at scatter traces, applying an inverse + // scale to individual points to counteract the scale of the trace + // as a whole: + scatterPoints.selectAll('.point') + .call(Drawing.setPointGroupScale, 1, 1) + .call(Drawing.hideOutsideRangePoints, subplot); - subplot.plot.select('.scatterlayer').selectAll('.points').selectAll('.textpoint') - .call(Drawing.setTextPointsScale, 1, 1); + scatterPoints.selectAll('.textpoint') + .call(Drawing.setTextPointsScale, 1, 1) + .call(Drawing.hideOutsideRangePoints, subplot); } function updateSubplot(subplot, progress) { @@ -217,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 38416678886..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,42 @@ 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, + // we need to clip at the trace module layer level; + // find this out here, once of for all. + plotinfo._hasClipOnAxisFalse = false; + + for(var j = 0; j < newFullData.length; j++) { + var trace = newFullData[j]; + + if( + trace.xaxis === plotinfo.xaxis._id && + trace.yaxis === plotinfo.yaxis._id && + trace.cliponaxis === false + ) { + plotinfo._hasClipOnAxisFalse = true; + break; + } + } } }; diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js index 2b24ed0938a..a915b23e6c3 100644 --- a/src/plots/ternary/index.js +++ b/src/plots/ternary/index.js @@ -67,6 +67,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) if(!newFullLayout[oldTernaryKey] && !!oldTernary) { oldTernary.plotContainer.remove(); oldTernary.clipDef.remove(); + oldTernary.clipDefRelative.remove(); } } diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 4670167ca18..c5840d5e9ec 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -46,9 +46,19 @@ proto.init = function(fullLayout) { }; proto.plot = function(ternaryCalcData, fullLayout) { - var _this = this, - ternaryLayout = fullLayout[_this.id], - graphSize = fullLayout._size; + var _this = this; + var ternaryLayout = fullLayout[_this.id]; + var graphSize = fullLayout._size; + + _this._hasClipOnAxisFalse = false; + for(var i = 0; i < ternaryCalcData.length; i++) { + var trace = ternaryCalcData[i][0].trace; + + if(trace.cliponaxis === false) { + _this._hasClipOnAxisFalse = true; + break; + } + } _this.adjustLayout(ternaryLayout, graphSize); @@ -66,12 +76,19 @@ proto.makeFramework = function() { .classed('clips', true); // clippath for this ternary subplot - var clipId = 'clip' + _this.layoutId + _this.id; + var clipId = _this.clipId = 'clip' + _this.layoutId + _this.id; _this.clipDef = defGroup.selectAll('#' + clipId) .data([0]); _this.clipDef.enter().append('clipPath').attr('id', clipId) .append('path').attr('d', 'M0,0Z'); + // 'relative' clippath (i.e. no translation) for this ternary subplot + var clipIdRelative = _this.clipIdRelative = 'clip-relative' + _this.layoutId + _this.id; + _this.clipDefRelative = defGroup.selectAll('#' + clipIdRelative) + .data([0]); + _this.clipDefRelative.enter().append('clipPath').attr('id', clipIdRelative) + .append('path').attr('d', 'M0,0Z'); + // container for everything in this ternary subplot _this.plotContainer = _this.container.selectAll('g.' + _this.id) .data([0]); @@ -120,7 +137,7 @@ proto.makeFramework = function() { .attr('class', function(d) { return 'grid ' + d; }) .each(function(d) { _this.layers[d] = d3.select(this); }); - _this.plotContainer.selectAll('.backplot,.frontplot,.grids') + _this.plotContainer.selectAll('.backplot,.grids') .call(Drawing.setClipUrl, clipId); }; @@ -175,6 +192,16 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { }; setConvert(_this.xaxis, _this.graphDiv._fullLayout); _this.xaxis.setScale(); + _this.xaxis.isPtWithinRange = function(d) { + return ( + d.a >= _this.aaxis.range[0] && + d.a <= _this.aaxis.range[1] && + d.b >= _this.baxis.range[1] && + d.b <= _this.baxis.range[0] && + d.c >= _this.caxis.range[1] && + d.c <= _this.caxis.range[0] + ); + }; _this.yaxis = { type: 'linear', @@ -187,6 +214,7 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { }; setConvert(_this.yaxis, _this.graphDiv._fullLayout); _this.yaxis.setScale(); + _this.yaxis.isPtWithinRange = function() { return true; }; // set up the modified axes for tick drawing var yDomain0 = _this.yaxis.domain[0]; @@ -257,10 +285,15 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { _this.clipDef.select('path').attr('d', triangleClip); _this.layers.plotbg.select('path').attr('d', triangleClip); + var triangleClipRelative = 'M0,' + h + 'h' + w + 'l-' + (w / 2) + ',-' + h + 'Z'; + _this.clipDefRelative.select('path').attr('d', triangleClipRelative); + var plotTransform = 'translate(' + x0 + ',' + y0 + ')'; _this.plotContainer.selectAll('.scatterlayer,.maplayer') .attr('transform', plotTransform); + _this.clipDefRelative.select('path').attr('transform', null); + // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle var bTransform = 'translate(' + x0 + ',' + (y0 + h) + ')'; @@ -302,6 +335,9 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { if(!_this.graphDiv._context.staticPlot) { _this.initInteractions(); } + + _this.plotContainer.select('.frontplot') + .call(Drawing.setClipUrl, _this._hasClipOnAxisFalse ? null : _this.clipId); }; proto.drawAxes = function(doTitles) { @@ -582,6 +618,9 @@ proto.initInteractions = function() { _this.plotContainer.selectAll('.scatterlayer,.maplayer') .attr('transform', plotTransform); + var plotTransform2 = 'translate(' + -dx + ',' + -dy + ')'; + _this.clipDefRelative.select('path').attr('transform', plotTransform2); + // move the ticks _this.aaxis.range = [mins.a, _this.sum - mins.b - mins.c]; _this.baxis.range = [_this.sum - mins.a - mins.c, mins.b]; @@ -589,6 +628,17 @@ proto.initInteractions = function() { _this.drawAxes(false); _this.plotContainer.selectAll('.crisp').classed('crisp', false); + + if(_this._hasClipOnAxisFalse) { + var scatterPoints = _this.plotContainer + .select('.scatterlayer').selectAll('.points'); + + scatterPoints.selectAll('.point') + .call(Drawing.hideOutsideRangePoints, _this); + + scatterPoints.selectAll('.textpoint') + .call(Drawing.hideOutsideRangePoints, _this); + } } function dragDone(dragged, numClicks) { diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 726bb94bfef..f48092247b3 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -168,6 +168,7 @@ module.exports = { ].join(' ') } }, + connectgaps: { valType: 'boolean', dflt: false, @@ -178,6 +179,19 @@ module.exports = { 'in the provided data arrays are connected.' ].join(' ') }, + cliponaxis: { + valType: 'boolean', + dflt: true, + role: 'info', + editType: 'doplot', + description: [ + 'Determines whether or not markers and text nodes', + '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(' ') + }, + fill: { valType: 'enumerated', values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'], diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index e7ed99fdda2..9699469615f 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -75,4 +75,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); + + coerce('cliponaxis'); }; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 0a877aee4de..eec2d39e05b 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -46,7 +46,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCo // the z-order of fill layers is correct. linkTraces(gd, plotinfo, cdscatter); - createFills(gd, scatterlayer); + createFills(gd, scatterlayer, plotinfo); // Sort the traces, once created, so that the ordering is preserved even when traces // are shown and hidden. This is needed since we're not just wiping everything out @@ -100,7 +100,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCo scatterlayer.selectAll('path:not([d])').remove(); }; -function createFills(gd, scatterlayer) { +function createFills(gd, scatterlayer, plotinfo) { var trace; scatterlayer.selectAll('g.trace').each(function(d) { @@ -138,6 +138,8 @@ function createFills(gd, scatterlayer) { tr.selectAll('.js-fill.js-tozero').remove(); trace._ownFill = null; } + + tr.selectAll('.js-fill').call(Drawing.setClipUrl, plotinfo.layerClipId); }); } @@ -324,6 +326,8 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition .call(Drawing.lineGroupStyle) .each(makeUpdate(true)); + Drawing.setClipUrl(lineJoin, plotinfo.layerClipId); + if(segments.length) { if(ownFillEl3) { if(pt0 && pt1) { @@ -426,7 +430,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition if(hasTransition) { enter .call(Drawing.pointStyle, trace, gd) - .call(Drawing.translatePoints, xa, ya, trace) + .call(Drawing.translatePoints, xa, ya) .style('opacity', 0) .transition() .style('opacity', 1); @@ -445,6 +449,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition if(hasNode) { Drawing.singlePointStyle(d, sel, trace, markerScale, lineScale, gd); + if(plotinfo.layerClipId) { + Drawing.hideOutsideRangePoint(d, sel, xa, ya); + } + if(trace.customdata) { el.classed('plotly-customdata', d.data !== null && d.data !== undefined); } @@ -475,19 +483,23 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition var g = d3.select(this); var sel = transition(g.select('text')); hasNode = Drawing.translatePoint(d, sel, xa, ya); - if(!hasNode) g.remove(); + + if(hasNode) { + if(plotinfo.layerClipId) { + Drawing.hideOutsideRangePoint(d, g, xa, ya); + } + } else { + g.remove(); + } }); join.selectAll('text') .call(Drawing.textPointStyle, trace, gd) .each(function(d) { - // This just *has* to be totally custom becuase of SVG text positioning :( // It's obviously copied from translatePoint; we just can't use that - // - // put xp and yp into d if pixel scaling is already done - var x = d.xp || xa.c2p(d.x), - y = d.yp || ya.c2p(d.y); + var x = xa.c2p(d.x); + var y = ya.c2p(d.y); d3.select(this).selectAll('tspan.line').each(function() { transition(d3.select(this)).attr({x: x, y: y}); @@ -512,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/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index c6d3b709760..4b5b73bca6f 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -95,6 +95,7 @@ module.exports = { smoothing: scatterLineAttrs.smoothing }, connectgaps: scatterAttrs.connectgaps, + cliponaxis: scatterAttrs.cliponaxis, fill: extendFlat({}, scatterAttrs.fill, { values: ['none', 'toself', 'tonext'], description: [ diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js index 5a3b8dd3e4f..9a1ca4f4fcd 100644 --- a/src/traces/scatterternary/defaults.js +++ b/src/traces/scatterternary/defaults.js @@ -99,4 +99,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout dfltHoverOn.push('fills'); } coerce('hoveron', dfltHoverOn.join('+') || 'points'); + + coerce('cliponaxis'); }; diff --git a/src/traces/scatterternary/plot.js b/src/traces/scatterternary/plot.js index 0ecfaa25601..8e4096d9fc4 100644 --- a/src/traces/scatterternary/plot.js +++ b/src/traces/scatterternary/plot.js @@ -22,7 +22,8 @@ module.exports = function plot(ternary, moduleCalcData) { var plotinfo = { xaxis: ternary.xaxis, yaxis: ternary.yaxis, - plot: plotContainer + plot: plotContainer, + layerClipId: ternary._hasClipOnAxisFalse ? ternary.clipIdRelative : null }; // add ref to ternary subplot object in fullData traces 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 new file mode 100644 index 00000000000..43f2dce8423 Binary files /dev/null and b/test/image/baselines/cliponaxis_false.png differ diff --git a/test/image/baselines/ternary_markers.png b/test/image/baselines/ternary_markers.png index f131b3266f5..9288c20458b 100644 Binary files a/test/image/baselines/ternary_markers.png and b/test/image/baselines/ternary_markers.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 new file mode 100644 index 00000000000..22b38fd91c0 --- /dev/null +++ b/test/image/mocks/cliponaxis_false.json @@ -0,0 +1,113 @@ +{ + "data": [ + { + "x": [1, 2, 3, 4], + "y": [0, 2, 3, 5], + "fill": "tozeroy", + "cliponaxis": false + }, + { + "x": [1, 2, 3, 4 ], + "y": [3, 5, 1, 7], + "fill": "tonexty", + "cliponaxis": false + }, + { + "mode": "markers+text", + "x": [1, 1, 2, 3, 3, 2], + "y": [2, 1, 1, 1, 2, 2], + "marker": { + "size": [50, 20, 40, 25, 15, 40], + "opacity": 1 + }, + "text": "not
clipped", + "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] + }, + "error_x": { + "array": [0.1, 0.2, 0.3, 0.1, 0.1, 0.2], + "arrayminus": [0.1, 0.2, 0.1, 0.1, 0.1, 0.2] + }, + "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", + "layer": "below traces" + }, + "yaxis": { + "range": [1, 2], + "showline": true, + "linewidth": 2, + "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", + "height": 500, + "width": 800 + } +} diff --git a/test/image/mocks/ternary_markers.json b/test/image/mocks/ternary_markers.json index 369460167a7..c7f7ed2196f 100644 --- a/test/image/mocks/ternary_markers.json +++ b/test/image/mocks/ternary_markers.json @@ -2,7 +2,7 @@ "data": [ { "type": "scatterternary", - "mode": "markers", + "mode": "markers+lines+text", "a": [ 75, 70, @@ -55,6 +55,22 @@ "point 10", "point 11" ], + "textposition": [ + "left", + "right", + "top right", + "top", + "bottom right", + "left", + "top", + "left", + "right", + "bottom", + "top" + ], + "textfont": { + "color": "#DB7365" + }, "marker": { "symbol": 100, "color": "#DB7365", @@ -62,7 +78,8 @@ "line": { "width": 2 } - } + }, + "cliponaxis": false } ], "layout": { @@ -83,6 +100,7 @@ "tickangle": 0 }, "baxis": { + "min": 20, "titlefont": { "size": 20 }, 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/assets/assert_dims.js b/test/jasmine/assets/assert_dims.js deleted file mode 100644 index 2db7f297b4f..00000000000 --- a/test/jasmine/assets/assert_dims.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var d3 = require('d3'); - -module.exports = function assertDims(dims) { - var traces = d3.selectAll('.trace'); - - expect(traces.size()) - .toEqual(dims.length, 'to have correct number of traces'); - - traces.each(function(_, i) { - var trace = d3.select(this); - var points = trace.selectAll('.point'); - - expect(points.size()) - .toEqual(dims[i], 'to have correct number of pts in trace ' + i); - }); -}; diff --git a/test/jasmine/assets/assert_style.js b/test/jasmine/assets/assert_style.js deleted file mode 100644 index c6684da041e..00000000000 --- a/test/jasmine/assets/assert_style.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -var d3 = require('d3'); - -module.exports = function assertStyle(dims, color, opacity) { - var N = dims.reduce(function(a, b) { - return a + b; - }); - - var traces = d3.selectAll('.trace'); - expect(traces.size()) - .toEqual(dims.length, 'to have correct number of traces'); - - expect(d3.selectAll('.point').size()) - .toEqual(N, 'to have correct total number of points'); - - traces.each(function(_, i) { - var trace = d3.select(this); - var points = trace.selectAll('.point'); - - expect(points.size()) - .toEqual(dims[i], 'to have correct number of pts in trace ' + i); - - points.each(function() { - var point = d3.select(this); - - expect(point.style('fill')) - .toEqual(color[i], 'to have correct pt color'); - expect(+point.style('opacity')) - .toEqual(opacity[i], 'to have correct pt opacity'); - }); - }); -}; diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js new file mode 100644 index 00000000000..756c0b8f290 --- /dev/null +++ b/test/jasmine/assets/custom_assertions.js @@ -0,0 +1,75 @@ +'use strict'; + +var d3 = require('d3'); + +exports.assertDims = function(dims) { + var traces = d3.selectAll('.trace'); + + expect(traces.size()) + .toEqual(dims.length, 'to have correct number of traces'); + + traces.each(function(_, i) { + var trace = d3.select(this); + var points = trace.selectAll('.point'); + + expect(points.size()) + .toEqual(dims[i], 'to have correct number of pts in trace ' + i); + }); +}; + +exports.assertStyle = function(dims, color, opacity) { + var N = dims.reduce(function(a, b) { + return a + b; + }); + + var traces = d3.selectAll('.trace'); + expect(traces.size()) + .toEqual(dims.length, 'to have correct number of traces'); + + expect(d3.selectAll('.point').size()) + .toEqual(N, 'to have correct total number of points'); + + traces.each(function(_, i) { + var trace = d3.select(this); + var points = trace.selectAll('.point'); + + expect(points.size()) + .toEqual(dims[i], 'to have correct number of pts in trace ' + i); + + points.each(function() { + var point = d3.select(this); + + expect(point.style('fill')) + .toEqual(color[i], 'to have correct pt color'); + expect(+point.style('opacity')) + .toEqual(opacity[i], 'to have correct pt opacity'); + }); + }); +}; + +exports.assertClip = function(sel, isClipped, size, msg) { + expect(sel.size()).toBe(size, msg + ' clip path (selection size)'); + + sel.each(function(d, i) { + var clipPath = d3.select(this).attr('clip-path'); + + if(isClipped) { + expect(String(clipPath).substr(0, 4)) + .toBe('url(', msg + ' clip path ' + '(item ' + i + ')'); + } else { + expect(clipPath) + .toBe(null, msg + ' clip path ' + '(item ' + i + ')'); + } + }); + +}; + +exports.assertNodeDisplay = function(sel, expectation, msg) { + expect(sel.size()) + .toBe(expectation.length, msg + ' display (selection size)'); + + sel.each(function(d, i) { + expect(d3.select(this).attr('display')) + .toBe(expectation[i], msg + ' display ' + '(item ' + i + ')'); + }); +}; 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 658285ec965..ec42e45a2e4 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -8,8 +8,12 @@ var Plotly = require('@lib/index'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); +var customAssertions = require('../assets/custom_assertions'); var fail = require('../assets/fail_test'); +var assertClip = customAssertions.assertClip; +var assertNodeDisplay = customAssertions.assertNodeDisplay; + describe('Test scatter', function() { 'use strict'; @@ -670,3 +674,135 @@ describe('scatter hoverPoints', function() { .then(done); }); }); + +describe('Test scatter *clipnaxis*:', function() { + afterEach(destroyGraphDiv); + + it('should show/hide point/text/errorbars in clipped and non-clipped layers', function(done) { + var gd = createGraphDiv(); + var fig = Lib.extendDeep({}, require('@mocks/cliponaxis_false.json')); + var xRange0 = fig.layout.xaxis.range.slice(); + var yRange0 = fig.layout.yaxis.range.slice(); + + // only show 1 *cliponaxis: false* trace + fig.data = [fig.data[2]]; + + // add lines + fig.data[0].mode = 'markers+lines+text'; + + function _assert(layerClips, nodeDisplays, errorBarClips, lineClips) { + var subplotLayer = d3.select('.plot'); + var scatterLayer = subplotLayer.select('.scatterlayer'); + + assertClip(subplotLayer, layerClips[0], 1, 'subplot layer'); + assertClip(subplotLayer.select('.barlayer'), layerClips[1], 1, 'bar layer'); + assertClip(scatterLayer, layerClips[2], 1, 'scatter layer'); + + assertNodeDisplay( + scatterLayer.selectAll('.point'), + nodeDisplays, + 'scatter points' + ); + assertNodeDisplay( + scatterLayer.selectAll('.textpoint'), + nodeDisplays, + 'scatter text points' + ); + + assertClip( + scatterLayer.selectAll('.errorbar'), + errorBarClips[0], errorBarClips[1], + 'error bars' + ); + assertClip( + scatterLayer.selectAll('.js-line'), + lineClips[0], lineClips[1], + 'line clips' + ); + } + + Plotly.plot(gd, fig) + .then(function() { + _assert( + [false, true, false], + [null, null, null, null, null, null], + [true, 6], + [true, 1] + ); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert( + [true, false, false], + [], + [false, 0], + [false, 0] + ); + return Plotly.restyle(gd, {visible: true, cliponaxis: null}); + }) + .then(function() { + _assert( + [true, false, false], + [null, null, null, null, null, null], + [false, 6], + [false, 1] + ); + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + _assert( + [true, false, false], + [], + [false, 0], + [false, 0] + ); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert( + [true, false, false], + [null, null, null, null, null, null], + [false, 6], + [false, 1] + ); + return Plotly.restyle(gd, 'cliponaxis', false); + }) + .then(function() { + _assert( + [false, true, false], + [null, null, null, null, null, null], + [true, 6], + [true, 1] + ); + return Plotly.relayout(gd, 'xaxis.range', [0, 1]); + }) + .then(function() { + _assert( + [false, true, false], + [null, null, 'none', 'none', 'none', 'none'], + [true, 6], + [true, 1] + ); + return Plotly.relayout(gd, 'yaxis.range', [0, 1]); + }) + .then(function() { + _assert( + [false, true, false], + ['none', null, 'none', 'none', 'none', 'none'], + [true, 6], + [true, 1] + ); + return Plotly.relayout(gd, {'xaxis.range': xRange0, 'yaxis.range': yRange0}); + }) + .then(function() { + _assert( + [false, true, false], + [null, null, null, null, null, null], + [true, 6], + [true, 1] + ); + }) + .catch(fail) + .then(done); + }); +}); diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js index 34c5e764370..da49b10a206 100644 --- a/test/jasmine/tests/scatterternary_test.js +++ b/test/jasmine/tests/scatterternary_test.js @@ -6,8 +6,12 @@ var ScatterTernary = require('@src/traces/scatterternary'); 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 customMatchers = require('../assets/custom_matchers'); +var customAssertions = require('../assets/custom_assertions'); +var assertClip = customAssertions.assertClip; +var assertNodeDisplay = customAssertions.assertNodeDisplay; describe('scatterternary defaults', function() { 'use strict'; @@ -374,3 +378,108 @@ describe('scatterternary hover', function() { }); }); + +describe('Test scatterternary *cliponaxis*', function() { + afterEach(destroyGraphDiv); + + it('should show/hide point/text/errorbars in clipped and non-clipped layers', function(done) { + var gd = createGraphDiv(); + var fig = Lib.extendDeep({}, require('@mocks/ternary_markers.json')); + + function _assert(layerClips, nodeDisplays, lineClips) { + var frontLayer = d3.select('.frontplot'); + var scatterLayer = d3.select('.scatterlayer'); + + assertClip(frontLayer, layerClips[0], 1, 'front layer'); + assertClip(scatterLayer, layerClips[1], 1, 'scatter layer'); + + assertNodeDisplay( + scatterLayer.selectAll('.point'), + nodeDisplays, + 'scatter points' + ); + assertNodeDisplay( + scatterLayer.selectAll('.textpoint'), + nodeDisplays, + 'scatter text points' + ); + + assertClip( + scatterLayer.selectAll('.js-line'), + lineClips[0], lineClips[1], + 'line clips' + ); + } + + Plotly.plot(gd, fig) + .then(function() { + _assert( + [false, false], + [null, 'none', null, null, null, null, null, null, 'none', 'none', 'none'], + [true, 1] + ); + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + _assert( + [false, false], + [], + [false, 0] + ); + return Plotly.restyle(gd, {visible: true, cliponaxis: null}); + }) + .then(function() { + _assert( + [true, false], + [null, null, null, null, null, null, null, null, null, null, null], + [false, 1] + ); + return Plotly.restyle(gd, 'cliponaxis', false); + }) + .then(function() { + _assert( + [false, false], + [null, 'none', null, null, null, null, null, null, 'none', 'none', 'none'], + [true, 1] + ); + return Plotly.relayout(gd, 'ternary.aaxis.min', 20); + }) + .then(function() { + _assert( + [false, false], + [null, 'none', null, 'none', 'none', 'none', null, 'none', 'none', 'none', 'none'], + [true, 1] + ); + return Plotly.relayout(gd, 'ternary.baxis.min', 40); + }) + .then(function() { + _assert( + [false, false], + ['none', 'none', 'none', 'none', 'none', 'none', null, 'none', 'none', 'none', 'none'], + [true, 1] + ); + return Plotly.relayout(gd, 'ternary.caxis.min', 30); + }) + .then(function() { + _assert( + [false, false], + ['none', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none'], + [true, 1] + ); + return Plotly.relayout(gd, { + 'ternary.aaxis.min': null, + 'ternary.baxis.min': null, + 'ternary.caxis.min': null + }); + }) + .then(function() { + _assert( + [false, false], + [null, null, null, null, null, null, null, null, null, null, null], + [true, 1] + ); + }) + .catch(fail) + .then(done); + }); +}); diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index d5bfe093c35..716c30bf6ca 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -6,9 +6,11 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); var customMatchers = require('../assets/custom_matchers'); +var customAssertions = require('../assets/custom_assertions'); + +var assertDims = customAssertions.assertDims; +var assertStyle = customAssertions.assertStyle; describe('filter transforms defaults:', function() { diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index 049a2856b5f..46ec7a3c1fb 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -3,8 +3,10 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); +var customAssertions = require('../assets/custom_assertions'); + +var assertDims = customAssertions.assertDims; +var assertStyle = customAssertions.assertStyle; describe('groupby', function() { diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index f10541b8e80..681c39f3737 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -6,9 +6,10 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); +var customAssertions = require('../assets/custom_assertions'); +var assertDims = customAssertions.assertDims; +var assertStyle = customAssertions.assertStyle; describe('general transforms:', function() { 'use strict';