Skip to content

Legend for Scatter #11127

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

Conversation

ImportanceOfBeingErnest
Copy link
Member

PR Summary

This PR proposes to include an easy, yet versatile option to create legends for scatterplots.

Motivation:

  • Scatter plots create a Collection of points, for which it is rather straight forward to create a colorbar. This is useful for continuous variables mapped to color. However, creating a legend with discrete entries requires to manually set up the necessary proxy artists. It appears that scatter plots are more and more used to show discrete of categorical variables. In such cases, a discrete legend is often more desireable than a (continuous, or even discrete) colorbar.

  • ggplot offers an easy method to create legends for scatter plots,
    ggplot(...) + geom_point(aes(x=.., y=.., size=.., color=..)), resulting in something like
    image

This PR adds a method legend_items to the PathCollection, which returns the handles and labels for a legend.
Creating a scatter legend would then look like

scatter = ax.scatter(x, y, c=c, s=s)

# produce a legend with the unique colors from the scatter
ax.legend(*scatter.legend_items(), loc=3, title="Classes")

# produce a legend with a cross section of sizes from the scatter
handles, labels = scatter.legend_items(mode="sizes", alpha=0.6)
ax.legend(handles, labels, loc=1, title="Sizes")

image

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)

@story645
Copy link
Member

I like the idea of a generalized way to do legends for PathCollections & PatchCollections, but wonder if this is overly complicated:

# produce a legend with the unique colors from the scatter
ax.legend(*scatter.legend_items(), loc=3, title="Classes")

# produce a legend with a cross section of sizes from the scatter
handles, labels = scatter.legend_items(mode="sizes", alpha=0.6)
ax.legend(handles, labels, loc=1, title="Sizes")

overlapping with the discussion in # #7383, I wonder if there shouldn't just be a subclass of legend that handles Path/Patch and cmap/norm stuff.

@ImportanceOfBeingErnest
Copy link
Member Author

How is a single line "too complicated"?

If I understand correctly, a Legend subclass would then need to take the artist (in this case the PathCollection) as input, as well as those arguments needed to steer the output. In that sense you could replace ax.legend(*scatter.legend_items(**kw)) by ax.enhanced_legend(scatter, **kw). So essentially enhanced_legend would take all the currently available 26 keyword arguments, and in addition those to manage what to show in the legend.

I also don't see much overlap to #7383. I think what's discussed there is interesting if at some point you want to allow for something like scatter(x,y, c=["A", "B", "B", "C", "A"])?

What is proposed in this PR is completely analogue to the existing ContourSet.legend_elements() which creates handles and labels for contours. (... which makes me realize that the name legend_items could of course be changed to legend_elements to be consistent between those.)

Copy link
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

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

I think this a useful thing to add. Some minor suggestions/comments below that I think will improve it. I feel somewhat strongly that the name should not just be legend_items() as the returned tuple is non-unique.

My only other issue is that we are going to add a ax.get_legend_handles_labels. I've not quite wrapped my brain around where this fits into that.


Parameters
----------
mode : string, optional, default *"colors"*
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I like "mode" here, though I guess its OK. "key" would be more natural to me...

Copy link
Member Author

Choose a reason for hiding this comment

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

What makes keymore natural? Any good argument for key?
In principle I'm open to other names here, e.g. prop because colors or sizes are properties?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, prop works for me.

for obtaining a legend for a :meth:`~.Axes.scatter` plot. E.g.::

scatter = plt.scatter([1,2,3], [4,5,6], c=[7,2,3])
plt.legend(*scatter.legend_items())
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I like the name legend_items just because it makes it sound like these are a property of the PathCollection; I think a verb would help like create_legend_items or make_legend_items

Copy link
Member Author

Choose a reason for hiding this comment

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

For consistency I would go with legend_elements as commented above already. Unless people are prepared to break the existing contour method and call both something else?

Copy link
Member

Choose a reason for hiding this comment

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

I still like having a verb because you are actually doing something versus just returning a property of the collection. But I don't feel super strongly...

