Skip to content

[Features] Allow setting legend title alignment #23140

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
merged 1 commit into from
Aug 2, 2022

Conversation

Mr-Milk
Copy link
Contributor

@Mr-Milk Mr-Milk commented May 26, 2022

PR Summary

Attempt to solve issue #12388

Closes #12388 (just so that it is automatically closed when merged).

Changes:

  • Add an alignment keyword argument to the Legend class.
  • Add set_alignment and get_alignment methods for Legend class.

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).

@oscargus
Copy link
Member

I noted that there were some doubts in the original issue, but cannot really say much about those. However, I believe that this warrants a "New feature" release note with an example of how to use it. A suggestion is to have three subplots and use a legend with different alignment in each. If you also could copy the result of that here, it would help the review process (always easier to see what the results are).

I also believe that there should be some tests for this, but I am not really sure about how to best test it. One way is to use something like:

@pytest.mark.parametrize('alignment', ('left', 'center', 'right'))
@check_figures_equal(extensions=['png'])
def test_align_legend_title(fig_test, fig_ref, alignment):
    fig_test.legend(..., title_align=alignment)

   l = fig_ref.legend(...)
   l.set_title_align(alignment)

which then strucks me that we probably would like to have a set_title_align method as well.

@Mr-Milk
Copy link
Contributor Author

Mr-Milk commented May 27, 2022

Here is an example to show the behavior of title_align using following code

import numpy as np
import matplotlib.pyplot as plt

_, axes = plt.subplots(1, 3, figsize=(15, 4))

x = np.arange(0, 10, 1)

for ax, title_alignment in zip(axes, ['left', 'center', 'right']):
    ax.plot(x, x, label="Line 1")
    ax.plot(x, x[::-1], label="Line 2")
    ax.legend(title=f"Long title with title_align='{title_alignment}'", title_align=title_alignment)

title_alignment_showcase

About the test case, I'm going through the testing document. I can add it later.

I think a set_title_align method may be a bit unnecessary? The set_title method for the Axes class set the alignment by passing ha= or va=. Just an align= seems more consistent?

@jklymak
Copy link
Member

jklymak commented May 27, 2022

I'm mildly against adding this directly to legend - legend already has a crazy number of options. As stated in the original issue, I think adding this as an optional argument to legend.set_title would be a reasonable interface that would keep the API cross section down.

@Mr-Milk
Copy link
Contributor Author

Mr-Milk commented May 30, 2022

That's reasonable, also most users would likely change the alignment after they visually examine the figure.

I think @oscargus might be right about adding a set_title_align method.
If users create a legend by specifying the title if they want to change the alignment. They need to pass the title argument again.

leg = ax.legend(title="title")
leg.set_title("title", align="left")

However, in APIs like set_xticklabels, sometimes users also need to get the labels first by get_xticklabels and then pass it through if they only want to change things like rotation. So maybe something like this is also acceptable?

leg = ax.legend(title="title")
leg.set_title(leg.get_title(), align="left")

@oscargus
Copy link
Member

oscargus commented May 30, 2022

I do not have a strong opinion. On one hand, one can set everything when creating the legend, on the other hand many other classes have dedicated methods for setting almost anything.

For sure one can add a set_title_align method later if it turns out that users do miss it.

Edit: I think I missed the point of the last edit, but anyway, fine with me. I think a user release note may be worthwhile stating that one can now modify the legend title alignment and how. Ideally with an example as you had above.

@jklymak
Copy link
Member

jklymak commented May 30, 2022

I feel set_title_align is too specialized. Why not set_title_fontfamily, set_title_fontsize, etc? We don't do this for other text objects, so I think we should avoid it here.

@oscargus
Copy link
Member

Is alignment a font property? If so, do we even need this? (This is not a rhetorical question, I do not know.)

If not, maybe the comparison is set_title_fontproperties? Which to me sounds quite reasonable. But as I said, I do not object against not adding it.

