Skip to content
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

Enable set_{x|y|}label(loc={'left'|'right'|'center'}...) #15974

Merged
merged 1 commit into from
Jan 20, 2020

Conversation

andrzejnovak
Copy link
Contributor

@andrzejnovak andrzejnovak commented Dec 19, 2019

PR Summary

Addressed #15839 by adding functionality analogous to axes.set_title(loc="") for x/y axis and cbar axis as well as new rcParam to have a default config

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@jklymak
Copy link
Member

jklymak commented Dec 19, 2019

Thanks a lot for the PR! However, I think we should make sure that there is some consensus on this before you put too much effort into it.

I'm -0.5, because I'm not really sure how many people want to do this, or should, and that making it available is just adding to our already impressive kwarg list and rcParams. Is there a field where putting an axis label somewhere other than the centre is common? Are there other packages that allow axis labels to be placed this way? Whats the prior art here?

@andrzejnovak
Copy link
Contributor Author

It's the default in high energy physics.

@anntzer
Copy link
Contributor

anntzer commented Dec 19, 2019

Can you provide some examples, e.g. links to papers?

matplotlibrc.template Outdated Show resolved Hide resolved
@@ -246,6 +256,15 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs):
"""
if labelpad is not None:
self.yaxis.labelpad = labelpad
if loc is None:
loc = rcParams['axis.titlelocation']
cbook._check_in_list(('left', 'center', 'right'), loc=loc)
Copy link
Contributor

Choose a reason for hiding this comment

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

can you screenshot how does that look?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image

Copy link
Contributor

Choose a reason for hiding this comment

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

So it's really top/bottom for y, which argues for the separate rcparams?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think top/bottom is unnecessarily adding more keywords without helping the semantics. Should be still left/right even when aligning along the y axis

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean? It's clearly at the top and the bottom?...
For example, if your y axis is oriented downwards, what do left and right correspond to?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is a downward oriented y-axis ? The only case I can think of where it could be ambiguous is if someone flipped the text alignment such that one would title head right to read it as opposed to left as it usually is. Does that happen?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see your point. I guess it was too subtle for me :)
I still think top/bottom are clearer (and these are names used elsewhere in the library, so we're not just making them up here), but won't block over that.

Copy link
Member

Choose a reason for hiding this comment

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

I think this also needs to be top/bottom. Even if you think this should be left/right I was surprised that you would say this was on the “right” side of the colorbar.

@dopplershift
Copy link
Contributor

I'm ok with this as a feature. We already have this for title, so in that regards it might actually make the API easier to comprehend since things are more uniform. The code change is also straightforward enough. I just wish this didn't have to be changed in so many places...

@anntzer
Copy link
Contributor

anntzer commented Dec 19, 2019

I think the feature is fine (... and likely to be merged faster than your drawstyle PR...), just needs polishing wrt rcParams and "top"/"bottom" vs "left"/"right" and docs?

@tacaswell tacaswell added this to the v3.3.0 milestone Dec 20, 2019
@tacaswell
Copy link
Member

Testing out what we talked about on this weeks call, I am going to nominate @dopplershift to be the liaison for getting this merged.

@dopplershift
Copy link
Contributor

😱 😭

lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
@andrzejnovak
Copy link
Contributor Author

@timhoffm I was thinking about this, but since ha and x are lower level it would maybe make sense to me the other way around. I am not sure what's the best way to handle it, I could see either a) Ignore loc is either ha/x/y are supplied or b) raise an error if they are all supplied.

@timhoffm
Copy link
Member

General comment on the design

The problem really is that loc and x/ horizontalalignment are interacting parameters on different abstraction levels. Generally, that's not a good pattern and I would avoid that. However, here I see the point of a loc parameter. The alternative using the same parameter on different levels, e.g. x='left' would be a bit confusing as well. Also there's precedence for loc for legend and title. Therefore I think loc is the best option. 👍

Managing conflicting parameters

But we need to handle the technically possible case that loc and x/ horizontalalignment are present. IMHO the best way is a hard failure with an exception. Having both is an incompatible specification. The user should just not do that and be made aware of it. We cannot know what he intended with that combination of parameters ("In the face of ambiguity, refuse the temptation to guess").

Behavior difference between set_xlabel() and set_title()

We should be aware that in this implementation set_xlabel(, loc=...) moves the label, but set_title(, loc=...) activates separate titles. This is different behavior and generally to be avoided. However I think the implementation is nevertheless right as is. Having multiple labels would be excessive (actually, I'd choose that behavior for title as well if I was able to design that API anew). Plus, going from one moving label to multiple labels later is almost non-breaking. The only break would occur if people would call set_xlabel() twice with different locs, but that is not a likely thing to happen. 👍

@andrzejnovak
Copy link
Contributor Author

andrzejnovak commented Dec 20, 2019

I'm fine with having a hard error as well.

Indeed it was not obvious where the changes should go, but it seemed the actual Text object wouldn't have the corresponding y/x axis differentiation...

The set_title behaviour is even more confusing imho. Consider:

fig, ax = plt.subplots()
ax.set_title("AA", loc='left')
ax.set_title("BB", loc='right', x=0)
ax.set_title("CC", loc='center', x=0, ha='left')
plt.show()

@anntzer
Copy link
Contributor

anntzer commented Dec 20, 2019

We could perhaps try to deprecate the triple-title system (and support just one)... and see if anyone complains.

@jklymak
Copy link
Member

jklymak commented Dec 20, 2019

Triple title is useful for a) b)c) subplot labelling with a centred title.

@anntzer
Copy link
Contributor

anntzer commented Dec 20, 2019

Not sure what you exactly mean, but I guess that works only if you have exactly three (columns of) subplots?

@jklymak
Copy link
Member

jklymak commented Dec 20, 2019

No. You put the panel label on the left “a)” often w a different font face, and the title in the centre.

@anntzer
Copy link
Contributor

anntzer commented Dec 20, 2019

Ah, I thought you meant specifically a) (left) b) (center) c) (right) :)
Wouldn't your usecase be better served by #15771 (if that gets in)?

@timhoffm
Copy link
Member

I wouldn't touch set_title for now, it's sort of ok-ish. But let's not get distracted that'd be for another PR anyway.

I'm just saying, even though there are some discrepancies to existing API, i think this PR is going the right direction.

doc/users/next_whats_new/2019-12-20-set_xy_position Outdated Show resolved Hide resolved
examples/subplots_axes_and_figures/axis_labels_demo.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
@anntzer
Copy link
Contributor

anntzer commented Dec 20, 2019

Agreed re: title.

@dopplershift
Copy link
Contributor

There was a discussion before about changing the rcparam to axis.labellocation. I don't see anywhere where we resolved it. Since we're talking about labels, labellocation seems more appropriate than titlelocation. Thoughts?

Copy link
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

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

Looks like there is consensus this is useful, so that is great. But we don’t call the axis labels “titles”. We call them “labels” so please use that term...

lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Show resolved Hide resolved
@@ -246,6 +256,15 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs):
"""
if labelpad is not None:
self.yaxis.labelpad = labelpad
if loc is None:
loc = rcParams['axis.titlelocation']
cbook._check_in_list(('left', 'center', 'right'), loc=loc)
Copy link
Member

