Thanks to visit codestin.com
Credit goes to github.com

Skip to content

MEP for a matplotlib geometry manager #1109

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

Closed
pelson opened this issue Aug 19, 2012 · 52 comments
Closed

MEP for a matplotlib geometry manager #1109

pelson opened this issue Aug 19, 2012 · 52 comments
Labels
MEP: MEP needed topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Milestone

Comments

@pelson
Copy link
Member

pelson commented Aug 19, 2012

Write a MEP for a matplotlib figure geometry manager.

There is a lot of experience in the matplotlib community with requirements & ideas. This would be a good place to collate before a MEP gets written.

@pwuertz
Copy link
Contributor

pwuertz commented Sep 1, 2012

I love programming with Qt4, so here are two considerations concerning layout handling based on that toolkit.


Graphical user interfaces usually consist of a bunch of widgets that are hierarchically organized by layout managers. The basic layout classes implement Vertical, Horizontal and Grid alignment. The widgets include attributes like spacing, padding or scaling policies the layout managers can take into account. Although this works perfectly for applications this model probably isn't flexible enough for plotting figures. For example, there is no layout manager equivalent for a legend being placed on top of a axes widget and being aligned to some edge.


I don't know if you have had the chance of looking into QML, but I think this way of aligning objects could be a really powerful solution! Every object has anchor attributes like top, bottom, left, right, center. Aligning two objects is as simple as defining an expression object1.anchors.left = object2.anchors.right. I also think that it would be awesome if lengths/coordinates could be defined as unit-aware objects, so a position/distance/length could be given in pixels, data coordinates, inches, centimeters, font-height etc. This way, the upper-right plot legend, spaced by 1.5 times the height of 'M', could be defined like

legend.anchors.top = axes.anchors.top - length(1.5, "em")
legend.anchors.right = axes.anchors.right - length(1.5, "em")

QML interprets these statements as bindings that are dynamically evaluated on demand, so the python equivalent of the right hand side would be a lambda function. Such alignments determine the position, width and height of an object unless the object defines a fixed value for one or multiple of these attributes.

Solving the dependencies of multiple objects referencing each other is a little bit tricky.. but I think I already got the python code for that task! In one of my projects I implemented a data table where a user can define an equation for a table cell that may depend on the values of other cells, just like Excel :). Using the python ast-module I figure out which objects are referenced in an expression and build a dependency tree. This tree is then solved so that the cell values are calculated in the right order. Faulty circular dependencies are detected and reported back to the user.

Opinions?

@pelson
Copy link
Member Author

pelson commented Sep 1, 2012

@pwuertz: I think this is a great start to this PR. Thank you.

I wonder if you could share any experience or wisdom about text handling with this anchor mechanism. Would text.anchors.bottom represent the bottom of a text box once the height of the text within the box is computed? Are QML concepts always rectangular? For instance, if a text "element" were rotated by 45 degrees, where might one expect the text.anchors.bottom to represent?

@pwuertz
Copy link
Contributor

pwuertz commented Sep 3, 2012

In QML rotations are applied at object level. This means that any child objects of a rotated text, which inherit the transformation, can be correctly aligned to its parent in the rotated space. This also implies that there is no meaningful anchoring definition for siblings or parent objects living in another coordinate system. Keeping such objects aligned is still possible since you can just define an equation for the x,y,width,height properties. It's just a matter of definition. So if the alignment of a rotated text in a non-rotated environment is the most frequent usecase one could very well define that a rotated text is a non-rotated object that encloses the text.

Another solution could be a the definition of a group object that adapts in width and height so it encloses all children and aligns them as whole.

@ppurka
Copy link

ppurka commented Oct 19, 2012

I looked up QML after finding it mentioned here. So, repeating what I commented on in #1415:
"The ideas in QML seem quite similar to the ones in edje, which is one of the libraries behind enlightenment and has been around for nearly a decade ( see http://www.enlightenment.org and http://docs.enlightenment.org/auto/edje/ ). So, it might be useful for you to look at that project since all the theming and layout in enlightenment happens through edje."

@pwuertz
Copy link
Contributor

pwuertz commented Oct 19, 2012

Yes, the ideas are pretty much the same. The real task however is to figure out how to apply this concept to matplotlib so all the issues and features mentioned above can be satisfied. When I got a little bit more time I'll try to stitch together a simple demo implementation to show you what I had in mind. Might be a matter of weeks though..

@Tillsten
Copy link
Contributor

