Skip to content

Improve handling of shared axes with specified aspect ratio #10033

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
Jan 26, 2018

Conversation

efiring
Copy link
Member

@efiring efiring commented Dec 17, 2017

This is an attempt to resurrect #8752. I stripped out the parts related to cla() efficiency, thinking that had been handled by #8626, but now I see that the latter was reverted and does not appear to have been revived. I think the Axes initialization slowness problem was attacked from a different angle since then, but I haven't been able to find the PR. In any case, it is possible, but by no means certain, that something like the approach I used in 8752 should go back in. The idea is that cla() is often too blunt an instrument; finer control over what is needed could be added via arguments to the function.

In addition to clarifying the handling of aspect ratios with shared axes (see the set_adjustable docstring edit), this adds tracking of twinned axes, and keeps their positions in sync. This solves a bug in which adding a colorbar to a twinned axes previously would leave a mess--the box of the other twin was not being changed to match.

  • Axis sharing works across figures.
  • The "box-forced" adjustable is no longer needed.
  • Sharing both axes requires the use of "box", not "datalim".
  • A new "share" kwarg triggers synchronized setting of aspect
    ratio and adjustable in Axes within shared axis groups.
  • Added a test for axis sharing with aspect ratio setting.
  • Fixed and updated skew_rects test.

@tacaswell tacaswell added this to the v2.2 milestone Dec 18, 2017
@efiring efiring changed the title WIP: Improve handling of shared axes with specified aspect ratio Improve handling of shared axes with specified aspect ratio Jan 6, 2018
@efiring
Copy link
Member Author

efiring commented Jan 6, 2018

I think this is ready for people to try out.

@jklymak
Copy link
Member

jklymak commented Jan 7, 2018

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax2 = ax.twinx()
ax.set_aspect(2)
pcm = ax.pcolormesh(np.random.rand(20,20))
fig.colorbar(pcm, ax = ax)
plt.show()

used to give

old

Now gives

new

Seems better to me.

OTOH, I don't understand why the ylim is not 0-20, and the xlim made larger to satisfy the aspect ratio arg, rather than the other way around. But maybe thats a different PR.

There appears to be a bunch of stuff to do w/ gridlines in this PR; was it related? What is that trying to address?

I still find it a tad annoying that

import matplotlib.pyplot as plt
import numpy as np

fig, axs = plt.subplots(2, 1, sharex=True)
axs[0].set_aspect(3.)
plt.show()

gives me this:

boo

instead of this:

boo2

Removing the xticklabels in axs[0] implies to me that they should line up with the xticklabels in axs[1].

@efiring
Copy link
Member Author

efiring commented Jan 7, 2018

@jklymak, your first point is a demonstration of how keeping track of twinning, and keeping twinned boxes identical, fixes a long-standing bug.

The answer to your question about which limits get adjusted is that we are severely limited by a fundamental aspect of the present implementation: apply_aspect is run inside each Axes.draw(). It can look to see whether any axes are shared or twinned, but it doesn't know anything about the sequence in which the draw commands are executed. In the case of twinned x axes, both the x view limits and the x (and y) box dimensions must be identical for the two axes. This means it is safe for apply_aspect only to adjust the y view limits, because they are independent (provided y is not also shared.)

The gridline stuff is not really related, but it was in the bigger PR from which this one was derived. It is simplifying and making more consistent the handling of rcParams and grid properties. Certainly it could be split out; I will do it if I have to, eventually, but it will chew up time that I would rather not spend on that, because the end result will be no different. (I admit I had forgotten about the gridline part, and only noticed it yesterday when I was reviewing the PR as a whole.)

For your last example, you need to use the new kwarg that I added to set the aspect ratio in both axes:

import matplotlib.pyplot as plt
import numpy as np

fig, axs = plt.subplots(2, 1, sharex=True)
axs[0].set_aspect(3, share=True)
plt.show()

subplots_with_aspect

@efiring
Copy link
Member Author

efiring commented Jan 7, 2018

Alternatively, one can set the adjustable to 'datalim':