(One benefit of having many set_-methods is that one can tab-complete them rather than reading the documentation...)

@oscargus
Copy link
Member

Note that legend has both title_fontsize and title_fontproperties, so one could think that title_align could actually go there. But if you are convinced that it should not go in, I'll leave it there.

(But I guess that ideally not both title_fontsize and title_fontproperties should be there.)

@jklymak
Copy link
Member

jklymak commented May 30, 2022

For me the proper API parallel is ax.set_title (https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.set_title.html), or ax.set_xlabel. There is no axes.set_title_align, etc. The fact that users can specify the legend title directly on legend is fine, but more fine-tuned control should get its own method.

Comment on lines 862 to 863
Set the legend title. Alignment can be set with *align* parameter.
Fontproperties can be optionally set with *prop* parameter.
Copy link
Member

Choose a reason for hiding this comment

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

It would be better if you could re-write these as numpydoc.

@tacaswell tacaswell added this to the v3.6.0 milestone Jun 2, 2022
@tacaswell
Copy link
Member

I am 👍🏻 on the feature, but am a bit confused by the name as it looks like what is moving in the legend handles / text not the title.

I think I see how we got to this name, but I'm not sure looking at those examples I would guess align was the name of the keyword argument that I needed to change to go between them. Maybe those names would make more sense if the title was shorter than the handles?

If you have variable length labels in the legend what does that do?

@Mr-Milk
Copy link
Contributor Author

Mr-Milk commented Jun 3, 2022

Personally, I feel the Packer container is similar to css flex box model. Maybe we could get the naming idea for there? Some arguments used to control alignment in css flex box: justify-content, align-items

@timhoffm
Copy link
Member

timhoffm commented Jun 6, 2022

Unfortunately, the topic is more difficult: packer.align is not generally suited for title alignment c.f. #12388 (comment) "The mechanism has to be more refined than just setting self._legend_box.align".

Explanation

Technically, we currently do not have a knob for title alignment. What we have here are two boxes _legend_title_box and _legend_handle_box that are are aligned by the VPacker.

If the title box is shorter than the handle box, using packer.align for the text alignment works (because the overall width is defined by the handle box and thus the title has additional space and shifts left/center/right).

align='left'      align='center'    align='right'
--------------    --------------    --------------

[title]              [title]               [title]
[long handles]    [long handles]    [long handles]
[handle2     ]    [handle2     ]    [handle2     ]

However, if the title is longer, the shifting happens for the handles.

align='left'      align='center'    align='right'
--------------    --------------    --------------

[long - title]    [long - title]    [long - title]
[handle  ]          [handle  ]          [handle  ]
[handle 2]          [handle 2]          [handle 2]

What we need

The packer functionality is not enough: The title alignment must not automatically affect the handle alignment. Either we need a per-element alignment option in the packer #12388 (comment); or we have to come up with other mechanisms for the legend alignment than using the packer.

@oscargus
Copy link
Member

oscargus commented Jun 7, 2022

the shifting happens for the handles.

Isn't the solution really to not call it title_align but align? It seems to me that the outlined cases are the ones one would expect if trying to align these things? Or is there a case when one does align=center or align=right with a long title and actually expects the result for align=left?

@jklymak
Copy link
Member

jklymak commented Jun 7, 2022

I'm not convinced the long title "problem" is really a problem? I guess you are complaining that the bottom part of the legend moves, but that seems consistent with what a VPacker does, and what the user has asked for.

@timhoffm
Copy link
Member

timhoffm commented Jun 8, 2022

Isn't the solution really to not call it title_align but align?

align would be a mostly reasonable description of the implemented behavior. - Mostly because I'm unclear whether one would want and/or expect that to align the title box and the handle box, and not to align the title and all individual handles.

That said, I suspect that a global alignment is not necessarily what people want and thus not a good solution. For example, I'd prefer the handles to be left-aligned and the title to be centered. While you could effectively achieve this by selecting a global "center" or " left" align depending on whether your title is longer than the handle, that'd be some awkward hacking on the user side to compensate a bad API.

