Skip to content

Walk the artist tree when preparing for saving with tight bbox. #14216

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
merged 1 commit into from
May 24, 2019

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented May 14, 2019

PR Summary

Closes (I think?) #14180 and #14134 (comment).
@astrofrog @pllim can you confirm?

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 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)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@anntzer anntzer added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label May 14, 2019
@anntzer anntzer added this to the v3.2.0 milestone May 14, 2019
@tacaswell
Copy link
Member

At this point my may as well just let print_method fully run in the line above?

@anntzer
Copy link
Contributor Author

anntzer commented May 14, 2019

So what's the patch you propose?

@astrofrog
Copy link
Contributor

I'll test this out tomorrow!

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'm not convinced we should fix this like this. AstroPy is overriding Axes.get_tightbbox but not doing all the things Axes.get_tightbbox does. I think the onus is on AstroPy to inherit get_tightbbox more robustly, not for us to sledgehammer the issue with an extra draw to the detriment of the vast majority of cases that don't need this.

If there is something we can do to make it easier for AstroPy to customize get_tightbbox to their needs, we should definitely try and do that. But I think if at all possible they should call super().get_tightbbox to make sure they stay current with us.

@astrofrog
Copy link
Contributor

@jklymak - I'd be fine with fixing Astropy - essentially what we need is a way to provide extra bounding boxes of plot items that need to be included in the calculation of the final bounding box. I guess one way would be to simply call Axes.get_tightbbox and then do a union of that and our extra bounding boxes?

@jklymak
Copy link
Member

jklymak commented May 14, 2019

@astrofrog - that'd work; but again, Axes.get_tightbbox now walks the artists in the axes (it didn't pre 3.0), and includes them all in the bbox, so maybe your artists are already included? If not, you can always call super.get_tightbbox(bbox_extra_artists=listofartists), though that will only include artists you specifically include.

BTW, since we were effectively doing the draw before with dry_run, maybe this PR should go in as we may have broken other folks as well, and doing the extra draw won't be slower than what we had before...

@astrofrog
Copy link
Contributor

astrofrog commented May 14, 2019

Ok so I've dug a little deeper into what we do in Astropy, and there are two separate things here - yes we can definitely simplify our get_tightbbox to just inherit the one from Axes then modify the BBox accordingly (and I'll do that shortly), but there is still an issue, which is that get_tightbbox only gets called before drawing (which arguably is sensible)

In WCSAxes we dynamically draw e.g. the tick labels by setting up a single Text instance and when WCSAxes.draw gets called we effectively modify the position of the text, draw it, modify the position, draw it again, and so on. Note that we don't add the artist to the axes, we just call artist.draw ourselves. At that point we also record the bbox of the drawn text, and combine it in get_tightbbox. However, as you can see from the following example, get_tightbbox now only gets called before draw, so that the extra bbox is never taken into account:

from matplotlib.axes import Axes
from matplotlib.text import Text
from matplotlib import pyplot as plt


class MyAxes(Axes):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.text = Text(0.5, 0.5, 'Text outside axes',
                         transform=self.transAxes, clip_on=False,
                         figure=self.figure, ha='center', va='center')
        self._extra_bbox = None

    def draw(self, renderer, *args, **kwargs):
        print('MyAxes.draw')
        result = super().draw(renderer, *args, **kwargs)
        self.text.set_position((-0.1, 0.5))
        self.text.draw(renderer)
        self._extra_bbox = self.text.get_window_extent(renderer)
        return result

    def get_tightbbox(self, *args, **kwargs):
        print('MyAxes.get_tightbbox')
        result = super().get_tightbbox(*args, **kwargs)
        if self._extra_bbox is None:
            return result
        else:
            return result.union(self._extra_bbox)


fig = plt.figure()
ax = MyAxes(fig, [0.1, 0.1, 0.8, 0.8])
fig.add_axes(ax)
print(ax.figure)
fig.savefig('test.png', bbox_inches='tight')

In this case, the label is truncated at the left of the window rather than having the bounding box be adjusted. The reason we dynamically draw the text like this is that we don't know in advance how many tick labels will be drawn and we don't want to keep adding/removing artists from the axes.

The only fix I can think of without changing Matplotlib would be for us to have some kind of 'dry run' on the code that draws the artists, and only collects their bboxes but doesn't draw them. This would then get called inside get_tightbbox. Is that the only reasonable fix here?

@jklymak
Copy link
Member

jklymak commented May 14, 2019

OK, so you can't position your text until draw time? But can you extract that (self._position_text) and then call that in get_tightbbox as well? Thats basically what we do in _apply_aspect, which you see is called in both draw and get_tightbbox.

Its possible we should re-architect this somehow.

Anyway, for now I guess I'm OK w/ this PR as a fix to get us back where we were.

@tacaswell
Copy link
Member

doing the extra draw won't be slower than what we had before

However, backends that did do something with dry_run you may see a performance hit (no idea how big that actually is or which backends actually implemented the short-cut).

@anntzer
Copy link
Contributor Author

anntzer commented May 16, 2019

Let's assume that print_foo must walk the artist tree twice when preparing to save with tight bbox (that's the origin of this discussion). The way print_foo is implemented is basically always

  • prepare stuff before walking the tree (e.g., write the SVG preamble) and create a renderer object
  • walk the tree (figure.draw(renderer))
  • do some stuff after walking the tree (e.g., sending the buffer to Pillow to convert to jpg, running a postscript distiller, etc.)

With #14134, when doing the "dry_run" call, we would only do the preparation to extract out the renderer object. With this PR, we do the preparation and walk the tree, but still skip the post-walking step.

What did backends do to optimize dry_run? The only optimizations right now are

  • backend_ps replaces the output buffer by a null writer object (such that buf.write = lambda *args, **kwargs: None)
  • backend_pgf patches out all drawing methods from the renderer object (renderer.draw_path = lambda *args, **kwargs: None; same for draw_image, etc.), which is strictly better (I don't think writing the (short) SVG preamble to a memory buffer really matters for performance anyways).
    It looks like we could actually just move the backend_pgf optimization to backend_bases (into _get_renderer, which could be called _get_nondrawing_renderer); then all backends (matplotlib's own and third-parties) would benefit from it instead of just backend_pgf.

Copy link
Contributor

@astrofrog astrofrog left a comment

Choose a reason for hiding this comment

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

This fixes Astropy's tests - I do still plan to try and make it so that get_tightbbox returns sensible results even without requiring Matplotlib to walk the draw tree first, but it will be good to have this fix in in the mean time.

@pllim
Copy link

pllim commented May 24, 2019

If @astrofrog is happy with this, I am happy too. I am looking forward for this being in dev sooner than later, so we can un-xfail two of our tests over at astropy. 😄

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.

5 participants