Skip to content

Commit 8f72d1f

Browse files
committed
implement cliponaxis
- keep track of which subplots have 1 or more `cliponaxis: false` traces + if so, don't clip the subplot layer + instead, clip all trace module layers except for 'scatterlayer' + when `cliponaxis: false`, clip all lines and errorbar groups - use ax.isWithinRange and Drawing.hideOutsideRangePoint to hide markers and text node that are out of range.
1 parent c28b26a commit 8f72d1f

File tree

10 files changed

+132
-32
lines changed

10 files changed

+132
-32
lines changed

src/components/drawing/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ drawing.translatePoints = function(s, xa, ya) {
9393
});
9494
};
9595

96+
drawing.hideOutsideRangePoint = function(d, sel, xa, ya) {
97+
sel.attr(
98+
'visibility',
99+
xa.isPtWithinRange(d) && ya.isPtWithinRange(d) ? null : 'hidden'
100+
);
101+
};
102+
96103
drawing.getPx = function(s, styleAttr) {
97104
// helper to pull out a px value from a style that may contain px units
98105
// s is a d3 selection (will pull from the first one)

src/components/errorbars/plot.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
1414

15+
var Drawing = require('../drawing');
1516
var subTypes = require('../../traces/scatter/subtypes');
1617

1718
module.exports = function plot(traces, plotinfo, transitionOpts) {
1819
var isNew;
1920

20-
var xa = plotinfo.xaxis,
21-
ya = plotinfo.yaxis;
21+
var xa = plotinfo.xaxis;
22+
var ya = plotinfo.yaxis;
2223

2324
var hasAnimation = transitionOpts && transitionOpts.duration > 0;
2425

@@ -60,6 +61,11 @@ module.exports = function plot(traces, plotinfo, transitionOpts) {
6061
.style('opacity', 1);
6162
}
6263

64+
errorbars.call(
65+
Drawing.setClipUrl,
66+
plotinfo._hasClipOnAxisFalse ? plotinfo.clipId : null
67+
);
68+
6369
errorbars.each(function(d) {
6470
var errorbar = d3.select(this);
6571
var coords = errorCoords(d, xa, ya);

src/plot_api/subroutines.js

+21-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var Drawing = require('../components/drawing');
2020
var Titles = require('../components/titles');
2121
var ModeBar = require('../components/modebar');
2222
var initInteractions = require('../plots/cartesian/graph_interact');
23+
var cartesianConstants = require('../plots/cartesian/constants');
2324

2425
exports.layoutStyles = function(gd) {
2526
return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd);
@@ -164,9 +165,27 @@ exports.lsInner = function(gd) {
164165
'height': ya._length
165166
});
166167

167-
168168
plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset);
169-
plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId);
169+
170+
var plotClipId;
171+
var layerClipId;
172+
173+
if(plotinfo._hasClipOnAxisFalse) {
174+
plotClipId = null;
175+
layerClipId = plotinfo.clipId;
176+
} else {
177+
plotClipId = plotinfo.clipId;
178+
layerClipId = null;
179+
}
180+
181+
plotinfo.plot.call(Drawing.setClipUrl, plotClipId);
182+
183+
for(i = 0; i < cartesianConstants.layers.length; i++) {
184+
var layer = cartesianConstants.layers[i];
185+
if(layer !== 'scatterlayer') {
186+
plotinfo.plot.selectAll('g.' + layer).call(Drawing.setClipUrl, layerClipId);
187+
}
188+
}
170189

171190
var xlw = Drawing.crispRound(gd, xa.linewidth, 1),
172191
ylw = Drawing.crispRound(gd, ya.linewidth, 1),

src/plots/cartesian/constants.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,21 @@ module.exports = {
4949

5050
// last resort axis ranges for x and y axes if we have no data
5151
DFLTRANGEX: [-1, 6],
52-
DFLTRANGEY: [-1, 4]
52+
DFLTRANGEY: [-1, 4],
53+
54+
// Layers to keep plot types in the right order.
55+
// from back to front:
56+
// 1. heatmaps, 2D histos and contour maps
57+
// 2. bars / 1D histos
58+
// 3. errorbars for bars and scatter
59+
// 4. scatter
60+
// 5. box plots
61+
layers: [
62+
'imagelayer',
63+
'maplayer',
64+
'barlayer',
65+
'carpetlayer',
66+
'boxlayer',
67+
'scatterlayer'
68+
]
5369
};

src/plots/cartesian/index.js

+6-19
Original file line numberDiff line numberDiff line change
@@ -294,24 +294,8 @@ function makeSubplotData(gd) {
294294
}
295295

296296
function makeSubplotLayer(plotinfo) {
297-
var plotgroup = plotinfo.plotgroup,
298-
id = plotinfo.id;
299-
300-
// Layers to keep plot types in the right order.
301-
// from back to front:
302-
// 1. heatmaps, 2D histos and contour maps
303-
// 2. bars / 1D histos
304-
// 3. errorbars for bars and scatter
305-
// 4. scatter
306-
// 5. box plots
307-
function joinPlotLayers(parent) {
308-
joinLayer(parent, 'g', 'imagelayer');
309-
joinLayer(parent, 'g', 'maplayer');
310-
joinLayer(parent, 'g', 'barlayer');
311-
joinLayer(parent, 'g', 'carpetlayer');
312-
joinLayer(parent, 'g', 'boxlayer');
313-
joinLayer(parent, 'g', 'scatterlayer');
314-
}
297+
var plotgroup = plotinfo.plotgroup;
298+
var id = plotinfo.id;
315299

316300
if(!plotinfo.mainplot) {
317301
var backLayer = joinLayer(plotgroup, 'g', 'layer-subplot');
@@ -354,7 +338,10 @@ function makeSubplotLayer(plotinfo) {
354338
}
355339

356340
// common attributes for all subplots, overlays or not
357-
plotinfo.plot.call(joinPlotLayers);
341+
342+
for(var i = 0; i < constants.layers.length; i++) {
343+
joinLayer(plotinfo.plot, 'g', constants.layers[i]);
344+
}
358345

359346
plotinfo.xlines
360347
.style('fill', 'none')

src/plots/cartesian/set_convert.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ function fromLog(v) {
5858
module.exports = function setConvert(ax, fullLayout) {
5959
fullLayout = fullLayout || {};
6060

61+
var axLetter = (ax._id || 'x').charAt(0);
62+
6163
// clipMult: how many axis lengths past the edge do we render?
6264
// for panning, 1-2 would suffice, but for zooming more is nice.
6365
// also, clipping can affect the direction of lines off the edge...
@@ -277,7 +279,6 @@ module.exports = function setConvert(ax, fullLayout) {
277279
ax.cleanRange = function(rangeAttr) {
278280
if(!rangeAttr) rangeAttr = 'range';
279281
var range = Lib.nestedProperty(ax, rangeAttr).get(),
280-
axLetter = (ax._id || 'x').charAt(0),
281282
i, dflt;
282283

283284
if(ax.type === 'date') dflt = Lib.dfltRange(ax.calendar);
@@ -341,8 +342,7 @@ module.exports = function setConvert(ax, fullLayout) {
341342

342343
// set scaling to pixels
343344
ax.setScale = function(usePrivateRange) {
344-
var gs = fullLayout._size,
345-
axLetter = ax._id.charAt(0);
345+
var gs = fullLayout._size;
346346

347347
// TODO cleaner way to handle this case
348348
if(!ax._categories) ax._categories = [];
@@ -435,6 +435,18 @@ module.exports = function setConvert(ax, fullLayout) {
435435
);
436436
};
437437

438+
if(axLetter === 'x') {
439+
ax.isPtWithinRange = function(d) {
440+
var x = d.x;
441+
return x >= ax.range[0] && x <= ax.range[1];
442+
};
443+
} else {
444+
ax.isPtWithinRange = function(d) {
445+
var y = d.y;
446+
return y >= ax.range[0] && y <= ax.range[1];
447+
};
448+
}
449+
438450
// for autoranging: arrays of objects:
439451
// {val: axis value, pad: pixel padding}
440452
// on the low and high sides

src/plots/plots.js

+19
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,25 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
656656

657657
plotinfo.xaxis = Plotly.Axes.getFromId(mockGd, id, 'x');
658658
plotinfo.yaxis = Plotly.Axes.getFromId(mockGd, id, 'y');
659+
660+
// By default, we clip at the subplot level,
661+
// but if one trace on a given subplot has *cliponaxis* set to false,
662+
// we need to clip at the trace module layer level;
663+
// find this out here, once of for all.
664+
plotinfo._hasClipOnAxisFalse = false;
665+
666+
for(var j = 0; j < newFullData.length; j++) {
667+
var trace = newFullData[j];
668+
669+
if(
670+
trace.xaxis === plotinfo.xaxis._id &&
671+
trace.yaxis === plotinfo.yaxis._id &&
672+
trace.cliponaxis === false
673+
) {
674+
plotinfo._hasClipOnAxisFalse = true;
675+
break;
676+
}
677+
}
659678
}
660679
};
661680

src/traces/scatter/attributes.js

+12
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ module.exports = {
168168
].join(' ')
169169
}
170170
},
171+
171172
connectgaps: {
172173
valType: 'boolean',
173174
dflt: false,
@@ -178,6 +179,17 @@ module.exports = {
178179
'in the provided data arrays are connected.'
179180
].join(' ')
180181
},
182+
cliponaxis: {
183+
valType: 'boolean',
184+
dflt: true,
185+
role: 'info',
186+
editType: 'doplot',
187+
description: [
188+
'Determines whether or not markers and text nodes',
189+
'are clipped about the subplot axes.'
190+
].join(' ')
191+
},
192+
181193
fill: {
182194
valType: 'enumerated',
183195
values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'],

src/traces/scatter/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7575

7676
errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'});
7777
errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'});
78+
79+
coerce('cliponaxis');
7880
};

src/traces/scatter/plot.js

+25-5
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCo
4646
// the z-order of fill layers is correct.
4747
linkTraces(gd, plotinfo, cdscatter);
4848

49-
createFills(gd, scatterlayer);
49+
createFills(gd, scatterlayer, plotinfo);
5050

5151
// Sort the traces, once created, so that the ordering is preserved even when traces
5252
// 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
100100
scatterlayer.selectAll('path:not([d])').remove();
101101
};
102102

103-
function createFills(gd, scatterlayer) {
103+
function createFills(gd, scatterlayer, plotinfo) {
104104
var trace;
105105

106106
scatterlayer.selectAll('g.trace').each(function(d) {
@@ -138,6 +138,10 @@ function createFills(gd, scatterlayer) {
138138
tr.selectAll('.js-fill.js-tozero').remove();
139139
trace._ownFill = null;
140140
}
141+
142+
if(plotinfo._hasClipOnAxisFalse) {
143+
tr.selectAll('.js-fill').call(Drawing.setClipUrl, plotinfo.clipId);
144+
}
141145
});
142146
}
143147

@@ -324,6 +328,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
324328
.call(Drawing.lineGroupStyle)
325329
.each(makeUpdate(true));
326330

331+
if(plotinfo._hasClipOnAxisFalse) {
332+
Drawing.setClipUrl(lineJoin, plotinfo.clipId);
333+
}
334+
327335
if(segments.length) {
328336
if(ownFillEl3) {
329337
if(pt0 && pt1) {
@@ -400,7 +408,8 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
400408
var trace = d[0].trace,
401409
s = d3.select(this),
402410
showMarkers = subTypes.hasMarkers(trace),
403-
showText = subTypes.hasText(trace);
411+
showText = subTypes.hasText(trace),
412+
hasClipOnAxisFalse = trace.cliponaxis === false;
404413

405414
var keyFunc = getKeyFunc(trace),
406415
markerFilter = hideFilter,
@@ -426,7 +435,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
426435
if(hasTransition) {
427436
enter
428437
.call(Drawing.pointStyle, trace, gd)
429-
.call(Drawing.translatePoints, xa, ya, trace)
438+
.call(Drawing.translatePoints, xa, ya)
430439
.style('opacity', 0)
431440
.transition()
432441
.style('opacity', 1);
@@ -445,6 +454,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
445454
if(hasNode) {
446455
Drawing.singlePointStyle(d, sel, trace, markerScale, lineScale, gd);
447456

457+
if(hasClipOnAxisFalse) {
458+
Drawing.hideOutsideRangePoint(d, sel, xa, ya);
459+
}
460+
448461
if(trace.customdata) {
449462
el.classed('plotly-customdata', d.data !== null && d.data !== undefined);
450463
}
@@ -475,7 +488,14 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
475488
var g = d3.select(this);
476489
var sel = transition(g.select('text'));
477490
hasNode = Drawing.translatePoint(d, sel, xa, ya);
478-
if(!hasNode) g.remove();
491+
492+
if(hasNode) {
493+
if(hasClipOnAxisFalse) {
494+
Drawing.hideOutsideRangePoint(d, g, xa, ya);
495+
}
496+
} else {
497+
g.remove();
498+
}
479499
});
480500

481501
join.selectAll('text')

0 commit comments

Comments
 (0)