| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
I am interested in the specifics of how the legend will be constrained. Perhaps through an easily extensible mechanism that can be used to add other AnchoredOffsetBox objects around axes and around gridspecs. Also a mechanism for adding arbitrary text around the axes or gridspecs. This would be hand in adding captions to plots. |
Sorry, something went wrong.
|
@has2k1 Thanks. I want to make sure I understand the desired behavior. We are getting into use cases I haven't ever needed for my personal plotting needs, so this is where I need help with what people envision. Are you hoping that if I call ax.legend(loc='center left', bbox_to_anchor=(1., 0.5)) then the layout manager will automatically make the axis object to accommodate the legend inside the subplotspec? But it should also accept ax.legend(loc='center left', bbox_to_anchor=(0.5, 0.5))? I think thats doable, but I need to think about how that gets implemented. Right now I just implement nested layouts and adjacent layouts w a pad. Arbitrarily displaced in axes co-ordinates is doable, but not implemented yet. I don't see why a subplotspec can't contain more than one child that also have displacements relative to their widths. I need to dig into offsetbox to see how to make it extensible to other offset boxes. Can you point me to an example of other uses beyond legend so I can test? |
Sorry, something went wrong.
|
share_x and share_y are a bit complicated. tight_layout differentiates between space between the subplots and the space around all the subplots (i.e. h_space and left). Here I don't, I just have left_margin and right_margin and they are the same for all subplots. So constrained_layout doesn't cover the often-used case of just labelling the outside rows and columns (it works, but it introduces a lot of extra white space between subplots). As far as I can see share_x and share_y are only accessible via figure.subplots(). So, this isn't a problem for general gridspec-derived axes added with figure.add_axes. See offsetbox.VPacker and offsetbox.HPacker for ideas. |
Sorry, something went wrong.
Yes.
Yes again. But I fear it would make the code complicated as it would mix two layout mechanisms. There are already four parameters to AnchoredOffsetbox that control the location, namely;
And so it seems like AnchoredOffsetbox was designed in an environment that lacked a layout manager. With a layout manager in place, perhaps a new ConstrainedOffsetbox can also serve as a container for other Offsetboxes. But this course of action would conflict with the first question above. i.e does the legend still use AnchoredOffsetbox and never be accommodated inside the subplotspec? Or can it be changed to use the ConstrainedOffsetbox, with appropriately deprecated parameters? I have no good answer. Plotnine extensively [1] uses offsetboxes for the legends and it has very finicky parts. Here is a simplified version that demonstrates a custom legend system similar to the one in plotnine. |
Sorry, something went wrong.
|
@has2k1 Thanks a lot for the examples. That helps. I think it'll be possible to have AnchoredOffsetbox (or a modified version) behavior the way you'd like. I can tackle this once I get sharex and sharey to work. |
Sorry, something went wrong.
|
@has2k1 this now makes space for legends. Since it actually queries the underlying _legend_box, which is an AnchoredBox, this is easily extensible to other cases. However, given that anchoredBox objects usually seem to be buried as implementation detail in other classes, my guess is the best thing is to have the checks on those classes rather than AnchoredBox directly. i.e. Legend isn't an AnchoredBox, but it uses one for its layout. See layoutbox.get_axall_tightbbox for the logic here. legend is a bit flakey as positions get set at the draw sequence, so for perversely large legends a resize is needed to make the legend fit properly. However, I think the method often yields a decent result. |
Sorry, something went wrong.
| for child in ax.get_children(): | ||
| if isinstance(child, Legend): | ||
| bboxn = child._legend_box.get_window_extent(renderer) | ||
| bbox = transforms.Bbox.union([bbox, bboxn]) |
There was a problem hiding this comment.
I think this would be more specific in sniffing out the legend type stuff.
possible_legends = ax.artists + [ax.legend_] if ax.legend_ else []
for child in possible_legends:
if isinstance(child, (Legend, AnchoredOffsetbox)):
...However, overall this is a neat little function that can bundle up all types of adornments to the axes (at the risk of complicating it).
Sorry, something went wrong.
I have encountered a related issue when creating an Animation class for plotnine. It may complicate animations, but on the whole specified figure sizes that are just rough estimates final sizes are a worthy price for a good layout. After all, tight_layout() has been an offender already. |
Sorry, something went wrong.
|
@has2k1 Note that OffsetBox children are already included in axes.get_tightbbox: for child in self.get_children():
if isinstance(child, OffsetBox) and child.get_visible():
bb.append(child.get_window_extent(renderer))
It almost seems that Legend should be added to axes.get_tightbbox as a possible child that would expand the bbox. See issue: #9130 |
Sorry, something went wrong.
|
Removed [WIP]. I think this works, but I'm sure there are edge cases I haven't found. |
Sorry, something went wrong.
|
kiwisolver is now on pypi:: pip install kiwisolver installs 1.0.0 and works here. I'm not 100% sure how to incorporate it into the matplotlib setup.py as a dependency. |
Sorry, something went wrong.
|
@has2k1 Just to follow up on your legend examples above. Your first two examples work fine w/o any fiddling. The third example, where you want a global legend doesn't work, and indeed makes the layout collapse in on itself. I'm not sure how to handle this, as you specify the anchored box in figure co-ordinates, but anchor it to one of your subplots, ax1. The constrained_layout then uses the whole area taken up by ax1 and the legend to determine the area the layout needs. That leaves no room for the other subplots, and the whole thing fails. Right now I have "global" colorbars stacked to the right of the gridspec that contains the relevant subplot specs (or to the left, bottom etc). This is hard coded into colorbars.py. Similarly suptitle is stacked over the parent gridspec inside the figure container. However, it seems to me that something more generic might be a future enhancement. Maybe something like gs = GridSpec(2, 2), gs.stack_right(newartist, align='v_center'). This would take some thought, however, because what if I want to stack something else to the right of newartist? |
Sorry, something went wrong.
|
test_pickle fails because kiwisolver objects can't be pickled, and they are now attached to all the other objects. |
Sorry, something went wrong.
|
The way the plot in the third example is constructed need not be how it should be done. In fact, using figure coordinates is part of the problem.
I have looked a little at what you currently have in place with the goal of making it extendable. i.e. For the
[top]
----------
| |
[left] | | [right]
| |
| |
----------
[bottom]
|
Sorry, something went wrong.
For SubplotSpec its pretty much done because it works on the difference between the spine position and the axes tightbbox, which includes AnchoredBoxes. However, we'd need an API for the GridSpec level. So far, I've mostly just worked with the existing API. Actually implementing the stacking is pretty trivial: see suptitle or colorbar. I think adding an external API to layoutbox is a reasonable way to go. I think you'd want something like
Edit: ...and do these artists then get layoutbox properties? |
Sorry, something went wrong.
|
@has2k1 How about this for an API: Right now we can do ax.add_artist(anchored_box). I'd proposed we add a method to GridSpec so we can call: gs.add_anchoredbox(anchored_box) That would entail the GridSpec having a bbox, but that doesn't seem to onerous. I'm a little fuzzy on what bbox_transform will be required to make this relative to the gridspec box, but I think its doable. That seems the most flexible, and consitent with an API folks will already know. |
Sorry, something went wrong.
|
Yes an API of the the form gs.add_anchoredbox(anchored_box) is simple and straight forward. I agree with the fuzziness around some of the parameters of the anchored_box. The solution may be a new offsetbox (the 2nd point) A GridSpec that properly encloses an offsetbox of some kind will make composing/arranging multiple plots a lot easier. Packages that use MPL will get a lot more interesting, in fact this feature would rely on it. |
Sorry, something went wrong.
|
I am trying to get up to speed with what you have so far, but despite the plentiful comments I hit a road block on the do_constrained_layout function. |
Sorry, something went wrong.
|
@has2k1 Fair enough. I'll try and improve the comments. There may even be obsolete comments in there... |
Sorry, something went wrong.
|
I was hoping I could add comments from a third party perspective immediately after I understood what was going on. |
Sorry, something went wrong.
|
@has2k1 That would be fabulous. I just pushed a commit that improved a bit of the doc, and removed the obsolete document bits. Let me know if you have questions, or I can point you towards tests that show how the different parts work. It was all more subtle than I originally thought it would be. |
Sorry, something went wrong.
|
docs-python27 fails with a unicode error in kiwisolver. This is now fixed in nucleic/kiwi#40 kiwisolver/master, so this test should run. The pickle test is always going to fail for matplotlib instances that attach kiwisolver variables because they can't be pickled. Does anyone use pickling with matpotlib? I admit I liked using pickle until I found out that its not compatible from py2.7 to py3.6 and I had to translate a bunch of old pickles. I need to change the architecture to only attach kiwisolver variables if constrained_layout == True, and then just say that constrained_layout won't work with pickeled plots. Unless someone knows how to serialize kiwisolver variables. |
Sorry, something went wrong.
|
Haven't followed the discussion at all, but re: pickling, you can perhaps remove the kiwisolver instance at pickle time (we already to that e.g. in Figure.__getstate__ (or grep for __getstate__ through the codebase to see how this is done). |
Sorry, something went wrong.
Yes, a surprising number of people. When we break this we find out rapidly. |
Sorry, something went wrong.
|
I found another interactive glitch. Make a minimal plot: fig, ax = plt.subplots(constrained_layout=True)
ax.set_xlabel('X label', fontsize=20)
ax.plot([0, 1])Now click the zoom/pan button and fiddle with the view limits. Then click the home button. Now the constraint management is no longer in effect: reducing the window height by dragging up the bottom chops off the bottom of the legend. |
Sorry, something went wrong.
|
@efiring - thanks a lot - try the latest commit. The second issue was some missed calls to ax.set_position that needed to be changed to ax._set_postion. Calling the former turns constrainedlayout off. The first issue was that I was setting both positions when I set the position in constrained layout. Setting only the original is the way to go, and allows set_aspect to still do its thing. |
Sorry, something went wrong.
There was a problem hiding this comment.
Partial review. Nothing major.
Sorry, something went wrong.
| ``figure`` keyword. This is backwards compatible, in that not supplying this | ||
| will simply cause ``constrained_layout`` to not operate on the subplots | ||
| orgainzed by this ``GridSpec`` instance. Routines that use ``GridSpec`` (i.e. | ||
| ``ax.subplots``) have been modified to pass the figure to ``GridSpec``. |
There was a problem hiding this comment.
should be fig.subplots. Is that the only one? If there are others, the "i.e." should be "e.g.".
Sorry, something went wrong.
There was a problem hiding this comment.
I can never remember the difference between i.e. and e.g. The failure of not teaching latin in high school any more...
Sorry, something went wrong.
| A new method to automatically decide spacing between subplots and their | ||
| organizing ``GridSpec`` instances has been added. It is meant to | ||
| replace the venerable ``tight_layout`` method. It is invoked via | ||
| a new ``plt.figure(constrained_layout=True)`` kwarg to |
There was a problem hiding this comment.
Delete the "plt.figure".
Sorry, something went wrong.
| which='major') | ||
|
|
||
| self.layoutbox = None | ||
| self.poslayoutbox = None |
There was a problem hiding this comment.
Should these have leading underscores? Or are they intended to be part of the public API?
Sorry, something went wrong.
There was a problem hiding this comment.
Well, a) when I started, I didn't even know about _privatestuff, so this naming was in ignorance. b) afterwards, I kind of thought that there may be occasion the knowledgable user would want access to these. However, I think I agree that we don't want this to be public because it could change, so I'll set accordingly.
Sorry, something went wrong.
|
|
||
| """ | ||
| self._set_position(pos, which='both') | ||
| # because this is being called esternally to the library we |
There was a problem hiding this comment.
-> "externally"
Sorry, something went wrong.
| """ | ||
| Set the spinelayoutbox for this axis... | ||
| """ | ||
| self.poslayoutbox = layoutbox |
There was a problem hiding this comment.
Is there a reason for using setters instead of directly assigning to the attributes? This is related to my question about whether the attributes are part of the API.
Sorry, something went wrong.
| maxn=rows*cols, num=num)) | ||
| self._subplotspec = GridSpec(rows, cols)[int(num) - 1] | ||
| ( | ||
| "num must be 1 <= num <= {maxn}, not {num}" |
There was a problem hiding this comment.
The string could start on the line with the opening parenthesis.
Sorry, something went wrong.
| if self.figure.get_constrained_layout(): | ||
| gridspecfig = fig | ||
| else: | ||
| gridspecfig = None |
There was a problem hiding this comment.
I don't see gridspecfig being used in this function. And fig is not defined there, either. And the comment suggests that maybe this function should be deprecated, unless someone knows what the use case is.
Sorry, something went wrong.
There was a problem hiding this comment.
I'll remove the gridspecfig stuff. No idea about whether the fcn should be deprecated or not...
Sorry, something went wrong.
| ax.poslayoutbox.edit_top_margin_min(bbox.y1-pos.y1+h_padt) | ||
| # _log.debug('left %f' % (-bbox.x0 + pos.x0 + w_pad)) | ||
| # _log.debug('right %f' % (bbox.x1 - pos.x1 + w_pad)) | ||
| _log.debug('left %f' % (-bbox.x0 + pos.x0 + w_pad)) |
There was a problem hiding this comment.
no need to call %() yourself
Sorry, something went wrong.
| closedmax=False)], | ||
|
|
||
| # do constrained_layout. | ||
| 'figure.constrainedlayout.do': [False, validate_bool], |
There was a problem hiding this comment.
Somehow the "do" grates on my aesthetic senses. Maybe "use" or "enable" or "active"? Regardless, the matplotlibrc.template also needs to be updated, since it presently has only "figure.constrainedlayout".
Also, since the kwarg is "constrained_layout", and that form (with underscore) seems prevalent (and readable), maybe the rcParam family name should match it. Unless we can think of something shorter... It's too bad "autolayout" is already used.
Sorry, something went wrong.
|
Sorry, something went wrong.
|
squash + rebase |
Sorry, something went wrong.
There was a problem hiding this comment.
The comments are minor issues mainly about style and and clarity.
One issue I have not commented on anywhere is the name generation for the layoutboxes. It may be clearer to have the names generated in one function maybe Layoutbox.make_name. The function would have an input type of layoutbox e.g 'subplotspec' then return a unique name. And if you store the type (or whatever suitable name) as a property. You can check on it with child.type == 'subplotspec'; this would limit the instances of +seq_id() and the splitting on dots as they are largely a distraction when reading the code.
The other nitpick is just an irritant, the dots after the numbers i.e 0., 1.. I'm not used to seeing them in python code.
Sorry, something went wrong.
| state = self.__dict__ | ||
| try: | ||
| state.pop('_layoutbox') | ||
| except: |
There was a problem hiding this comment.
except KeyError
Sorry, something went wrong.
| state = super(_AxesBase, self).__getstate__() | ||
| state['_cachedRenderer'] = None | ||
| state.pop('_layoutbox') | ||
| state.pop('_poslayoutbox') |
There was a problem hiding this comment.
You can use del state['_poslayoutbox']
Sorry, something went wrong.
There was a problem hiding this comment.
Is there an advantage? The other state calls are to pop().
Sorry, something went wrong.
There was a problem hiding this comment.
Other than the intention of the statement being clear. A pop() is effectively
def pop(self, key):
value = self[key]
del self[key]
return value
Sorry, something went wrong.
| invTransFig = fig.transFigure.inverted().transform_bbox | ||
|
|
||
| # list of unique gridspecs that contain child axes: | ||
| gss = set([]) |
There was a problem hiding this comment.
It does not look like gss uses any set properties.
Sorry, something went wrong.
There was a problem hiding this comment.
It saves me from checking if the gridspec is already in the list.
Sorry, something went wrong.
There was a problem hiding this comment.
Ahh I see, axes do not have a 1:1 relationship with gridspecs. Subtle way to handle it.
Sorry, something went wrong.
| sschildren = [] | ||
| for child in gs.children: | ||
| name = (child.name).split('.')[-1][:-3] | ||
| if name == 'ss': |
There was a problem hiding this comment.
For clarity I think hiding the naming conventions behind functions like is_subplotspec(child) and is_gridspec(child) would help.
Sorry, something went wrong.
| self._constrained_layout_pads['w_pad'] = None | ||
| self._constrained_layout_pads['h_pad'] = None | ||
| self._constrained_layout_pads['wspace'] = None | ||
| self._constrained_layout_pads['hspace'] = None |
There was a problem hiding this comment.
I think this whole block can be contained in the set_constrained_layout method.
Sorry, something went wrong.
|
|
||
| padd : dict | ||
| Dictionary with possible keywords as above. If a keyword | ||
| is specified then it takes precedence over the dictionary. |
There was a problem hiding this comment.
No need for padd parameter, you can use do set_constrained_layout_pads(**constrained).
Sorry, something went wrong.
| constrained = rcParams['figure.constrained_layout.use'] | ||
| self._constrained = bool(constrained) | ||
| if isinstance(constrained, dict): | ||
| self.set_constrained_layout_pads(padd=constrained) |
There was a problem hiding this comment.
self.set_constrained_layout_pads(**constrained)
Sorry, something went wrong.
There was a problem hiding this comment.
Yeah, I didn't know about this construct until recently. I think its a bit obscure that you can specify kwarg dictionaries this way, but perhaps you are correct that we should just make this simpler and use only this form from the start.
Sorry, something went wrong.
|
|
||
| self.stale = True | ||
|
|
||
| def set_constrained_layout_pads(self, **kwargs): |
There was a problem hiding this comment.
I think changing the signature of the method to set_constrained_layout_pads(self, w_pad=None, h_pad=None, ...), would make the logic below clearer.
Sorry, something went wrong.
There was a problem hiding this comment.
OK, I didn't quite get to setting those explicitly, because I wanted to have a for-loop over the possible kwargs, but I simplified as you suggested.
Sorry, something went wrong.
| state = self.__dict__ | ||
| try: | ||
| state.pop('_layoutbox') | ||
| except: |
There was a problem hiding this comment.
Use del and except KeyError:
Sorry, something went wrong.
There was a problem hiding this comment.
.... except I kept state.pop just for consistency. If there is a good reason to del, I can easily change.
Sorry, something went wrong.
I think if you don't do that in some languages you force integer arithmetic, which can be very unfortunate. i.e. 3./10. neq 3./10 Nice that doesn't happen in python, I'll try and find most instances and clean up. |
Sorry, something went wrong.
|
Thanks for the review @has2k1 I think I addressed most of your suggestions. Let me know about the pop/del thing. |
Sorry, something went wrong.
|
Rebase... |
Sorry, something went wrong.
|
I'm going to make an executive decision and merge this
Thanks @jklymak and everyone who put effort into reviewing this (particularly @has2k1 , @anntzer and @efiring )! |
Sorry, something went wrong.
|
Thanks a lot everyone! Now here comes the bug reports... 🦆 |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
PR Summary
This is my implementation of the geometry manager (See #1109).
This is heavily based on exploratory work by @Tillsten using the kiwi solver, and of course on tight_layout.
Latest what's new: https://5396-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/users/next_whats_new/constrained_layout.html
Latest tutorial: https://5398-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/tutorials/intermediate/constrainedlayout_guide.html#sphx-glr-tutorials-intermediate-constrainedlayout-guide-py
Intro
tight_layout() is great for what it does, but it doesn't handle some cases generally. i.e. if we have two GridSpecFromSubplotSpec instances, they need to be positioned manually using tight_layout(). Colorbars for individual axes work fine, but colorbars for more than one axis doesn't work. Etc.
This PR implements "constrained_layout" via plt.figure(constrained_layout=True) for automatic re-drawing. Currently, it works for subplots and colorbars, legends, and suptitles. Nested gridspecs are respected. Spacing is provided both as a pad around axes and as a space between subplots.
The following code gives this plot:
A few things to note in the above:
Installation:
In addition to the normal matplotlib installation, you will need to involve kiwisolver: pip install kiwisolver now works.
API dfferences (so far)
In order to get this to work, there are a couple of API changes:
I think most of these changes are backwards compatible, in that if you don't want to use constrained_layout you don't need to change any calls. Backwards compatibility introduces some awkwardness, but is probably worth it.
Implementation:
Implementation is via a new package layoutbox.py. Basically a nested array of layoutboxe objects are set up, attached to the figure, the gridspec(s), the subplotspecs. An axes has two layoutboxes: one that contains the tight bounding box of the axes, and the second that contains the "position" of the axes (i.e the rectangle that is passed to ax.set_position()).
A linear constraint solver is used to decide dimensions. The trick here is that we define margins between an axes bounding box and its position. These margins, and the outer dimesnion of the plot (0,0,1,1) are what constrains the problem externally. Internally, the problem is constrained by relationships between the layoutboxes.
Tests:
These are in lib/matplotlib/tests/test_constrainedlayout.py.
PR Checklist