I played around with the pure python cassowary implementation and matplotlib, doing the most simple thing to see what can be done (https://gist.github.com/cee3373e3967c84be496). Even this very simple example - using the solver to align a y label - is showing some of the requirements of a real geometry manager.

  1. For doing the layouts, it is necessary to know the size of the parts. In matplotlib the size is sometimes only known after the drawing.
  2. Some kind of tree structure for a figure would be very helpful to collect the various constraints.
  3. Also exact definitions of the elements are needed, e.g. does axes.left is just the left side of the axes patch or also counting in the tick labels. One would probably want to untangle the components and only use the constraints to put them together.

If anyone wants to use a constraints based geometry manager, i suggest to take a deep look at enaml, which uses cassowary for gui-layout and has a lot of useable code.

@tacaswell
Copy link
Member

I talked with @sccolbert (the author of kiwi and enamal) over the weekend at am 👍 on making use of it.

@efiring
Copy link
Member

efiring commented Sep 30, 2014

@tacaswell are you referring to using enaml in mpl? It looks like it pulls in heavy dependencies, so as usual, I'm skeptical.

@tacaswell
Copy link
Member

Sorry I wasn't clear, I just want to use kiwi, the constraint solver.

@Tillsten
Copy link
Contributor

There is a slightly more advanced prototype at https://gist.github.com/Tillsten/cee3373e3967c84be496. The Problem at the moment is that i am note able to get the right size of text artists in figure space, is there a way?

@pwuertz
Copy link
Contributor

pwuertz commented Sep 30, 2014

Just a guss: The correct text-size can only be determined by the rendering backend (Renderer.get_text_width_height_descent()). Do you know if the t.get_extents() method you are calling retrieves the extent from the backend or is it just returning some default value until the text is actually drawn?

@sccolbert
Copy link

To allay any fears, Kiwi only has a dependency on a C++ compiler. I
wouldn't recommend Enaml for this application, but Kiwi would be right at
home.
On Sep 30, 2014 8:27 AM, "Thomas A Caswell" [email protected]
wrote:

Sorry I wasn't clear, I just want to use kiwi, the constraint solver.


Reply to this email directly or view it on GitHub
#1109 (comment)
.

@Tillsten
Copy link
Contributor

Tillsten commented Oct 6, 2014

Am i seeing it that there is no easy way to get the base line from a text object? Because
halign(left_text.v_center, right_text.v_center) doesn't work like expected with e.g. 'g' and 'l' as text.

@sccolbert
Copy link

A pure Python implementation of Cassowary is going to be painfully slow. Any particular reason you don't want to use Kiwi for this?

@Tillsten
Copy link
Contributor

Tillsten commented Oct 8, 2014

scolbert: It is fast enought for a proof of concept and has better documentation which was helpful for me get an better idea of cassowary, but if this ever get out of the poc-stage, changing it to kiwi will be quite simple i think.

@Tillsten
Copy link
Contributor

Tillsten commented Oct 8, 2014

Btw i am using an already existing package called cassowary, if this wasn't clear.

@sccolbert
Copy link

Yep, I saw that. I looked at cassowary, it's basically a port of the original Cassowary C++ code to Python. Given that Kiwi is 40x faster than the original algorithm, I don't think the pure Python version will be suitable for anything outside of proof-of-concept.

The Kiwi API is very simple:

In [1]: import kiwisolver as kiwi

In [2]: x = kiwi.Variable('x')

In [3]: y = kiwi.Variable('y')

In [4]: c1 = x + y == 20

In [5]: c2 = y <= -10

In [6]: s = kiwi.Solver()

In [7]: s.addConstraint(c1)

In [8]: s.addConstraint(c2)

In [9]: s.updateVariables()

In [10]: print x.value()
30.0

In [11]: print y.value()
-10.0

In [12]: s.addEditVariable(x, 'strong')

In [13]: s.suggestValue(x, 44)

In [14]: s.updateVariables()

In [15]: print x.value()
44.0

In [16]: print y.value()
-24.0

In [17]: s.suggestValue(x, 14)

In [18]: s.updateVariables()

In [19]: print x.value()
30.0

In [20]: print y.value()
-10.0

@Tillsten
Copy link
Contributor

@sccolbert Playing around with kiwi again, the follwing questions pop up:

  • Is there a difference between using s.addConstraint(x == 5) and s.suggestValue(x, 5)
  • Can i specify the strength of an constraint?

@sccolbert
Copy link

@Tillsten The combination of addEditVariable + suggestValue is used when you want to resolve the same problem with slightly varying inputs. As a simple example, consider the layout of widgets in a panel. The input to the system is the width and height of the panel, and the output is the new geometry of the child widgets. In this type of problem, the width and height variables for the panel are added as edit variables in the solver instead of as constraint expressions. When the size of the panel changes, the new values for width and height will be provided to the solver via suggestValue and the system will be resolved. The solver is optimized for this use case: set up a system, then resolve it several times with slightly varying inputs.

If your particular problem doesn't have this type of setup, that's okay. There are lots of problems which don't really have "inputs" of which to speak, and the system is just solved once in its entirety. In that case, you can just ignore edit variables and suggestValue. Indeed, you can solve the problem example above without using suggestValue by adding and removing constraints for the panel width and height as needed, it just wont be as fast since the solver will not take the optimized path.

You can change the strength of a constraint expression using the | operator and ORing the constraint with a symbolic strength, or a number. The valid symbolic strengths are 'required', 'strong', 'medium', and 'weak'. The default strength is 'required'. The mapping from symbolic strength to numeric strength can be found here:
https://github.com/nucleic/kiwi/blob/master/kiwi/strength.h

User code will typically always use a symbolic strength:
(x == 5) | 'weak'

@Tillsten
Copy link
Contributor

Thanks a lot! I even tried ORing, but i forgot the braces around the constraint.

There is now a atleast somewhat working very basic prototype at https://github.com/Tillsten/MplLayouter. At the moment it uses its own methods for adding labels and titles, any intregation would requiere to subclass Axes and overwrite the corresponding methods. As most here will understand, i don't want to touch Axes at the moment.

Still a open issuse is the handling of ticklabels: right now u reuse the approach of tight_layout, that means drawing the axes checking and adjust the size afterwards. But this makes the handling of axes alignment more difficult than necessery. I hope that i can still work around it or maybe distangle the ticklabels from the rest.

Also while it is possible to get the text extent, i still don't know how to get the text baseline. Any help or comments are welcome.

@Tillsten
Copy link
Contributor

To see the (boring) example, just run the python file. Needs kiwisolver (avaible via conda). The only nice thing which can be seen is that the top labels are automatically aligned with resepect to each other, regardless of titles or the size of the ticklabels.

API, the figure layout itself and the state handling are still quite basic. Here on problay could copy some things directly from enaml.

@tacaswell
Copy link
Member

@Tillsten 👍 Awesome thanks for working on this.

When you say 'disentangle the ticktlabels' do you mean pulling apart the Tick objects (that draw the tick label, the tick mark, and the grid line)?

attn @mdboom re the internals of text objects.

@pwuertz
Copy link
Contributor

pwuertz commented Apr 22, 2015

Concerning text baseline, see my earlier comment (hint to Renderer.get_text_width_height_descent()).

@jklymak
Copy link
Member

jklymak commented Aug 2, 2017

Somewhat modifed version of @Tillsten layout manager that tries to take into account the tick and ticklabel sizes. It places the axes elements "somewhere" and then looks at the difference between the ax.get_tightbbox(self.renderer) and ax.get_position() to descide how much space there needs to be for the ticks and their labels. Subsequent resizes let it iterate on this.

Its fragile to backends. It mostly seems to work for Qt5Agg. It doesn't work for macosx or nbagg both of which seem to do funky things with the figure transforms, maybe due to Retina?

https://github.com/jklymak/mplconstrainedlayout

example

@anntzer
Copy link
Contributor

anntzer commented Aug 2, 2017

You may want to give a try with #8771 -- not so much because of the cairo rendering, but because that PR tries to cleanly separate the rendering part (by Agg) from the windowing part (by Qt or whatever GUI toolkit you're using).

@jklymak
Copy link
Member

jklymak commented Aug 3, 2017

@anntzer That has quite a few dependencies that don't seem trivial to install on OS X, unless I'm missing something...

@jklymak
Copy link
Member

jklymak commented Aug 3, 2017

What I am still not following is how people think this would work w/o having made all the plot commands you are interested in making before calling the layout. @Tillsten example calls do_layout after all the plot calls have been made so that he can adjust the axis size. My version does the same thing just in inverse. I don't see how this is conceptually different than calling tight_layout, except the mechanism by which you determine what the axis positions are deterministic in tight_layout, perhaps more fluid in the constraint system. I'm not saying you can't do it, but I'm not clear what the mechanism is.

I will also say that the constraints thing is cool, but I'm not 100% sold that its the easiest way to do this problem. Just consider the width dimension in the example above. You know dx for the ylabel, and for the tickmarks+labels, so dx for the axis is determined. You can constrain it to match another axis, and I guess thats a bit easier with the constraints, but is it far easier? I'm not sure. It still seems to me that tight_layout could be made a bit more flexible by respecting containers other than the whole figure, and not just working with subplot parameters, and it would be just as good. But again, maybe I'm missing something.

@anntzer
Copy link
Contributor

anntzer commented Aug 3, 2017

To use cairo, possibly. But the refactor of Qt5Agg doesn't have any dependency (beyond qt5 and agg which are already present).

@jklymak
Copy link
Member

jklymak commented Aug 3, 2017

@anntzer As usual, I was missing something condo install pyqt=5 worked fine ;-) Sorry for the noise...

