diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 9efcca25773..aa185cbf328 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -89,8 +89,9 @@ function lsInner(gd) { // figure out the main axis line and main mirror line position. // it's easier to follow the logic if we handle these separately from - // ax._linepositions, which are really only used by mirror=allticks - // for the non-main-subplot ticks. + // ax._linepositions, which are only used by mirror=allticks + // for non-main-subplot ticks, and mirror=all(ticks)? for zero line + // hiding logic ax._mainLinePosition = getLinePosition(ax, counterAx, ax.side); ax._mainMirrorPosition = (ax.mirror && counterAx) ? getLinePosition(ax, counterAx, @@ -270,7 +271,7 @@ function lsInner(gd) { // each subplot that gets ticks from "allticks" gets an entry: // [left or bottom, right or top] extraSubplot = (!xa._anchorAxis || subplot !== xa._mainSubplot); - if(extraSubplot && xa.ticks && xa.mirror === 'allticks') { + if(extraSubplot && (xa.mirror === 'allticks' || xa.mirror === 'all')) { xa._linepositions[subplot] = [xLinesYBottom, xLinesYTop]; } @@ -306,8 +307,8 @@ function lsInner(gd) { yLinesXLeft = getLinePosition(ya, xa, 'left'); yLinesXRight = getLinePosition(ya, xa, 'right'); - extraSubplot = (!ya._anchorAxis || subplot !== xa._mainSubplot); - if(extraSubplot && ya.ticks && ya.mirror === 'allticks') { + extraSubplot = (!ya._anchorAxis || subplot !== ya._mainSubplot); + if(extraSubplot && (ya.mirror === 'allticks' || ya.mirror === 'all')) { ya._linepositions[subplot] = [yLinesXLeft, yLinesXRight]; } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index df122968484..69141a0b784 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2146,6 +2146,51 @@ axes.doTicksSingle = function(gd, arg, skipTitle) { return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter; } + function lineNearZero(ax2, position) { + if(!ax2.showline || !ax2.linewidth) return false; + var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1); + + function closeEnough(pos2) { + return typeof pos2 === 'number' && Math.abs(pos2 - position) < tolerance; + } + + if(closeEnough(ax2._mainLinePosition) || closeEnough(ax2._mainMirrorPosition)) { + return true; + } + var linePositions = ax2._linepositions || {}; + for(var k in linePositions) { + if(closeEnough(linePositions[k][0]) || closeEnough(linePositions[k][1])) { + return true; + } + } + } + + function anyCounterAxLineAtZero(counterAxis, rng) { + var mainCounterAxis = counterAxis._mainAxis; + if(!mainCounterAxis) return; + + var zeroPosition = ax._offset + ( + ((Math.abs(rng[0]) < Math.abs(rng[1])) === (axLetter === 'x')) ? + 0 : ax._length + ); + + var plotinfo = fullLayout._plots[counterAxis._mainSubplot]; + if(!(plotinfo.mainplotinfo || plotinfo).overlays.length) { + return lineNearZero(counterAxis, zeroPosition); + } + + var counterLetterAxes = axes.list(gd, counterLetter); + for(var i = 0; i < counterLetterAxes.length; i++) { + var counterAxis2 = counterLetterAxes[i]; + if( + counterAxis2._mainAxis === mainCounterAxis && + lineNearZero(counterAxis2, zeroPosition) + ) { + return true; + } + } + } + function drawGrid(plotinfo, counteraxis, subplot) { if(fullLayout._hasOnlyLargeSploms) return; @@ -2182,12 +2227,19 @@ axes.doTicksSingle = function(gd, arg, skipTitle) { break; } } - var rng = Lib.simpleMap(ax.range, ax.r2l), - showZl = (rng[0] * rng[1] <= 0) && ax.zeroline && + var rng = Lib.simpleMap(ax.range, ax.r2l); + var zlData = {x: 0, id: axid}; + + var showZl = (rng[0] * rng[1] <= 0) && ax.zeroline && (ax.type === 'linear' || ax.type === '-') && gridvals.length && - (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline); + ( + hasBarsOrFill || + clipEnds(zlData) || + !anyCounterAxLineAtZero(counteraxis, rng) + ); + var zl = zlcontainer.selectAll('path.' + zcls) - .data(showZl ? [{x: 0, id: axid}] : []); + .data(showZl ? [zlData] : []); zl.enter().append('path').classed(zcls, 1).classed('zl', 1) .classed('crisp', 1) .attr('d', gridpath) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 361d624ea7f..580f040990a 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -573,7 +573,7 @@ module.exports = { valType: 'boolean', dflt: false, role: 'style', - editType: 'layoutstyle', + editType: 'ticks+layoutstyle', description: [ 'Determines whether or not a line bounding this axis is drawn.' ].join(' ') diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 06e593da385..a5d91c7718e 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -2783,6 +2783,148 @@ describe('Test axes', function() { }); }); + + describe('zeroline visibility logic', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + function assertZeroLines(expectedIDs) { + var sortedIDs = expectedIDs.slice().sort(); + var zlIDs = []; + d3.select(gd).selectAll('.zl').each(function() { + var cls = d3.select(this).attr('class'); + var clsMatch = cls.match(/[xy]\d*(?=zl)/g)[0]; + zlIDs.push(clsMatch); + }); + zlIDs.sort(); + expect(zlIDs).toEqual(sortedIDs); + } + + it('works with a single subplot', function(done) { + Plotly.newPlot(gd, [{x: [1, 2, 3], y: [1, 2, 3]}], { + xaxis: {range: [0, 4], showzeroline: true, showline: true}, + yaxis: {range: [0, 4], showzeroline: true, showline: true}, + width: 600, + height: 600 + }) + .then(function() { + assertZeroLines([]); + return Plotly.relayout(gd, {'xaxis.showline': false}); + }) + .then(function() { + assertZeroLines(['y']); + return Plotly.relayout(gd, {'xaxis.showline': true, 'yaxis.showline': false}); + }) + .then(function() { + assertZeroLines(['x']); + return Plotly.relayout(gd, {'yaxis.showline': true, 'yaxis.range': [4, 0]}); + }) + .then(function() { + assertZeroLines(['y']); + return Plotly.relayout(gd, {'xaxis.range': [4, 0], 'xaxis.side': 'top'}); + }) + .then(function() { + assertZeroLines(['x']); + return Plotly.relayout(gd, {'yaxis.side': 'right', 'xaxis.anchor': 'free', 'xaxis.position': 1}); + }) + .then(function() { + assertZeroLines([]); + return Plotly.relayout(gd, {'xaxis.range': [0, 4], 'yaxis.range': [0, 4]}); + }) + .then(function() { + assertZeroLines(['x', 'y']); + return Plotly.relayout(gd, {'xaxis.mirror': 'all', 'yaxis.mirror': true}); + }) + .then(function() { + assertZeroLines([]); + return Plotly.relayout(gd, {'xaxis.range': [-0.1, 4], 'yaxis.range': [-0.1, 4]}); + }) + .then(function() { + assertZeroLines(['x', 'y']); + }) + .catch(failTest) + .then(done); + }); + + it('works with multiple coupled subplots', function(done) { + Plotly.newPlot(gd, [ + {x: [1, 2, 3], y: [1, 2, 3]}, + {x: [1, 2, 3], y: [1, 2, 3], xaxis: 'x2'}, + {x: [1, 2, 3], y: [1, 2, 3], yaxis: 'y2'} + ], { + xaxis: {range: [0, 4], showzeroline: true, domain: [0, 0.4]}, + yaxis: {range: [0, 4], showzeroline: true, domain: [0, 0.4]}, + xaxis2: {range: [0, 4], showzeroline: true, domain: [0.6, 1]}, + yaxis2: {range: [0, 4], showzeroline: true, domain: [0.6, 1]}, + width: 600, + height: 600 + }) + .then(function() { + assertZeroLines(['x', 'x', 'y', 'y', 'x2', 'y2']); + return Plotly.relayout(gd, {'xaxis.showline': true, 'xaxis.mirror': 'all'}); + }) + .then(function() { + assertZeroLines(['x', 'x', 'y', 'x2']); + return Plotly.relayout(gd, {'yaxis.showline': true, 'yaxis.mirror': 'all'}); + }) + .then(function() { + // x axis still has a zero line on xy2, and y on x2y + // all the others have disappeared now + assertZeroLines(['x', 'y']); + return Plotly.relayout(gd, {'xaxis.mirror': 'allticks', 'yaxis.mirror': 'allticks'}); + }) + .then(function() { + // allticks works the same as all + assertZeroLines(['x', 'y']); + }) + .catch(failTest) + .then(done); + }); + + it('works with multiple overlaid subplots', function(done) { + Plotly.newPlot(gd, [ + {x: [1, 2, 3], y: [1, 2, 3]}, + {x: [1, 2, 3], y: [1, 2, 3], xaxis: 'x2', yaxis: 'y2'} + ], { + xaxis: {range: [0, 4], showzeroline: true}, + yaxis: {range: [0, 4], showzeroline: true}, + xaxis2: {range: [0, 4], showzeroline: true, side: 'top', overlaying: 'x'}, + yaxis2: {range: [0, 4], showzeroline: true, side: 'right', overlaying: 'y'}, + width: 600, + height: 600 + }) + .then(function() { + assertZeroLines(['x', 'y', 'x2', 'y2']); + return Plotly.relayout(gd, {'xaxis.showline': true, 'yaxis.showline': true}); + }) + .then(function() { + assertZeroLines([]); + return Plotly.relayout(gd, { + 'xaxis.range': [4, 0], + 'yaxis.range': [4, 0], + 'xaxis2.range': [4, 0], + 'yaxis2.range': [4, 0] + }); + }) + .then(function() { + assertZeroLines(['x', 'y', 'x2', 'y2']); + return Plotly.relayout(gd, { + 'xaxis.showline': false, + 'yaxis.showline': false, + 'xaxis2.showline': true, + 'yaxis2.showline': true + }); + }) + .then(function() { + assertZeroLines([]); + }) + .catch(failTest) + .then(done); + }); + }); }); function getZoomInButton(gd) {