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
MNT: Don't require renderer for window_extent and tightbbox #22745
Conversation
a4ca2ca
to
bc93a9e
Compare
lib/matplotlib/figure.py
Outdated
@@ -2430,6 +2436,14 @@ def axes(self): | |||
|
|||
get_axes = axes.fget | |||
|
|||
def _get_cached_renderer(self): |
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 is the main new (private) method. Users should generally not need to fetch the renderer going forward...
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 find the naming a bit confusing. I'd suggest just _get_renderer(self)
, the caching should be an implementation detail. The if self.cachedRenderer is not None
seems like it should be implemented in the self.canvas.get_renderer()
method (and indeed, there is some logic to re-use the Agg renderer for this)
matplotlib/lib/matplotlib/backends/backend_agg.py
Lines 415 to 425 in 7b2d828
def get_renderer(self, cleared=False): | |
w, h = self.figure.bbox.size | |
key = w, h, self.figure.dpi | |
reuse_renderer = (hasattr(self, "renderer") | |
and getattr(self, "_lastKey", None) == key) | |
if not reuse_renderer: | |
self.renderer = RendererAgg(w, h, self.figure.dpi) | |
self._lastKey = key | |
elif cleared: | |
self.renderer.clear() | |
return self.renderer |
3c95c69
to
cbf807e
Compare
Hmm, for some reason |
I think that for internal calls, if we have the renderer we should pass it through rather than relying on the cache to implicitly pass it to something deep in the call stack if it is needed. |
Maybe? However, a lot of the time we are only fetching the renderer to pass it through. |
Would it be possible to use |
@greglucas I think that's what the new The point here is that all the artists know what their figure is, so if they need the renderer, they can just ask their figure for it. However, it seems a considerable simplification not to ask users to get the renderer. |
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.
Overall I think this is a nice simplification for users to have the renderer sorted out "under the hood" for them rather than having to explicitly pass in a renderer. Just a few comments wondering about where the lines are drawn for when the renderer gets requested (get_tightbbox, window_extent, somewhere else...) and which object/method's job it is to keep track of the caching.
lib/matplotlib/_tight_layout.py
Outdated
@@ -199,15 +199,7 @@ def auto_adjust_subplotpars( | |||
|
|||
|
|||
def get_renderer(fig): |
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 you remove this entirely? It looks like this is only used within LayoutEngine
now, so could be replaced there.
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.
Yep
lib/matplotlib/axes/_base.py
Outdated
if renderer is None: | ||
renderer = self.figure._get_cached_renderer() |
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 reason to put this inside get_tightbbox()
versus get_window_extent()
? It looks like further down you put it inside the get_window_extent()
call, so I wasn't following where this is needed versus where None
could be passed through...
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 locator needs the renderer, however, its debatable whether we should remove that as well.
lib/matplotlib/figure.py
Outdated
@@ -2430,6 +2436,14 @@ def axes(self): | |||
|
|||
get_axes = axes.fget | |||
|
|||
def _get_cached_renderer(self): |
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 find the naming a bit confusing. I'd suggest just _get_renderer(self)
, the caching should be an implementation detail. The if self.cachedRenderer is not None
seems like it should be implemented in the self.canvas.get_renderer()
method (and indeed, there is some logic to re-use the Agg renderer for this)
matplotlib/lib/matplotlib/backends/backend_agg.py
Lines 415 to 425 in 7b2d828
def get_renderer(self, cleared=False): | |
w, h = self.figure.bbox.size | |
key = w, h, self.figure.dpi | |
reuse_renderer = (hasattr(self, "renderer") | |
and getattr(self, "_lastKey", None) == key) | |
if not reuse_renderer: | |
self.renderer = RendererAgg(w, h, self.figure.dpi) | |
self._lastKey = key | |
elif cleared: | |
self.renderer.clear() | |
return self.renderer |
Changed the name to |
cbf807e
to
24d92e1
Compare
Marking as "Needs discussion" because I think there are ideas to go the other way here and make Artists more independent of the figure. I guess if that is really going to happen, then we should think about this. But I'm unconvinced the piping necessary to make Artists portable between figures is worth the considerable complication. |
@tacaswell I doubt I can attend the weekly meeting (travelling), but feel free to discuss this and decide whether we think we will really ever disentangle artists from the figure that owns them. |
FWIW, note that right now some artists can end up being drawn without knowing who their parent figure is, e.g. the Perhaps we may consider that a bug and state that if you set up artists manually, it is your responsibility to make sure that they know their parent figure, but that would probably need to be documented. |
I guess I'd consider it a bad idea that when we add them to the figure they do not get the figure info added to them. But we can do that at the add_artist time. |
Following up on this, yes, I think this is a bug. The example even says that the results are dpi dependent - I don't understand how this is even supposed to work if the artist doesn't know its figure. |
Sure. If you don't want to fix that example here, can you open a separate issue to track that problem? I think we also need to state explicitly somewhere in the docs (and changelog) that artists must have their .figure attribute set. |
I take it back about that being a bug. Whether an artist has a parent figure or not depends on the artists, as you point out. This PR doesn't really worry about that - it is not trying to force all artists to not require a renderer for their get_window_extent, its just trying to make the argument optional for those artists that already have enough info to not need the renderer. In the example you provided of BboxImage, it requires a renderer, and indeed in the fallback in BboxImage doesn't work (it calls Note that the layout managers, where the extents are mostly used internally, won't drill down beyond the artists in the axes. Downstream libraries don't need to use this simplification if they don't want (they can continue to pass the renderer). The only disadvantage is a bit of inconsistency for users knowing whether an artist needs the renderer passed or not. However, most folks asking for the bbox are presumably doing something semi complicated, and should be able to handle the inconsistency (or just always pass renderer if in doubt). |
My understanding of how we got here is:
I think that the source of the bugs here was that anything was accessing a cached renderer and my instinct would be to go the other way and try to remove the cache ("There are are two hard problems in computer science: naming things, cache invalidation, and off-by-one bugs"). However, it is probably too late for that (as we have long had the cache and it is useful even if it gives the users a foot-cannon) so centralizing it is a net good, however we should view this cache as for the users not for us to use internally. I think it is much better for users to use our cache rather than keep their own cached renderer (which is definitely going to go out of sync because we do not know about it to invalidate it). One thing done in this PR with I'm very 👎🏻 on is to then rely on this central cache for internal operations. I think it bakes too many assumption that a (should be optional) cache is definitely there and that the reverse connections up the Artist tree are there. I am also concerned that this is a private method so that down-stream library should probably not use it. On a bit of a philosophical note, mpl as three layers the chrome on top ( I think the medium term program we are currently engaging in is to start to tease apart all of this state into parts that are clearer and easier to reason about. One place where we get ourselves into consistent trouble is that we have lots of circular references: I think that we should not change the signature of any of the methods, but allow The arguemnet here is is it "simpler" to do def foo(obj):
a = obj.a
....
my_obj.a = 'a'
foo(my_obj) or def foo(obj, a):
...
foo(my_obj, 'a') I would argue that in cases where In this case I have the position 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 left a long essay. Anyone can dismiss this.
Thanks @tacaswell for your thoughts. This PR does two things:
So far as we can tell, we only explicitly need WRT whether we should stash |
c7ad89d
to
b352c5c
Compare
Latest commit reverts the changes in |
FWIW I have been wanting to make renderer optional for a long time too (because threading it through layers and layers of calls just so that texts can know their extents is a bit annoying). While I also agree with @tacaswell's point that cache invalidation is tricky, note that the question here is about the invalidation of |
"""Return lists of bboxes for ticks' label1's and label2's.""" | ||
if renderer is None: |
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 don't think you need to normalize here; just let the labels do it?
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.
That depends not he strategy we want to employ - it could wait to normalize until the end, but this does save a bunch of calls to _get_renderer
.
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.
Fair.
@tacaswell, can you reassess now that the internal uses have been cleared up. Note that this also cleans up our internal definition of the renderer to one location. |
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 am on board with this 👍🏻
b352c5c
to
5824124
Compare
5824124
to
24b1680
Compare
PR Summary
This PR is a proof of concept of making renderers globally optional. We don't need to use this in
tight_layout
because it has a funky_draw_disabled
context. However, constrained_layout can have all the renderer tracking pulled out and still works in the tests and interactively so far as I can tell.See #22744 for more discussion, but I think deciding on the renderer at the Figure level is the right thing to do, and stop trying to sort it out at a higher level.
See also:
PR Checklist
Tests and Styling
pytest
passes).flake8-docstrings
and runflake8 --docstring-convention=all
).Documentation
doc/users/next_whats_new/
(follow instructions in README.rst there).doc/api/next_api_changes/
(follow instructions in README.rst there).