@jklymak
Copy link
Member

jklymak commented Aug 3, 2017

@anntzer

You may want to give a try with #8771 -- not so much because of the cairo rendering, but because that PR tries to cleanly separate the rendering part (by Agg) from the windowing part (by Qt or whatever GUI toolkit you're using).

OK, this still has trouble printing.

  • If I run using your pyQT5 and use the pring button to create a png all the axes labels move around. Similarly if I do fig2.savefig('Example.png').
  • If I run using the pdf backend and fig2.savefig('Example.pdf') the results look great.
  • If I run using the pdf backend and fig2.savefig('Example.png') again the labels don't get positioned properly.

If I use backend cairo then I get the following backtrace:

Traceback (most recent call last):
  File "layout.py", line 453, in <module>
    fl = FigureLayout(fig2)
  File "layout.py", line 433, in __init__
    self.renderer = find_renderer(mpl_figure)
  File "layout.py", line 423, in find_renderer
    fig.canvas.print_pdf(io.BytesIO())
  File "/Users/jklymak/anaconda3/envs/matplotlibqt5cairo/lib/python3.6/site-packages/matplotlib/backends/backend_cairo.py", line 454, in print_pdf
    return self._save(fobj, 'pdf', *args, **kwargs)
  File "/Users/jklymak/anaconda3/envs/matplotlibqt5cairo/lib/python3.6/site-packages/matplotlib/backends/backend_cairo.py", line 504, in _save
    renderer.set_ctx_from_surface(surface)
  File "/Users/jklymak/anaconda3/envs/matplotlibqt5cairo/lib/python3.6/site-packages/matplotlib/backends/backend_cairo.py", line 110, in set_ctx_from_surface
    self.set_width_height(surface.get_width(), surface.get_height())