Can be *"colors"* or *"sizes"*. In case of *"colors"*, the legend
handles will show the different colors of the collection. In case
of "sizes", the legend will show the different sizes.
useall : bool or string "auto", optional (default "auto")
Copy link
Member

Choose a reason for hiding this comment

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

EDIT: oops, this somehow got lost: I don't think you need both useall and num kwargs. num=None is all data, num=int is number bins, num='auto' is auto

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that won't work, but I will recheck.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, you're right, that works. The only "drawback" is that in that case num cannot act as a parameter for the automatic determination; but I guess it's fine to have that parameter hardcoded (choosing num=9 now for the border between showing all data or some cross section of it).

Copy link
Member

Choose a reason for hiding this comment

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

I didn't get that num interacted w/ auto somehow. (still don't get it w/o checking the code).

I wonder if what you really want is a "bins" argument, and make this completely analagous to the histogram bins argument. https://docs.scipy.org/doc/numpy/reference/generated/numpy.histogram.html You could even optionally put in the legend how many elements there are in each bin.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think I want "bins". I want "ticks"; and I get them using a Locator. From my side this is all working fine. The idea is of course to get nice "tick positions", unlike in the case of histogram bins where you'd end up with arbitrarily ugly numbers like 1.57214.

What this function does not allow for is to add custom ticks (which is possible for bins in histogram). I decided against that because I think this is exactly the point where you'd anyways simply create the legend the way you want it with the artists created manually. But if someone feels that this is needed, I could also add it.

Copy link
Member

@jklymak jklymak Apr 30, 2018

Choose a reason for hiding this comment

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

OK, thats cool. What about letting a (power) user pass in a Locator?

Copy link
Member Author

Choose a reason for hiding this comment

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

That is sure possible. I think it would then simply ignore num completely. I'm not sure if it's needed, but if there is the wish to add it, I can do that.

Copy link
Member

Choose a reason for hiding this comment

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

I meant let them pass in a locator as the argument to num. I don’t think anyone who knows how to write a locator would be put off by the slight semantic incongruity.

The format or formatter to use for the labels. If a string must be
a valid input for a `~.StrMethodFormatter`. If None (the default),
use a `~.ScalarFormatter`.
func : function, default *lambda x: x*
Copy link
Member

Choose a reason for hiding this comment

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

I found this too confusing. What does *func* do and why do you need it? Maybe this needs an example...

Copy link
Member Author

Choose a reason for hiding this comment

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

func would allow to calculate the values to be shown in the legend. This is mainly useful for "sizes", because unlike the c parameter in scatter, which takes numbers and maps them to color using a Normalize, the s parameter does not have a normalization. This means that usually you would encode your data into sizes and provide those sizes in the scatter call. However in the legend you might still want to show the original data's values. Example: You use the s to show price ranging between 0.5 and 10 dollars. Now you cannot use those numbers directly as scatter sizes, because that would result in tiny dots. So you convert them to sizes via s = lambda price : 0.5*(price*10)^2. Now func would allow you to provide price = lambda s: np.sqrt(2*s)/10 to have a nice legend using the actual prices in dollars.

I think you are right that an example might be useful here. I could add that in addition to the already extended example?

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. I was particularly flummoxed by "This converts the initial values for color or size and needs to take a numpy array as input." I wasn't sure what "initial" meant.

For the doc string maybe: "Function to calculate the labels. Often the size (or color) argument to scatter will have been pre-processed by the user using a function s = f(x) to make the markers visible; eg size = np.log10(x)`. Providing this function allows that pre-processing to be inverted, so that the legend labels have the correct values. eg np.exp(x, 10)``"

Copy link
Member Author

Choose a reason for hiding this comment

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

I reformulated the docstring and also added an additional example now.

@story645
Copy link
Member

story645 commented Apr 29, 2018

How is a single line "too complicated"?

Sorry, I was reacting to the * notation and should have looked at this way more carefully. It's fine as is, especially if you go with legend_elements() to make it consistent with existing functionality.

@jklymak
Copy link
Member

jklymak commented Apr 29, 2018

Actually ax.get_legend_handles_labels already exists. As does the concept of legend handlers. Any way this can be turned into a legend handler?