@jklymak
Copy link
Member

jklymak commented Jun 11, 2022

I'd prefer the handles to be left-aligned and the title to be centered. While you could effectively achieve this by selecting a global "center" or " left" align depending on whether your title is longer than the handle,

I think I know what you are saying, but you are just suggesting one of two ways to deal with long titles if align='centre' (basically left aligning them if the title is too long) whereas the current implementation uses the other way (moving the legend to the right and centred under the title). If we somehow change the behaviour, and someone else prefers the other way, then they now have to do the awkward hacking.

I also don't think we should be too interested in super flexible long-title behaviour - if someone has a long title, and they are bothered by how we decide to centre it or not, they can always shorten it by adding carriage returns (and probably should)

Given that we already have a knob, I don't think there is anything a-priori wrong with how it behaves, and its pretty easy to describe, I don't think we need to create a new packer behaviour.


align: str, default: 'center'
One of {'center', 'left', 'right'}
control the alignment of title
Copy link
Member

Choose a reason for hiding this comment

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

This should explain the long-title behaviour so it is not a surprise.

@timhoffm
Copy link
Member

Given that we already have a knob, I don't think there is anything a-priori wrong with how it behaves, and its pretty easy to describe, I don't think we need to create a new packer behaviour.

IMHO the knob is not really good because it's a knob tuning both title and entry alignment at the same time. Independent knobs would be better, but they are more complicated to align.

I'm not particulary fond of this soIution, but it's viable if we change the API. The alignment is a global alignment of the legend (box). Therefore:

  1. Remove the parameter from set_title
  2. Instead add it to legend() itself.
  3. You may want an artist property alignment for Legend i.e. Legend.get_alignment, Legend.set_alignment. Note that I've changed from align to alignment since properties should be nouns, not verbs.


prop : `.font_manager.FontProperties` or `str` or `pathlib.Path`
The fontproperties of the legend title,
:rc:`text.Text.set_fontproperties`.
Copy link
Member

Choose a reason for hiding this comment

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

That's not the name of an rcParam.

Comment on lines 619 to 622
for align in ['center', 'left', 'right']:
leg.set_alignment(align)
assert leg.get_children()[0].align == align
assert leg.get_alignment() == align
Copy link
Member

Choose a reason for hiding this comment

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

This is redundant: you are running this again for each of the parametrizations. I suggest to put the test of set_alignment this in a separate test function instead:

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

This is almost good to go. Only some wording improvements.

@timhoffm
Copy link
Member

One flake8 complaint left:

./lib/matplotlib/legend.py:881:1: W293 blank line contains whitespace

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

This is good to go. Thanks @Mr-Milk for keeping this up and having patience with our careful but lengthy design process!

I suggest that either the second reviewer or the author squashes the commits.

Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
@tacaswell
Copy link
Member

I rebased and squashed this. To check that I did it correctly I created a synthetic merge of 74d53d2 into main:

$ git show --pretty=raw test
commit 2ad762e5f7ba8e4ee2006cecffd62b7f05ef01ad
tree 8f68c542eb6e47eadb8d7a2f0ef1848ef375e275
parent 54bbf200ff88b7855db4e34153ba728b472b3727
parent 74d53d210752e65e172113f9aaf8e7a30b9a3243
$ git diff test
$

@tacaswell tacaswell force-pushed the legend-title-alignment branch from 74d53d2 to b9cdf3e Compare August 2, 2022 01:35
@tacaswell
Copy link
Member

I second @timhoffm 's comment, thank you for sticking with us and getting what looks like a very useful feature in!

@tacaswell tacaswell merged commit d61ebe9 into matplotlib:main Aug 2, 2022
@tacaswell
Copy link
Member

Congratulations on your first merged Matplotlib PR @Mr-Milk 🎉 Hopefully we will hear from you again!]

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.

Legend Title Left Alignment
6 participants