AttributeError: 'cairo.PDFSurface' object has no attribute 'get_width'

So, printing is a bit of an issue.

@anntzer
Copy link
Contributor

anntzer commented Aug 3, 2017

  1. was probably due to the figure-saving codepath falling back on Agg for png or matplotlib's own pdf/ps/svg backends. Now fixed: everything should use cairo's implementations.
  2. was due to the fact that the code paths were not tested. Also fixed (well they're still not unit-tested but that's another story).

@jklymak
Copy link
Member

jklymak commented Aug 3, 2017

https://github.com/jklymak/mplconstrainedlayout now has a colorbar implemented as an AxesContainer stacked inside the main plot AxesContainer. Could just as easily be a legend. So this allows nesting of axes. You of course need to decide what sort of contraints to set on such an axis - here I made it's width and height fixed fractions of the raw_axes that contains the pcolormesh, and made it vertically centered on that axis. It has a default pad. Note how the upper axis became smaller as well (obviously you wouldn't always want that).

figure_0_and_readme_md_ ___dropbox_layout_and_matplotlib_colorbar_-_google_search

@jklymak
Copy link
Member

jklymak commented Aug 4, 2017

Printing issue was due to the fact that @Tillsten was placing the text using pixel rather than figure units. Caused a bunch of label funkiness, but now it all works well. Modified the code to do the constraint in figure units as well.

@jklymak
Copy link
Member

jklymak commented Aug 4, 2017

I think I understand what is going on well enough via @Tillsten example to make progress. Now Taking this back a step, some questions:

ax.get_tightbbox(renderer) gives the bounding box for the whole axis including the x and y labels and title. The code for placing these items is quite mature, and to my mind does the "right" thing, and is deterministic (i.e. the xlabel goes to the left/right of the ticks. )

So what are the strong reasons for getting more granular than an axes object (spines+all labels) in the layout manager? The only one I've personally come across is allowing right-justification of tick labels that are on the right hand side of an axis. The issues referenced above don't seem to pertain to anything that can't be handled at a level above an axes object: i.e. lining up axes or their spines, making sure they fit on the canvas, and adding various parasitic axes (colorbar, legend, suptitle). For a first pass, doing things at the axis level (and higher level objects like suptitles, colorbars, legends) would be far easier than going all the way down to the artist level. What am I missing?

