diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index c2e2540ef97..ec4c5957b30 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -62,17 +62,20 @@ drawing.setRect = function(s, x, y, w, h) { * @param {sel} sel : d3 selction of node to translate * @param {object} xa : corresponding full xaxis object * @param {object} ya : corresponding full yaxis object + * @param {object} trace : corresponding full trace object * * @return {boolean} : * true if selection got translated * false if selection could not get translated */ -drawing.translatePoint = function(d, sel, xa, ya) { +drawing.translatePoint = function(d, sel, xa, ya, trace) { // 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 = d.xp = xa.c2p(d.x); + var y = d.yp = ya.c2p(d.y); - if(isNumeric(x) && isNumeric(y) && sel.node()) { + if(isNumeric(x) && isNumeric(y) && sel.node() && + (trace.cliponaxis !== false || xa.isPtWithinRange(d) && ya.isPtWithinRange(d)) + ) { // for multiline text this works better if(sel.node().nodeName === 'text') { sel.attr('x', x).attr('y', y); diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js index 84bc05504bf..5571afc7fc5 100644 --- a/src/components/errorbars/plot.js +++ b/src/components/errorbars/plot.js @@ -14,22 +14,24 @@ var isNumeric = require('fast-isnumeric'); var subTypes = require('../../traces/scatter/subtypes'); -module.exports = function plot(traces, plotinfo, transitionOpts) { +module.exports = function plot(traces, plotinfo, transitionOpts, clipOnAxis) { var isNew; - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; var hasAnimation = transitionOpts && transitionOpts.duration > 0; traces.each(function(d) { - var trace = d[0].trace, - // || {} is in case the trace (specifically scatterternary) - // doesn't support error bars at all, but does go through - // the scatter.plot mechanics, which calls ErrorBars.plot - // internally - xObj = trace.error_x || {}, - yObj = trace.error_y || {}; + var tr = d3.select(this); + var trace = d[0].trace; + + // || {} is in case the trace (specifically scatterternary) + // doesn't support error bars at all, but does go through + // the scatter.plot mechanics, which calls ErrorBars.plot + // internally + var xObj = trace.error_x || {}; + var yObj = trace.error_y || {}; var keyFunc; @@ -44,8 +46,8 @@ module.exports = function plot(traces, plotinfo, transitionOpts) { if(!yObj.visible && !xObj.visible) return; - var errorbars = d3.select(this).selectAll('g.errorbar') - .data(d, keyFunc); + var errorbars = tr.selectAll('g.errorbar') + .data(trace.cliponaxis === clipOnAxis ? d : [], keyFunc); errorbars.exit().remove(); @@ -66,7 +68,7 @@ module.exports = function plot(traces, plotinfo, transitionOpts) { if(sparse && !d.vis) return; - var path; + var path, yerror, xerror; if(yObj.visible && isNumeric(coords.x) && isNumeric(coords.yh) && @@ -77,11 +79,9 @@ module.exports = function plot(traces, plotinfo, transitionOpts) { coords.yh + 'h' + (2 * yw) + // hat 'm-' + yw + ',0V' + coords.ys; // bar - if(!coords.noYS) path += 'm-' + yw + ',0h' + (2 * yw); // shoe - var yerror = errorbar.select('path.yerror'); - + yerror = errorbar.select('path.yerror'); isNew = !yerror.size(); if(isNew) { @@ -108,8 +108,7 @@ module.exports = function plot(traces, plotinfo, transitionOpts) { if(!coords.noXS) path += 'm0,-' + xw + 'v' + (2 * xw); // shoe - var xerror = errorbar.select('path.xerror'); - + xerror = errorbar.select('path.xerror'); isNew = !xerror.size(); if(isNew) { @@ -124,6 +123,12 @@ module.exports = function plot(traces, plotinfo, transitionOpts) { xerror.attr('d', path); } + + if(trace.cliponaxis === false && + !(xa.isPtWithinRange(d) && ya.isPtWithinRange(d))) { + yerror.remove(); + xerror.remove(); + } }); }); }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c067d798f58..33ae5bfd865 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1635,8 +1635,13 @@ function _restyle(gd, aobj, _traces) { } // some attributes declare an 'editType' flaglist - if(valObject.editType === 'docalc') { - flags.docalc = true; + switch(valObject.editType) { + case 'docalc': + flags.docalc = true; + break; + case 'doplot': + flags.doplot = true; + break; } // all the other ones, just modify that one attribute diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 64d1663910d..6809f836e65 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -164,9 +164,12 @@ exports.lsInner = function(gd) { 'height': ya._length }); + plotinfo.plot + .call(Drawing.setTranslate, xa._offset, ya._offset) + .call(Drawing.setClipUrl, plotinfo.clipId); - plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset); - plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); + plotinfo.plotnoclip + .call(Drawing.setTranslate, xa._offset, ya._offset); var xlw = Drawing.crispRound(gd, xa.linewidth, 1), ylw = Drawing.crispRound(gd, ya.linewidth, 1), diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 603e88d2f78..744c93a05b0 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -749,16 +749,23 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { subplot.plot .call(Drawing.setTranslate, plotDx, plotDy) - .call(Drawing.setScale, 1 / xScaleFactor2, 1 / 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: - .select('.scatterlayer').selectAll('.points').selectAll('.point') - .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); + subplot.plotnoclip + .call(Drawing.setTranslate, plotDx, plotDy) + .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2); + + var points = subplot.plotgroup + .selectAll('.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: + points.selectAll('.point') + .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); - subplot.plot.select('.scatterlayer') - .selectAll('.points').selectAll('.textpoint') + points.selectAll('.textpoint') .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); } } diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 883acb4d0d8..fadbaf1171b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -162,6 +162,12 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) .selectAll('g.trace') .remove(); } + + if(subplotInfo.plotnoclip) { + subplotInfo.plotnoclip.select('g.scatterlayer') + .selectAll('g.trace') + .remove(); + } } oldFullLayout._infolayer.selectAll('g.rangeslider-container') @@ -334,6 +340,9 @@ function makeSubplotLayer(plotinfo) { plotinfo.xaxislayer = joinLayer(plotgroup, 'g', 'xaxislayer'); plotinfo.yaxislayer = joinLayer(plotgroup, 'g', 'yaxislayer'); plotinfo.overaxes = joinLayer(plotgroup, 'g', 'overaxes'); + + plotinfo.plotnoclip = joinLayer(plotgroup, 'g', 'plotnoclip'); + plotinfo.overplotnoclip = joinLayer(plotgroup, 'g', 'overplotnoclip'); } else { var mainplotinfo = plotinfo.mainplotinfo; @@ -345,16 +354,17 @@ function makeSubplotLayer(plotinfo) { plotinfo.gridlayer = joinLayer(mainplotinfo.overgrid, 'g', id); plotinfo.zerolinelayer = joinLayer(mainplotinfo.overzero, 'g', id); - plotinfo.plot = joinLayer(mainplotinfo.overplot, 'g', id); plotinfo.xlines = joinLayer(mainplotinfo.overlines, 'path', id); plotinfo.ylines = joinLayer(mainplotinfo.overlines, 'path', id); plotinfo.xaxislayer = joinLayer(mainplotinfo.overaxes, 'g', id); plotinfo.yaxislayer = joinLayer(mainplotinfo.overaxes, 'g', id); + plotinfo.plotnoclip = joinLayer(mainplotinfo.overplotnoclip, 'g', id); } // common attributes for all subplots, overlays or not plotinfo.plot.call(joinPlotLayers); + joinLayer(plotinfo.plotnoclip, 'g', 'scatterlayer'); plotinfo.xlines .style('fill', 'none') 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..aae06a268a1 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -143,15 +143,23 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo 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); + subplot.plotnoclip + .call(Drawing.setTranslate, xa2._offset, ya2._offset) + .call(Drawing.setScale, 1, 1); + + var points = subplot.plotgroup + .selectAll('.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: + points.selectAll('.point') + .call(Drawing.setPointGroupScale, 1, 1); - subplot.plot.select('.scatterlayer').selectAll('.points').selectAll('.textpoint') + points.selectAll('.textpoint') .call(Drawing.setTextPointsScale, 1, 1); } diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index fa9fcbfcf67..32d3818376f 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -89,7 +89,8 @@ proto.makeFramework = function() { 'grids', 'frontplot', 'zoom', - 'aaxis', 'baxis', 'caxis', 'axlines' + 'aaxis', 'baxis', 'caxis', 'axlines', + 'frontplotnoclip' ]; var toplevel = _this.plotContainer.selectAll('g.toplevel') .data(plotLayers); @@ -113,6 +114,7 @@ proto.makeFramework = function() { d3.select(this).classed(d, true); }); } + else if(d === 'frontplotnoclip') s.append('g').classed('scatterlayer', true); }); var grids = _this.plotContainer.select('.grids').selectAll('g.grid') @@ -180,6 +182,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', @@ -192,6 +204,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]; diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index f7e5b58ae7c..df618b239f6 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -204,7 +204,7 @@ module.exports = function plot(gd, plotinfo, cdbox) { }); }) .enter().append('path') - .call(Drawing.translatePoints, xa, ya); + .call(Drawing.translatePoints, xa, ya, trace); } // draw mean (and stdev diamond) if desired if(trace.boxmean) { diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 726bb94bfef..d8068e77d93 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,17 @@ 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, text nodes and errobars', + 'are clipped about the subplot axes.' + ].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..112a8646c8a 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -21,47 +21,51 @@ var linkTraces = require('./link_traces'); var polygonTester = require('../../lib/polygon').tester; module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCompleteCallback) { - var i, uids, selection, join, onComplete; + var i; var scatterlayer = plotinfo.plot.select('g.scatterlayer'); - // If transition config is provided, then it is only a partial replot and traces not - // updated are removed. - var isFullReplot = !transitionOpts; - var hasTransition = !!transitionOpts && transitionOpts.duration > 0; - - selection = scatterlayer.selectAll('g.trace'); - - join = selection.data(cdscatter, function(d) { return d[0].trace.uid; }); + // 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 + // and recreating on every update. + var uids = {}; + for(i = 0; i < cdscatter.length; i++) { + uids[cdscatter[i][0].trace.uid] = i; + } - // Append new traces: - join.enter().append('g') - .attr('class', function(d) { - return 'trace scatter trace' + d[0].trace.uid; - }) - .style('stroke-miterlimit', 2); + var join = bindData(gd, plotinfo, cdscatter, scatterlayer, uids); // After the elements are created but before they've been draw, we have to perform // this extra step of linking the traces. This allows appending of fill layers so that // the z-order of fill layers is correct. linkTraces(gd, plotinfo, cdscatter); - createFills(gd, scatterlayer); - // 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 - // and recreating on every update. - for(i = 0, uids = {}; i < cdscatter.length; i++) { - uids[cdscatter[i][0].trace.uid] = i; + var scatterlayerNoClip, cdscatterNoClip, joinNoClip; + + if(plotinfo.plotnoclip) { + scatterlayerNoClip = plotinfo.plotnoclip.select('g.scatterlayer'); + cdscatterNoClip = []; + + for(i = 0; i < cdscatter.length; i++) { + var cdi = cdscatter[i]; + + if(cdi[0].trace.cliponaxis === false) { + cdscatterNoClip.push(cdi); + } + } + + joinNoClip = bindData(gd, plotinfo, cdscatterNoClip, scatterlayerNoClip, uids); } - scatterlayer.selectAll('g.trace').sort(function(a, b) { - var idx1 = uids[a[0].trace.uid]; - var idx2 = uids[b[0].trace.uid]; - return idx1 > idx2 ? 1 : -1; - }); + // If transition config is provided, then it is only a partial replot and traces not + // updated are removed. + var isFullReplot = !transitionOpts; + var hasTransition = !!transitionOpts && transitionOpts.duration > 0; if(hasTransition) { + var onComplete; + if(makeOnCompleteCallback) { // If it was passed a callback to register completion, make a callback. If // this is created, then it must be executed on completion, otherwise the @@ -85,21 +89,54 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCo scatterlayer.selectAll('g.trace').each(function(d, i) { plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts); }); + + if(scatterlayerNoClip) { + scatterlayerNoClip.selectAll('g.trace').each(function(d, i) { + plotOneNoClip(gd, i, plotinfo, d, cdscatter, this, transitionOpts); + }); + } }); } else { scatterlayer.selectAll('g.trace').each(function(d, i) { plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts); }); + + if(scatterlayerNoClip) { + scatterlayerNoClip.selectAll('g.trace').each(function(d, i) { + plotOneNoClip(gd, i, plotinfo, d, cdscatter, this, transitionOpts); + }); + } } if(isFullReplot) { join.exit().remove(); + if(joinNoClip) joinNoClip.exit().remove(); } // remove paths that didn't get used scatterlayer.selectAll('path:not([d])').remove(); }; +function bindData(gd, plotinfo, cdscatter, layer, uids) { + var selection = layer.selectAll('g.trace'); + var join = selection.data(cdscatter, function(d) { return d[0].trace.uid; }); + + // Append new traces: + join.enter().append('g') + .attr('class', function(d) { + return 'trace scatter trace' + d[0].trace.uid; + }) + .style('stroke-miterlimit', 2); + + layer.selectAll('g.trace').sort(function(a, b) { + var idx1 = uids[a[0].trace.uid]; + var idx2 = uids[b[0].trace.uid]; + return idx1 > idx2 ? 1 : -1; + }); + + return join; +} + function createFills(gd, scatterlayer) { var trace; @@ -142,11 +179,8 @@ function createFills(gd, scatterlayer) { } function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transitionOpts) { - var join, i; + var i; - // Since this has been reorganized and we're executing this on individual traces, - // we need to pass it the full list of cdscatter as well as this trace's index (idx) - // since it does an internal n^2 loop over comparisons with other traces: selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll); var hasTransition = !!transitionOpts && transitionOpts.duration > 0; @@ -155,16 +189,22 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition return hasTransition ? selection.transition() : selection; } - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + var trace = cdscatter[0].trace; + var line = trace.line; + var tr = d3.select(element); - var trace = cdscatter[0].trace, - line = trace.line, - tr = d3.select(element); + // Option passed to errorbar and marker/text renderers: + // if 'cliponaxis' is undefined in full trace at this stage, + // it means that the callee does not support `cliponaxis: false`, + // hence the renderers don't need to worry about which layer + // (e.g. plot vs plotnoclip) they're plotting in. + var clipOnAxis = trace.cliponaxis === undefined ? undefined : true; // (so error bars can find them along with bars) // error bars are at the bottom - tr.call(ErrorBars.plot, plotinfo, transitionOpts); + tr.call(ErrorBars.plot, plotinfo, transitionOpts, clipOnAxis); if(trace.visible !== true) return; @@ -374,7 +414,31 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition trace._prevPolygons = thisPolygons; } + plotPoints(gd, tr, cdscatter, xa, ya, hasTransition, transition, clipOnAxis); +} + +function plotOneNoClip(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transitionOpts) { + var hasTransition = !!transitionOpts && transitionOpts.duration > 0; + + function transition(selection) { + return hasTransition ? selection.transition() : selection; + } + + var trace = cdscatter[0].trace; + var tr = d3.select(element); + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + + tr.call(ErrorBars.plot, plotinfo, transitionOpts, false); + if(trace.visible !== true) return; + + transition(tr).style('opacity', trace.opacity); + + plotPoints(gd, tr, cdscatter, xa, ya, hasTransition, transition, false); +} + +function plotPoints(gd, tr, cdscatter, xa, ya, hasTransition, transition, clipOnAxis) { function visFilter(d) { return d.filter(function(v) { return v.vis; }); } @@ -406,12 +470,18 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition markerFilter = hideFilter, textFilter = hideFilter; - if(showMarkers) { - markerFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity; - } + if(trace.cliponaxis === clipOnAxis) { + if(showMarkers) { + markerFilter = (trace.marker.maxdisplayed || trace._needsCull) ? + visFilter : + Lib.identity; + } - if(showText) { - textFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity; + if(showText) { + textFilter = (trace.marker.maxdisplayed || trace._needsCull) ? + visFilter : + Lib.identity; + } } // marker points @@ -440,7 +510,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition join.each(function(d) { var el = d3.select(this); var sel = transition(el); - hasNode = Drawing.translatePoint(d, sel, xa, ya); + hasNode = Drawing.translatePoint(d, sel, xa, ya, trace); if(hasNode) { Drawing.singlePointStyle(d, sel, trace, markerScale, lineScale, gd); @@ -474,7 +544,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition join.each(function(d) { var g = d3.select(this); var sel = transition(g.select('text')); - hasNode = Drawing.translatePoint(d, sel, xa, ya); + hasNode = Drawing.translatePoint(d, sel, xa, ya, trace); if(!hasNode) g.remove(); }); @@ -501,7 +571,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition var pointSelection = tr.selectAll('.points'); // Join with new data - join = pointSelection.data([cdscatter]); + var join = pointSelection.data([cdscatter]); // Transition existing, but don't defer this to an async .transition since // there's no timing involved: @@ -514,6 +584,9 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition join.exit().remove(); } +// Since this has been reorganized and we're executing this on individual traces, +// we need to pass it the full list of cdscatter as well as this trace's index (idx) +// since it does an internal n^2 loop over comparisons with other traces: function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) { var xa = plotinfo.xaxis, ya = plotinfo.yaxis, 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..6452fd98f5c 100644 --- a/src/traces/scatterternary/plot.js +++ b/src/traces/scatterternary/plot.js @@ -15,14 +15,15 @@ var scatterPlot = require('../scatter/plot'); module.exports = function plot(ternary, moduleCalcData) { var plotContainer = ternary.plotContainer; - // remove all nodes inside the scatter layer - plotContainer.select('.scatterlayer').selectAll('*').remove(); + // remove all nodes inside the scatter layers + plotContainer.selectAll('.scatterlayer').selectAll('*').remove(); // mimic cartesian plotinfo var plotinfo = { xaxis: ternary.xaxis, yaxis: ternary.yaxis, - plot: plotContainer + plot: plotContainer.select('.frontplot'), + plotnoclip: plotContainer.select('.frontplotnoclip') }; // add ref to ternary subplot object in fullData traces diff --git a/test/image/baselines/cliponaxis_false.png b/test/image/baselines/cliponaxis_false.png new file mode 100644 index 00000000000..c2a63f2c2f7 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..7dc7f72f9ea 100644 Binary files a/test/image/baselines/ternary_markers.png and b/test/image/baselines/ternary_markers.png differ diff --git a/test/image/mocks/cliponaxis_false.json b/test/image/mocks/cliponaxis_false.json new file mode 100644 index 00000000000..15aaab4d57d --- /dev/null +++ b/test/image/mocks/cliponaxis_false.json @@ -0,0 +1,39 @@ +{ + "data": [ + { + "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] + }, + "text": "not
clipped", + "textposition": ["left", "left", "bottom", "right", "right", "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 + } + ], + "layout": { + "xaxis": { + "range": [1, 3], + "showline": true, + "linewidth": 2, + "mirror": "all" + }, + "yaxis": { + "range": [1, 2], + "showline": true, + "linewidth": 2, + "mirror": "all" + }, + "dragmode": "pan", + "hovermode": "closest" + } +} diff --git a/test/image/mocks/ternary_markers.json b/test/image/mocks/ternary_markers.json index 369460167a7..b029e127522 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+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": { diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index f8fba331d24..19859651da5 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -70,9 +70,9 @@ describe('Test plot structure', function() { expect(countDraggers()).toEqual(1); }); - it('has one *scatterlayer* node', function() { + it('has two *scatterlayer* node (one clipped, one non-clipped)', function() { var nodes = d3.selectAll('g.scatterlayer'); - expect(nodes.size()).toEqual(1); + expect(nodes.size()).toEqual(2); }); it('has as many *trace scatter* nodes as there are traces', function() { diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 658285ec965..4366e7b8549 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -670,3 +670,67 @@ 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(); + + function assertVisible(cnt, cntNoClip, msg) { + var selectors = ['.point', '.textpoint', '.yerror', '.xerror']; + var scatterLayer = d3.select('.plot > .scatterlayer'); + var scatterLayerNoClip = d3.select('.plotnoclip > .scatterlayer'); + + selectors.forEach(function(s) { + expect(scatterLayer.selectAll(s).size()) + .toBe(cnt, s + ' ' + msg); + expect(scatterLayerNoClip.selectAll(s).size()) + .toBe(cntNoClip, s + ' (noclip) ' + msg); + }); + } + + Plotly.plot(gd, fig) + .then(function() { + assertVisible(0, 6, 'cliponaxis:false'); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + assertVisible(0, 0, 'visible:false'); + return Plotly.restyle(gd, {visible: true, cliponaxis: null}); + }) + .then(function() { + assertVisible(6, 0, 'cliponaxis:dflt'); + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + assertVisible(0, 0, 'visible:legendonly'); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + assertVisible(6, 0, 'back to clipnaxis:dflt'); + return Plotly.restyle(gd, 'cliponaxis', false); + }) + .then(function() { + assertVisible(0, 6, 'back to cliponaxis:false'); + return Plotly.relayout(gd, 'xaxis.range', [0, 1]); + }) + .then(function() { + assertVisible(0, 2, 'smaller x-range'); + return Plotly.relayout(gd, 'yaxis.range', [0, 1]); + }) + .then(function() { + assertVisible(0, 1, 'smaller y-range'); + return Plotly.relayout(gd, {'xaxis.range': xRange0, 'yaxis.range': yRange0}); + }) + .then(function() { + assertVisible(0, 6, 'back to original xy ranges'); + }) + .catch(fail) + .then(done); + }); + +}); diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js index 34c5e764370..5c5a5fcf390 100644 --- a/test/jasmine/tests/scatterternary_test.js +++ b/test/jasmine/tests/scatterternary_test.js @@ -6,6 +6,7 @@ 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'); @@ -374,3 +375,64 @@ 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 assertVisible(cnt, cntNoClip, msg) { + var selectors = ['.point', '.textpoint']; + var scatterLayer = d3.select('.frontplot > .scatterlayer'); + var scatterLayerNoClip = d3.select('.frontplotnoclip > .scatterlayer'); + + selectors.forEach(function(s) { + expect(scatterLayer.selectAll(s).size()) + .toBe(cnt, s + ' ' + msg); + expect(scatterLayerNoClip.selectAll(s).size()) + .toBe(cntNoClip, s + ' (noclip) ' + msg); + }); + } + + Plotly.plot(gd, fig) + .then(function() { + assertVisible(0, 11, 'cliponaxis:false'); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + assertVisible(0, 0, 'visible:false'); + return Plotly.restyle(gd, {visible: true, cliponaxis: null}); + }) + .then(function() { + assertVisible(11, 0, 'cliponaxis:dflt'); + return Plotly.restyle(gd, 'cliponaxis', false); + }) + .then(function() { + assertVisible(0, 11, 'back to cliponaxis:false'); + return Plotly.relayout(gd, 'ternary.aaxis.min', 20); + }) + .then(function() { + assertVisible(0, 5, 'zoomed about a-axis'); + return Plotly.relayout(gd, 'ternary.baxis.min', 20); + }) + .then(function() { + assertVisible(0, 3, 'zoomed about b-axis'); + return Plotly.relayout(gd, 'ternary.caxis.min', 10); + }) + .then(function() { + assertVisible(0, 1, 'zoomed about c-axis'); + return Plotly.relayout(gd, { + 'ternary.aaxis.min': null, + 'ternary.baxis.min': null, + 'ternary.caxis.min': null + }); + }) + .then(function() { + assertVisible(0, 11, 'back to original view'); + }) + .catch(fail) + .then(done); + }); +});