Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[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

Merged
tacaswell merged 1 commit intomatplotlib:masterfromjklymak:fixfiglegend
Oct 31, 2017

Conversation

jklymak
Copy link
Member

@jklymakjklymak commentedOct 8, 2017
edited
Loading

PR Summary

Allowsfig.legend(handles=hs, labels=labs) keyword calls.

Fixes#9320

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • Check reworked docs are correct...

@dstansby
Copy link
Member

Thanks for fixing this. I can't think of any reason not to just have them as kwargs with defaults ofNone as you said in the bug report, but maybe I'm missing something.

warnings.warn('The handle {!r} has a label of {!r} which '
'cannot be automatically added to the '
'legend.'.format(handle, label))
labels.remove(label)
Copy link
Member

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?

Copy link
MemberAuthor

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.

Copy link
MemberAuthor

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

Copy link
Member

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

Copy link
MemberAuthor

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.

@dstansbydstansby added the Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. labelOct 8, 2017
@dstansbydstansby added this to the2.1.1 (next bug fix release) milestoneOct 8, 2017
@@ -74,6 +74,56 @@ def test_labels_first():
ax.legend(loc=0, markerfirst=False)


@image_comparison(baseline_images=['legend_fig_noargs'], extensions=['png'],
Copy link
Member

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?

Copy link
MemberAuthor

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
MemberAuthor

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

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.

Copy link
Member

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_.legendHandlesseems to get you back the labels and handles used, but that is just from poking at a legend object interactively....

@jklymak
Copy link
MemberAuthor

@dstansby Yes, I'm still not sure. But I largely copied fromaxes.legend and its done with*args, *kwargs as well, so I did that here.

@jklymak
Copy link
MemberAuthor

jklymak commentedOct 9, 2017
edited
Loading

OK, I refactored. Nowfigure.legend andaxes.legend have exactly the same argument parsing (now inlegend._parse_legend_args).

Probably need the docs forfigure.legend changed. Its possible this is non-backwards compatible.figure.legend used to allow three positional arguments, with the third beingloc, but I don't thinkaxes.legend ever did.

Question: Is there a way forfigure.legend andaxes.legend to share part of their docs since the calls are the same now?

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
Copy link
MemberAuthor

jklymak commentedOct 9, 2017
edited
Loading

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 aset_arg that has anACCEPTS: line in the doc. But I'm not doing that. So I assume I can define a kw doc string and then reference it somehow.

Ah, I knew I'd seen it somwhere.colorbar_doc is a great example.

@jklymakjklymak changed the titleAllow kwarg handles and labels figure.legendWIP: Allow kwarg handles and labels figure.legend and make doc for kwargs the sameOct 9, 2017
@tacaswell
Copy link
Member

Well, that escalated quickly. I thought this was going to be a ~5 line fix....

jnothman reacted with laugh emoji

@@ -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 = '''
Copy link
Contributor

Choose a reason for hiding this comment

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

make this_private?

@jklymakjklymak mentioned this pull requestOct 10, 2017
@jklymak
Copy link
MemberAuthor

@tacaswell We could easily go back to the 5-line fix...

But the fundamental problem was that*args, **kwargs were being handled differently betweenfigure.legend andaxes.legend for no good reason, and it will just be a maintenance headache in the future. Worse thelegend.Legend keywords were described three different places in three different formats, inconsistently in each place.

The changes are actually pretty small.

  1. moved the argument-parsing logic out ofaxes.legend intolegend._parse_legend_args
  2. used the same parsing infigure.legend
  3. changed the three docstrings to refer tolegend_kw_doc.

I need to do some cleanup, and I still have no idea how to address the failingmock test (see comment above), but otherwise I hope this is an improvement.

@jklymak
Copy link
MemberAuthor

Oh, hmmmm.fig.legend() did this funky thing where if you called

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
Copy link
Contributor

anntzer commentedOct 11, 2017
edited
Loading

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
Copy link
MemberAuthor

jklymak commentedOct 11, 2017
edited
Loading

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

@anntzer
Copy link
Contributor

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):
Copy link
MemberAuthor

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.

'(artist handles, figure labels, legend location)')

l = Legend(self, handles, labels, **kwargs)
print("Axes:", self.axes)
Copy link
Member

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:
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 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

Copy link
MemberAuthor

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.

kwargs.pop('handles', None)
kwargs.pop('labels', None)
# check for third arg
if len(args) == 3:
Copy link
Member

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!

@tacaswell
Copy link
Member

Modulo the handling of a positionalloc being inconsistent between axes and figure 👍

Copy link
Member

@tacaswelltacaswell left a 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
Copy link
MemberAuthor

jklymak commentedOct 11, 2017
edited
Loading

@tacaswell Can we just deprecate the third positional argument in both? Right now the code doesn't care if its called fromfigure oraxes.

That said, I'm not atall arguing for this third positional argument.

@tacaswell
Copy link
Member

In the spirit of minimal change / semver I would rather deprecateloc onFigure.legend in 2.2 rather than 2.1.1. It is alreadyremoved fromAxes.legend from 1.5 so we don't need to deprecate it there ;)

@jklymak
Copy link
MemberAuthor

@tacaswell Allowing the third positional argument is back inAxes.legend with this PR, becauseall the code to handle the arguments is the now same betweenAxes.legend andfigure.legend. Thats how this PR got rather large-looking. If we handlefigure andAxes differently, their code bases will diverge again, and thats how we ended up in this mess in the first place!

I'm suggesting we silently go back to allowing the third positional argument inAxes.legend, but deprecate if anyone happens to use it, and deprecate it infigure.legend as well. Then we can take it out again for 2.2. Its not like it hurts anything to handle the third positional argument.

@tacaswell
Copy link
Member

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.

InAxes.legend()

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 inFigure.legend()

deflegend(self,*args,**kwargs):handles,labels,extra_args=_parse_legend_args(...)l=legend.Legend(handles,labels,*extra_args,**kwargs)    ...

and in_parse_legend_args

extra_args= ()if ...:    ...eliflen(args)>=2:handles,labels=args[:2]extra_args=args[2:]returnhandles,labels,extra_args

The current behavior ofFigure.legend is thatall inputs tolegend.Legend can be passed as positional so this is a bit worse than I initially thought.

@jklymak
Copy link
MemberAuthor

Ah OK, I understand. Thats easy enough, if a bit of a PITA.

Not sure I understand your last comment, though. Currently onlyhandles,labels, andloc can be passed as positional, andloc actually gets converted to akwarg.

@tacaswell
Copy link
Member

In 2.0 the truncatedFigure.legend is

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 breakinghandlers andlabels as kwargs. If we are going to go back to the < 2.1 behavior we should bring it all back, add deprecation warnings to positional args in 2.2 and then remove in the next release.

@tacaswell
Copy link
Member

Sorry, I was using confusing tense!

@jklymak
Copy link
MemberAuthor

Believe it or not,ax.legend(arg1) canalso accept just a list of handles, a fact I found out only because@choldgraf did it inhttp://matplotlib.org/devdocs/gallery/text_labels_and_annotations/custom_legends.html#sphx-glr-gallery-text-labels-and-annotations-custom-legends-py

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)

figure_1

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:

figure_1

I won't fix in this PR, but wanted to register my dismay.

I thinkax.legend(arg) should accept either a list fo strings (old label format), or a list of handles. If the first, it does what it does now and just finds the first N handles to attach the strings to. If the latter, it provides a legend for that list.

The arg = list of handles is not documented, and I think@choldgraf example above should be modified because it is doing somethinglegend isn't really meant to do (all tests and docs passed for my change before this example was put into master). I think we should add support for lists of handles, but only display them if the label is explicitly set, and document this asnew behaviour. If you'd like that as a 2.2 separate PR, thats fine. I think the PR here, as it stands, gives us back what we had before 2.1. Pinging@tacaswell@anntzer@dstansby Thanks!

@anntzer
Copy link
Contributor

anntzer commentedOct 23, 2017
edited
Loading

As your second example shows,ax.legend is not "accepting a list of handles" (you will note that the artists that went into the legend in the second example are not every third, but the first 4); instead, it is interpreting the (positional first) argument as a list of labels (after forcefully converting them usingstr) and assigning them as labels to the firstn artists.

Yes this is nonsensical, but should really be the object of a separate PR.

jklymak reacted with thumbs up emoji

@jklymakjklymak changed the titleAllow kwarg handles and labels figure.legend and make doc for kwargs the same[MRG] Allow kwarg handles and labels figure.legend and make doc for kwargs the sameOct 24, 2017
@jklymak
Copy link
MemberAuthor

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?

@jklymakjklymak mentioned this pull requestOct 24, 2017
@tacaswell
Copy link
Member

@jklymak I think we did something funny with git :(

@jklymak
Copy link
MemberAuthor

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!

Copy link
Contributor

@dopplershiftdopplershift left a 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.

@jklymak
Copy link
MemberAuthor

@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
Copy link
MemberAuthor

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?

Copy link
Member

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.

Copy link
MemberAuthor

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!

Copy link
MemberAuthor

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?

Copy link
Member

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
@tacaswelltacaswell merged commitd2a8e67 intomatplotlib:masterOct 31, 2017
lumberbot-appbot pushed a commit that referenced this pull requestOct 31, 2017
@jklymak
Copy link
MemberAuthor

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()
/Users/jklymak/matplotlib/lib/matplotlib/legend.py:932: UserWarning: Legend does not support [<matplotlib.lines.Line2D object at 0x107cfc3c8>] instances.A proxy artist may be used instead.See: http://matplotlib.org/users/legend_guide.html#creating-artists-specifically-for-adding-to-the-legend-aka-proxy-artists  "aka-proxy-artists".format(orig_handle)/Users/jklymak/matplotlib/lib/matplotlib/legend.py:932: UserWarning: Legend does not support [<matplotlib.lines.Line2D object at 0x107cfc908>] instances.A proxy artist may be used instead.See: http://matplotlib.org/users/legend_guide.html#creating-artists-specifically-for-adding-to-the-legend-aka-proxy-artists  "aka-proxy-artists".format(orig_handle)

@jklymak
Copy link
MemberAuthor

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!

afvincent reacted with laugh emoji

@jklymakjklymak deleted the fixfiglegend branchOctober 31, 2017 19:44
QuLogic added a commit that referenced this pull requestNov 3, 2017
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@jnothmanjnothmanjnothman left review comments

@NelleVNelleVNelleV left review comments

@tacaswelltacaswelltacaswell approved these changes

@QuLogicQuLogicQuLogic left review comments

@anntzeranntzeranntzer left review comments

@dopplershiftdopplershiftdopplershift approved these changes

@dstansbydstansbydstansby approved these changes

Assignees
No one assigned
Labels
Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Milestone
v2.1.1
Development

Successfully merging this pull request may close these issues.

2.1 figure.legend broken
8 participants
@jklymak@dstansby@tacaswell@anntzer@QuLogic@NelleV@jnothman@dopplershift

[8]ページ先頭

©2009-2025 Movatter.jp