Skip to content

Share and unshare axes after creation. #9923

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
wants to merge 7 commits into from
Closed

Share and unshare axes after creation. #9923

wants to merge 7 commits into from

Conversation

lkjell
Copy link
Contributor

@lkjell lkjell commented Dec 4, 2017

PR Summary

Implementing share and unshare axes after creation ref PR #1312.

Currently sharing axes has a parent/master axes. The class attribute is _sharex, _sharey and _sharez.
In addition there is a Grouper object which store which axes share their axes. Because of technical limitation, it is suggested that sharing should be transitive and there is no master axes. That is if the master axes is unshared the other axes in the group will still share their axes. The parent/master attribute are kept for backward compatibility of the behaviour of cla().

Reverting back to WeakSet implementation to fix #4827 too.

Test code

import matplotlib.pyplot as plt
import numpy as np

t = np.arange(0.01, 5.0, 0.01)
s1 = np.sin(2 * np.pi * t)
s2 = np.exp(-t)
s3 = np.sin(4 * np.pi * t)

ax1 = plt.subplot(311)
plt.plot(t, s1)

ax2 = plt.subplot(312)
plt.plot(t, s2)

ax3 = plt.subplot(313)
plt.plot(t, s3)

ax1.share_x_axes(ax2)
ax1.share_y_axes(ax2)

# Share both axes.
ax3.share_axes(ax1)
plt.xlim(0.01, 5.0)

ax3.unshare_y_axes()
ax2.unshare_x_axes()

