diff --git a/draftlogs/6967_fix.md b/draftlogs/6967_fix.md new file mode 100644 index 00000000000..ff57fc8ba27 --- /dev/null +++ b/draftlogs/6967_fix.md @@ -0,0 +1,2 @@ + - Fix applying `autotickangles` on axes with `showdividers` as well as cases where `tickson` is set to "boundaries" [[#6967](https://github.com/plotly/plotly.js/pull/6967)], + with thanks to @my-tien for the contribution! diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 1386cb8418c..782751f49c3 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -3748,12 +3748,42 @@ axes.drawLabels = function(gd, ax, opts) { }); }); - if((ax.tickson === 'boundaries' || ax.showdividers) && !opts.secondary) { + // autotickangles + // if there are dividers or ticks on boundaries, the labels will be in between and + // we need to prevent overlap with the next divider/tick. Else the labels will be on + // the ticks and we need to prevent overlap with the next label. + + // TODO should secondary labels also fall into this fix-overlap regime? + var preventOverlapWithTick = (ax.tickson === 'boundaries' || ax.showdividers) && !opts.secondary; + + var vLen = vals.length; + var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1); + + var adjacent = preventOverlapWithTick ? tickSpacing / 2 : tickSpacing; + var opposite = preventOverlapWithTick ? ax.ticklen : maxFontSize * 1.25 * maxLines; + var hypotenuse = Math.sqrt(Math.pow(adjacent, 2) + Math.pow(opposite, 2)); + var maxCos = adjacent / hypotenuse; + var autoTickAnglesRadians = ax.autotickangles.map( + function(degrees) { return degrees * Math.PI / 180; } + ); + var angleRadians = autoTickAnglesRadians.find( + function(angle) { return Math.abs(Math.cos(angle)) <= maxCos; } + ); + if(angleRadians === undefined) { + // no angle with smaller cosine than maxCos, just pick the angle with smallest cosine + angleRadians = autoTickAnglesRadians.reduce( + function(currentMax, nextAngle) { + return Math.abs(Math.cos(currentMax)) < Math.abs(Math.cos(nextAngle)) ? currentMax : nextAngle; + } + , autoTickAnglesRadians[0] + ); + } + var newAngle = angleRadians * (180 / Math.PI /* to degrees */); + + if(preventOverlapWithTick) { var gap = 2; if(ax.ticks) gap += ax.tickwidth / 2; - // TODO should secondary labels also fall into this fix-overlap regime? - for(i = 0; i < lbbArray.length; i++) { var xbnd = vals[i].xbnd; var lbb = lbbArray[i]; @@ -3761,14 +3791,11 @@ axes.drawLabels = function(gd, ax, opts) { (xbnd[0] !== null && (lbb.left - ax.l2p(xbnd[0])) < gap) || (xbnd[1] !== null && (ax.l2p(xbnd[1]) - lbb.right) < gap) ) { - autoangle = 90; + autoangle = newAngle; break; } } } else { - var vLen = vals.length; - var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1); - var ticklabelposition = ax.ticklabelposition || ''; var has = function(str) { return ticklabelposition.indexOf(str) !== -1; @@ -3779,29 +3806,7 @@ axes.drawLabels = function(gd, ax, opts) { var isBottom = has('bottom'); var isAligned = isBottom || isLeft || isTop || isRight; var pad = !isAligned ? 0 : - (ax.tickwidth || 0) + 2 * TEXTPAD; - - // autotickangles - var adjacent = tickSpacing; - var opposite = maxFontSize * 1.25 * maxLines; - var hypotenuse = Math.sqrt(Math.pow(adjacent, 2) + Math.pow(opposite, 2)); - var maxCos = adjacent / hypotenuse; - var autoTickAnglesRadians = ax.autotickangles.map( - function(degrees) { return degrees * Math.PI / 180; } - ); - var angleRadians = autoTickAnglesRadians.find( - function(angle) { return Math.abs(Math.cos(angle)) <= maxCos; } - ); - if(angleRadians === undefined) { - // no angle with smaller cosine than maxCos, just pick the angle with smallest cosine - angleRadians = autoTickAnglesRadians.reduce( - function(currentMax, nextAngle) { - return Math.abs(Math.cos(currentMax)) < Math.abs(Math.cos(nextAngle)) ? currentMax : nextAngle; - } - , autoTickAnglesRadians[0] - ); - } - var newAngle = angleRadians * (180 / Math.PI /* to degrees */); + (ax.tickwidth || 0) + 2 * TEXTPAD; for(i = 0; i < lbbArray.length - 1; i++) { if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) { diff --git a/test/image/baselines/tickson_boundaries.png b/test/image/baselines/tickson_boundaries.png index 0b1c752609e..4c96e37006f 100644 Binary files a/test/image/baselines/tickson_boundaries.png and b/test/image/baselines/tickson_boundaries.png differ diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 8a9ff54d1f0..04173c70326 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -4896,13 +4896,13 @@ describe('Test axes', function() { function _assert(msg, exp) { var tickLabels = d3SelectAll('.xtick > text'); - expect(tickLabels.size()).toBe(exp.angle.length, msg + ' - # of tick labels'); + expect(tickLabels.size()).withContext(msg + ' - # of tick labels').toBe(exp.angle.length); tickLabels.each(function(_, i) { var t = d3Select(this).attr('transform'); var rotate = (t.split('rotate(')[1] || '').split(')')[0]; var angle = rotate.split(',')[0]; - expect(Number(angle)).toBe(exp.angle[i], msg + ' - node ' + i); + expect(Number(angle)).withContext(msg + ' - node ' + i).toBeCloseTo(exp.angle[i], 2); }); } @@ -4920,7 +4920,7 @@ describe('Test axes', function() { }) .then(function() { _assert('base - rotated', { - angle: [90, 90, 90] + angle: [30, 30, 30] }); return Plotly.relayout(gd, 'xaxis.range', [-0.4, 1.4]); @@ -4934,7 +4934,7 @@ describe('Test axes', function() { }) .then(function() { _assert('narrow range / wide ticks - rotated', { - angle: [90, 90] + angle: [30, 30] }); }) .then(done, done.fail); diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 1f9524e7a6f..c2b97c67071 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -779,13 +779,13 @@ describe('axis zoom/pan and main plot zoom', function() { function _assertLabels(msg, exp) { var tickLabels = d3Select(gd).selectAll('.xtick > text'); - expect(tickLabels.size()).toBe(exp.angle.length, msg + ' - # of tick labels'); + expect(tickLabels.size()).withContext(msg + ' - # of tick labels').toBe(exp.angle.length); tickLabels.each(function(_, i) { var t = d3Select(this).attr('transform'); var rotate = (t.split('rotate(')[1] || '').split(')')[0]; var angle = rotate.split(',')[0]; - expect(Number(angle)).toBe(exp.angle[i], msg + ' - node ' + i); + expect(Number(angle)).withContext(msg + ' - node ' + i).toBeCloseTo(exp.angle[i], 2); }); var tickLabels2 = d3Select(gd).selectAll('.xtick2 > text'); @@ -813,7 +813,7 @@ describe('axis zoom/pan and main plot zoom', function() { }) .then(function() { return _run('drag to wide-range -> rotates labels', [-340, 0], { - angle: [90, 90, 90, 90, 90, 90, 90], + angle: [30, 30, 30, 30, 30, 30, 30], y: [430, 430] }); })