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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions doc/api/next_api_changes/2017-12-06-KL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Change return value of Axes.get_shared_[x,y,z]_axes()
-----------------------------------------------------

The method `matplotlib.Axes.get_shared_x_axes` (and y and z) used to return `~.cbook.Grouper` objects.
Now it returns a `~.weakref.WeakSet` object.

Workarounds:
* If the intention is to get siblings as previous then the WeakSet contains all the siblings.
An example::

sharedx = ax.get_shared_x_axes().get_siblings()
# is now
sharedx = list(ax.get_shared_x_axes())

* If the intention was to use `join` then there is a new share axes method. An example::

ax1.get_shared_x_axes().join(ax1, ax2)
# is now
ax1.share_x_axes(ax2)

* If the intention was to check if two elements are in the same group then use the `in` operator. An example::

ax1.get_shared_x_axes().joined(ax1, ax2)
# is now
ax2 in ax1.get_shared_x_axes()

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Remove share through `matplotlib.Axes.get_shared_{x,y,z}_axes`
--------------------------------------------------------------

Previously when different axes are created with different parent/master axes,
the share would still be symmetric and transitive if an unconventional
method through `matplotlib.Axes.get_shared_x_axes`
is used to share the axes after creation. With the new sharing mechanism
this is no longer possible.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Share and unshare `axes` after creation
---------------------------------------

`matplotlib.axes.Axes` have `matplotlib.axes.Axes.unshare_x_axes`,
`matplotlib.axes.Axes.unshare_y_axes`, `matplotlib.axes.Axes.unshare_z_axes`
and `matplotlib.axes.Axes.unshare_axes` methods to unshare axes.
Similiar there are `matplotlib.axes.Axes.share_x_axes`,
`matplotlib.axes.Axes.share_y_axes`, `matplotlib.axes.Axes.share_z_axes` and
`matplotlib.axes.Axes.share_axes` methods to share axes.

Unshare an axis will decouple the viewlimits for further changes.
Share an axis will couple the viewlimits.
39 changes: 39 additions & 0 deletions examples/mplot3d/share_unshare_3d_axes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
============================================
Parametric Curve with Share and Unshare Axes
============================================

This example demonstrates plotting a parametric curve in 3D,
and how to share and unshare 3D plot axes.
"""
import matplotlib as mpl
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt

mpl.rcParams['legend.fontsize'] = 10

# Prepare arrays x, y, z
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z ** 2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)

fig = plt.figure()
ax = fig.add_subplot(311, projection='3d')

ax.plot(x, y, z, label='parametric curve')
ax.legend()

ax1 = fig.add_subplot(312)
ax1.plot(range(10))
ax1.share_axes(ax)

ax2 = fig.add_subplot(313, projection='3d', sharex=ax)
ax2.plot(x, y, z)

ax2.unshare_x_axes()
ax2.share_z_axes(ax)

plt.show()
36 changes: 36 additions & 0 deletions examples/subplots_axes_and_figures/unshare_axis_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
======================
Unshare and share axis
======================

The example shows how to share and unshare axes after they are created.
"""

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()
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.


