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

ax.get_legend().get_title().get_visible() does not work #10391

Closed
boeddeker opened this issue Feb 7, 2018 · 20 comments

Comments

Projects
None yet
5 participants
@boeddeker
Copy link

commented Feb 7, 2018

I use a wrapper around matplotlib and set there automatic the legend.
When the legend do not contain a title, everything works fine.
When I set after the first plot the legend title and call ax.legend() after the second plot, this overwrites the title and I have to use ax.legend(title=title). With ax.get_legend().get_title().get_text() I get the current legend title.

The Problem is that the default title value (None) is converted to text, so ax.get_legend().get_title().get_text() returns the str None and not the object None.
So calling ax.legend(title=ax.get_legend().get_title().get_text()) does not work. When there was initially no title.

So I thought I set the legend title only when ax.get_legend().get_title().get_visible() is True, but this does not work because the call returns always True.

Currently, I have to call ax.get_legend()._legend_title_box.get_visible(), but _legend_title_box is a private property.

The relevant matplotlib code is here:

def set_title(self, title, prop=None):

An example, when someone what to test what I wrote:

import matplotlib.pylab as plt

x1, y1 = [1, 2, 3], [3, 4, 5]
x2, y2 = [1, 2, 3], [3, 4, 6]

def plot(x, y, label, title=None, ax=None):
    if ax is None:
        _, ax = plt.subplots(1, 1)
    ax.plot(x, y, label=label)
    if title:
        ax.legend(title=title)
    else:
        ax.legend(title=ax.get_legend().get_title().get_text() if ax.get_legend() and ax.get_legend()._legend_title_box.get_visible() else None)
        # ax.legend()  # does not keep the title
    return ax
    
ax = None
ax = plot(x1, y1, '1', title='title', ax=ax)
ax = plot(x2, y2, '2', ax=ax)  # no legend title, when ax.legend()  is used

ax = None
ax = plot(x1, y1, '1', title='title1', ax=ax)
ax = plot(x2, y2, '2', title='title2', ax=ax)  # title2 win, is ok

ax = plot(x1, y1, '1', ax=ax)
# Figure out if there is a legend title
print(ax.get_legend().get_title().get_visible())  # wrong output (True), legend title is not visible
print(ax.get_legend()._legend_title_box.get_visible())  # correct output (False), legend title is not visible
$ pip show matplotlib
Name: matplotlib
Version: 2.1.0
Summary: Python plotting package
Home-page: http://matplotlib.org
Author: John D. Hunter, Michael Droettboom
Author-email: matplotlib-users@python.org
License: BSD
Location: /opt/anaconda/lib/python3.6/site-packages
Requires: numpy, six, python-dateutil, pytz, cycler, pyparsing

Depending on your opinion the Bug is one of the following points:

  • ax.legend() should keep the title
  • ax.get_legend().get_title() or ax.get_legend().get_title().get_text() is None
  • ax.get_legend().get_title().get_visible() returns False when the legend title is not visible
@jklymak

This comment has been minimized.

Copy link
Contributor

commented Feb 7, 2018

OK, confirmed.

The confusion is that get_title returns the actual text of the title, and the visibility of that text is always true. The only visibility that gets set to False is _title_box, which has no public method.

Worse, the (hidden) title gets set to the string "None" which is silly.

@jklymak

This comment has been minimized.

Copy link
Contributor

commented Feb 7, 2018

Check out #10392 and see if it makes sense to you. Thanks for the bug report!

@boeddeker

This comment has been minimized.

Copy link
Author

commented Feb 9, 2018

Thanks for the fix, that simplifies my code.

How about keeping the title of the legend?

The code would then be something like (I do not know, why _parse_legend_args has axs and not ax):

        # Check if the old legend (if exist) has a title
        ax = ...  # self or axs[0]
        if ax.legend_:
            kwargs.setdefault(
                'title',
                ax.legend_.get_title().get_text()
            )

Here the positions, where it could be fixed:

self.legend_ = mlegend.Legend(self, handles, labels, **kwargs)

return handles, labels, extra_args, kwargs

@Vickershhh

This comment has been minimized.

Copy link

commented Feb 12, 2018

So this bug has been fixed already?

@boeddeker

This comment has been minimized.

Copy link
Author

commented Feb 12, 2018

It depends.
Should ax.legend() reset the legend title?
If yes, than this bug is fixed, because I can achieve what I want without using a private attribute.

If not, something like my last commit needs to be implemented.

In my mind it feels more expectable when ax.legend do not reset the legend title.

@jklymak

This comment has been minimized.

Copy link
Contributor

commented Feb 12, 2018

Yes, if you call ax.legend() it resets the legend, including the title. Not quite sure how making legend properties persistent across calls to legend would work.

@boeddeker

This comment has been minimized.

Copy link
Author

commented Feb 12, 2018

Inside of ax.legend is the old legend known, so it is possible to get the old title and provide it as fallback in kwargs with setdefault. And when your PR is merged there is no check required if the old text is "None".

@jklymak

This comment has been minimized.

Copy link
Contributor

commented Feb 12, 2018

It’s possible to do either. But the question is if you hav to explicitly set the title or explicitly clear it. I’d argue you should explicitly have to set it, and clearing is implicit. But I don’t often use legend titles, and wouldn’t object to needing explicit clearing...

@boeddeker

This comment has been minimized.

