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

Proposal: add_subfigs.... #17375

Closed
jklymak opened this issue May 9, 2020 · 8 comments
Closed

Proposal: add_subfigs.... #17375

jklymak opened this issue May 9, 2020 · 8 comments
Labels
New feature topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Milestone

Comments

@jklymak
Copy link
Member

jklymak commented May 9, 2020

In #14421 there was some concern that gridspec was being overloaded and that something else should be doing that layout.

I've mocked the following up, and it works quite well so far. Getting past mockup will be a fair bit of work, so wanted some comment.

For sub-grids, currently we would do:

fig = plt.figure()
gs = fig.add_gridspec(2, 1)

axs0 = gs[0, 0].subgridspec(3, 2).subplots()
axs1 = gs[1, 0].subgridspec(3, 2).subplots()

demo1

Which is fine, for what it is. But imagine you want to have a colorbar that only pertains to gs[0, 0] or a legend just for that gridspec, or a suptitle. Currently we would have to add all that to gridspec. But as @timhoffm pointed out, gridspec is not really equipped for this sort of thing.

Here instead I propose we create a new base class for Figure, say FigureBase and make Figure a subclass of that, and SubFigure a subclass of that.

FigureBase contains all the layout-specific functions of Figure and all the figure-level artists (figure legends, figure colorbars, suptitle, future supx/ylabel, etc). FigureBase then gets a new method subfigures and add_subfigure, which give a virtual figure within the current figure.

Now, instead of the above, we write:

fig = plt.figure()
sfigs = fig.subfigures(2, 1)
axs0 = sfigs[0, 0].subplots(3, 2)
axs1 = sfigs[1, 0].subplots(2, 2)

demoNew

You can also do add_subfigure using subplotspecs

fig = plt.figure()
gs = fig.add_gridspec(2, 2)
sfig = fig.add_subfigure(gs[:, 1])
axs0 = sfig.subplots(3, 2)

ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0])
plt.show()

demoNew2

The subfigs should be API-wise the same as a normal figure. And a subfig can then create subfigs. The subfigs keep track of their parent fig, and the parent keeps track of its subfigs, so traversing the tree is straight forward.

I think this is cleaner than overloading gridspec. Its semi-major surgery on Figure, but I've already implemented the above without too much pain. The layout is all achieved relatively painlessly by having a second transform with the parent so that if x0, y0 etc are in the parent's co-ordinates the subfigure has a transFigure given by:

        self.bbox_relative = Bbox.from_bounds(x0, y0, widthf, heightf)
        self.bbox = TransformedBbox(self.bbox_relative,
                                    self._parent.transFigure)
        self.transFigure = BboxTransformTo(self.bbox)

Anyway, wanted to throw this out there for comment or thoughts. This seems a potentially cleaner way to arrange hierarchies of subplots.

@jklymak jklymak added the topic: geometry manager LayoutEngine, Constrained layout, Tight layout label May 9, 2020
@timhoffm
Copy link
Member

Thanks for thinking this further.

Maybe it's just the terminology, but basing everything on a Figure inheritance hirarchy seems be too restrictive or impose undesired side-effects (e.g. if SubFigure inherits from Figure, can it have it's own dpi ?!?).

I think of the layouting as a hirarchy of containers. Each container can contain further containers and artists. The container can decide how to distribute it's space among the contained elements. There is no explicit "sub"-anything, because the structure is recursive: The figure has a top-level container that spans the whole figure. In the simplest case, that could contain two Axes, but it could alternatively also contain more containers, that contain containers, ... , that contain an Axes, or a legend or ....

I would design these containers around new classes, not inheriting from figure, subplot gridspec, etc. And only investigate afterwards how subplots and gridspecs can be mapped into this concept. Starting from scratch gives the freedom to come up with a clean an generic concept. OTOH I'm convinced if that is made flexible it can mimic subplots and gridspecs.

@jklymak
Copy link
Member Author

jklymak commented May 10, 2020

Maybe it's just the terminology, but basing everything on a Figure inheritance hirarchy seems be too restrictive or impose undesired side-effects (e.g. if SubFigure inherits from Figure, can it have it's own dpi ?!?).

So there are very few things that a Figure takes care of that are not layout related. So far, I think its just _axstack (for pyplot), dpi, canvas, and dpi_scale_trans are specific to the top-level Figure. In terms of methods, get/set_figwidth/height, set/get_dpi, savefig and a few others to do with keyboard input. So sure the thing I'm calling a SubFigure does not have a set_dpi() because we can only have on DPI.

