-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Update legend to automatically grow with marker_size #10765
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
…mensions method and helpers
@@ -241,7 +241,7 @@ class AnyObject(object): | |||
pass | |||
|
|||
|
|||
class AnyObjectHandler(object): | |||
class AnyObjectHandler(HandlerBase): |
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.
Isn't the idea of this example to show that you can use any object (hence the name) and do not actually need to subclass any existing object?
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 "AnyObject" is referring to the idea that you can pass any object to the legend as a handle. The Legend handler should still be a child of HandlerBase.
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.
No, this example is meant to show that you may use any object as handler as long as it implements the legend_artist
method. There also seems no reason to subclass HandlerBase
, why "should" it?
The next example then shows how to subclass HandlerBase
for the ellipse case. This is as intended, because it actually uses some of the parent's methods.
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 should subclass HandlerBase because the legend expects to be dealing with HandlerBase's. It should be enough to modify legend and HandlerBase to change handler functionality. That's why I made AnyObjectHandler a HandlerBase in the first place. I made Legend call a new function. It doesn't make sense to have legend interact with any arbitrary random object you want because it makes it more difficult to update in the future.
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 text above that example reads:
A custom handler can be implemented to turn any handle into a legend key (handles don’t necessarily need to be matplotlib artists). The handler must implement a “legend_artist” method which returns a single artist for the legend to use. Signature details about the “legend_artist” are documented at legend_artist().
Are you proposing to change this text as well? Otherwise this code example shows exactly what the text says; and it's good the way it is, because it allows people to easily grasp the concept.
I made Legend call a new function.
Maybe you shouldn't do that.
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 agree with @ImportanceOfBeingErnest , this is showing off duck typing (we try to use isinstance
as rarely as possible).
If this PR is expanding the required API for things that can be handlers, then that should be documented as such.
In general I have a problem understanding this PR. Seen from the tests, this would result in the normal code
suddenly producing a huge legend in the plot. Shouldn't it be the other way around, namely that arbitrarily large or small markers have a normalized size in the legend? At least that would actually be the desired way to create a legend - as far as I see it from a user's perspective. Or even better, to allow to choose whether markersize should be adapted or not, i.e. adding a possible |
I can’t tell from the images above what the change is; they look the same to me. @ImportanceOfBeingErnest I sort of agree w your comment above, except, that’s potentially harder than what’s done here, and I imagine we can all agree that legends that overspill their borders are undesirable. But the issue w normalization of marker sizes is that they are often used to differentiate data. I.e. a marker size of 30 could get a different legend entry than a marker size of 40. So I think normalaization would not work for many folks. Some sort of shrink factor might make sense in some cases, but legends are already a mess of kwargs so I’m not really seriously suggesting that. |
@jklymak The one being highlighted in the PR is just one of the old regression tests that broke due to these changes. The real changes are shown in the new tests added to test_legend.py, where the legend box is now being resized while previously the markers would go off-bounds. |
it seems like we messed up with the images , changing it. |
... indeed there already is a |
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.
A couple of small things... This looks good to me. Not sure if it breaks anything, but looks like the tests are happy, so if it does, then its something we aren't testing.
lib/matplotlib/legend_handler.py
Outdated
@@ -88,6 +88,58 @@ def adjust_drawing_area(self, legend, orig_handle, | |||
height = height - self._ypad * fontsize | |||
return xdescent, ydescent, width, height | |||
|
|||
def scale_dimensions(self, legend, width, height, orig_handle): |
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.
Should this be private?
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.
scale dimensions is called by legend so it's public.
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.
We can call private methods from w/in the rest of Matplotlib. I don't think there is any stricture that private means module-only. I don't see any reason for this to be part of the public API (i.e. appear on the webpage).
lib/matplotlib/tests/test_legend.py
Outdated
|
||
|
||
@image_comparison(baseline_images=['legend_large_markers'], | ||
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.
Ooops, somehow managed to lose this comment: These should be style='mpl20' if possible. You need the text, but you could clear the x and y ticks to reduce the amount of text. We are trying to make image tests have less text to make it easier for them to pass different FreeType versions.
@jklymak My point is that this PR seems to automatically change the legend size depending on the size of the markers. This is often not desired. Suppose e.g. having two subplots both with a legend and suddenly one of those legends is bigger - this would only be acceptable if an option exists to revert that and have both legends the same size independent on the markersize. On the other hand we currently do have the option to change the line height etc in the legend, so using large markers would just require to tweak those parameters a bit. If I set a certain |
So would it maybe make sense to have |
Yes, |
The fix is not affecting the labelspacing. It is changing the size of the DrawingArea the handles are drawn in. In the test that has been changed, the legend looks like there is more labelspacing because in the original version, the artists were spilling over into the padding between DrawingAreas. |
Currently, setting handlelength and handleheight can be used to grow the DrawingArea. I can add a normalize flag and add code for scaling down artists but how should I handle that? If there is a size 50 marker and a size 10 marker should they both be scaled down by the same factor or should they be scaled independently to both fit the DrawingAreas? |
There needs to be the option to have the same distance between legend labels, independent on how large the marker size is. So for a given Btw. Downscaling the markers can currently be done using |
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 additional API on handler objects should be optional so that any existing third-party handler classes that do not inherit from HandlerBase continue to work.
lib/matplotlib/legend_handler.py
Outdated
enough to fit. | ||
|
||
''' | ||
if isinstance(orig_handle, Line2D): |
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 way to handle this without isinstance
?
Do we actually need the additional API? Can we not infer the size of the handle (e.g., by reusing the tight_bbox code) and directly use that? |
The new tests are showing the symbol labels in the legend aligned with the bottom of the symbol. Wouldn't most people want the labels to be horizontally aligned with the center of the symbol? In addition, I'm wondering about the cost/benefit tradeoff here. It is adding code and complexity to the already complex legend; is it really worth it? |
Yes they would. I could change the alignment to center but then I would need to replace every image comparison test image with a legend it because the handles would be drawn a few pixels away from where they are now. |
Given that the change to centering is required for your other change to yield the desired result, I don't see any way to avoid it. Even for the less extreme example in your PR summary, the alignment looks wrong. I also wonder whether the default pad should be reduced. Or, getting back to my second point in my earlier comment: how important is it to have this automatic machinery, as opposed to leaving it to the user to handle larger symbols by manually increasing the padding? Will it have unintended consequences? Will eventually handling the scatter case make the legend code even more unwieldy? |
My opinion is that:
|
@efiring handling scatters would just require overriding the handle_width/height methods in the correct legend handler. The methods are intended to be overridden for different handle types if necessary so it's not too unwieldy. |
This PR seems rather independent on I think it is mostly agreed that the automatic change should be optional or at least allow to be turned off. The only way I see this happening is to either
|
Sorry forgot about rcParams. I’d have rcParams set both to None and use the auto spacing be default and recover the old behaviour with the old params. But that’s just my vote. In general I don’t think we should have overlapping text and symbols if we can help it and the resulting code isn’t ridiculously convoluted. |
I can add a
|
Indeed, that is basically the comment I made already, but with I think one would add this Since all rcParams are lower case, one would go with Adding an rcParam would be done in the |
I think the alignment change is needed, and helps even without your other changes. Maybe the thing to do is use the opportunity to update the affected tests to our current style mode instead of classic mode. Let's see what others say, though. |
I don't think you need the new kwarg or rcParam, and I think adding a new one is more confusing than adding yet another kwarg controlling the vertical spacing of legend elements. i.e. by default you set I actually think its unfortunate that there is both a |
Yes if handleheight. is none, then the automatic algorithm is used and whatever value is needed for handleheight in the code is hard coded. I don’t see why it’s needed at all if we automatically determine the height of the handle but it certainly need not be a variable or if it does you will have to walk me through it Thanks for explaining the technical diff between label spacing and handle height. What an unfortunate pair of names. Hence why I’m being a bit of a pita about the API here. I agree that this variable need not be part of this PR. But I still do not think that he height and width of the handles should have three kwargs controlling their size. |
I would agree to that if "the algorithm" was a true optimization in both directions. However, as it is now it's only an expansion in one direction in case the marker is larger than the |
The "algorithm" does expand both width and height. |
Sorry, by "direction" I mean of course that it only expands - it does not shrink. |
Adding yet another confusing kwarg to legend would be a step backwards compared to having the code do the right thing via a simple API. Right now a legend entry needs a certain Ideally, the "automatic" height should be the larger of the text height and the marker height. If the user wants to overide this with a smaller or larger height, they should be welcome to, using non- |
Using the font size as the default from which to potentially enlarge the What will determine the "standard" |
Again, I don't see a fundamental problem with an autoscale in the horizontal, unless the user specifies a |
Thanks a lot for the contribution. I'm going to close this as having had no action for quite a while. OTOH, if there is still a community that wants this, please feel free to re-open, with my apologies! |
PR Summary
Legend used to not grow with increasingly large marker sizes, resulting in large markers flowing out of the legend.
Note: This fix results in an image comparison test failing; this is a result of the legend now being drawn differently and the image drawn now is slightly larger.
This is the original image:

This is the updated image:

Issue #8705
PR Checklist