import matplotlib.pyplot as plt
import numpy as np
fig, axs = plt.subplots(2, 1, sharex=True)
axs[0].set_aspect(3, adjustable='datalim')
axs[0].plot([1,2,3])
axs[1].plot([3,2,1])
plt.show()

subplots2_datalim_1
There are problems with all of these. In your example, a reasonable expectation is violated; in my first example it is assumed the user wants the same aspect ratio for both plots, which might not be the case; and in my second example, the autoscaling of the shared x axis is occurring before the aspect ratio adjustment, so the y-range is getting chopped. This is not new; the difference with this PR is that the adjustable is no longer being automatically (and silently) changed from the default, 'box', to 'datalim' when sharing. Note that 'datalim' can only work with a single shared axis, not with both axes shared.

@jklymak
Copy link
Member

jklymak commented Jan 7, 2018

That all sounds reasonable to me, and I don't think should slow this PR.

My larger-scale complaint with what "share" means could be dealt with separately: I think the x-limit should be the same and the adjusted width be the same in 'box' mode, but the aspect ratios need not be the same, though the set_aspect(shared=True) is very useful as well.

I don't mind the gridline stuff being in there, if it makes your life easier. But for review purposes it might help to have some test code and an explanation of what you are fixing/enhancing.

@efiring
Copy link
Member Author

efiring commented Jan 7, 2018

Maybe in the case of subplots with only one axis shared, the adjustable should be automatically set to 'datalim'. This would be less of a break with previous behavior. It can't be done in the general case of sharing because it would not work with both axes shared and with axes shared across figures.

@efiring
Copy link
Member Author

efiring commented Jan 8, 2018

I moved the gridline part to #10193 and squashed the rest.

@tacaswell tacaswell added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Jan 8, 2018
@jklymak jklymak self-requested a review January 9, 2018 17:27
Copy link
Contributor

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

A couple minor things. I'll kick the tires locally and report back if anything goes sideways.

# locator and formatter attributes
self.xaxis.major = self._sharex.xaxis.major
self.xaxis.minor = self._sharex.xaxis.minor
x0, x1 = self._sharex.get_xlim()
self.set_xlim(x0, x1, emit=False, auto=None)
self.xaxis._scale = mscale.scale_factory(
self._sharex.xaxis.get_scale(), self.xaxis)
self._sharex.xaxis.get_scale(), self.xaxis)
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this new indentation 21 spaces? That seems wrong.

@@ -1010,14 +1004,13 @@ def cla(self):
y0, y1 = self._sharey.get_ylim()
self.set_ylim(y0, y1, emit=False, auto=None)
self.yaxis._scale = mscale.scale_factory(
self._sharey.yaxis.get_scale(), self.yaxis)
self._sharey.yaxis.get_scale(), self.yaxis)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Those are accidental, and I will revert them.

@@ -4429,6 +4429,57 @@ def test_empty_shared_subplots():
assert y1 >= 6


def test_shared_with_aspect():
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to turn this into 3 separate tests? This is going to stop running at the first failed assert, which seems like a bad thing when stuff breaks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I split it.

Copy link
Contributor

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

A found a couple more minor things, but I wouldn't hold this up for them if you're not interested in changing them.

@@ -4195,9 +4226,9 @@ def twiny(self):
return ax2

def get_shared_x_axes(self):
"""Return a copy of the shared axes Grouper object for x axes."""
'Return a reference to the shared axes Grouper object for x axes'
Copy link
Contributor

Choose a reason for hiding this comment

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

This is actually less compliant with most of the tools I know, including the lack of period. Is this an intentional change?

Copy link
Member Author

Choose a reason for hiding this comment

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