Copy link
Author

commented Feb 13, 2018

Following your argumentation, I would say explicitly clear it would be better because when you rarely use it, it is few "overhead" to clear the title. But when you use it, it is annoying to write the code to keep the title.
A further point, when you set the legend title you normally want to have a title in the legend and it is in my mind rarely the case that you clean it (Or maybe this never happen?).

In my example where I hit this problem, I have a function that plots many lines and write the parameter values into the legend and the parameter keys into the legend title. After that plot I added one further line also with a label. But since the label require a call of ax.legend(), I had to get the title and set the title again.

This is the code, that say keep the title:

ax.legend(title=ax.get_legend().get_title().get_text() if ax.get_legend() and ax.get_legend().get_title().get_visible() else None)

and this for reset

ax.legend(title=None)

For me is the reset example easy to remember and feels natrual, but for the keep example I have to look into old code to remember what I have to use.

@jklymak

This comment has been minimized.

Copy link
Contributor

commented Feb 13, 2018

Or you could just have a local variable and keep track of the title yourself.

@boeddeker

This comment has been minimized.

Copy link
Author

commented Feb 13, 2018

Keeping the legend title as a local variable is in my example to much work, above code to keep the legend is easier.
I use an abstraction layer between my code in the ipython notebook and matplotlib, so the call to ax.legend is not in the notebook and also the code that sets the legend title is not in the notebook.
So I keeping it as a local variable means, I have to return the value (bad style) or extract it from the legend (similar to keeping the legend title).

Here an example plot where I hit this problem (Note: pd_plot.line is a private function).
After that plot I wanted to add some further lines to the graphs with a label that reset the legend title.

import numpy as np
import pandas as pd

# produce a Dataframe for all combinations with random y value
data = {'x1': [1, 2], 'x2': [2, 3], 'hue1': [4, 5], 'hue2': [6, 7], 'hue2': [8, 9], 'subplot1': [10, 11], 'subplot2': [12, 13], 'y': [1]}
k, v = zip(*sorted(data.items()))
from itertools import product
example_df = pd.DataFrame(
    [dict(zip(k, values)) for values in product(*v)]
)
example_df['y'] = example_df['y'] + np.random.normal(size=len(df_tmp['y']))

pd_plot.line(
        data=example_df,
        x=['x1', 'x2'],
        y='y',
        hue=['hue1', 'hue2'],
        subplots=['subplot1', 'subplot2']
    )

download 11

Since your opinion is that an implicit reset is the desired behavior, this issue can be closed and I change my matplotlib wrappers to handle this correctly how I need it.

@boeddeker boeddeker closed this Feb 13, 2018

@jklymak

This comment has been minimized.

Copy link
Contributor

commented Feb 13, 2018

It’s just my opinion. You are welcome to submit a PR and get traction for it. I’m not saying my opinion carries any particular weight.

@boeddeker

This comment has been minimized.

Copy link
Author

commented Feb 13, 2018

When there is at least one other person interested in a changed behavior, I will prepare a PR when #10392 is merged, else a PR is to much work (in the case it gets rejected) because my code already has a fix.

@RpiController

This comment has been minimized.

Copy link

commented Apr 3, 2018

I am interested! I'm surprised no one else has noticed that in addition to
ax.get_legend().get_title().get_visible() always returning True
ax.get_legend().get_visible() also always returns True

@ImportanceOfBeingErnest

This comment has been minimized.

Copy link
Contributor

commented Apr 3, 2018

You usually would not call legend more than once. But if you do, that is most likely to clear the current legend and get a fresh one. I would argue that a fresh legend with a title from ancient times in it is not the desired functionality.

@RpiController

This comment has been minimized.

Copy link

commented Apr 4, 2018

Exactly. I just want to show and hide the legend on demand but keep all its properties the same.

I've been working all morning to shorten my code (which is usually around 1300 lines) for a sscce. Is 264 lines acceptable to post here?

@ImportanceOfBeingErnest

This comment has been minimized.

Copy link
Contributor

commented Apr 4, 2018

I don't think anyone will read that code. Usually a problem is depictable with some 20 lines.

@jklymak

This comment has been minimized.

Copy link
Contributor

commented Apr 4, 2018

You want to call TempAxes.get_legend() or save as a variable. Calling TempAxes.legend() remakes the legend each time so of course its visible.

@ImportanceOfBeingErnest

This comment has been minimized.

Copy link
Contributor

commented Apr 4, 2018

As I said above:

You usually would not call legend more than once.

In your code you call it twice per button click.
Here is a minimal example of how your code should look like. This is more to show you how a minimal example should look like than solving your issue.

import matplotlib.pyplot as plt

fig, ax= plt.subplots()
ax.plot([1,2,3], label="bla bla")
ax.legend()

def toggleLegend(evt):  
    vis = ax.get_legend().get_visible()
    print vis
    ax.get_legend().set_visible(not vis)
    fig.canvas.draw_idle()

fig.canvas.mpl_connect("button_press_event", toggleLegend)

plt.show()
@RpiController

This comment has been minimized.

Copy link

commented Apr 4, 2018

Oh wow, thanks my dudes! I didn't notice the subtle difference of the "get_" in the first half of my command. What a moron I am. I guess you have to get the get_ of the get_.

Yeah my next project is to mess around with that mpl_connect() business to toggle individual lines on and off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.