@ImportanceOfBeingErnest
Copy link
Member Author

ax.get_legend_handles_labels exists indeed. What it does is to return the legend handles and labels that would produce a legend for an axes ax. Example,

ax.scatter(...., label="mylabel")
h, l = ax.get_legend_handles_labels()
ax.legend(h, l)

would produce a single legend row with one scatter dot and the label "mylabel". It is hence equalvalent to simply calling legend without arguments,

ax.scatter(...., label="mylabel")
ax.legend()

image

Now what is possible using a legend handler would be to change the single handle from a single point to something more characteristic of a scatter, e.g.:

image

However, this is still a single legend entry for the complete scatter.

What this PR proposes is a way to get several legend entries for one single scatter plot. This is not possible with a Handler.

One might consider putting the proposed API for scatter.legend_elements(**API) into ax.get_legend_handles_labels(**API). I think this might be undesired because then the axes method takes all those arguments, which only make sense in the context of a scatter. At some point internally it would in any case still need to call something like scatter._legend_elements(**API). So I guess the proposed method here would still be needed and can be public as well.

@tacaswell tacaswell added this to the v3.0 milestone Apr 29, 2018
@@ -899,6 +900,118 @@ def set_paths(self, paths):
def get_paths(self):
return self._paths

def legend_items(self, mode="colors", useall="auto", num=10,
Copy link
Member

Choose a reason for hiding this comment

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

How far up the class hierarchy does it make sense to move this?

Copy link
Member Author

Choose a reason for hiding this comment

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

This particular method only makes sense for the PathCollection. A less useful version of it could be created for any Collection and yes, even Artist could in principle have such method. However the point being that at least for more complex things like scatter (PathCollection), LineCollection, contour (ContourSet), imshow (AxesImage) there need to be some options for the user to decide on the final appearance. I'm unsure how to generalize such arguments and how to use them, other than letting the user call this method and supply the returned handles/labels to legend - see comment below.

@tacaswell
Copy link
Member

The issue here is that the "one artist : one dataset : one legend entry" mapping that exist for ex Line2D is badly broken by scatter.

A concern with the way we currently do legends is that the handlers are created by lookup up state held in the Legend class rather than asking the Artist objects in the draw tree what they think their legend handlers should be. On one hand, this is pretty nice as now the user can customize per-legend (so ex changing out the handler for every Line2D object can be done in userspace at runtime), but can be a bit indirect and cumbersome.

How far can the notion of 'legend_elements' be generalized? Even if we only have a real implemenation for PathCollection, having a NotImpletened marker farther up the class tree would be good (so the next time we want something like this on another class we don't invent a different name!). Does it make sense to push this all the way up to Artist? I can see a use for that in grave (https://github.com/networkx/grave/blob/master/grave/grave.py#L221) where the network artist may have strong opinions about what should go in the legend.

I think for now coming up with a consistent name and API for Artists to return a list of labels and handles as an optional public method is a good start and we can sort out how to integrate it into the auto-legend code later.

@ImportanceOfBeingErnest
Copy link
Member Author

I think neither of
image or
image may actually be undesired in the general case. The initial proposal here was to make it easier to provide more handles and labels to the legend for a case where more detailed legend is desired.

What this shows is that it is probably rather hard to let the artist decide for a way of creating a legend by itself. It still needs some guidance by the user. This makes things rather complicated as in that case the arguments that steer the creation of one/several legend entries need to be given to legend as well, adding up to the existing 26 legend arguments and leading to a lot of useless arguments in cases where the respective artists is not even present in an axes. It also would be hard to distinguish which argument would need to be used for which artist. Alternatively, one could provide those arguments to the artist itself. So the artist has those properties stored and would use them if the legend calls its legend_elements() method.

From a user's perspective, it may not actually make that much of a difference, whether to create the legend elements first and supply them to the legend (as in this proposal)

sc = ax.scatter(x,y,c=.., s=...)
ax.legend(*sc.legend_elements(**legend_viz_params))

or let the artist take the required parameters

sc = ax.scatter(x,y,c=.., s=..., **legend_viz_params)
ax.legend()

or let the legend take them,

sc = ax.scatter(x,y,c=.., s=...,)
ax.legend(**legend_viz_params)

Or does it?
In any case, if that proposal to give artists a general obtain_copy_of_me_for_legend (or __legend_repr__) method and let the legend use it is to be pursued further it would kind of revolutionize the Legend and replace the existing Handlers. Would it make sense to discuss this in a separate issue? I only fear that such issues will just end up never being implemented and step-like improvements like the inital proposal here are being blocked on them.

@jklymak
Copy link
Member

jklymak commented Apr 29, 2018

I wasn't exactly sure the scope of what legend_handler could do.

I agree that the functionality here is useful, and I don't really see how it can easily be made extensible.

What I'm hung up on is that I am not sure I like the API of having it a method on PathCollection. Thats not where I would look for it, and I think makes it obscure. Would calling ax.scatter_legend(sc) be too restrictive? One could even imagine passing it other handles and labels if you wanted more than just the scatter info on the legend: ax.scatter_legend(sc, h_add, l_add, **otherkwargs).

OTOH, if we do this for every different artist type, I imagine this gets to be a mess so I can see the point of having it attached to the artist.

Again, just trying to think the API through a bit. I think this is a good idea overall.

@ImportanceOfBeingErnest
Copy link
Member Author

ax.scatter_legend(sc, mode=.., useall=.., num=.., .. ,**kw) is totally possible. It would make the separation between legend kwargs and kwargs needed to steer the specific scatter legend output easier. I would agree to it getting messy if further down the road many similar ax.<plottype>_legend() methods were to be added.
It also goes in a completely different direction than the proposal of an Artist method by @tacaswell.
Following his suggestion,

I think for now coming up with a consistent name and API for Artists to return a list of labels and handles as an optional public method is a good start

we currently have:

  • Artist.legend_elements() (This PR)
    • Pro: name is consistent with existing ContourSet.legend_elements
    • Con: name has no verb, hides the non-unique nature of the return
  • Something like Artist.{create/make}_legend_{items/handles/elements}()
    • Pro: could better reflect the process of producing something from the artist.
    • Con: inconsistent with existing contour method; adjusting contour method would break existing API
  • Artist.get_legend_handles_labels()
    • Pro: consistent with existing Axes.get_legend_handles_labels()
    • Con: inconsistent with existing contour method; adjusting contour method would break existing API, rather long name.

Concerning the API, i.e. the needed arguments, I think there might be little to no overlap between what different artists need as input. Even num (the number of created legend elements) is not really needed for many artists, for which anything but num=1 does not make sense at all.

@jklymak
Copy link
Member

jklymak commented Apr 30, 2018

Sorry, I missed the point about legend_elements already being used for contour. And I see that is a method on ContourSet. Given that precedent, I'm fine w/ the general level of the API here.

jklymak
jklymak previously approved these changes May 1, 2018
Copy link
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

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

I think this is useful, and consistent w/ the similar method to ContourSet. "num" could possible pass a Locator if we want more flexibility in the binning of the sizes, but that could either happen now or as a follow up if someone wants more flexibility. Thanks a lot for this!

@ImportanceOfBeingErnest ImportanceOfBeingErnest force-pushed the legend-for-scatter branch 2 times, most recently from dfe09f3 to 1097412 Compare May 2, 2018 08:41
@ImportanceOfBeingErnest
Copy link
Member Author

Update: You may now pass a locator to num.


Concerning the discussion about having a legend_elements method for Artist, I think one may do something like

class Artist(object):
    # ...
    def legend_elements():
        return [self], [self.get_label()]

However, as long as this isn't used anywhere internally, it's not really useful. In any case this can be part of a different PR, in case one wants to let legend use this for obtaining its handles/labels.

@jklymak
Copy link
Member

jklymak commented May 14, 2018

@ImportanceOfBeingErnest we talks about this on the weekly phone call, and the issue is with attaching this method to PathCollection which is a more general class than just for scatter.

The request would be that we add a ScatterArtist subclass of PathCollections that has extras like this as new methods just on the subclass, and of course change the call to scatter to use this new subclass. Thats a somewhat bigger job than just this PR, but should be manageable and would make other things easier; the object returned from scatter really should have been more than just a PathCollection earlier than this.

If you don't feel like doing this, I imagine someone else will be up for picking it up! Thanks for a great idea...

@jklymak jklymak dismissed their stale review May 14, 2018 20:07

Stale: requesting a bit more of a refactor...

@ImportanceOfBeingErnest
Copy link
Member Author

But this is not only useful for scatters. If you have a different PathCollection than the one being created by a scatter, you may still use it to create legend entries for it. As of now, it seems no other matplotlib function other than scatter creates PathCollections, but if there were others, why restrict this functionality to only ScatterArtist?

@ImportanceOfBeingErnest
Copy link
Member Author

What's the status here?
Should there really be a new ScatterArtist created which then inherits the PathCollection's legend_elements? Or is there any profound reason to only make this method available for a ScatterArtist even though it's perfectly usable for a PathCollection as well?

@jklymak
Copy link
Member

jklymak commented May 28, 2018

My understanding which may be incomplete, is that PathCollection is meant to be a more general path container and hence we want to not have any methods on it that are specific to scatter. Yes, PathCollection is only currently used for scatter, but thats not the intended end-use case.

If you still disagree with this organization, then we can bring other folks into the conversation.

@ImportanceOfBeingErnest
Copy link
Member Author

As said, this is not specific to scatter - although clearly motivated by it.
If you can show me a PathCollection for which the newly added method would fail, that would all make sense. One could also argue that for the most general case one should add a third option as mode argument, which would create legend entries for several paths. But in that case, the method would still reside in PathCollection, not in an inherited class, right?
At the beginning, a question asked how far up the hierarchy this should sit. Now suddely the question is how far down it should be moved.
Just to be clear here: I'm not opposed to add a ScatterArtist at all. But even if doing so, I would still let the legend_elements method stay in PathCollection.

@jklymak
Copy link
Member

jklymak commented May 28, 2018

I don't have any skin in this game - you'll have to work it out w/ @tacaswell

@tacaswell tacaswell self-assigned this May 28, 2018
@jklymak
Copy link
Member

jklymak commented Jun 11, 2018

ping @tacaswell

@jklymak
Copy link
Member

jklymak commented Aug 19, 2018

Marking as "Needs Review" to hopefully burble to the top of people's lists...

@dstansby
Copy link
Member

dstansby commented Nov 3, 2018

Looks like doc build and travis aren't happy at the moment?

@ImportanceOfBeingErnest
Copy link
Member Author

I'm happy to fix all the little hickups that come and go with updated dependencies (pep8 to flake, sphinx 1.7 to 1.8 etc.) once the general fate of this PR is decided upon.
I will not waste my time continuously keeping up with updated depenencies in order to have the checkmarks green every day for the next 5 months, just to then have someone decide that this is not the route to go at all.

@jklymak jklymak added status: needs review Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. and removed status: needs revision labels Feb 7, 2019
@jklymak
Copy link
Member

jklymak commented Feb 9, 2019

... is this really release critical? No, but I can imagine @ImportanceOfBeingErnest is a bit frustrated its sat around for months...

Copy link
Member

@efiring efiring left a comment

Choose a reason for hiding this comment

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

Overall, this seems like important functionality and a reasonable implementation. I would be reluctant to hold it up indefinitely based on the controversy over whether the method should be on something other than the PathCollection. I don't see any harm in putting it there; that doesn't preclude some future refactoring, so long as the user would still be able to call it as a method of whatever a future scatter returns. I would like to see some consideration of my suggestion regarding the "num" kwarg (possibly with a new "locator" kwarg) to facilitate what I would expect to be the very common use case of wanting to specify the levels as a sequence.

scatter = ax.scatter(x, y, c=c, s=s)

# produce a legend with the unique colors from the scatter
legend1 = ax.legend(*scatter.legend_elements(), loc=3, title="Classes")
Copy link
Member

Choose a reason for hiding this comment

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

Minor nit-pick: I think that the numeric location codes are a terrible API and should not be used in examples. (I would favor removing them entirely from the legend documentation.)

Can be *"colors"* or *"sizes"*. In case of *"colors"*, the legend
handles will show the different colors of the collection. In case
of "sizes", the legend will show the different sizes.
num : int, None, "auto" (default), or `~.ticker.Locator`, optional
Copy link
Member

Choose a reason for hiding this comment

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

Although the ability to use a Locator is good, and provides the ability to put in a list via a FixedLocator, wouldn't one of the most common--maybe the most common--use cases be an explicit list? For example, sizes represent populations of cities, so the legend shows sizes for [10000, 100000, 1000000, 10000000]. If this would be a common use case, then it might be worth supporting it directly by accepting a sequence. To make all this more like contour with its 'levels', a separate kwarg could be used to specify a Locator for the rare cases when it is needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would still use the same argument num though, because a list of values is an alternative to those other options. Else one would need to go through checking what happens if both are provided etc.

So what is a canonical way of checking if something is a list, tuple or array of numbers? (isinstance(x, collections.abc.Iterable) only does half the job; is there something in cbook that I overlooked?)

Copy link
Member

Choose a reason for hiding this comment

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

np.iterable() should suffice, I think.

-------
tuple (handles, labels)
with *handles* being a list of `.Line2D` objects
and *labels* a list of strings of the same length.
Copy link
Member

Choose a reason for hiding this comment

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

Very minor suggestion: 'and labels being a matching list of strings.' (Otherwise, it could be misinterpreted as meaning the strings must all be the same length.)

u = np.unique(self.get_sizes())
color = kwargs.pop("color", "k")
else:
warnings.warn("Invalid prop provided, or collection without "
Copy link
Member

Choose a reason for hiding this comment

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

What is the rationale for a warning rather than an Exception here? At least in the case of an invalid prop, shouldn't this raise an Exception?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think my rationale here was that similar to how a legend is still created even if no labeled objects are found, here it should not break the code in case no handles/labels can be determined. So one would still get a plot out, just without propper legend.

Copy link
Member

Choose a reason for hiding this comment

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

OK, but then I think that the "invalid prop" case should raise ValueError and only the lack of an array should raise the warning.

loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1,
steps=[1, 2, 2.5, 3, 5, 6, 8, 10])
label_values = loc.tick_values(func(arr).min(), func(arr).max())
cond = (label_values >= func(arr).min()) & \
Copy link
Member

Choose a reason for hiding this comment

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

Style preference: when possible, as it is here, use parentheses rather than trailing backslashes for line continuation.

kw.update(kwargs)

for val, lab in zip(values, label_values):
if prop == "colors" and hasarray:
Copy link
Member

Choose a reason for hiding this comment

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

Again, if I am reading the code correctly, the hasarray check is not needed. (But it makes me wonder whether I am missing something, which is quite possible.)

@ImportanceOfBeingErnest ImportanceOfBeingErnest force-pushed the legend-for-scatter branch 2 times, most recently from af5bb74 to 646e2c8 Compare February 11, 2019 15:24
@tacaswell
Copy link
Member

I am 👍 on this, @efiring is responsible for getting this merged.

@ImportanceOfBeingErnest
Copy link
Member Author

Thanks for the review @efiring. I think I have now incoorporated all the requested changes.

I think it's good to have the API fixed for the case of the scatter (which is the most common usecase I assume). We can now still implement this for other collections. If you want I can already now put a NotImplemented into the general Collection.

@efiring efiring merged commit 93aadf5 into matplotlib:master Feb 12, 2019
@efiring
Copy link
Member

efiring commented Feb 12, 2019

Looks good, so I merged it. I don't think that putting the method into the Collection base would be good, because it probably makes sense to implement it only for some types of Collection. NotImplemented would imply that it should be implemented for everything, which I don't think is the case.

@jklymak
Copy link
Member

jklymak commented Feb 12, 2019

Thanks @efiring and thanks @ImportanceOfBeingErnest for your patience with this!

@tacaswell
Copy link
Member

🎉 I am glad this landed!

@ImportanceOfBeingErnest ImportanceOfBeingErnest deleted the legend-for-scatter branch August 2, 2019 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants