diff --git a/draftlogs/7822_fix.md b/draftlogs/7822_fix.md new file mode 100644 index 00000000000..3b4e4a25e56 --- /dev/null +++ b/draftlogs/7822_fix.md @@ -0,0 +1 @@ +- Prevent outside bar text labels from overlapping tilted axis ticks [[#7872](https://github.com/plotly/plotly.js/pull/7872)] \ No newline at end of file diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 37653f9c3a8..3b13b62c352 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -554,6 +554,7 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, op // get trace attributes var trace = cd[0].trace; var isHorizontal = trace.orientation === 'h'; + var zeroBarDir = getZeroBarDir(cd, isHorizontal, xa, ya); var text = getText(fullLayout, cd, i, xa, ya); @@ -707,7 +708,10 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, op transform = toMoveOutsideBar(x0, x1, y0, y1, textBB, { isHorizontal: isHorizontal, constrained: constrained, - angle: angle + angle: angle, + xa: xa, // Pass the X-Axis configuration + ya: ya, // Pass the Y-Axis configuration + zeroBarDir: zeroBarDir }); } else { constrained = trace.constraintext === 'both' || trace.constraintext === 'inside'; @@ -957,12 +961,35 @@ function toMoveOutsideBar(x0, x1, y0, y1, textBB, opts) { var anchorX = 0; var anchorY = 0; + // Dynamic presentation safety buffer to clear tilted axis tick labels + var axisPad = 0; + if (!isHorizontal && opts.xa && opts.xa.side === 'top') { + if (opts.xa._g && opts.xa._g.node()) { + var axisBB = opts.xa._g.node().getBBox(); + if (axisBB && axisBB.height > 0) { + // Shift exactly past the bounding height of the tilted labels plus a clean 6px visual gap + axisPad = axisBB.height + 6; + } + } + } else if (isHorizontal && opts.ya && opts.ya.side === 'right') { + if (opts.ya._g && opts.ya._g.node()) { + var axisBB = opts.ya._g.node().getBBox(); + if (axisBB && axisBB.width > 0) { + // Shift exactly past the bounding width of the side labels plus a clean 6px visual gap + axisPad = axisBB.width + 6; + } + } + } + var dir = isHorizontal ? dirSign(x1, x0) : dirSign(y0, y1); + if ((isHorizontal ? x0 === x1 : y0 === y1) && opts.zeroBarDir) { + dir = opts.zeroBarDir; + } if (isHorizontal) { - targetX = x1 - dir * textpad; + targetX = x1 - dir * (textpad + axisPad); anchorX = dir * extrapad; } else { - targetY = y1 + dir * textpad; + targetY = y1 + dir * (textpad + axisPad); anchorY = -dir * extrapad; } @@ -999,6 +1026,38 @@ function getTextPosition(trace, index) { return helpers.coerceEnumerated(attributeTextPosition, value); } +function getZeroBarDir(cd, isHorizontal, xa, ya) { + var hasPositive = false; + var hasNegative = false; + + for (var i = 0; i < cd.length; i++) { + var s = cd[i].s; + + if (s > 0) { + hasPositive = true; + } else if (s < 0) { + hasNegative = true; + } + + if (hasPositive && hasNegative) { + return 0; + } + } + + var axis = isHorizontal ? xa : ya; + var positiveDir = -dirSign(axis.range[0], axis.range[1]); + + if (!hasNegative) { + return positiveDir; + } + + if (!hasPositive) { + return -positiveDir; + } + + return 0; +} + function calcTexttemplate(fullLayout, cd, index, xa, ya) { var trace = cd[0].trace; var texttemplate = Lib.castOption(trace, index, 'texttemplate'); diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 107021ba1cd..e77a7a00820 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1411,6 +1411,34 @@ describe('A bar plot', function() { .then(done, done.fail); }); + it('should keep zero-value outside labels on the same side as negative bars', function(done) { + var data = [{ + y: [-10, 0, -30], + type: 'bar', + text: ['a', 'zero', 'c'], + textposition: 'outside' + }]; + + Plotly.newPlot(gd, data).then(function() { + var traceNodes = getAllTraceNodes(gd); + var barNodes = getAllBarNodes(traceNodes[0]); + var foundTextNodes; + + for(var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i]; + var pathNode = barNode.querySelector('path'); + var textNode = barNode.querySelector('text'); + if(textNode) { + foundTextNodes = true; + assertTextIsBelowPath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + }) + .then(done, done.fail); + }); + it('should show bar texts (horizontal case)', function(done) { var data = [{ x: [10, -20, 30],