Skip to content

Conversation

@emilykl
Copy link
Contributor

@emilykl emilykl commented Dec 16, 2025

Closes #2001

Adds a step during bar calc to add extra padding to axis range if bar has outside text labels.

Note: The first commit in this PR is just formatting changes. Use this diff to skip the first commit and see only the meaningful changes.

Codepen here! https://codepen.io/emilykl-code/pen/JoXQqLL

This is a stopgap fix for the fact that text labels are not taken into account during autorange calculations (see #2000).

Unfortunately, as discussed in that issue, modifying autorange to account for text labels is a significant amount of work, so I've opted for a more limited fix to address the issue identified in #2001.

Changes in this PR:

  • Add a function which computes an estimate for the amount of additional axis space (in pixels) required by bar text labels
    • This estimate is equal to the number of lines in the bar text (the maximum of the whole array), times the font size, plus the padding between text and bar
    • This many pixels are then added to the axis range on the relevant side of the bar plot
    • The extra padding is ONLY applied when all of the following are true: textposition is 'outside', orientation is vertical, and text or texttemplate is provided and non-falsy, and textangle is 0 or 'auto'
      • These exclusions are to avoid cases where the text is not parallel with the top of the bar, which are significantly harder to estimate due to needing to estimate the text width rather than the height
      • The goal is to improve the current situation by adding padding in the most common cases where needed, while avoiding adding too much padding to the extent possible (even if this means excluding some cases where padding is needed)
      • I am aware of one edge case where too much padding is added; this is when bars have different numbers of lines of text, but the most lines are not aligned with the longest bar. I'm willing to accept that tradeoff, but open to feedback.
  • Update image baselines which show visual differences caused by this change
  • Remove "cliponaxis": "false" from the hist_stacked image mock, to demonstrate the effect of the changes in this PR (see below)

Screenshots

Before:
Screenshot 2025-12-17 at 11 30 09 AM

After:
Screenshot 2025-12-17 at 11 29 37 AM

hist_stacked mock (with cliponaxis: false removed) Before:
Screenshot 2025-12-17 at 3 11 04 PM

hist_stacked mock (with cliponaxis: false removed) After:
Screenshot 2025-12-17 at 3 11 56 PM

Steps for testing

Use the following mock:

{
    "data": [
        {
            "type": "bar",
            "orientation": "v",
            "x": ["A", "B", "C"],
            "y": [100, 250, -95],
            "base": [100, 100, 100],
            "texttemplate": "%{y}<br>units",
            "textposition": "outside",
        },
    ],
    "layout": {
    }
}

Verify that it looks like the "before" picture on master and the "after" picture on this branch.

Other variations to test:

  • Setting layout.yaxis.autorange to "reversed"
  • Using the bar.text property instead of bar.texttemplate
  • Adding a second bar trace and setting layout.barmode to "stack"

You can also load the hist_stacked mock in the devtools as another test case.

@emilykl emilykl marked this pull request as ready for review December 17, 2025 16:28
@emilykl emilykl requested a review from camdecoster December 17, 2025 16:28
@emilykl emilykl changed the title DRAFT: Fix for cut-off bar labels Increase axis autorange when bars have outside text labels Dec 17, 2025
Copy link
Contributor

@camdecoster camdecoster left a comment

Choose a reason for hiding this comment

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

I left some syntax and refactoring comments, but this looks good.

Comment on lines 12 to 13
var TEXTPAD = require('./constants').TEXTPAD;
var BR_TAG_ALL = require('../../lib/svg_text_utils').BR_TAG_ALL;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var TEXTPAD = require('./constants').TEXTPAD;
var BR_TAG_ALL = require('../../lib/svg_text_utils').BR_TAG_ALL;
const { TEXTPAD } = require('./constants');
const { BR_TAG_ALL } = require('../../lib/svg_text_utils');

var nLines = trace.texttemplate
? countLines(trace.texttemplate)
: isArrayOrTypedArray(trace.text)
? Math.max(...trace.text.map((t) => countLines(t)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Will map work on a typed array?

if (!text || typeof text !== 'string') return 0;
return (text.match(BR_TAG_ALL) || []).length + 1;
}
var nLines = trace.texttemplate
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var nLines = trace.texttemplate
const nLines = trace.texttemplate

Comment on lines +802 to +803
// Yes, I know this looks backwards from what it should be,
// but it works like this
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you reword this? I trust that you're correct, but I don't understand what's happening from your comment.

Comment on lines +786 to +787
trace.textposition == 'outside' &&
(trace.textangle == 'auto' || trace.textangle == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
trace.textposition == 'outside' &&
(trace.textangle == 'auto' || trace.textangle == 0)
trace.textposition === 'outside' &&
(trace.textangle === 'auto' || trace.textangle === 0)

Comment on lines +759 to +764
const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
tozero: tozero,
padded: padded
padded: padded,
ppadplus: extraPad.ppadplus,
ppadminus: extraPad.ppadminus
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
tozero: tozero,
padded: padded
padded: padded,
ppadplus: extraPad.ppadplus,
ppadminus: extraPad.ppadminus
const { ppadminus, ppadplus } = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
tozero,
padded,
ppadplus,
ppadminus

Comment on lines +571 to +576
const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
tozero: tozero,
padded: true
padded: true,
ppadplus: extraPad.ppadplus,
ppadminus: extraPad.ppadminus
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
tozero: tozero,
padded: true
padded: true,
ppadplus: extraPad.ppadplus,
ppadminus: extraPad.ppadminus
const { ppadminus, ppadplus } = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
tozero,
padded: true,
ppadplus,
ppadminus

Comment on lines +648 to +655
const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
// N.B. we don't stack base with 'base',
// so set tozero:true always!
tozero: true,
padded: true
padded: true,
ppadplus: extraPad.ppadplus,
ppadminus: extraPad.ppadminus
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const extraPad = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
// N.B. we don't stack base with 'base',
// so set tozero:true always!
tozero: true,
padded: true
padded: true,
ppadplus: extraPad.ppadplus,
ppadminus: extraPad.ppadminus
const { ppadminus, ppadplus } = estimateAxisPaddingForText(fullTrace, calcTrace);
fullTrace._extremes[sa._id] = Axes.findExtremes(sa, pts, {
// N.B. we don't stack base with 'base',
// so set tozero:true always!
tozero: true,
padded: true,
ppadplus,
ppadminus

Comment on lines +794 to +798
var nLines = trace.texttemplate
? countLines(trace.texttemplate)
: isArrayOrTypedArray(trace.text)
? Math.max(...trace.text.map((t) => countLines(t)))
: countLines(trace.text);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you refactor this to remove the nested ternary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Text is cut off on bars where textposition=outside

3 participants