EDIT: I guess folks could want to line up y/x labels between axes and that could be an extra constraint on the labels. That doesn't strike me as hugely useful, but I could be convinced otherwise.

I'm still after some guidance on how this gets executed. When in matplotlb does the constraint solver get executed?

I have lots of API questions, but I think guidance on if an axes-level constraint system would be good enough to start would help make that task a lot easier.

Just pinging a few devs/interested folks (sorry if I missed someone) : @anntzer @tacaswell @pelson @pwuertz @WeatherGod @efiring

@WeatherGod
Copy link
Member

WeatherGod commented Aug 4, 2017 via email

@jklymak
Copy link
Member

jklymak commented Aug 4, 2017

@WeatherGod Cool - both those cases are inside axes. Would we ever constrain from the inside out? As in the size of the axis would change due to what is inside it? Right now we certainly don't and something like the toy or tight_layout only constrain and axis size by elements outside the axis like how big tick labels and other labels are.

If we are OK with the axes size being set by the outside constraints (like it is now), then plotting within the axis could have its own layout manager that constrains elements just inside the axis.

So what is needed is a LayoutBox class (similar to @Tillsten Box class) that defines most common layout desires and gives access to the underlying constraints. A group of LayoutBox items would share a kiwi.Solver() and determine layout for that group. Then you have a high-level layout group that attempts to lay out the axes, suptitles, externally placed legends, colorbars etc. If desired, inside each axis you could have a separate layout group that attempts to layout axes elements. Etc. The advantage of this is that the high-level axis layout could be implemented w/o implementing the inside axis layout. If someone wanted to tackle in-axis layout, they could have at it.

@WeatherGod
Copy link
Member

WeatherGod commented Aug 4, 2017 via email

@WeatherGod
Copy link
Member

WeatherGod commented Aug 4, 2017 via email

@anntzer
Copy link
Contributor

anntzer commented Aug 5, 2017

(going to have on-and-off internet access in the coming days, so don't wait for me :-))

@jklymak
Copy link
Member

jklymak commented Aug 10, 2017

Progress report: I have made a new package LayoutBox that implements a LayoutBox class and provides a bunch of helper functions. Heavily based on @Tillsten . Not properly documented yet etc.
https://github.com/jklymak/mplconstrainedlayout/blob/master/LayoutBox.py

I've also made a test notebook for this class: https://github.com/jklymak/mplconstrainedlayout/blob/master/TestLayoutBox.ipynb

The notebook starts to get towards an API for lining up axes using the layout manager. See the section with constrained_layout(fig, axs), where axs is a list of axes (in the figure, I hope) that you want to be constrained together. They will stay within their gridspec, and constraining them together will give them all the same label, ticklabel, title - accommodating margins.

I need to work on implementing nested gridspecs, but I think this is moving in the right direction. Whether this happens automatically at some point in the future, or gets added as a bolt-on method to figure (i.e. fig.contrain_layout()) can be up for discussion once the rest of the ideas are fleshed out.

Any comments/criticisms most welcome.

@jklymak
Copy link
Member

jklymak commented Aug 14, 2017

A draft of a MEP (in notebook format) https://github.com/jklymak/mplconstrainedlayout/blob/master/DraftMEP.ipynb

Comments very welcome. As you can see, the scope here is pretty modest, and maybe not quite what folks were thinking, so any suggestions would be great. Of course the same ideas could be used deeper inside an "axes", but I kept the scope "axes and larger" for now. I've managed to implement most of this already, but not very cleanly yet. If there is positive feedback I'll keep going. If not, I'm happy to let someone else run with the ball.

@tacaswell

@jklymak
Copy link
Member

jklymak commented Aug 23, 2017

Update:

https://github.com/jklymak/matplotlib/tree/constrainedlayout is a fork of relatively recent master that is close to pull release.

Right now all it does it "constrained_layout" on subplots, (nested) gridspecs, and colorbars. Todo is to add suptitle support and maybe legends.

Note that right now there are two API changes:

  1. gridspec has to be called with a figure argument. Thats a pretty big non-backwards compatible change. I'm considering how to get rid of it.
  2. To get the constrained layout you specify plt.figure(costrained_layout=True).

I have tests, but I haven't put them in proper test format yet.

@jklymak
Copy link
Member

jklymak commented Aug 23, 2017

Update: See PR #9082 (WIP)

@dstansby
Copy link
Member

Closing since #9082 went into 2.2

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

No branches or pull requests