Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Improve bar_label annotation #22447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

Valdes-Tresanco-MS
Copy link
Contributor

@Valdes-Tresanco-MS Valdes-Tresanco-MS commented Feb 10, 2022

PR Summary

Improve bar_label method
Fix #22414

PR Checklist

Tests and Styling

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (install flake8-docstrings and run flake8 --docstring-convention=all).

Documentation

  • New features are documented, with examples if plot related.
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a while, please feel free to ping @matplotlib/developers or anyone who has commented on the PR. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@tacaswell tacaswell added this to the v3.6.0 milestone Feb 10, 2022
@@ -2608,6 +2608,11 @@ def bar_label(self, container, labels=None, *, fmt="%g", label_type="edge",

# want to know whether to put label on positive or negative direction
# cannot use np.sign here because it will return 0 if x == 0
a, b = self.yaxis.get_view_interval()
yinverted = -1 if a > b else 1
Copy link
Member

Choose a reason for hiding this comment

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

Why not use True / False here? This looks like it is boolean state (and is unlikely to ever grow a third state).

Copy link
Member

Choose a reason for hiding this comment

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

We have two usages below: a boolean and a sign. This tries to push both notions into one variable, which makes it a bit odd: The variable naming suggests a boolean, but the value carries the sign.

I suggest

Suggested change
yinverted = -1 if a > b else 1
yinverted = a > b

and create the sign below; see there.

@tacaswell
Copy link
Member

Thank you for both reporting and fixing this @Valdes-Tresanco-MS !

I left one small comment in the code about the implementation. Could you also add a test so that we are sure that it does not come back? I do not think we need an image test. I suspect there is a way to query the location of the text, convert it to data coordinates and assert that it is greater than the data it is above.

@Valdes-Tresanco-MS
Copy link
Contributor Author

I described the solution in #22414 and in this comment #22414 (comment). In this comment, I include also an image with the test result.

This is enough?
In the issue, @mwaskom also suggests a deeper revision, but unfortunately, I don't have time for it

Test code

f, axes = plt.subplots(3, 4, figsize=(16, 12))
(ax1, ax2, ax3, ax4), (ax5, ax6, ax7, ax8), (ax9, ax10, ax11, ax12) = axes
bars1 = ax1.bar(["a", "b", "c", "d"], [1, 2, 3, -3])
ax1.bar_label(bars1, padding=0, label_type='center')
ax1.set_title('Vertical Normal yaxis padding=0')

bars2 = ax2.bar(["a", "b", "c", "d"], [1, 2, 3, -3])
ax2.invert_yaxis()
ax2.bar_label(bars2, padding=0, label_type='center')
ax2.set_title('Vertical Inverted yaxis padding=0')

bars5 = ax5.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax5.bar_label(bars5, padding=0, label_type='center')
ax5.set_title('Horizontal Normal yaxis padding=0')

bars6 = ax6.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax6.invert_yaxis()
ax6.bar_label(bars6, padding=0, label_type='center')
ax6.set_title('Horizontal Inverted yaxis padding=0')

bars9 = ax9.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax9.invert_xaxis()
ax9.bar_label(bars9, padding=0, label_type='center')
ax9.set_title('Horizontal Inverted xaxis padding=0')

bars10 = ax10.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax10.invert_yaxis()
ax10.invert_xaxis()
ax10.bar_label(bars10, padding=0, label_type='center')
ax10.set_title('Horizontal Inverted yaxis and xaxis padding=0')

bars3 = ax3.bar(["a", "b", "c", "d"], [1, 2, 3, -3])
ax3.bar_label(bars3, padding=10, label_type='center')
ax3.set_title('Vertical Normal yaxis padding=10')

bars4 = ax4.bar(["a", "b", "c", "d"], [1, 2, 3, -3])
ax4.invert_yaxis()
ax4.bar_label(bars4, padding=10, label_type='center')
ax4.set_title('Vertical Inverted yaxis padding=10')

bars7 = ax7.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax7.bar_label(bars7, padding=10, label_type='center')
ax7.set_title('Horizontal Normal yaxis padding=10')

bars8 = ax8.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax8.invert_yaxis()
ax8.bar_label(bars8, padding=10, label_type='center')
ax8.set_title('Horizontal Inverted yaxis padding=10')

bars11 = ax11.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax11.invert_xaxis()
ax11.bar_label(bars11, padding=10, label_type='center')
ax11.set_title('Horizontal Inverted xaxis padding=10')

bars12 = ax12.barh(["a", "b", "c", "d"], [1, 2, 3, -3])
ax12.invert_yaxis()
ax12.invert_xaxis()
ax12.bar_label(bars12, padding=10, label_type='center')
ax12.set_title('Horizontal Inverted yaxis and xaxis padding=10')

f.tight_layout()

@mwaskom
Copy link

mwaskom commented Feb 11, 2022

Worth pointing out that this is now inconsistent with how annotations work:

ax = plt.gca()
ax.invert_yaxis()
ax.bar([0, 1], [1, .5])
ax.annotate("hello", (1, .5), xytext=(0, 10), textcoords="offset points", ha="center", va="center")

image

I'm not sure if there is other precedence for point offset adjustments in the library to cross-reference.

@QuLogic
Copy link
Member

QuLogic commented Feb 11, 2022

Please give your PRs and commits more descriptive titles; no-one will know what 22414 is in two weeks.

@Valdes-Tresanco-MS Valdes-Tresanco-MS changed the title Fix #22414 Improve bar_label annotation Feb 11, 2022
@Valdes-Tresanco-MS
Copy link
Contributor Author

Valdes-Tresanco-MS commented Feb 11, 2022

@mwaskom Sorry if I missed something, but why wouldn't it be inconsistent? bar_label makes an annotation passing modified arguments

annotation = self.annotate(fmt % value if lbl is None else lbl,
xy, xytext, textcoords="offset points",
ha=ha, va=va, **kwargs)
annotations.append(annotation)

If I add this line to your example, everything seems to work fine.

ax = plt.gca()
ax.invert_yaxis()
b = ax.bar([0, 1], [1, .5])
ax.annotate("hello", (1, .5), xytext=(0, 20), textcoords="offset points", ha="center", va="center")
ax.bar_label(b)

output

Suddenly something related to the handling of the attributes of the annotations escapes me, so I would not like to be left with the doubt.

Additionally, could this be a solution to what you suggest?

va = 'center'
if np.isnan(dat):
lbl = ''
annotation = self.annotate(fmt % value if lbl is None else lbl,
xy, xytext, textcoords="offset points",
ha=ha, va=va, **kwargs)
annotations.append(annotation)

if 'ha' not in kwargs:
    kwargs.update([('ha', ha)])
if 'va' not in kwargs:
    kwargs.update([('va', va)])

annotation = self.annotate(fmt % value if lbl is None else lbl,
                           xy, xytext, textcoords="offset points",
                           **kwargs)

Although it is not elegant in principle, it does what you suggest. If the alignment parameters are not explicitly defined, then those determined in the method are passed, otherwise, the user's parameters are used. I only tested a few cases, so I don't know if it could have a wrong behavior in some cases

@mwaskom
Copy link

mwaskom commented Feb 11, 2022

Sorry if I missed something, but why wouldn't it be inconsistent? bar_label makes an annotation passing modified arguments

Right, the existing behavior is happening because of the way that annotate works. I'm just pointing out that "fixing" it will introduce an inconsistency. I am not sure that I have an opinion about which behavior is preferable, just noting.

could this be a solution to what you suggest?

Yes but it can be even simpler:

kwargs.setdefault("ha", ha)
kwargs.setdefault("va", va)

@tacaswell
Copy link
Member

@Valdes-Tresanco-MS Posts on GH are certainly useful! However, the test code needs to end up in the test suite that is run on PR so the computer can tell us if things are broken.

@Valdes-Tresanco-MS
Copy link
Contributor Author

Valdes-Tresanco-MS commented Feb 11, 2022

kwargs.setdefault("ha", ha)
kwargs.setdefault("va", va)

@mwaskom I found a small problem with the implementation of the ha and va parameters defined by the user. Since the annotation, in this case, is done on each bar, assigning the items ha and va envelopes overwrites the configuration, making all labels appear the same, whether they are for positive or negative values. The solution is to create a copy of kwargs on each iteration to define these particular variables in each iteration. However, the fact that the labels are defined in the same way for all the values I think can be confusing for users.

@Valdes-Tresanco-MS
Copy link
Contributor Author

@tacaswell I think it's ready.

@QuLogic QuLogic added the status: needs workflow approval For PRs from new contributors, from which GitHub blocks workflows by default. label Feb 11, 2022
@Valdes-Tresanco-MS
Copy link
Contributor Author

@tacaswell I see you added it to the 3.6.0 milestone. Since it is an improvement of a method that was already implemented, is it possible to add it in the current version (3.5.x)? It would be a great help for my projects.
Thanks

@@ -2608,6 +2608,11 @@ def bar_label(self, container, labels=None, *, fmt="%g", label_type="edge",

# want to know whether to put label on positive or negative direction
# cannot use np.sign here because it will return 0 if x == 0
a, b = self.yaxis.get_view_interval()
yinverted = -1 if a > b else 1
Copy link
Member

Choose a reason for hiding this comment

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

We have two usages below: a boolean and a sign. This tries to push both notions into one variable, which makes it a bit odd: The variable naming suggests a boolean, but the value carries the sign.

I suggest

Suggested change
yinverted = -1 if a > b else 1
yinverted = a > b

and create the sign below; see there.

@Valdes-Tresanco-MS
Copy link
Contributor Author

@timhoffm I see, definitely better

@timhoffm
Copy link
Member

@Valdes-Tresanco-MS since you wrote that you don't have much time, I took the liberty and pushed my proposed changes as well as error handling if the user passes alignment parameters to your branch. I hope that's ok.

Comment on lines +2691 to +2694
if y_inverted:
va = 'top' if dat > 0 else 'bottom' # also handles NaN
else:
va = 'top' if dat < 0 else 'bottom' # also handles NaN
Copy link
Member

Choose a reason for hiding this comment

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

I'm not entirely certain this is correct, because there are no tests with NaN on an inverted axis.

Copy link
Member

Choose a reason for hiding this comment

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

@Valdes-Tresanco-MS di this get addressed? If so, please move back out of draft. Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

There was a new test added, but the problem is that the result that is being checked is the same as what occurs when you run the code on main. So I'm not certain if the test is asserting that this PR changes things correctly (and the test just happens to match main coincidentally), or the test is asserting that this PR doesn't break how things currently work (and the match is intentional).

Copy link
Member

Choose a reason for hiding this comment

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

So what I guess I'm saying is it would be better if the test were paramterized across all the 4 cases here.

@jklymak jklymak marked this pull request as draft March 30, 2022 11:50
@Valdes-Tresanco-MS
Copy link
Contributor Author

@jklymak Sorry, I don't understand what I should do. Since it was indicated I included the test (a64db70). The failure in the check seems to be related to a CLI failure not to the test itself. This commit should I not include it?

@jklymak
Copy link
Member

jklymak commented Mar 30, 2022

I just wanted to be sure @QuLogic 's comment had been addressed....

@Valdes-Tresanco-MS
Copy link
Contributor Author

I just wanted to be sure @QuLogic 's comment had been addressed....

Yes. Should I mark the conversations as resolved? It's my first PR here

@jklymak jklymak marked this pull request as ready for review March 30, 2022 15:10
@jklymak jklymak requested a review from QuLogic March 30, 2022 15:10
@jklymak
Copy link
Member

jklymak commented Mar 30, 2022

Making as resolved depends how trivial the comment is. If its a typo I go ahead and mark as resolved. If the commenter might want to think about it more I leave it open. Thanks!

@Valdes-Tresanco-MS
Copy link
Contributor Author

Making as resolved depends how trivial the comment is. If its a typo I go ahead and mark as resolved. If the commenter might want to think about it more I leave it open. Thanks!

Thank you for the clarification. I remain attentive to your comments to help with what I can in this PR.

Comment on lines +2611 to +2612
for key in ['horizontalalignment', 'ha', 'verticalalignment', 'va']:
if key in kwargs:
Copy link
Member

Choose a reason for hiding this comment

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

Might alternatively written as

Suggested change
for key in ['horizontalalignment', 'ha', 'verticalalignment', 'va']:
if key in kwargs:
unsupported = set(['horizontalalignment', 'ha', 'verticalalignment', 'va']):
for key in unsupported.intersection(kwargs):

But no strong preference. I'll leave this up to you.

Copy link
Member

Choose a reason for hiding this comment

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

That should be:

unsupported = {'horizontalalignment', 'ha', 'verticalalignment', 'va'}

Copy link
Member

Choose a reason for hiding this comment

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

Merged as is since the existing code style is good enough.

@timhoffm
Copy link
Member

Python 3.8 on ubuntu-18.04 test failure is unrelated.

@timhoffm timhoffm merged commit 33b3015 into matplotlib:main Apr 5, 2022
@timhoffm
Copy link
Member

timhoffm commented Apr 5, 2022

@Valdes-Tresanco-MS thanks, and congratulations on your first contribution to Matplotlib! We hope to see you back.

@timhoffm timhoffm removed the status: needs workflow approval For PRs from new contributors, from which GitHub blocks workflows by default. label Apr 5, 2022
@Valdes-Tresanco-MS
Copy link
Contributor Author

@Valdes-Tresanco-MS thanks, and congratulations on your first contribution to Matplotlib! We hope to see you back.

Definitely... I will try to contribute what I can in the future.

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]: bar_label overlaps bars when y-axis is inverted
7 participants