Repository navigation
Description
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()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)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()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.



Activity
timhoffm commentedon May 10, 2020
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 commentedon May 10, 2020
So there are very few things that a
Figuretakes care of that are not layout related. So far, I think its just_axstack(for pyplot),dpi,canvas, anddpi_scale_transare specific to the top-levelFigure. In terms of methods,get/set_figwidth/height,set/get_dpi,savefigand a few others to do with keyboard input. So sure the thing I'm calling aSubFiguredoes not have aset_dpi()because we can only have on DPI.Thats exactly what this does. The sub-figures can have their own sub figures. If we want to call it something else, thats fine.
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
Figureclass, and in my work so far, it is entirely back compatible with the existingFigure.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_gridsituation. I don't think we are going to get to a situation where, instead of sayingfig.suptitlewe sayfig.layout[0].suptitleAlternatively, we could tear out the guts of all the layout logic in
Figureand point the relevant methods inFigureto 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 commentedon May 11, 2020
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.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 commentedon May 11, 2020
So right now I have
FigureBase(Artist),Figure(FigureBase)andSubFigure(FigureBase), where the classes implement anything that needs to be different at each level, andFigureBaseis just figure-level artist methods (add_axes,subplots,suptitle, etc).tacaswell commentedon May 11, 2020
I'm with with @timhoffm , calling this
SubFigureis extremely confusing to me as the extra things that aFiguredoes (owns the entry point to the draw tree, the size, and the dpi) are to my mind are the core of what aFigureis. If I see the wordFigurein 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:
Words that I think of off-limits:
jklymak commentedon May 11, 2020
I'm fine calling it
panelor something, but I'd still argue it has to share its methods with the top levelFigure. If folks would be happier withPanelBase(Artist)andFigure(PanelBase),Panel(PanelBase)I'd be fine with that. Then the calls above would befig.subpanels(2, 1),fig.add_subpanel(gs[0, 1]), etc (and of coursesubpanel.add_subpanel()for recursive nesting.)jklymak commentedon May 11, 2020
I guess the other thing that maybe should be clear is that
subpanel.transFigis 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 thentransFigcould 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 wheretransFigis used.anntzer commentedon Jan 26, 2021
@jklymak I guess this was closed by #18356? Feel free to reopen if I missed something.