Skip to content

New textcolor kwarg for legend #15898

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
May 5, 2020

Conversation

DanHickstein
Copy link
Contributor

As discussed in #10720, I've made a first attempt to introduce a textcolor keyword argument for the legend method. The legend argument can either be:

  • A string corresponding to a matplotlib color.
  • A list or tuple of color strings
  • (The best feature) A string corresponding to the line or marker color. So far, the options are 'linecolor', 'markerfacecolor', and 'markeredgecolor'.

Also I've included a short description in the "whats new" documentation and tests of the new functionality.

Please have a look at what I have so far and let me know how it can be improved.

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related (no examples - are they needed?)
  • Documentation is sphinx and numpydoc compliant (not sure how to check)
  • 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 (this change is backwards compatible)

@Tillsten
Copy link
Contributor

This is awesome. Is it possible to include an option to hide the handles?

@DanHickstein
Copy link
Contributor Author

@Tillsten: Glad you like it! Yes, if the text color matches the line, then the next logical step for some graphs is to remove the handles (lines and markers). This Stackoverflow post discusses various methods for removing the handles from the legend, so clearly there is some desire for users to do this.

But, I think that suppressing the handles should be a separate discussion and pull request. First, I don't currently know the best way to do this. Second, I think that it would require a separate kwarg. Shall I go ahead and open a new issue for us to discuss this feature request?

Also, I see that the circleCI tests are failing. As far as I can tell, the only error is:

make: *** [Makefile:30: html] Error 1
Exited with code exit status 2

So, I'm not quite sure how to fix this. Help would be appreciated.

@DanHickstein
Copy link
Contributor Author

Here is some code to test the various options for textcolor:

import numpy as np
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2,3,figsize=(10,6), tight_layout=True)
x = np.linspace(0,2,5)

textcolors = 'linecolor', 'mfc', 'mec', 'red', 'blue', ['r','g','b']

for ax, textcolor in zip(axs.ravel(), textcolors):
    for n in np.arange(1,4):
        ax.plot(x, n*x, label='Line #%i'%n, marker='o', mec='m', mfc='b')
    
    if type(textcolor) is str:
        ax.legend(title='textcolor=\'%s\''%textcolor, textcolor=textcolor)
    else:
        ax.legend(title='textcolor=%s'%textcolor,     textcolor=textcolor)
        
plt.show()

And the output:

Figure_1

@jklymak
Copy link
Member

jklymak commented Dec 10, 2019

If you are at the point of hiding the handles, I'd argue you don't need legend anymore...

Overall, this looks nice and compact so seems to me to be a good addition...

@Tillsten
Copy link
Contributor

If I have only few solid colored lines, the handles could be seen as unnecessary clutter... 😈
Even without handles, the legend has lot of utility over using annotate or text directly which i would really love to use. But @DanHickstein is right, this is a separate discussion/PR.

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

I think this looks like a great idea! However, I think this needs a bit more work to make sure it will handle more artist types (it should at least behave correctly for Lines if other artist types are in the legend) and to make sure all of the valid ways we pass colors through (list / tuple / ndarray of RGB(A)) are supported.

@DanHickstein
Copy link
Contributor Author

DanHickstein commented Dec 11, 2019

Okay, I reworked this to accommodate patches (it now uses self.legendHandles, which should provide both Line2D and Patch objects in the correct order).

Also, this version should allow for RGB(A) tuples, np.ndarrays, and lists of RGB(A) tuples.

Here is a new version of my example code, which includes a rectangle patch:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches

fig, axs = plt.subplots(2,3,figsize=(10,6), tight_layout=True)
x = np.linspace(0,2,5)

textcolors = ['linecolor', 'mfc', 'mec', 'blue', ['r','g','b','m'], np.array((1,0,0,1))]
labels =     ['\'linecolor\'', '\'mfc\'', '\'mec\'', '\'blue\'', '[\'r\',\'g\',\'b\',\'m\']', 'np.array((1,0,0,1))']

for ax, textcolor, label in zip(axs.ravel(), textcolors, labels):    
    rect = patches.Rectangle((1.3,0),.5,.7,linewidth=1,edgecolor='r',facecolor='g', label='Rect patch')
    ax.add_patch(rect)
    
    for n in np.arange(1,4):
        ax.plot(x, n*x, label='Line #%i'%n, marker='o', mec='m', mfc='b')
        ax.legend(title='textcolor=%s'%label, textcolor=textcolor)
     
plt.show()

And here is the new output:
Figure_1

As you can see, in the case where textcolor=='linecolor', it's not clear what color should be used for the rectangle patch, since patches do not have a linecolor property. I've elected to use the facecolor, since I think that this is the behavior that users might expect. I'm open to other suggestions, such as just using the default color for patches or using the edgecolor.

Also, the circleCI tests are still failing, and I'm not sure why...

@jklymak
Copy link
Member

jklymak commented Dec 11, 2019

Download the log - you have a bunch of warnings and we fail on warnings.

