Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
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 intolegend.Legend.__init__
, and removed fromaxes.legend
andfigure.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 likebenlaken/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 knowanything 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()
andax.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 |
jklymak commentedOct 9, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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... deftest_legend_handler_map(self):lines=plt.plot(range(10),label='hello world')withmock.patch('matplotlib.axes.Axes.''get_legend_handles_labels')ashandles_labels:handles_labels.return_value=lines, ['hello world']plt.legend(handler_map={'1':2})handles_labels.assert_called_with({'1':2}) |
jklymak commentedOct 9, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
It seems that both instances of legend should have pass-through kwarg documentation. It seems theright 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? |
anntzer commentedOct 11, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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). |
jklymak commentedOct 11, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@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==
, passingloc
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 takeloc
as a positional, but passed*args
through to theLegend.__init__
so it worked.
This should probably returnhandles, labels, extra_args
and inAxes.legend
raise iflen(extra_args) > 0
and pass them through inFigure.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 inlib/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.5http://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 positionalloc
in Axes.legend, still pass it through onFigure.legend
(but we should deprecate that on master once this lands).
jklymak commentedOct 11, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@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 atall 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 deflegend(self,*args,**kwargs):handles,labels,extra_args=_parse_legend_args([self],*args,**kwargs)iflen(extra_args):raiseTypeError(....)l=legend.Legend(handles,labels,**kwargs) ... and in deflegend(self,*args,**kwargs):handles,labels,extra_args=_parse_legend_args(...)l=legend.Legend(handles,labels,*extra_args,**kwargs) ... and in extra_args= ()if ...: ...eliflen(args)>=2:handles,labels=args[:2]extra_args=args[2:]returnhandles,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 deflegend(self,handles,labels,*args,**kwargs):l=Legend(self,handles,labels,*args,**kwargs)self.legends.append(l)l._remove_method=lambdah:self.legends.remove(h)self.stale=Truereturnl 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=10data= [np.logspace(0,1,100)+np.random.randn(100)+iiforiiinrange(N)]data=np.array(data).Tcmap=plt.cm.coolwarmrcParams['axes.prop_cycle']=cycler(color=cmap(np.linspace(0,1,N)))fig,ax=plt.subplots()lines=[]foriinrange(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=[]foriinrange(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 |
anntzer commentedOct 23, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 somethingelse 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 keepingfigure.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 preventfig.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 thatloc
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 Changedlegend_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.legendSmall refactor of check for labels so it is inside legend.Legendattempt to refactor legend; not passing testssmall typoMoved kwarg documentation out of child functions and into legend.Fixed documentation passthroughs so they work properlyFixed documentation passthroughs so they work properlyRemerge master, small changesFixed testsRemove repeated labels if same linestyleRemove repeated labels if same linestyleRemoved ability of third positional argument for axes.legendDeprecate third psotionsal argument for fig.legendAllow parasite axes to call legend(): parasite_simple.pyFixed docFixed docAdded twinx test for legendmerge fixmerge fixFixed legend handling of handles-only input; added error messageFixed legend handling of handles-only input for py27Fixed small doc changeMNT: do not special-case loc as positional argFixed small doc changeFix PEP8 and test errorFix 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']fornn,axxinenumerate(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! importnumpyasnpimportmatplotlib.pyplotaspltfig,ax=plt.subplots(1,2)hs= []foraxxinax: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
Uh oh!
There was an error while loading.Please reload this page.
PR Summary
Allows
fig.legend(handles=hs, labels=labs)
keyword calls.Fixes#9320
PR Checklist