plt.show()
145 changes: 119 additions & 26 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from collections import OrderedDict
from weakref import WeakSet
import copy
import itertools
import logging
import math
Expand Down Expand Up @@ -400,8 +402,6 @@ class _AxesBase(martist.Artist):
"""
name = "rectilinear"

_shared_x_axes = cbook.Grouper()
_shared_y_axes = cbook.Grouper()
_twinned_axes = cbook.Grouper()

def __str__(self):
Expand Down Expand Up @@ -482,12 +482,18 @@ def __init__(self, fig, rect,
self._aspect = 'auto'
self._adjustable = 'box'
self._anchor = 'C'
#Adding yourself to shared xy axes. Reflexive.
self._shared_x_axes = WeakSet([self])
self._shared_y_axes = WeakSet([self])
self._sharex = sharex
self._sharey = sharey

if sharex is not None:
self._shared_x_axes.join(self, sharex)
self.share_x_axes(sharex)

if sharey is not None:
self._shared_y_axes.join(self, sharey)
self.share_y_axes(sharey)

self.set_label(label)
self.set_figure(fig)

Expand Down Expand Up @@ -571,10 +577,14 @@ def __getstate__(self):
state.pop('_layoutbox')
state.pop('_poslayoutbox')

state['_shared_x_axes'] = list(self._shared_x_axes)
state['_shared_y_axes'] = list(self._shared_y_axes)
return state

def __setstate__(self, state):
self.__dict__ = state
self._shared_x_axes = WeakSet(state['_shared_x_axes'])
self._shared_y_axes = WeakSet(state['_shared_y_axes'])
self._stale = True
self._layoutbox = None
self._poslayoutbox = None
Expand Down Expand Up @@ -1102,8 +1112,6 @@ def cla(self):
self.xaxis.set_clip_path(self.patch)
self.yaxis.set_clip_path(self.patch)

self._shared_x_axes.clean()
self._shared_y_axes.clean()
if self._sharex:
self.xaxis.set_visible(xaxis_visible)
self.patch.set_visible(patch_visible)
Expand Down Expand Up @@ -1280,8 +1288,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
if not (isinstance(aspect, str) and aspect in ('equal', 'auto')):
aspect = float(aspect) # raise ValueError if necessary
if share:
axes = set(self._shared_x_axes.get_siblings(self)
+ self._shared_y_axes.get_siblings(self))
axes = set(self._shared_x_axes | self._shared_y_axes)
else:
axes = [self]
for ax in axes:
Expand Down Expand Up @@ -1335,8 +1342,7 @@ def set_adjustable(self, adjustable, share=False):
if adjustable not in ('box', 'datalim', 'box-forced'):
raise ValueError("argument must be 'box', or 'datalim'")
if share:
axes = set(self._shared_x_axes.get_siblings(self)
+ self._shared_y_axes.get_siblings(self))
axes = set(self._shared_x_axes | self._shared_y_axes)
else:
axes = [self]
for ax in axes:
Expand Down Expand Up @@ -1403,8 +1409,7 @@ def set_anchor(self, anchor, share=False):
raise ValueError('argument must be among %s' %
', '.join(mtransforms.Bbox.coefs))
if share:
axes = set(self._shared_x_axes.get_siblings(self)
+ self._shared_y_axes.get_siblings(self))
axes = set(self._shared_x_axes | self._shared_y_axes)
else:
axes = [self]
for ax in axes:
Expand Down Expand Up @@ -1556,8 +1561,8 @@ def apply_aspect(self, position=None):
xm = 0
ym = 0

shared_x = self in self._shared_x_axes
shared_y = self in self._shared_y_axes
shared_x = self.is_sharing_x_axes()
shared_y = self.is_sharing_y_axes()
# Not sure whether we need this check:
if shared_x and shared_y:
raise RuntimeError("adjustable='datalim' is not allowed when both"
Expand Down Expand Up @@ -2389,8 +2394,7 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval,
if not (scale and autoscaleon):
return # nothing to do...

shared = shared_axes.get_siblings(self)
dl = [ax.dataLim for ax in shared]
dl = [ax.dataLim for ax in shared_axes]
# ignore non-finite data limits if good limits exist
finite_dl = [d for d in dl if np.isfinite(d).all()]
if len(finite_dl):
Expand Down Expand Up @@ -3126,7 +3130,7 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
if emit:
self.callbacks.process('xlim_changed', self)
# Call all of the other x-axes that are shared with this one
for other in self._shared_x_axes.get_siblings(self):
for other in self._shared_x_axes:
if other is not self:
other.set_xlim(self.viewLim.intervalx,
emit=False, auto=auto)
Expand Down Expand Up @@ -3165,8 +3169,7 @@ def set_xscale(self, value, **kwargs):

matplotlib.scale.LogisticTransform : logit transform
"""
g = self.get_shared_x_axes()
for ax in g.get_siblings(self):
for ax in self._shared_x_axes:
ax.xaxis._set_scale(value, **kwargs)
ax._update_transScale()
ax.stale = True
Expand Down Expand Up @@ -3459,7 +3462,7 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
if emit:
self.callbacks.process('ylim_changed', self)
# Call all of the other y-axes that are shared with this one
for other in self._shared_y_axes.get_siblings(self):
for other in self._shared_y_axes:
if other is not self:
other.set_ylim(self.viewLim.intervaly,
emit=False, auto=auto)
Expand Down Expand Up @@ -3498,8 +3501,7 @@ def set_yscale(self, value, **kwargs):