plt.show()

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@lkjell lkjell changed the title Sharexy Share and unshare axes after creation. Dec 4, 2017
@@ -496,22 +494,18 @@ def __init__(self, fig, rect,
self.set_aspect('auto')
self._adjustable = 'box'
self.set_anchor('C')
self._sharex = sharex
self._sharey = sharey
self._shared_x_axes = WeakSet()
Copy link
Contributor

Choose a reason for hiding this comment

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

self._shared_x_axes = WeakSet([self]) (throughout the PR)


# keep original intention
axes._adjustable = 'datalim'
self._adjustable = 'datalim'
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if one calls set_adjustable() on an axes which is shared with others? Should that be prevented, or somehow propagated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a preliminary pr. Discussion is needed. See there are some build error related to this set_adjustable()

Copy link
Member

Choose a reason for hiding this comment

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

This is a messy question partly addressed by, or at least related to #8752, which needs work to handle twinx, twiny properly and to remove some parts that are no longer needed because of #8626.

@lkjell
Copy link
Contributor Author

lkjell commented Dec 4, 2017

Funny, forgot there where a 3d plot where you can share the z axis as well...

@lkjell
Copy link
Contributor Author

lkjell commented Dec 4, 2017

should

def test_inverted_cla():
    # Github PR #5450. Setting autoscale should reset
    # axes to be non-inverted.
    # plotting an image, then 1d graph, axis is now down
    fig = plt.figure(0)
    ax = fig.gca()
    # 1. test that a new axis is not inverted per default
    assert not(ax.xaxis_inverted())
    assert not(ax.yaxis_inverted())
    img = np.random.random((100, 100))
    ax.imshow(img)
    # 2. test that a image axis is inverted
    assert not(ax.xaxis_inverted())
    assert ax.yaxis_inverted()
    # 3. test that clearing and plotting a line, axes are
    # not inverted
    ax.cla()
    x = np.linspace(0, 2*np.pi, 100)
    ax.plot(x, np.cos(x))
    assert not(ax.xaxis_inverted())
    assert not(ax.yaxis_inverted())

    # 4. autoscaling should not bring back axes to normal
    ax.cla()
    ax.imshow(img)
    plt.autoscale()
    assert not(ax.xaxis_inverted())
    assert ax.yaxis_inverted()

    # 5. two shared axes. Clearing the master axis should bring axes in shared
    # axes back to normal
    ax0 = plt.subplot(211)
    ax1 = plt.subplot(212, sharey=ax0)
    ax0.imshow(img)
    ax1.plot(x, np.cos(x))
    ax0.cla()
  assert not(ax1.yaxis_inverted())

still be valid?

@lkjell
Copy link
Contributor Author

lkjell commented Dec 4, 2017

Think I have everything working now. However, WeakSet is not pickable. Any suggestion on this? Or perhaps just use the old grouper?

@lkjell
Copy link
Contributor Author

lkjell commented Dec 5, 2017

Ok reverting to use Grouper. But now grouper will remove the item from other set as well. Pickle is possible now. Hopefully everything is ok now.

@dstansby dstansby added this to the v2.2 milestone Dec 5, 2017
@lkjell
Copy link
Contributor Author

lkjell commented Dec 6, 2017

Was not aware of #4827. Guess it is reverting to WeakSet implementation and hack getstate and setstate

plt.xlim(0.01, 5.0)

ax3.unshare_y_axes()
ax2.unshare_x_axes()
Copy link
Member

Choose a reason for hiding this comment

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

I didn't try to run this, but its hard to imagine this actually demos anything in a non-interactive session.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You do not create share as usual by setting a share parent axes. Sharing is set after the creation of the axes.

Copy link
Member

Choose a reason for hiding this comment

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

I think demos should be readily readable by users since they are one of the primary ways we document Matplotlib. I find this demo is very cryptic. Why are you un-sharing? Just to show that these functions exist? What effect does this have on the example? The description of the demo should state the usual way of sharing, and this alternate way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm... no comment.

@efiring
Copy link
Member

efiring commented Jan 9, 2018

When you unshare you are taking an Axes out of the set, but it looks to me like more is needed. When the Axes was created as shared, its xaxis.major etc. attributes are made as references to the attributes in the master. This is done in cla. Don't you need to also break those connections, perhaps executing cla for the newly orphaned axis? And similarly, don't you need to make those connections in your new method for sharing?

I haven't looked at this in great detail so I might be missing a lot, but I'm uneasy about this major change in the sharing machinery.

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

There was some behaviour with cla() that I left for backward compatible. That is the parent axes attribute I believe you are referring too. The old shared axes reference will get zero out when that axes is unshared. However, the parent reference will not get propagated to new shared axes. During cla the those attributed are cleared or copy over depending on the parent axes attribute.

Sharing axes is a transitive behavior thus the code will only link the axes together and you need to specify yourself which datalim you want to use. Unlink is therefore similar.

@efiring
Copy link
Member

efiring commented Jan 9, 2018

What do you mean by "transitive behavior"? I saw that terminology in #4827 and I didn't understand it there either.

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

It is a mathematical term. Say A share axes with B, and B share axes with C. Transitive property means that A share axes with C also.

Say you now unlink B and C. With parent axes approach A and C should no longer share but A and B still share.

But if you think about it. Because sharing is transitive. And you did not spesify to unlink A and C. Unlink B and C will also unlink B with A, but A and C still keep the link.

@efiring
Copy link
Member

efiring commented Jan 9, 2018

I see the attraction of using a WeakSet and having what looks like a relationship among equals instead of the master/slave paradigm, but the heart of sharing is not the grouping itself, it is the question of what is actually shared and where it lives. With the master/slave system, the slaves have references to attributes in the master, replacing their own attributes. This is the critical part. Having connect/disconnect functions doesn't make much sense to me without a re-thinking of the handling of these attributes--or some other scheme for switching between shared and single-owner attributes.

Are you assuming that someone using the unshare function will then either call cla on the newly isolated Axes, or manually recreate the missing attributes?

What use case is motivating your work on this?

Looking farther, it might be good to have fine-grained control over sharing, allowing sharing of viewlims but independent locators and formatters, for example. I think that this would require some changes to the way locators and formatters work.

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

The old behaviour is not removed. If you share axes as the old way on axes creation you will get the behaviour you describe. In the old way you could still share the axes after creation, but that feature is sort of hidden. However, there were no way to unshare axes.

I am not quite sure what other attributes the slaves keep reference to. My understanding is that the shared axes kept a reference to the parent axes instance. Upon axes creation cla() is called. This will look if there is a reference to a parent axes. If there is then attributes are copy over. There are no other links than that.

If you are right that the slave keep reference on master's attribute and not a copy then I have to check on that. A simple test is to share the axes as the old way. Unshare. Then change the attribute you mean that are shared in the master axes and check if those changes in the other axes as well.

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

I think I understand what you mean. So sharex.xaxis.major and sharex.xaxis.minor are reference only.
Perhaps instead of reference a copy should be made.

        if sharex is not None:
            # major and minor are class instances with
            # locator and formatter attributes
            self.xaxis.major = sharex.xaxis.major
            self.xaxis.minor = sharex.xaxis.minor
            x0, x1 = sharex.get_xlim()
            self.set_xlim(x0, x1, emit=False, auto=None)
            self.xaxis._scale = mscale.scale_factory(
                    sharex.xaxis.get_scale(), self.xaxis)

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

However, I think the best solution without altering too much. When you unshare. You just make a copy of the attributes.

My work is to compare some plots. Sometimes you want to share the datalim, but you also want the option to disable and look at a different location.

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

I see that doing a deep copy of Axis.major and Axis.minor is not that simple. Therefore I revert the last commit. This just make unshare incompatible with the old behaviour. Doing share axes after creation is compatible with unshare, assuming the sharing group does not have a parent axes.

@efiring
Copy link
Member

efiring commented Jan 9, 2018

I think you are finding is that the present implementation of sharing is more complicated than it appears on the surface. Several mpl components are coupled in not-so-obvious ways. It might be that for your use case, short of a major overhaul of mpl, sharing is not the best solution. Instead, could you use event notification to connect datalims when desired?

@jklymak jklymak modified the milestones: v2.2, v3.0 Jan 9, 2018
@jklymak
Copy link
Member

jklymak commented Jan 9, 2018

@efring (and @lkjell) As we discussed on the phone call, it does seem that all the different ways sharing gets used could use a rethink.

I think @lkjell 's use case is something other interactive users would like, and therefore it would be nice to support. I think this interactive use of "sharing" where the x/y-limits are kept the same so zooms are carried out across axes could/should be disambiguated from the other meaning of sharing: "I want this x-axis to look like this other x-axis" which is a layout problem rather than a communication problem during interactive use.

As discussed, the two don't need as much care when the aspect ratio is kept at auto, but as soon as aspect ratios are involved, the two aspects of sharing become complicated.

So, I support the idea of this PR, but I think it might be more of a feature request. I've bumped to 3.0 because I don't think this will/should go in until after 2.2 does and we have some time to think this through. If other disagree and would like to see it in sooner, by all means switch back!

@efiring
Copy link
Member

efiring commented Jan 9, 2018 via email

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

When 3.0 is released? With all respect the previous PR was 5 years ago. And I am not eager to see this goes to dust. The new sharing mechanism is not compatible with the old. But you could still use it exclusively. Or to make it more distinct a rename to share_datalim is more appropriate?

@jklymak
Copy link
Member

jklymak commented Jan 9, 2018

2.2 is three weeks. 3.0 is meant to be July. I think July is enough time to sort this out, but I don't think three weeks is.

Lets step back and make sure this is all done properly for 3.0 (where we will also have some more leeway to change things.)

@lkjell
Copy link
Contributor Author

lkjell commented Jan 9, 2018

Discussion on when you merge can take another time.

I do want to highlight with current way of sharing axis (datalim) with valid API. Of course there is no way to unlink and you still have the problem with Axis.major and Axis.minor that the new axes you share with is different from parent axes. Now with this it is possible say A->B and C->D.
And now you join B and C together. This is possible with current implementation of sharing.
you just need to access _shared_x_axes attribute from get_shared_x_axes(). The share group would now have two parent axes.

Therefore I suggest that:

  1. Make it possible to do a deep copy of Ticker() instance.
  2. All axes keep their own reference of major and minor.
  3. No more parent axes. cla() will default to the other shared axes attributes.
  4. Share axes means to share datalim.
  5. Share will only link axes together. Updating datalim must be called explicitly. The same with the other attributes.
import numpy as np
import matplotlib.pyplot as plt

t = np.arange(0.01, 5.0, 0.01)
s1 = np.sin(2*np.pi*t)
s2 = np.exp(-t)
s3 = np.sin(4*np.pi*t)
ax1 = plt.subplot(311)
plt.plot(t,s1)
plt.setp(ax1.get_xticklabels(), fontsize=6)

## share x only
ax2 = plt.subplot(312, sharex=ax1)
plt.plot(t, s2)
# make these tick labels invisible
plt.setp( ax2.get_xticklabels(), visible=False)

ax3 = plt.subplot(313)
plt.plot(t, s3)
plt.xlim(0.01,5.0)

# Share x and y axis
ax2.get_shared_x_axes().join(ax1, ax3)
ax2.get_shared_y_axes().join(ax1, ax3)

print(ax1.xaxis.major == ax3.xaxis.major)
print(ax1.xaxis.major == ax2.xaxis.major)

@jklymak
Copy link
Member

jklymak commented Mar 9, 2018

Not fully parsing the above yet, but just for completeness, what are you considering the "default" in the above?

@lkjell
Copy link
Contributor Author

lkjell commented Mar 9, 2018

I would consider 5 as not to break old behaviour. But currently in the PR 4 is the default.

@lkjell
Copy link
Contributor Author

lkjell commented Mar 10, 2018

For testing use commit 837723f for behaviour 4, and commit d31d4fa for behaviour 5.
Latest commit has some suggested behaviour for symmetric=True and transitive=True.
Comment them out to test them. They have their strength and weaknesses. See test code in original post.

@jklymak
Copy link
Member

jklymak commented Mar 11, 2018

I'll cheerfully admit I'm not getting this PR. I understand what you mean by transitive and symmetricbut I'm not understanding why you'd want either of them. If I change A's limits and its shared with B, I expect B's limits to change. If C is shared with B, I expect its limits to change as well. Transitive and symmetric are what I'd always expect, and I don't see the use case for not making shared axes have these properties.

As for "reverse", I have no idea what thats trying to do? Link the self axes with the list of axes, but only in one direction?

I thought I'd try to understand it, but the example above returns the following, which as far as I can tell has no shared y axes? So I'm confused. Not at all clear why it fixes the seaboard issue.

test

@lkjell
Copy link
Contributor Author

lkjell commented Mar 11, 2018

@jklymak sharing will link only. Propagation of viewlimit must be done by user. Transitive and symmetric is by default on. Reverse would do the opposite, i.e ax1.share_x_axes(ax2) = ax2.share_x_axes(ax2, reverse=True).

The example just mimic behaviour, you still need to make twins and share with the correct axes.

@lkjell
Copy link
Contributor Author

lkjell commented Mar 11, 2018

@jklymak pehaps you are right. I am thinking too complicated. Just use from commit
804bd66

@ImportanceOfBeingErnest
Copy link
Member

I do have problems understanding what this PR is doing and especially which problem it is trying to solve.

Currently if you want to share axes you'd use the join method of the Grouper. If you want to unshare axes, you'd use the remove method of the Grouper

import matplotlib.pyplot as plt

fig, (ax, ax2) = plt.subplots(ncols=2)

ax.plot([0,1],[0,1])
ax2.plot([1,2],[1,2])


#share axes
ax.get_shared_y_axes().join(ax,ax2)
ax.autoscale()

#unshare axes
ax.get_shared_y_axes().remove(ax2)
ax.autoscale()
ax2.autoscale()

plt.show()

Also joining operation with the current Grouper are transitive.

import matplotlib.pyplot as plt

fig, (ax, ax2, ax3) = plt.subplots(ncols=3)

ax.plot([0,1],[0,1])
ax2.plot([1,2],[1,2])
ax3.plot([2,3],[2,3])

# join ax - ax2
ax.get_shared_y_axes().join(ax,ax2)
# join ax2 - ax3
ax2.get_shared_y_axes().join(ax2,ax3)
# transitivity is given
print(ax3.get_shared_y_axes().joined(ax,ax3))
ax.autoscale()

plt.show()

So, I think it would be good if there was a problem shown that is currently hard to come by, which this PR would solve.

@lkjell
Copy link
Contributor Author

lkjell commented Jun 5, 2018

@ImportanceOfBeingErnest the discussion is long. The join and remove from Grouper I believe is not intended to be used like that.

The PR fixes pickling, share of formatter and locator, unset parent axes, and a clean API for doing sharing and unsharing after axes creation.

PS. the rebase is not finished.

@ImportanceOfBeingErnest
Copy link
Member

ImportanceOfBeingErnest commented Jun 5, 2018

Precisely because this is very long, could you add example cases for the issues this solves?

If the Grouper methods are not to be used like I did, it would be good to know why.

@lkjell
Copy link
Contributor Author

lkjell commented Jun 5, 2018

Sigh... Some of the problems listed above in the comments will also be a problem with current implementation.

@lkjell
Copy link
Contributor Author

lkjell commented Jun 5, 2018

Small example that shows formatter and locator is left as crufts.

import matplotlib.pyplot as plt

ax1 = plt.subplot(1,2,1)
ax2 = plt.subplot(1,2,2, sharex=ax1)

# ax1.invert_xaxis() #pick one to see different effect
ax2.invert_xaxis()

ax2.get_shared_x_axes().remove(ax2)
ax2.cla()
ax1.cla()

plt.show()

lkjell added 7 commits June 24, 2018 07:26
Sharexy use Weakref instead of cbook.Grouper

Add a share/unshare function to share/unshare both x,y,z axis

remove() unshare axes successfully.

Revert to Grouper. But now grouper remove will also remove from other sets.

unshare will also remove parent/master axes.

unshare also remove parent axes in the orphan axes.

Adding unshare and share tests.

Add what is new.

Adding unshare axis demo.

Revert "Revert to Grouper. But now grouper remove will also remove from other sets."

Converting Weakset to list and back during pickle.

Adding pickle test.

Update tests to use Weakset backend.

Add example of how to share 3D plot.

Add an API breakage message.

change twinx, twiny to use the new share api.

Adding an is sharing axes method.

Fix overline in example too short.

Use the new is_sharing_[x,y]_axes when appropriate

update tests to use is sharing axes methods

Simplify share and unsharing code to one.

remove crufts.

Change quotation marks.

Update descriptions.
Update docs. Unshare axes if related.

Sharing will implicit set adjustable to datalim.

Copy major and minor when unshare axes.
If unshare a parent axes its children will copy a new major minor.
Delete api change file in old api folder.
get shared return a shallow copy.
pep8 is a girl
@jklymak jklymak modified the milestones: v3.0, needs sorting Jul 9, 2018
@mwaskom
Copy link

mwaskom commented Jun 24, 2020

Does #15287 close this?

@anntzer
Copy link
Contributor

anntzer commented Jun 24, 2020

No, it doesn't allow unsharing (I guess mostly because we still can't decide on the exact semantics of that).

@jklymak
Copy link
Member

jklymak commented Dec 20, 2020

#15287 does some of this. The rest is probably a bit of an edge use case. Thanks for the suggestion @lkjell !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pickled Figure Loses sharedx Properties
10 participants