-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
[MRG] Allow kwarg handles and labels figure.legend and make doc for kwargs the same #9324
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
Conversation
Thanks for fixing this. I can't think of any reason not to just have them as kwargs with defaults of |
lib/matplotlib/figure.py
Outdated
warnings.warn('The handle {!r} has a label of {!r} which ' | ||
'cannot be automatically added to the ' | ||
'legend.'.format(handle, label)) | ||
labels.remove(label) |
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.
Can we let this be handled down stream?
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.
The underscore check? Maybe. I just shamelessly copied from axes.legend.
Yes yes I know I should have refactored it... otoh I don’t like that axes.legend if there is only one argument assumes that it is a list of labels. I think a list of handles makes much more sense.
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.
Put this check into legend.Legend.__init__
, and removed from axes.legend
and figure.legend
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.
Taking labels (not handles) is a bit problematic and leads to issues like benlaken/European_wind#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.
@tacaswell. Um yeah that does look to be a bad decision.
I’d suggest we just strip the illegal underscore and warn (or logging.warning 😉) or set it to an empty string.
But I didn’t originate this code. I’m not actually sure why an underscore is a problem in the first place.
lib/matplotlib/tests/test_legend.py
Outdated
@@ -74,6 +74,56 @@ def test_labels_first(): | |||
ax.legend(loc=0, markerfirst=False) | |||
|
|||
|
|||
@image_comparison(baseline_images=['legend_fig_noargs'], extensions=['png'], |
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.
Is there a reasonable way to check that this works without an image test?
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.
Happy to try that. Or reduce the tests. I don’t know how you would test it without a plot but that’s just my ignorance. Happy to implement something
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.
You only need to test equivalence between the results of different invocations, not equivalence to some human-verified gold standard output.
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 problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK< I think I understand. Use the trick linked above and then assert that the pngs are the same w/o actually having to save them or do the image comparison? That sounds great.
Sorry for being slow, but I don't know anything about writing tests for python...
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.
And I'm not an expert at testing with matplotlib, but I would've thought there might be a way to check the calculated legend artists are equivalent. But perhaps not.
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.
ax.legend_.get_texts()
and ax.legend_.legendHandles
seems to get you back the labels and handles used, but that is just from poking at a legend object interactively....
@dstansby Yes, I'm still not sure. But I largely copied from |
OK, I refactored. Now Probably need the docs for Question: Is there a way for This is not passing a legend test that I don't understand so I'm having difficulty debugging... def test_legend_handler_map(self):
lines = plt.plot(range(10), label='hello world')
with mock.patch('matplotlib.axes.Axes.'
'get_legend_handles_labels') as handles_labels:
handles_labels.return_value = lines, ['hello world']
plt.legend(handler_map={'1': 2})
handles_labels.assert_called_with({'1': 2}) |
It seems that both instances of legend should have pass-through kwarg documentation. It seems the right way to do this is for each keyword argument to have a Ah, I knew I'd seen it somwhere. |
Well, that escalated quickly. I thought this was going to be a ~5 line fix.... |
lib/matplotlib/legend.py
Outdated
@@ -105,6 +106,177 @@ def _update_bbox_to_anchor(self, loc_in_canvas): | |||
self.legend.set_bbox_to_anchor(loc_in_bbox) | |||
|
|||
|
|||
legend_kw_doc = ''' |
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.
make this _private
?
@tacaswell We could easily go back to the 5-line fix... But the fundamental problem was that The changes are actually pretty small.
I need to do some cleanup, and I still have no idea how to address the failing |
Oh, hmmmm. fig, axs = plt.subplots(2,1)
l1 = axs[0].plot(x, 'r', label='boo')
l2 = axs[1].plot(x, 'r', label='boo')
fig.legend() It would only put 'boo' in the legend once. This PR doesn't respect this. Easy to fix, but do we really want to? |
I guess we "have to" for backcompatibility but that sounds like yet another thing I'd like to get rid of... (for example Axes.legend does not provide the same functionality). |
@anntzer Ahem, now it does ;-) Well, if this PR gets accepted. I actually think there is some sense to it, but I wouldn't have offered the functionality if it'd just have been up to me... |
I can't say I really hate this feature (so if others like it I'm fine with it), but in my view it's a bit similar to auto reuse of axes (#9037 and linked isses) and would prefer not having it for the same reason. |
handles = [] | ||
labels = [] | ||
|
||
def _in_handles(h, l): |
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.
@anntzer This is where the logic to remove duplicate label/line pairs is. Easy to remove if there is a consensus.
lib/matplotlib/figure.py
Outdated
'(artist handles, figure labels, legend location)') | ||
|
||
l = Legend(self, handles, labels, **kwargs) | ||
print("Axes:", self.axes) |
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.
rouge print
|
||
# Two arguments: | ||
# * user defined handles and labels | ||
elif len(args) >= 2: |
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.
This should be ==
, passing loc
as a positional argument in deprecated for the Axes version but apparently we missed doing the same to the Figure version (as it did not explicitly take loc
as a positional, but passed *args
through to the Legend.__init__
so it worked.
This should probably return handles, labels, extra_args
and in Axes.legend
raise if len(extra_args) > 0
and pass them through in Figure.legend
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.
OK, this is caught in lib/matplotlib/axes/_axes.py
. But it still needs to be in here.
lib/matplotlib/axes/_axes.py
Outdated
kwargs.pop('handles', None) | ||
kwargs.pop('labels', None) | ||
# check for third arg | ||
if len(args) == 3: |
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.
This was intentionally deprecated in 1.5 http://matplotlib.org/api/api_changes.html#legend it should not come back!
Modulo the handling of a positional |
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.
Raise on positional loc
in Axes.legend, still pass it through on Figure.legend
(but we should deprecate that on master once this lands).
@tacaswell Can we just deprecate the third positional argument in both? Right now the code doesn't care if its called from That said, I'm not at all arguing for this third positional argument. |
In the spirit of minimal change / semver I would rather deprecate |
@tacaswell Allowing the third positional argument is back in I'm suggesting we silently go back to allowing the third positional argument in |
A user will discover it and complain when it goes away. I the current behavior can be maintained on top of this refactor without too much extra complexity. In def legend(self, *args, **kwargs):
handles, labels, extra_args = _parse_legend_args([self], *args, **kwargs)
if len(extra_args):
raise TypeError(....)
l = legend.Legend(handles, labels, **kwargs)
... and in def legend(self, *args, **kwargs):
handles, labels, extra_args = _parse_legend_args(...)
l = legend.Legend(handles, labels, *extra_args, **kwargs)
... and in extra_args = ()
if ...:
...
elif len(args) >= 2:
handles, labels = args[:2]
extra_args = args[2:]
return handles, labels, extra_args The current behavior of |
Ah OK, I understand. Thats easy enough, if a bit of a PITA. Not sure I understand your last comment, though. Currently only |
In 2.0 the truncated def legend(self, handles, labels, *args, **kwargs):
l = Legend(self, handles, labels, *args, **kwargs)
self.legends.append(l)
l._remove_method = lambda h: self.legends.remove(h)
self.stale = True
return l so the API change from 2.0 -> 2.1 is bigger than just breaking |
Sorry, I was using confusing tense! |
Believe it or not, But, there is no way to attach a proper label to the legend when you call the legend like this, even if the underlying handles have labels! In master: N = 10
data = [np.logspace(0, 1, 100) + np.random.randn(100) + ii for ii in range(N)]
data = np.array(data).T
cmap = plt.cm.coolwarm
rcParams['axes.prop_cycle'] = cycler(color=cmap(np.linspace(0, 1, N)))
fig, ax = plt.subplots()
lines=[]
for i in range(N):
lines += [ax.plot(data[:, i], label='%s'%i)]
print(lines[-1])
ax.legend(lines) That is broken beyond belief in my opinion, because: fig, ax = plt.subplots()
lines=[]
for i in range(N):
lines += [ax.plot(data[:, i], label='%s'%i)]
print(lines[-1])
ax.legend(lines[::3]) is a completely reasonable thing to do, but yields: I won't fix in this PR, but wanted to register my dismay. I think The arg = list of handles is not documented, and I think @choldgraf example above should be modified because it is doing something |
As your second example shows, Yes this is nonsensical, but should really be the object of a separate PR. |
This is ready to go, unless we find something else whacky that the various versions of legend do... I have two approvals, but I assume its bad form to self-merge? |
@jklymak I think we did something funny with git :( |
Sorry! My fault. Let me re-push. I made a local change and was pushing onto your change. But I got your change. I won't touch it until tonight, honest! |
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 wild about such a big PR for a patch release, but I get why.
I don't like the doc handling, but that's a bit of a mess in matplotlib throughout, so this is up to par. Let's save the big docstring handling throw down for #9414.
@dopplershift Yes fair enough - its easy to fix the docs later. I'll add a note that this is one place to fix in #9414 if its not there already.... |
# "instead.") | ||
# kwargs['loc'] = extra_args[0] | ||
# extra_args = extra_args[1:] | ||
pass |
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.
@tacaswell This now fails Travis - what was the plan here? I thought we were keeping figure.legend
w/ three possible args?
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.
This shouldn't have changed anything other than to prevent fig.legend(a, b, l, loc=c)
case.
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.
Ah, I see:
self = <matplotlib.tests.test_legend.TestLegendFigureFunction object at 0x7fffd6b1fc50>
def test_legend_label_three_args(self):
fig, ax = plt.subplots()
lines = ax.plot(range(10))
with mock.patch('matplotlib.legend.Legend') as Legend:
fig.legend(lines, ['foobar'], 'right')
> Legend.assert_called_with(fig, lines, ['foobar'], loc='right')
The test was checking that loc
got loaded w/ the third element. I'll fix later. 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.
@tacaswell Changed legend_test
to check for an Exception should someone call: fig.legend(handles, labels, 'right', loc='left')
(test_legend_label_three_args_pluskw
) I think thats what you wanted.
Any reason to leave this check/pass in here? Just posterity sake?
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.
To avoid changing the API as much as possible. I am 👍 on deprecating the other positional arguments for 2.2, but not in 2.1.1 (so leave that code in place so we can just un-comment it in a follow on PR).
I don't think we needed a test for what is core-python behavior (you can not pass a value as both positional and keyword) nor should we break it!
Allow kwarg handles and labels figure.legend Small refactor of check for labels so it is inside legend.Legend attempt to refactor legend; not passing tests small typo Moved kwarg documentation out of child functions and into legend. Fixed documentation passthroughs so they work properly Fixed documentation passthroughs so they work properly Remerge master, small changes Fixed tests Remove repeated labels if same linestyle Remove repeated labels if same linestyle Removed ability of third positional argument for axes.legend Deprecate third psotionsal argument for fig.legend Allow parasite axes to call legend(): parasite_simple.py Fixed doc Fixed doc Added twinx test for legend merge fix merge fix Fixed legend handling of handles-only input; added error message Fixed legend handling of handles-only input for py27 Fixed small doc change MNT: do not special-case loc as positional arg Fixed small doc change Fix PEP8 and test error Fix PEP8 and test error
… and make doc for kwargs the same
Ummm, help! The original example no longer works!! fig, ax = plt.subplots(1, 2)
hs = []
lab = ['Boo', 'Hoo']
for nn, axx in enumerate(ax):
h, = axx.plot(np.arange(10))
hs += [h]
fig.legend(hs, lab)
plt.show()
|
Ooops, false alarm. I had a typo in my test! import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2)
hs = []
for axx in ax:
h, = axx.plot(np.arange(10))
hs += [h]
fig.legend(handles=hs, labels=['Boo', 'Hoo'])
plt.show() works fine! |
Backport PR #9324 on branch v2.1.x
PR Summary
Allows
fig.legend(handles=hs, labels=labs)
keyword calls.Fixes #9320
PR Checklist