The change from "copy of" to "reference to" is a correction I made. I have reverted the other formatting changes, although my preference would actually be to use single double-quotes for these. I don't know why I used single-quotes.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the record, I like the single double- (or even single- !) quote too. I just know I've had to push away from those to make some checkers happy. (They're good to catch other problems.)

- Axis sharing works across figures.
- The "box-forced" adjustable is no longer needed.
- Sharing both axes requires the use of "box", not "datalim".
- A new "share" kwarg triggers synchronized setting of aspect
  ratio and adjustable in Axes within shared axis groups.
- Added a test for axis sharing with aspect ratio setting.
- Fixed and updated skew_rects test.
@efiring
Copy link
Member Author

efiring commented Jan 26, 2018

@dopplershift I restored the original docstring format (but not the incorrect content), and then rebased to solve a recently-introduced merge conflict. Mercifully, it was a simple one.

@jklymak jklymak merged commit fde699c into matplotlib:master Jan 26, 2018
@jklymak
Copy link
Member

jklymak commented Jan 26, 2018

Thanks a lot @efiring!

shared_y = self in self._shared_y_axes
# Not sure whether we need this check:
if shared_x and shared_y:
raise RuntimeError("adjustable='datalim' is not allowed when both"
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it not be possible to share datalim with both axes?

Copy link
Member Author

Choose a reason for hiding this comment

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

When the box dimensions are fixed, as is the case with "adjustable='datalim'", it is not possible to maintain a specified aspect ratio without being able to adjust at least one of the axes and have it stick. You can't do that when they are both shared.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let see if I get this straight. When adjustable is "box" they share both the datalim and the physical dim of the axis, but when adjustable is datalim it just it is the xlim or ylim that are shared?

What is the default adjustable when axes are created?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sharing applies only to datalim (in this context, but it is actually done via a reference to the same Axis, so locators and formatters as well as datalim are locked together). Twinning locks the box to be identical between the twins. The default adjustable is now box.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thus datalim/box are just used by the set_aspect.

With the example @stonebig and @mwaskom posted at #10585 twinning would become a new share group. That is the diagonal plots share y-axis. If you move up in (i,i) plot then i row will also move, but if you move up in (i,j) j not i then only that row except (i,i) will move.

This of course make sharing a bit harder with the new API (if it gets accepted), since sharing is transitive.
The only way to get the same result would be to listen to LocationEvent and break/create share
as one move between the axes. Or you need to open the make_twin API to the public.

PS. @efiring I fixed such that major and minor and copy over when share is broken in the new API.

@jessebett
Copy link

I have come across this problem but it's not clear to me from the comments here how it was resolved.

I have an imshow and a plot with a shared x axis. I need to scale the aspects of the imshow without sharing the aspect of the plot vertical axis.

For example, I want a tall imshow with large aspect on top of a short plot but with shared x-axis. How can I achieve this? The settings above aren't working for me.

@snail123815
Copy link

Does this mean that with twinned axis, one cannot adjust the figure aspect ratio based on 'box' anymore?
To be more precise, to set the exact aspect ratio of the plotting area, I do this:

ax.set_aspect(abs((xright - xleft) / (ytop - ybottom)) * ratio, adjustable='box')

Can I achieve this with twinned axis?

@ImportanceOfBeingErnest
Copy link
Member

"aspect" in matplotlib always refers to the data, not the axes box. Therefore setting the aspect for twinned or shared axes and letting the box be adjustable actually only makes sense when the scales are the same - or differ by an offset (as opposed to any other linear or nonlinear function). Matplotlib does not perform any check on this, so it disallows for adjustable='box' in such case.

If have the feeling that people often want to use the aspect to set the ratio of box height to box length. E.g. to achieve a square subplot. This is rather unrelated to how "aspect" is defined in matplotlib, but is sure a valid desire to have. Maybe something like ax.set_box_aspect or so would make sense?

@jklymak
Copy link
Member

jklymak commented Jul 26, 2019

If have the feeling that people often want to use the aspect to set the ratio of box height to box length. E.g. to achieve a square subplot. This is rather unrelated to how "aspect" is defined in matplotlib, but is sure a valid desire to have. Maybe something like ax.set_box_aspect or so would make sense?

Thats possible, but would require a good bit of thought as to how it would interact with the usual way of placing axes and the layout managers.

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.

9 participants