I think of the layouting as a hirarchy of containers. Each container can contain further containers and artists. The container can decide how to distribute it's space among the contained elements. There is no explicit "sub"-anything, because the structure is recursive: The figure has a top-level container that spans the whole figure. In the simplest case, that could contain two Axes, but it could alternatively also contain more containers, that contain containers, ... , that contain an Axes, or a legend or ....

Thats exactly what this does. The sub-figures can have their own sub figures. If we want to call it something else, thats fine.

I would design these containers around new classes, not inheriting from figure, subplot gridspec, etc. And only investigate afterwards how subplots and gridspecs can be mapped into this concept. Starting from scratch gives the freedom to come up with a clean an generic concept. OTOH I'm convinced if that is made flexible it can mimic subplots and gridspecs.

One of my goals is for this to be back-compatible with the existing API. If we were to start over we might do things differently. To be back compatible, the highest level "container" is the Figure class, and in my work so far, it is entirely back compatible with the existing Figure.

If we want to break back compatibility, that would be fine, but I don't see how or when that would be accepted into Matplotlib, and I forsee another axes_grid situation. I don't think we are going to get to a situation where, instead of saying fig.suptitle we say fig.layout[0].suptitle

Alternatively, we could tear out the guts of all the layout logic in Figure and point the relevant methods in Figure to the top-level layout object, but that seems to be the same thing to me with a lot of extra boiler plate to keep track of.

@timhoffm
Copy link
Member

So sure the thing I'm calling a SubFigure does not have a set_dpi() because we can only have on DPI.

Then, the inheritance hirachy is not FigureBase -> Figure -> SubFigure. If you want to add the layout semantics via inheritance, it's probably better to add that via a mixin.

Alternatively, we could tear out the guts of all the layout logic in Figure and point the relevant methods in Figure to the top-level layout object, but that seems to be the same thing to me with a lot of extra boiler plate to keep track of.

That's approximately the direction I'm thinking of. It provides a more clean separation beween layouting and plot elements such as figure and subplots. In the long run, I consider this to be more flexible and in particular also simpler to maintain than mangling the layout with (sub)figures. I would use composition and rather than inheritance for adding the layout capability.

I don't have the time right now to dig into this, but would in principle be willing to work on a prototype for the concept.

@jklymak
Copy link
Member Author

jklymak commented May 11, 2020

So right now I have FigureBase(Artist), Figure(FigureBase) and SubFigure(FigureBase), where the classes implement anything that needs to be different at each level, and FigureBase is just figure-level artist methods (add_axes, subplots, suptitle, etc).

@tacaswell
Copy link
Member

I'm with with @timhoffm , calling this SubFigure is extremely confusing to me as the extra things that a Figure does (owns the entry point to the draw tree, the size, and the dpi) are to my mind are the core of what a Figure is. If I see the word Figure in the class name I expect those to be there.

Which is a long way of saying, I am 👍 💯 on the concept. I think that a nice "middle" container is something we are missing, however I don't think we can use the word "Figure" in it's name (which is independent of implementation details). But, as we are painfully aware, getting the names wrong can cause a lot of issues later so should put in the effort to get this name right!

Proposed names:

  • panel
  • region
  • layout

Words that I think of off-limits:

  • figure
  • canvas
  • axes
  • plot

@tacaswell tacaswell added this to the v3.4.0 milestone May 11, 2020
@jklymak
Copy link
Member Author

jklymak commented May 11, 2020

I'm fine calling it panel or something, but I'd still argue it has to share its methods with the top level Figure. If folks would be happier with PanelBase(Artist) and Figure(PanelBase), Panel(PanelBase) I'd be fine with that. Then the calls above would be fig.subpanels(2, 1), fig.add_subpanel(gs[0, 1]), etc (and of course subpanel.add_subpanel() for recursive nesting.)

@jklymak
Copy link
Member Author

jklymak commented May 11, 2020

I guess the other thing that maybe should be clear is that subpanel.transFig is needed to make everything magically work with the minimum of fuss. i.e. subpanel.add_axes([0.5, 0.5, 0.3, 0.3]) places the axes in the subpanel's reference frame, not the parent figure's reference frame.

I suppose its possible we could go the route of making a different transform (i.e. transPanel) that gets used by default and then transFig could stay the figure-level transform. That moves the changes to more files, but I just did a search and there are not actually that many places where transFig is used.

@anntzer
Copy link
Contributor

anntzer commented Jan 26, 2021

@jklymak I guess this was closed by #18356? Feel free to reopen if I missed something.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
New feature topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Projects
None yet
Development

No branches or pull requests

5 participants