matplotlib.scale.LogisticTransform : logit transform
"""
g = self.get_shared_y_axes()
for ax in g.get_siblings(self):
for ax in self._shared_y_axes:
ax.yaxis._set_scale(value, **kwargs)
ax._update_transScale()
ax.stale = True
Expand Down Expand Up @@ -4231,9 +4233,100 @@ def twiny(self):
return ax2

def get_shared_x_axes(self):
"""Return a reference to the shared axes Grouper object for x axes."""
return self._shared_x_axes
"""Return a copy of the shared axes Weakset object for x axes"""
return WeakSet(self._shared_x_axes)

def get_shared_y_axes(self):
"""Return a reference to the shared axes Grouper object for y axes."""
return self._shared_y_axes
"""Return a copy of the shared axes Weakset object for y axes"""
return WeakSet(self._shared_y_axes)

def is_sharing_x_axes(self):
return len(self.get_shared_x_axes()) > 1

def is_sharing_y_axes(self):
return len(self.get_shared_y_axes()) > 1

def _unshare_axes(self, shared_axes):
for ax in getattr(self, "_shared_{}_axes".format(shared_axes)):
parent = getattr(ax, "_share{}".format(shared_axes))

if parent is self:
setattr(ax, "_share{}".format(shared_axes), None)
self._copy_axis_major_minor(
getattr(ax, "{}axis".format(shared_axes)))

getattr(self, "_shared_{}_axes".format(shared_axes)).remove(self)
setattr(self, "_shared_{}_axes".format(shared_axes), WeakSet([self]))
setattr(self, "_share{}".format(shared_axes), None)

self._copy_axis_major_minor(
getattr(self, "{}axis".format(shared_axes)))

@staticmethod
def _copy_axis_major_minor(axis):
major = axis.major
minor = axis.minor

axis.major = copy.deepcopy(major)
axis.minor = copy.deepcopy(minor)

axis.major.set_axis(axis)
axis.minor.set_axis(axis)

def unshare_x_axes(self):
""" Unshare x axis. """
self._unshare_axes("x")

def unshare_y_axes(self):
""" Unshare y axis. """
self._unshare_axes("y")

def unshare_axes(self):
""" Unshare both x and y axes. """
self.unshare_x_axes()
self.unshare_y_axes()

def _share_axes(self, axes, shared_axes):
if not iterable(axes):
axes = [axes]

shared = getattr(self, "_shared_{}_axes".format(shared_axes))
for ax in axes:
shared |= getattr(ax, "_shared_{}_axes".format(shared_axes))

for ax in shared:
setattr(ax, "_shared_{}_axes".format(shared_axes), shared)

def share_x_axes(self, axes):
"""
Share x axis.

Parameters
----------
axes: Axes
Axes to share.
"""
self._share_axes(axes, 'x')

def share_y_axes(self, axes):
"""
Share y axis.

Parameters
----------
axes: Axes
Axes to share.
"""
self._share_axes(axes, 'y')

def share_axes(self, axes):
"""
Share both x and y axes.

Parameters
----------
axes: Axes
Axes to share.
"""
self.share_x_axes(axes)
self.share_y_axes(axes)
Loading