I'm now a little leery of all the special casing we have to do now to meet all the possible input types. For the other devs: is there no way to abstract this logic so its done consistently in places that accept a list of colors in various forms?

@timhoffm
Copy link
Member

The first part is special to legends and it's quite involved because one has to obtain the color from the handle in the right way. It can be reduced to:

        color_getters = {
            'linecolor': ['get_color', 'get_facecolor'],
            'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'],
            'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'],
        }
        if textcolor is None:
            pass
        elif textcolor in color_getters:
            getter_names = color_getters[textcolor]
            for handle, text in zip(self.legendHandles, self.texts):
                for getter_name in getter_names:
                    try:
                        color = getattr(handle, getter_name)()
                        text.set_color(color)
                        break
                    except AttributeError:
                        pass

I've left out "mfc" and "mec" because I'm not sure we want to support the abbreviations as well. But they could be added to the color_getters dict.

For explict colors (single color or iterable)

for text, color in zip(self.texts, itertools.cycle(mcolors.to_rgba_array(textcolor))):
    text.set_color(color)

should probably work.

@DanHickstein
Copy link
Contributor Author

I re-worked the code to use the suggestions by @timhoffm, and I think that it's much more concise and readable now. I used Tim's code nearly verbatim, with the additions of a check to ensure that that textcolor is a string:

        elif type(textcolor) is str and textcolor in color_getters:

I also included the mec and mfc abbreviations as options. I don't have strong opinions if these should be included or not, but it seems that they are used elsewhere. Is there a policy about using versus not using these?

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.

Looks much better 👍 Just some small comments left.

@DanHickstein
Copy link
Contributor Author

Thanks @timhoffm! I made the changes that you suggested.

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.

Good to go! Should be squashed before merging - either by the author (squash+force-push) or by the second reviewer when merging though the github UI.

@DanHickstein
Copy link
Contributor Author

Thanks @timhoffm!

If it's okay, I'll leave the squashing to the second reviewer (@tacaswell?). Rebasing has always been a confusing process for me.

@timhoffm
Copy link
Member

timhoffm commented Jan 5, 2020

For info: I usually do git commit --amend and git push origin mybranch --force for changes to a PR, so that I always have a single clean commit (unless of course it's a larger PR which is structured by multiple commits).

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

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

Given the typo with edgecolor/facecolor, I think this needs tests for those as well.

@DanHickstein
Copy link
Contributor Author

DanHickstein commented Jan 9, 2020

Thanks for all of the good suggestions @QuLogic! I made all of the changes that you suggested, and I included the tests for markeredgecolor and markerfacecolor.

@DanHickstein DanHickstein requested a review from QuLogic January 9, 2020 23:17
Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

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

Just two minor things, LGTM otherwise.

@QuLogic
Copy link
Member

QuLogic commented Mar 12, 2020

@DanHickstein are you interested in picking this up? It just needs a rebase and a small typo fix.

@DanHickstein
Copy link
Contributor Author

Thanks for the reminder! I made the changes.

Can you squash when you merge? I always mess things up when I try to rebase. (I've been using Git for several years, but somehow I am still a noob. :) )

@QuLogic
Copy link
Member

QuLogic commented Mar 13, 2020

This had conflicts, so it needed to be rebased before merging. I've done that now.

@QuLogic
Copy link
Member

QuLogic commented Mar 13, 2020

Looking back at the original issue, I see a list of several other 'color' values:

labelcolor='color'
labelcolor='facecolor'
labelcolor='edgecolor'
labelcolor='linecolor'
labelcolor='markerfacecolor'
labelcolor='markeredgecolor'
labelcolor='markerfacecoloralt'
labelcolor='errorbarcolor'
labelcolor='red'

Only a few of those are handled, do we want to add those others as well? And also change the name from textcolor to labelcolor?

@timhoffm
Copy link
Member

+1 for labelcolor. A legend title is not affected by this, so textcolor would be misleading.

I would also like the other color value options, but that may also be added in a later PR.

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.

Blocking to make sure we switch to labelcolor before merging.

Modified legend.py to include the new labelcolor kwarg. Also included a
short description in the "whats new" documentation and tests of the new
functionality.
@QuLogic QuLogic force-pushed the legend_textcolor branch from c6fd288 to 73d28ac Compare May 5, 2020 00:14
@QuLogic
Copy link
Member

QuLogic commented May 5, 2020

There's probably no need to hold this up for a quick rename. I went ahead and did the squash, rebased, and renamed the argument.

@QuLogic QuLogic requested a review from timhoffm May 5, 2020 00:16
@DanHickstein
Copy link
Contributor Author

Thanks @QuLogic! Sorry to have dropped the ball one this :)

I think that the "labelcolor" keyword is fine.

@timhoffm timhoffm dismissed their stale review May 5, 2020 21:40

Comments handled.

@bytelinker
Copy link

Hello,

how to apply this with 3 y-axis and one common legend?
(also posted in #10720)

@jklymak
Copy link
Member

jklymak commented Jan 7, 2021

@byetlinker - please discuss on discourse.matplotlib.org. Thanks,

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.

8 participants