-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Improve bar_label annotation #22447
Conversation
There was a problem hiding this 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.
lib/matplotlib/axes/_axes.py
Outdated
@@ -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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
yinverted = -1 if a > b else 1 | |
yinverted = a > b |
and create the sign below; see there.
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. |
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? 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() |
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") I'm not sure if there is other precedence for point offset adjustments in the library to cross-reference. |
Please give your PRs and commits more descriptive titles; no-one will know what 22414 is in two weeks. |
@mwaskom Sorry if I missed something, but why wouldn't it be inconsistent? matplotlib/lib/matplotlib/axes/_axes.py Lines 2685 to 2688 in 541a786
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) 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? matplotlib/lib/matplotlib/axes/_axes.py Lines 2680 to 2688 in 541a786
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 |
Right, the existing behavior is happening because of the way that
Yes but it can be even simpler: kwargs.setdefault("ha", ha)
kwargs.setdefault("va", va) |
@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. |
@mwaskom I found a small problem with the implementation of the |
@tacaswell I think it's ready. |
@tacaswell I see you added it to the |
lib/matplotlib/axes/_axes.py
Outdated
@@ -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 |
There was a problem hiding this comment.
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
yinverted = -1 if a > b else 1 | |
yinverted = a > b |
and create the sign below; see there.
@timhoffm I see, definitely better |
@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. |
if y_inverted: | ||
va = 'top' if dat > 0 else 'bottom' # also handles NaN | ||
else: | ||
va = 'top' if dat < 0 else 'bottom' # also handles NaN |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
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 |
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. |
for key in ['horizontalalignment', 'ha', 'verticalalignment', 'va']: | ||
if key in kwargs: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might alternatively written as
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.
There was a problem hiding this comment.
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'}
There was a problem hiding this comment.
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.
Python 3.8 on ubuntu-18.04 test failure is unrelated. |
@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. |
PR Summary
Improve bar_label method
Fix #22414
PR Checklist
Tests and Styling
pytest
passes).flake8-docstrings
and runflake8 --docstring-convention=all
).Documentation
doc/users/next_whats_new/
(follow instructions in README.rst there).doc/api/next_api_changes/
(follow instructions in README.rst there).