Choose a reason for hiding this comment

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

I think this also needs to be top/bottom. Even if you think this should be left/right I was surprised that you would say this was on the “right” side of the colorbar.

lib/matplotlib/tests/test_axes.py Outdated Show resolved Hide resolved
@andrzejnovak
Copy link
Contributor Author

@jklymak I feel like top/bottom replacement is unnecessary.

  • Prior to this PR the effect has to be achieved by setting x/y and ha='right'/'left' regardless of whether it's x os y axis, so it more consistent
  • It refers to the text alignment along the axis and in the majority of cases the text is written such that if you turn your head to read it, there's a clear 'left'/'right' direction
  • It would be an additional complication when plotting colorbar - switching the orientation would then also require to switch the loc keyword

@timhoffm
Copy link
Member

Intuitively, I'd expect top/center/bottom for the yaxis. Everybody will understand that, whereas left/center/right needs some explanation.

Also semantically, this is the high-level approach only specifying the location. In contrast to alignment it does not have a directional/orientational character.

AFAIK switching the orientation later is not possible. So it's only a matter of parameter checking/translation when creating the colorbar. Let's also not forget that this is an advanced parameter to tune your plot. If people switch the colorbar direction, it's unclear if a vertical bottom label should be translated to a horizontal left label. IMHO being more explicit and strict in this case is actually a plus.

@andrzejnovak
Copy link
Contributor Author

andrzejnovak commented Dec 21, 2019

Well actually semantically it is just two options - min/max along the direction of the axis whatever the orientation. Whereas an ensemble of top/bottom/right/left options hints at 4 different positions, which is not the case. But I do seem to be in the minority.

The code to do this for cbar would have to have the same exact logic too:

        if loc in ['right', 'top']:
            kwargs[_pos_xy] = 1.
            kwargs['horizontalalignment'] = 'right'
        elif loc in ['left', 'bottom']:
            kwargs[_pos_xy] = 0.
            kwargs['horizontalalignment'] = 'left'

lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Show resolved Hide resolved
lib/matplotlib/colorbar.py Show resolved Hide resolved
@andrzejnovak
Copy link
Contributor Author

@timhoffm That's not how verticalalignment works. ha/va are in relation the text orientation, they don't swap between x/y axis.

ax.set_ylabel('YLabel', va='top')

image

ax.set_ylabel('YLabel', va='bottom')

image

@timhoffm
Copy link
Member

timhoffm commented Jan 5, 2020

Changes should be squashed (either using a force-push or by the second reviewer through the GitHub UI when merging).

lib/matplotlib/rcsetup.py Outdated Show resolved Hide resolved
@@ -988,7 +988,12 @@ def validate_webagg_address(s):
raise ValueError("'webagg.address' is not a valid IP address")


validate_axes_titlelocation = ValidateInStrings('axes.titlelocation', ['left', 'center', 'right'])
_validate_axes_titlelocation = ValidateInStrings('axes.titlelocation',
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes. Sorry I missed that; it'll need to start public for now (and get properly deprecated later).

_protected_kw = ['x', 'horizontalalignment', 'ha']
if any([k in kwargs for k in _protected_kw]):
if loc is not None:
raise KeyError('Specifying *loc* is disallowed when any of '
Copy link
Contributor

@anntzer anntzer Jan 20, 2020

Choose a reason for hiding this comment

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

This should be a TypeError (it's a basically a signature mismatch, which normally raise TypeErrors).

_protected_kw = ['y', 'horizontalalignment', 'ha']
if any([k in kwargs for k in _protected_kw]):
if loc is not None:
raise KeyError('Specifying *loc* is disallowed when any of '
Copy link
Contributor

Choose a reason for hiding this comment

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

as above

@anntzer
Copy link
Contributor

anntzer commented Jan 20, 2020

looks like you messed up the rebase :(

@andrzejnovak
Copy link
Contributor Author

looks like you messed up the rebase :(

indeed

@anntzer
Copy link
Contributor

anntzer commented Jan 20, 2020

Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

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

Anyone can merge after CI passes.

@anntzer
Copy link
Contributor

anntzer commented Jan 20, 2020

Thanks for the contribution :)

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

Successfully merging this pull request may close these issues.

6 participants