Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7872_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Prevent outside bar text labels from overlapping tilted axis ticks [[#7872](https://github.com/plotly/plotly.js/pull/7872)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Prevent outside bar text labels from overlapping tilted axis ticks [[#7872](https://github.com/plotly/plotly.js/pull/7872)]
- Prevent outside bar text for zero-length bars from overlapping axis tick labels [[#7872](https://github.com/plotly/plotly.js/pull/7872)]

68 changes: 64 additions & 4 deletions src/traces/bar/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Comment on lines +711 to +713

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are also no longer needed

Suggested change
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';
Expand Down Expand Up @@ -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;
}
}
}

Comment on lines +964 to +983

@emilykl emilykl Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can all be removed now that the zeroBarDir logic has been added (please correct me if I'm missing something!). I tested it out locally and the issue seems resolved with just the zeroBarDir changes.

Along with removing the references to axisPad below

Suggested change
// 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;
}
Comment on lines 984 to +987

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is logically equivalent, just a style suggestion

Suggested change
var dir = isHorizontal ? dirSign(x1, x0) : dirSign(y0, y1);
if ((isHorizontal ? x0 === x1 : y0 === y1) && opts.zeroBarDir) {
dir = opts.zeroBarDir;
}
var dir;
if ((isHorizontal ? x0 === x1 : y0 === y1) && opts.zeroBarDir) {
dir = opts.zeroBarDir;
} else {
dir = isHorizontal ? dirSign(x1, x0) : dirSign(y0, y1);
}

if (isHorizontal) {
targetX = x1 - dir * textpad;
targetX = x1 - dir * (textpad + axisPad);
anchorX = dir * extrapad;
} else {
targetY = y1 + dir * textpad;
targetY = y1 + dir * (textpad + axisPad);
Comment on lines +989 to +992

@emilykl emilykl Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
targetX = x1 - dir * (textpad + axisPad);
anchorX = dir * extrapad;
} else {
targetY = y1 + dir * textpad;
targetY = y1 + dir * (textpad + axisPad);
targetX = x1 - dir * textpad;
anchorX = dir * extrapad;
} else {
targetY = y1 + dir * textpad;

anchorY = -dir * extrapad;
}

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -1160,5 +1219,6 @@ function calcTextinfo(cd, index, xa, ya) {

module.exports = {
plot: plot,
toMoveInsideBar: toMoveInsideBar
toMoveInsideBar: toMoveInsideBar,
toMoveOutsideBar: toMoveOutsideBar

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can also be removed

Suggested change
toMoveOutsideBar: toMoveOutsideBar

};
28 changes: 28 additions & 0 deletions test/jasmine/tests/bar_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test!

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],
Expand Down
Loading