Skip to content

Commit a8dd8dd

Browse files
authored
Merge pull request #2936 from plotly/zeroline-fix
fix logic for hiding zero lines when they conflict with axis lines
2 parents b5f8b23 + 0f59ca2 commit a8dd8dd

File tree

4 files changed

+205
-10
lines changed

4 files changed

+205
-10
lines changed

src/plot_api/subroutines.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ function lsInner(gd) {
8989

9090
// figure out the main axis line and main mirror line position.
9191
// it's easier to follow the logic if we handle these separately from
92-
// ax._linepositions, which are really only used by mirror=allticks
93-
// for the non-main-subplot ticks.
92+
// ax._linepositions, which are only used by mirror=allticks
93+
// for non-main-subplot ticks, and mirror=all(ticks)? for zero line
94+
// hiding logic
9495
ax._mainLinePosition = getLinePosition(ax, counterAx, ax.side);
9596
ax._mainMirrorPosition = (ax.mirror && counterAx) ?
9697
getLinePosition(ax, counterAx,
@@ -270,7 +271,7 @@ function lsInner(gd) {
270271
// each subplot that gets ticks from "allticks" gets an entry:
271272
// [left or bottom, right or top]
272273
extraSubplot = (!xa._anchorAxis || subplot !== xa._mainSubplot);
273-
if(extraSubplot && xa.ticks && xa.mirror === 'allticks') {
274+
if(extraSubplot && (xa.mirror === 'allticks' || xa.mirror === 'all')) {
274275
xa._linepositions[subplot] = [xLinesYBottom, xLinesYTop];
275276
}
276277

@@ -306,8 +307,8 @@ function lsInner(gd) {
306307
yLinesXLeft = getLinePosition(ya, xa, 'left');
307308
yLinesXRight = getLinePosition(ya, xa, 'right');
308309

309-
extraSubplot = (!ya._anchorAxis || subplot !== xa._mainSubplot);
310-
if(extraSubplot && ya.ticks && ya.mirror === 'allticks') {
310+
extraSubplot = (!ya._anchorAxis || subplot !== ya._mainSubplot);
311+
if(extraSubplot && (ya.mirror === 'allticks' || ya.mirror === 'all')) {
311312
ya._linepositions[subplot] = [yLinesXLeft, yLinesXRight];
312313
}
313314

src/plots/cartesian/axes.js

+56-4
Original file line numberDiff line numberDiff line change
@@ -2146,6 +2146,51 @@ axes.doTicksSingle = function(gd, arg, skipTitle) {
21462146
return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter;
21472147
}
21482148

2149+
function lineNearZero(ax2, position) {
2150+
if(!ax2.showline || !ax2.linewidth) return false;
2151+
var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1);
2152+
2153+
function closeEnough(pos2) {
2154+
return typeof pos2 === 'number' && Math.abs(pos2 - position) < tolerance;
2155+
}
2156+
2157+
if(closeEnough(ax2._mainLinePosition) || closeEnough(ax2._mainMirrorPosition)) {
2158+
return true;
2159+
}
2160+
var linePositions = ax2._linepositions || {};
2161+
for(var k in linePositions) {
2162+
if(closeEnough(linePositions[k][0]) || closeEnough(linePositions[k][1])) {
2163+
return true;
2164+
}
2165+
}
2166+
}
2167+
2168+
function anyCounterAxLineAtZero(counterAxis, rng) {
2169+
var mainCounterAxis = counterAxis._mainAxis;
2170+
if(!mainCounterAxis) return;
2171+
2172+
var zeroPosition = ax._offset + (
2173+
((Math.abs(rng[0]) < Math.abs(rng[1])) === (axLetter === 'x')) ?
2174+
0 : ax._length
2175+
);
2176+
2177+
var plotinfo = fullLayout._plots[counterAxis._mainSubplot];
2178+
if(!(plotinfo.mainplotinfo || plotinfo).overlays.length) {
2179+
return lineNearZero(counterAxis, zeroPosition);
2180+
}
2181+
2182+
var counterLetterAxes = axes.list(gd, counterLetter);
2183+
for(var i = 0; i < counterLetterAxes.length; i++) {
2184+
var counterAxis2 = counterLetterAxes[i];
2185+
if(
2186+
counterAxis2._mainAxis === mainCounterAxis &&
2187+
lineNearZero(counterAxis2, zeroPosition)
2188+
) {
2189+
return true;
2190+
}
2191+
}
2192+
}
2193+
21492194
function drawGrid(plotinfo, counteraxis, subplot) {
21502195
if(fullLayout._hasOnlyLargeSploms) return;
21512196

@@ -2182,12 +2227,19 @@ axes.doTicksSingle = function(gd, arg, skipTitle) {
21822227
break;
21832228
}
21842229
}
2185-
var rng = Lib.simpleMap(ax.range, ax.r2l),
2186-
showZl = (rng[0] * rng[1] <= 0) && ax.zeroline &&
2230+
var rng = Lib.simpleMap(ax.range, ax.r2l);
2231+
var zlData = {x: 0, id: axid};
2232+
2233+
var showZl = (rng[0] * rng[1] <= 0) && ax.zeroline &&
21872234
(ax.type === 'linear' || ax.type === '-') && gridvals.length &&
2188-
(hasBarsOrFill || clipEnds({x: 0}) || !ax.showline);
2235+
(
2236+
hasBarsOrFill ||
2237+
clipEnds(zlData) ||
2238+
!anyCounterAxLineAtZero(counteraxis, rng)
2239+
);
2240+
21892241
var zl = zlcontainer.selectAll('path.' + zcls)
2190-
.data(showZl ? [{x: 0, id: axid}] : []);
2242+
.data(showZl ? [zlData] : []);
21912243
zl.enter().append('path').classed(zcls, 1).classed('zl', 1)
21922244
.classed('crisp', 1)
21932245
.attr('d', gridpath)

src/plots/cartesian/layout_attributes.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ module.exports = {
573573
valType: 'boolean',
574574
dflt: false,
575575
role: 'style',
576-
editType: 'layoutstyle',
576+
editType: 'ticks+layoutstyle',
577577
description: [
578578
'Determines whether or not a line bounding this axis is drawn.'
579579
].join(' ')

test/jasmine/tests/axes_test.js

+142
Original file line numberDiff line numberDiff line change
@@ -2783,6 +2783,148 @@ describe('Test axes', function() {
27832783

27842784
});
27852785
});
2786+
2787+
describe('zeroline visibility logic', function() {
2788+
var gd;
2789+
beforeEach(function() {
2790+
gd = createGraphDiv();
2791+
});
2792+
afterEach(destroyGraphDiv);
2793+
2794+
function assertZeroLines(expectedIDs) {
2795+
var sortedIDs = expectedIDs.slice().sort();
2796+
var zlIDs = [];
2797+
d3.select(gd).selectAll('.zl').each(function() {
2798+
var cls = d3.select(this).attr('class');
2799+
var clsMatch = cls.match(/[xy]\d*(?=zl)/g)[0];
2800+
zlIDs.push(clsMatch);
2801+
});
2802+
zlIDs.sort();
2803+
expect(zlIDs).toEqual(sortedIDs);
2804+
}
2805+
2806+
it('works with a single subplot', function(done) {
2807+
Plotly.newPlot(gd, [{x: [1, 2, 3], y: [1, 2, 3]}], {
2808+
xaxis: {range: [0, 4], showzeroline: true, showline: true},
2809+
yaxis: {range: [0, 4], showzeroline: true, showline: true},
2810+
width: 600,
2811+
height: 600
2812+
})
2813+
.then(function() {
2814+
assertZeroLines([]);
2815+
return Plotly.relayout(gd, {'xaxis.showline': false});
2816+
})
2817+
.then(function() {
2818+
assertZeroLines(['y']);
2819+
return Plotly.relayout(gd, {'xaxis.showline': true, 'yaxis.showline': false});
2820+
})
2821+
.then(function() {
2822+
assertZeroLines(['x']);
2823+
return Plotly.relayout(gd, {'yaxis.showline': true, 'yaxis.range': [4, 0]});
2824+
})
2825+
.then(function() {
2826+
assertZeroLines(['y']);
2827+
return Plotly.relayout(gd, {'xaxis.range': [4, 0], 'xaxis.side': 'top'});
2828+
})
2829+
.then(function() {
2830+
assertZeroLines(['x']);
2831+
return Plotly.relayout(gd, {'yaxis.side': 'right', 'xaxis.anchor': 'free', 'xaxis.position': 1});
2832+
})
2833+
.then(function() {
2834+
assertZeroLines([]);
2835+
return Plotly.relayout(gd, {'xaxis.range': [0, 4], 'yaxis.range': [0, 4]});
2836+
})
2837+
.then(function() {
2838+
assertZeroLines(['x', 'y']);
2839+
return Plotly.relayout(gd, {'xaxis.mirror': 'all', 'yaxis.mirror': true});
2840+
})
2841+
.then(function() {
2842+
assertZeroLines([]);
2843+
return Plotly.relayout(gd, {'xaxis.range': [-0.1, 4], 'yaxis.range': [-0.1, 4]});
2844+
})
2845+
.then(function() {
2846+
assertZeroLines(['x', 'y']);
2847+
})
2848+
.catch(failTest)
2849+
.then(done);
2850+
});
2851+
2852+
it('works with multiple coupled subplots', function(done) {
2853+
Plotly.newPlot(gd, [
2854+
{x: [1, 2, 3], y: [1, 2, 3]},
2855+
{x: [1, 2, 3], y: [1, 2, 3], xaxis: 'x2'},
2856+
{x: [1, 2, 3], y: [1, 2, 3], yaxis: 'y2'}
2857+
], {
2858+
xaxis: {range: [0, 4], showzeroline: true, domain: [0, 0.4]},
2859+
yaxis: {range: [0, 4], showzeroline: true, domain: [0, 0.4]},
2860+
xaxis2: {range: [0, 4], showzeroline: true, domain: [0.6, 1]},
2861+
yaxis2: {range: [0, 4], showzeroline: true, domain: [0.6, 1]},
2862+
width: 600,
2863+
height: 600
2864+
})
2865+
.then(function() {
2866+
assertZeroLines(['x', 'x', 'y', 'y', 'x2', 'y2']);
2867+
return Plotly.relayout(gd, {'xaxis.showline': true, 'xaxis.mirror': 'all'});
2868+
})
2869+
.then(function() {
2870+
assertZeroLines(['x', 'x', 'y', 'x2']);
2871+
return Plotly.relayout(gd, {'yaxis.showline': true, 'yaxis.mirror': 'all'});
2872+
})
2873+
.then(function() {
2874+
// x axis still has a zero line on xy2, and y on x2y
2875+
// all the others have disappeared now
2876+
assertZeroLines(['x', 'y']);
2877+
return Plotly.relayout(gd, {'xaxis.mirror': 'allticks', 'yaxis.mirror': 'allticks'});
2878+
})
2879+
.then(function() {
2880+
// allticks works the same as all
2881+
assertZeroLines(['x', 'y']);
2882+
})
2883+
.catch(failTest)
2884+
.then(done);
2885+
});
2886+
2887+
it('works with multiple overlaid subplots', function(done) {
2888+
Plotly.newPlot(gd, [
2889+
{x: [1, 2, 3], y: [1, 2, 3]},
2890+
{x: [1, 2, 3], y: [1, 2, 3], xaxis: 'x2', yaxis: 'y2'}
2891+
], {
2892+
xaxis: {range: [0, 4], showzeroline: true},
2893+
yaxis: {range: [0, 4], showzeroline: true},
2894+
xaxis2: {range: [0, 4], showzeroline: true, side: 'top', overlaying: 'x'},
2895+
yaxis2: {range: [0, 4], showzeroline: true, side: 'right', overlaying: 'y'},
2896+
width: 600,
2897+
height: 600
2898+
})
2899+
.then(function() {
2900+
assertZeroLines(['x', 'y', 'x2', 'y2']);
2901+
return Plotly.relayout(gd, {'xaxis.showline': true, 'yaxis.showline': true});
2902+
})
2903+
.then(function() {
2904+
assertZeroLines([]);
2905+
return Plotly.relayout(gd, {
2906+
'xaxis.range': [4, 0],
2907+
'yaxis.range': [4, 0],
2908+
'xaxis2.range': [4, 0],
2909+
'yaxis2.range': [4, 0]
2910+
});
2911+
})
2912+
.then(function() {
2913+
assertZeroLines(['x', 'y', 'x2', 'y2']);
2914+
return Plotly.relayout(gd, {
2915+
'xaxis.showline': false,
2916+
'yaxis.showline': false,
2917+
'xaxis2.showline': true,
2918+
'yaxis2.showline': true
2919+
});
2920+
})
2921+
.then(function() {
2922+
assertZeroLines([]);
2923+
})
2924+
.catch(failTest)
2925+
.then(done);
2926+
});
2927+
});
27862928
});
27872929

27882930
function getZoomInButton(gd) {

0 commit comments

Comments
 (0)