diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 305be474860..d2c90fcb271 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -105,7 +105,7 @@ function getAutoRange(gd, ax) { lBreaks += brk.max - brk.min; } } - return (axReverse ? -1 : 1) * lBreaks; + return lBreaks; }; var mbest = 0; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 798d53cae33..62aedb1d05f 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -636,8 +636,10 @@ axes.calcTicks = function calcTicks(ax) { var newTickVals = []; var prevPos; - var signAx = axrev ? -1 : 1; - for(var q = axrev ? 0 : len - 1; signAx * q >= signAx * (axrev ? len - 1 : 0); q -= signAx) { // apply reverse loop to pick greater values in breaks first + var dir = axrev ? 1 : -1; + var first = axrev ? 0 : len - 1; + var last = axrev ? len - 1 : 0; + for(var q = first; dir * q <= dir * last; q += dir) { // apply reverse loop to pick greater values in breaks first var pos = ax.c2p(tickVals[q].value); if(prevPos === undefined || Math.abs(pos - prevPos) > tf2) { diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 2547f2413b7..920c06eaf6c 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -192,57 +192,46 @@ module.exports = function setConvert(ax, fullLayout) { }; if(ax.rangebreaks) { + var isY = axLetter === 'y'; + l2p = function(v) { if(!isNumeric(v)) return BADNUM; var len = ax._rangebreaks.length; if(!len) return _l2p(v, ax._m, ax._b); - var isY = axLetter === 'y'; - var pos = isY ? -v : v; + var flip = isY; + if(ax.range[0] > ax.range[1]) flip = !flip; + var signAx = flip ? -1 : 1; + var pos = signAx * v; var q = 0; for(var i = 0; i < len; i++) { - var nextI = i + 1; - var brk = ax._rangebreaks[i]; - - var min = isY ? -brk.max : brk.min; - var max = isY ? -brk.min : brk.max; + var min = signAx * ax._rangebreaks[i].min; + var max = signAx * ax._rangebreaks[i].max; if(pos < min) break; - if(pos > max) q = nextI; + if(pos > max) q = i + 1; else { // when falls into break, pick 'closest' offset - q = pos > (min + max) / 2 ? nextI : i; + q = pos < (min + max) / 2 ? i : i + 1; break; } } - return _l2p(v, (isY ? -1 : 1) * ax._m2, ax._B[q]); + var b2 = ax._B[q] || 0; + if(!isFinite(b2)) return 0; // avoid NaN translate e.g. in positionLabels if one keep zooming exactly into a break + return _l2p(v, ax._m2, b2); }; p2l = function(px) { - if(!isNumeric(px)) return BADNUM; var len = ax._rangebreaks.length; if(!len) return _p2l(px, ax._m, ax._b); - var isY = axLetter === 'y'; - var pos = isY ? -px : px; - var q = 0; for(var i = 0; i < len; i++) { - var nextI = i + 1; - var brk = ax._rangebreaks[i]; - - var min = isY ? -brk.pmax : brk.pmin; - var max = isY ? -brk.pmin : brk.pmax; - - if(pos < min) break; - if(pos > max) q = nextI; - else { - q = i; - break; - } + if(px < ax._rangebreaks[i].pmin) break; + if(px > ax._rangebreaks[i].pmax) q = i + 1; } - return _p2l(px, (isY ? -1 : 1) * ax._m2, ax._B[q]); + return _p2l(px, ax._m2, ax._B[q]); }; } @@ -541,7 +530,8 @@ module.exports = function setConvert(ax, fullLayout) { var rl0 = ax.r2l(ax[rangeAttr][0], calendar); var rl1 = ax.r2l(ax[rangeAttr][1], calendar); - if(axLetter === 'y') { + var isY = axLetter === 'y'; + if(isY) { ax._offset = gs.t + (1 - ax.domain[1]) * gs.h; ax._length = gs.h * (ax.domain[1] - ax.domain[0]); ax._m = ax._length / (rl0 - rl1); @@ -569,8 +559,6 @@ module.exports = function setConvert(ax, fullLayout) { Math.min(rl0, rl1), Math.max(rl0, rl1) ); - var axReverse = rl0 > rl1; - var signAx = axReverse ? -1 : 1; if(ax._rangebreaks.length) { for(i = 0; i < ax._rangebreaks.length; i++) { @@ -578,30 +566,27 @@ module.exports = function setConvert(ax, fullLayout) { ax._lBreaks += Math.abs(brk.max - brk.min); } - ax._m2 = ax._length / (rl1 - rl0 - ax._lBreaks * signAx); - - if(axLetter === 'y') { - ax._rangebreaks.reverse(); - // N.B. top to bottom (negative coord, positive px direction) - ax._B.push(ax._m2 * rl1); - } else { - ax._B.push(-ax._m2 * rl0); - } + var flip = isY; + if(rl0 > rl1) flip = !flip; + if(flip) ax._rangebreaks.reverse(); + var sign = flip ? -1 : 1; + ax._m2 = sign * ax._length / (Math.abs(rl1 - rl0) - ax._lBreaks); + ax._B.push(-ax._m2 * (isY ? rl1 : rl0)); for(i = 0; i < ax._rangebreaks.length; i++) { brk = ax._rangebreaks[i]; - ax._B.push(ax._B[ax._B.length - 1] - ax._m2 * (brk.max - brk.min) * signAx); - } - if(axReverse) { - ax._B.reverse(); + ax._B.push( + ax._B[ax._B.length - 1] - + sign * ax._m2 * (brk.max - brk.min) + ); } // fill pixel (i.e. 'p') min/max here, // to not have to loop through the _rangebreaks twice during `p2l` for(i = 0; i < ax._rangebreaks.length; i++) { brk = ax._rangebreaks[i]; - brk.pmin = l2p(axReverse ? brk.max : brk.min); - brk.pmax = l2p(axReverse ? brk.min : brk.max); + brk.pmin = l2p(brk.min); + brk.pmax = l2p(brk.max); } } } diff --git a/test/image/baselines/axes_breaks-night_autorange-reversed.png b/test/image/baselines/axes_breaks-night_autorange-reversed.png index fe4c5b0a942..8de472e9a5b 100644 Binary files a/test/image/baselines/axes_breaks-night_autorange-reversed.png and b/test/image/baselines/axes_breaks-night_autorange-reversed.png differ diff --git a/test/image/baselines/axes_breaks-reversed-without-pattern.png b/test/image/baselines/axes_breaks-reversed-without-pattern.png new file mode 100644 index 00000000000..010223d9103 Binary files /dev/null and b/test/image/baselines/axes_breaks-reversed-without-pattern.png differ diff --git a/test/image/mocks/axes_breaks-reversed-without-pattern.json b/test/image/mocks/axes_breaks-reversed-without-pattern.json new file mode 100644 index 00000000000..8155917735c --- /dev/null +++ b/test/image/mocks/axes_breaks-reversed-without-pattern.json @@ -0,0 +1,237 @@ +{ + "data": [ + { + "type": "scatter", + "mode": "markers+lines", + "x": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.020", + "1970-01-01 00:00:00.030", + "1970-01-01 00:00:00.040", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.060", + "1970-01-01 00:00:00.070", + "1970-01-01 00:00:00.080", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.110", + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.130", + "1970-01-01 00:00:00.140", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.160", + "1970-01-01 00:00:00.170", + "1970-01-01 00:00:00.180", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ] + }, + { + "xaxis": "x2", + "yaxis": "y2", + "type": "scatter", + "mode": "markers+lines", + "x": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.020", + "1970-01-01 00:00:00.030", + "1970-01-01 00:00:00.040", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.060", + "1970-01-01 00:00:00.070", + "1970-01-01 00:00:00.080", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.110", + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.130", + "1970-01-01 00:00:00.140", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.160", + "1970-01-01 00:00:00.170", + "1970-01-01 00:00:00.180", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ] + }, + { + "xaxis": "x3", + "yaxis": "y3", + "type": "scatter", + "mode": "markers+lines", + "orientation": "h", + "y": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.020", + "1970-01-01 00:00:00.030", + "1970-01-01 00:00:00.040", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.060", + "1970-01-01 00:00:00.070", + "1970-01-01 00:00:00.080", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.110", + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.130", + "1970-01-01 00:00:00.140", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.160", + "1970-01-01 00:00:00.170", + "1970-01-01 00:00:00.180", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ] + }, + { + "xaxis": "x4", + "yaxis": "y4", + "type": "scatter", + "mode": "markers+lines", + "orientation": "h", + "y": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.020", + "1970-01-01 00:00:00.030", + "1970-01-01 00:00:00.040", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.060", + "1970-01-01 00:00:00.070", + "1970-01-01 00:00:00.080", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.110", + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.130", + "1970-01-01 00:00:00.140", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.160", + "1970-01-01 00:00:00.170", + "1970-01-01 00:00:00.180", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ] + } + ], + "layout": { + "showlegend": false, + "width": 800, + "height": 800, + "xaxis": { + "rangebreaks": [ + { + "bounds": [ + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.075" + ] + }, + { + "bounds": [ + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.180" + ] + } + ], + "domain": [ + 0, + 0.48 + ] + }, + "xaxis2": { + "rangebreaks": [ + { + "bounds": [ + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.075" + ] + }, + { + "bounds": [ + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.180" + ] + } + ], + "autorange": "reversed", + "anchor": "y2", + "domain": [ + 0.52, + 1 + ] + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0, + 0.48 + ] + }, + "xaxis4": { + "anchor": "y4", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis": { + "domain": [ + 0, + 0.48 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis3": { + "rangebreaks": [ + { + "bounds": [ + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.075" + ] + }, + { + "bounds": [ + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.180" + ] + } + ], + "anchor": "x3", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis4": { + "rangebreaks": [ + { + "bounds": [ + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.075" + ] + }, + { + "bounds": [ + "1970-01-01 00:00:00.120", + "1970-01-01 00:00:00.180" + ] + } + ], + "autorange": "reversed", + "anchor": "x4", + "domain": [ + 0, + 0.48 + ] + } + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index c0882817e33..ed36147de3e 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -4761,7 +4761,7 @@ describe('Test axes', function() { .then(function() { _assert('2 disjoint rangebreaks within range', 'y', { rangebreaks: [[101, 189], [11, 89]], - m2: 6.923076923076923, + m2: -6.923076923076923, B: [1401.923, 792.692, 252.692] }); }) @@ -4781,7 +4781,7 @@ describe('Test axes', function() { .then(function() { _assert('2 overlapping rangebreaks within range', 'y', { rangebreaks: [[11, 189]], - m2: 10.714285714283243, + m2: -10.714285714283243, B: [2160, 252.857] }); }) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b599afb25b0..c117315d5e4 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -2778,6 +2778,75 @@ describe('Hover on axes with rangebreaks', function() { .then(done); }); + it('should work when rangebreaks are present on x-axis (reversed range)', function(done) { + Plotly.plot(gd, [{ + mode: 'lines', // i.e. no autorange padding + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.095', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: { + autorange: 'reversed', + rangebreaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }, + width: 400, + height: 400, + margin: {l: 10, t: 10, b: 10, r: 10}, + hovermode: 'x' + }) + .then(function() { + gd.on('plotly_hover', function(d) { + eventData = d.points[0]; + }); + }) + .then(function() { _hover(11, 11); }) + .then(function() { + _assert('leftmost interval', { + nums: '8', + axis: 'Jan 1, 1970, 00:00:00.2', + x: '1970-01-01 00:00:00.2', + y: 8 + }); + }) + .then(function() { _hover(200, 200); }) + .then(function() { + _assert('middle interval', { + nums: '4', + axis: 'Jan 1, 1970, 00:00:00.095', + x: '1970-01-01 00:00:00.095', + y: 4 + }); + }) + .then(function() { _hover(388, 388); }) + .then(function() { + _assert('rightmost interval', { + nums: '0', + axis: 'Jan 1, 1970', + x: '1970-01-01', + y: 0 + }); + }) + .catch(failTest) + .then(done); + }); + it('should work when rangebreaks are present on y-axis using hovermode x (case of bar and autorange reversed)', function(done) { Plotly.plot(gd, [{ type: 'bar', @@ -2924,6 +2993,75 @@ describe('Hover on axes with rangebreaks', function() { .catch(failTest) .then(done); }); + + it('should work when rangebreaks are present on y-axis (reversed range)', function(done) { + Plotly.plot(gd, [{ + mode: 'lines', // i.e. no autorange padding + y: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.095', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + yaxis: { + autorange: 'reversed', + rangebreaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }, + width: 400, + height: 400, + margin: {l: 10, t: 10, b: 10, r: 10}, + hovermode: 'y' + }) + .then(function() { + gd.on('plotly_hover', function(d) { + eventData = d.points[0]; + }); + }) + .then(function() { _hover(388, 30); }) + .then(function() { + _assert('topmost interval', { + nums: '0', + axis: 'Jan 1, 1970', + x: 0, + y: '1970-01-01' + }); + }) + .then(function() { _hover(200, 200); }) + .then(function() { + _assert('middle interval', { + nums: '4', + axis: 'Jan 1, 1970, 00:00:00.095', + x: 4, + y: '1970-01-01 00:00:00.095' + }); + }) + .then(function() { _hover(11, 370); }) + .then(function() { + _assert('bottom interval', { + nums: '8', + axis: 'Jan 1, 1970, 00:00:00.2', + x: 8, + y: '1970-01-01 00:00:00.2' + }); + }) + .catch(failTest) + .then(done); + }); }); describe('hover updates', function() {