Skip to content
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]: FigureCanvasSVG has no attribute .get_renderer #26007

Open
raphaelquast opened this issue May 30, 2023 · 10 comments
Open

[MNT]: FigureCanvasSVG has no attribute .get_renderer #26007

raphaelquast opened this issue May 30, 2023 · 10 comments

Comments

@raphaelquast
Copy link
Contributor

raphaelquast commented May 30, 2023

Summary

Hey,

while investigating possibilities for svg exports with EOmaps I realized that the FigureCanvasSVG class does not expose the renderer via a .get_renderer() method but initializes the renderer in .pring_svg().

dpi = self.figure.dpi
self.figure.dpi = 72
width, height = self.figure.get_size_inches()
w, h = width * 72, height * 72
renderer = MixedModeRenderer(
self.figure, width, height, dpi,
RendererSVG(w, h, fh, image_dpi=dpi, metadata=metadata),
bbox_inches_restore=bbox_inches_restore)
self.figure.draw(renderer)

Is this intentional or is this a API inconsistency that should be fixed?

  • The same is also true for the pdf backend

Proposed fix

Implement .get_renderer() similar to the agg backend:

def get_renderer(self):
w, h = self.figure.bbox.size
key = w, h, self.figure.dpi
reuse_renderer = (self._lastKey == key)
if not reuse_renderer:
self.renderer = RendererAgg(w, h, self.figure.dpi)
self._lastKey = key
return self.renderer

@raphaelquast raphaelquast changed the title [MNT]: RendererSVG has no attribute .get_renderer [MNT]: FigureCanvasSVG has no attribute .get_renderer May 30, 2023
@anntzer
Copy link
Contributor

anntzer commented May 30, 2023

The implementation is currently private at Figure._get_renderer/backend_bases._get_renderer. It would be nice to decide once and for all where the public API should be -- I think putting that implementation as FigureCanvasBase.get_renderer, letting canvas subclasses override that with their normal implementation if needed (which is already the case e.g. for agg), would be normal.

(See also #13099, though.)

@jklymak
Copy link
Member

jklymak commented May 30, 2023

It's might help decide the right way forward to explain why one would need/want the renderer? Usually folks just switch to svg to print.

@tacaswell
Copy link
Member

get_renderer is a feature of the Agg backend family not of the base.

My recollection is that the vector backends stream their output so outside of the save code it is not clear to me that we can even give the user a Renderer is a usable state.

I would have to check the history, but I suspect get_renderer was added to support blitting which really only makes sense with the raster (Agg derived) backends.

@raphaelquast
Copy link
Contributor Author

raphaelquast commented May 30, 2023

Hey, thanks a lot for all the responses!!

In general I agree with @tacaswell that this primarily makes sense for blitting (which is exactly where I ran into this) and in 99% of the use-cases this will concern Agg derived backends...

Now, to answer the question of @jklymak on why one would need the renderer in the first place...
In EOmaps I somewhat abuse the blitting capabilities to create "multi-layered figures" to delay/avoid draws of very large datasets etc.

To do this, I handle drawing artists myself which is currently implemented (very roughly) like this:

import matplotlib.pyplot as plt
f, ax = plt.subplots()
ax.set_animated(True)
l, = ax.plot([0, 0.5, 1], [0, 0.5, 1], lw=20, animated=True)

def drawit(*args, **kwargs):
    renderer = f.canvas.get_renderer()
    
    l.set_color(plt.cm.prism(min(ax.get_xlim())))
    l.draw(renderer)
    for _, s in ax.spines.items():
        s.draw(renderer)

f.canvas.mpl_connect("draw_event", drawit)

This worked nicely for all my needs so far (including png, jpeg... export) but it fails on svg and pdf exports since there is no .get_renderer() for the svg/pdf canvas.

It might be that there is an entirely better way of implementing all of this (any insights are highly welcome!!) ... a general use-case concerning this issue would be:

  • how to trigger drawing an individual artist in the svg/pdf backend without relying on methods from the private API? (ax.draw_artist uses get_renderer renderer as well)

@tacaswell
Copy link
Member

When saving the animated attribute is ignored and all of the artists are re-drawn from scratch:

if not self.figure.canvas.is_saving():
artists = [
a for a in artists
if not a.get_animated() or isinstance(a, mimage.AxesImage)]

There is still the timing issue of when that will report true / false, when the draw_event callbacks fire, and when the backends are swapped back, but I suspect there is a working path through.

@raphaelquast
Copy link
Contributor Author

@tacaswell
thanks! this is why i explicitly use ax.set_animated(True) right now to completely avoid triggering the axes-draw cycle.
(I do this so that I can selectively cache backgrounds, avoid re-drawing complex artists, allow interactive comparison of cached layers and to have a fast-export of figures at the current-dpi)

Since for "svg/pdf" exports cached (agg-based) backgrounds are anyways irrelevant, a painless way to fix "svg/pdf" export would be to temporarily use ax.set_animated(False) to re-enable the ordinary axes draw cycle (and make sure only relevant artists are visible).... thanks for triggering this idea!

If there is in fact no way to ensure a meaningful object is returned by get_renderer for svg/pdf backends, feel free to close this issue if you think there's no point in discussing this further!

@tacaswell
Copy link
Member

But if you are in fig.savefig we ignore the state of a.get_animated() (unless you are doing really wild things....)

@raphaelquast
Copy link
Contributor Author

yes, but only for children of the axes in the ax.draw() call. However, if the whole axes is set to animated, ax.draw() is never triggered in the first place and so no artists (animated or not) of the axes are drawn at all. (not sure this is intentional, but that's what I use so far to avoid the default draw-cycle)

@tacaswell
Copy link
Member

Oh, that is a bug:

artists = sorted(
(artist for artist in artists if not artist.get_animated()),
key=lambda artist: artist.get_zorder())

That should be following the same rules as the filtering inside of Axes.

@raphaelquast
Copy link
Contributor Author

ok... that's unfortunate... 😅
please ping me in case you tackle this since it would have quite an affect on some of EOmaps functionalities
(I'll see to get this out of my code asap).

At the moment this was the most easy way I found to completely avoid the normal draw-cycle of an axes... is there a better way (preferably without subclassing Axes or Figure)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants