From e157edce1002906f1ca9f77824713a355422b195 Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Mon, 4 Dec 2017 05:48:39 +0100 Subject: [PATCH 1/7] Implementing shareaxis. 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. --- doc/api/api_changes/2017-12-06-KL.rst | 26 +++ ...2-05_share_unshare_axes_after_creation.rst | 8 + examples/mplot3d/share_unshare_3d_axes.py | 39 ++++ .../unshare_axis_demo.py | 36 ++++ lib/matplotlib/axes/_base.py | 167 +++++++++++++++--- lib/matplotlib/axis.py | 26 +++ lib/matplotlib/backend_bases.py | 4 +- lib/matplotlib/backend_tools.py | 4 +- lib/matplotlib/figure.py | 18 +- lib/matplotlib/tests/test_axes.py | 112 ++++++++++++ lib/matplotlib/tests/test_pickle.py | 39 ++++ lib/mpl_toolkits/mplot3d/axes3d.py | 91 ++++++++-- 12 files changed, 522 insertions(+), 48 deletions(-) create mode 100644 doc/api/api_changes/2017-12-06-KL.rst create mode 100644 doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst create mode 100644 examples/mplot3d/share_unshare_3d_axes.py create mode 100644 examples/subplots_axes_and_figures/unshare_axis_demo.py diff --git a/doc/api/api_changes/2017-12-06-KL.rst b/doc/api/api_changes/2017-12-06-KL.rst new file mode 100644 index 000000000000..118d77310836 --- /dev/null +++ b/doc/api/api_changes/2017-12-06-KL.rst @@ -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() + diff --git a/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst b/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst new file mode 100644 index 000000000000..78814ff3b91c --- /dev/null +++ b/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst @@ -0,0 +1,8 @@ +Share and unshare `axes` after creation +--------------------------------------- + +`~.Axes` have `~.Axes.unshare_x_axes`, `~.Axes.unshare_y_axes`, `~.Axes.unshare_z_axes` and `~.Axes.unshare_axes` methods to unshare axes. +Similiar there are `~.Axes.share_x_axes`, `~.Axes.share_y_axes`, `~.Axes.share_z_axes` and `~.Axes.share_axes` methods to share axes. + +Unshare an axis will decouple the viewlimits for further changes. +Share an axis will couple the viewlimits. \ No newline at end of file diff --git a/examples/mplot3d/share_unshare_3d_axes.py b/examples/mplot3d/share_unshare_3d_axes.py new file mode 100644 index 000000000000..04112e6acfc5 --- /dev/null +++ b/examples/mplot3d/share_unshare_3d_axes.py @@ -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(ax) +ax2.share_z_axes(ax) + +plt.show() diff --git a/examples/subplots_axes_and_figures/unshare_axis_demo.py b/examples/subplots_axes_and_figures/unshare_axis_demo.py new file mode 100644 index 000000000000..8baa48128fd7 --- /dev/null +++ b/examples/subplots_axes_and_figures/unshare_axis_demo.py @@ -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() + +plt.show() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index c201c09b9189..3c1481e8d9a3 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1,4 +1,6 @@ from collections import OrderedDict +from weakref import WeakSet +import copy import itertools import logging import math @@ -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): @@ -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) @@ -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 @@ -989,6 +999,7 @@ def cla(self): self.ignore_existing_data_limits = True self.callbacks = cbook.CallbackRegistry() + if self._sharex is not None: # major and minor are axis.Ticker class instances with # locator and formatter attributes @@ -1102,8 +1113,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) @@ -1335,8 +1344,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: @@ -1403,8 +1411,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: @@ -1556,8 +1563,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" @@ -2389,8 +2396,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): @@ -3126,7 +3132,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) @@ -3165,8 +3171,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 @@ -3459,7 +3464,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) @@ -3498,8 +3503,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 @@ -4231,9 +4235,128 @@ def twiny(self): return ax2 def get_shared_x_axes(self): - """Return a reference to the shared axes Grouper object for x axes.""" + """Return a copy of the shared axes Weakset object for x axes""" return self._shared_x_axes def get_shared_y_axes(self): - """Return a reference to the shared axes Grouper object for y axes.""" + """Return a copy of the shared axes Weakset object for y axes""" return 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, parent): + children = [] + + for ax in getattr(self, shared_axes): + if getattr(ax, parent) is self: + setattr(ax, parent, None) + children.append(ax) + + getattr(self, shared_axes).remove(self) + setattr(self, shared_axes, WeakSet([self])) + setattr(self, parent, None) + + return children + + @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, axes=None): + """ + Unshare x axis. + + Parameters + ---------- + axes: Axes + Axes to unshare, if related. None will unshare itself. + """ + if axes is None or axes is self: + children = self._unshare_axes('_shared_x_axes', '_sharex') + for ax in children: + self._copy_axis_major_minor(ax.xaxis) + self._copy_axis_major_minor(self.xaxis) + elif axes in self._shared_x_axes: + axes.unshare_x_axes() + + def unshare_y_axes(self, axes=None): + """ + Unshare y axis. + + Parameters + ---------- + axes: Axes + Axes to unshare, if related. None will unshare itself. + """ + if axes is None or axes is self: + children = self._unshare_axes('_shared_y_axes', '_sharey') + for ax in children: + self._copy_axis_major_minor(ax.yaxis) + self._copy_axis_major_minor(self.yaxis) + elif axes in self._shared_y_axes: + axes.unshare_y_axes() + + def unshare_axes(self, axes=None): + """ + Unshare both x and y axes. + + Parameters + ---------- + axes: Axes + Axes to unshare, if related. None will unshare itself. + """ + self.unshare_x_axes(axes) + self.unshare_y_axes(axes) + + def _share_axes(self, axes, shared_axes): + shared = getattr(self, shared_axes) + shared |= getattr(axes, shared_axes) + + for ax in shared: + setattr(ax, shared_axes, shared) + ax._adjustable = 'datalim' + + def share_x_axes(self, axes): + """ + Share x axis. + + Parameters + ---------- + axes: Axes + Axes to share. + """ + self._share_axes(axes, '_shared_x_axes') + + def share_y_axes(self, axes): + """ + Share y axis. + + Parameters + ---------- + axes: Axes + Axes to share. + """ + self._share_axes(axes, '_shared_y_axes') + + 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) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 8ca2209b3a0f..83ad56b50626 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -7,6 +7,7 @@ import warnings import numpy as np +import copy from matplotlib import rcParams import matplotlib.artist as artist @@ -651,6 +652,31 @@ class Ticker(object): locator = None formatter = None + def __deepcopy__(self, memodict={}): + """ + Perform deep copy by disable axis attribute from + locator and formatter. + + """ + axisLocator = self.locator.axis + axisFormatter = self.formatter.axis + self.locator.axis = None + self.formatter.axis = None + + cls = self.__class__ + result = cls.__new__(cls) + result.locator = copy.deepcopy(self.locator) + result.formatter = copy.deepcopy(self.formatter) + + self.locator.axis = axisLocator + self.formatter.axis = axisFormatter + + return result + + def set_axis(self, axis): + self.locator.axis = axis + self.formatter.axis = axis + class _LazyTickList(object): """ diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 54728fe05d6b..ffa98ed4e147 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2922,9 +2922,9 @@ def release_zoom(self, event): twinx, twiny = False, False if last_a: for la in last_a: - if a.get_shared_x_axes().joined(a, la): + if la in a.get_shared_x_axes(): twinx = True - if a.get_shared_y_axes().joined(a, la): + if la in a.get_shared_y_axes(): twiny = True last_a.append(a) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 803ce1c02c26..d8b29d435de6 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -934,9 +934,9 @@ def _release(self, event): twinx, twiny = False, False if last_a: for la in last_a: - if a.get_shared_x_axes().joined(a, la): + if la in a.get_shared_x_axes(): twinx = True - if a.get_shared_y_axes().joined(a, la): + if la in a.get_shared_y_axes(): twiny = True last_a.append(a) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 81ecfe9781bd..514648d82385 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1395,21 +1395,17 @@ def _reset_loc_form(axis): axis.set_minor_formatter(axis.get_minor_formatter()) axis.set_minor_locator(axis.get_minor_locator()) - def _break_share_link(ax, grouper): - siblings = grouper.get_siblings(ax) - if len(siblings) > 1: - grouper.remove(ax) - for last_ax in siblings: - if ax is not last_ax: - return last_ax - return None - self.delaxes(ax) - last_ax = _break_share_link(ax, ax._shared_y_axes) + + shared_y_axes = ax._shared_y_axes + shared_x_axes = ax._shared_x_axes + ax.unshare_axes() + + last_ax = next((a for a in shared_y_axes if a is not ax), None) if last_ax is not None: _reset_loc_form(last_ax.yaxis) - last_ax = _break_share_link(ax, ax._shared_x_axes) + last_ax = next((a for a in shared_x_axes if a is not ax), None) if last_ax is not None: _reset_loc_form(last_ax.xaxis) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index fda0fe0f362b..50dd78f28f86 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5719,3 +5719,115 @@ def test_tick_padding_tightbbox(): bb2 = ax.get_window_extent(fig.canvas.get_renderer()) assert bb.x0 < bb2.x0 assert bb.y0 < bb2.y0 + + +def test_share_unshare_axes(): + fig = plt.figure() + ax1 = fig.add_subplot(211) + ax2 = fig.add_subplot(212, sharex=ax1, sharey=ax1) + + # Testing unsharing + ax1.unshare_axes() + assert ax2._sharex is None + assert ax2._sharey is None + + sharedx = ax2._shared_x_axes + sharedy = ax2._shared_y_axes + + assert ax1 not in sharedx + assert ax1 not in sharedy + + sharedx = ax1._shared_x_axes + sharedy = ax1._shared_y_axes + + assert ax2 not in sharedx + assert ax2 not in sharedy + + assert not ax1.is_sharing_x_axes() + assert not ax1.is_sharing_y_axes() + + assert not ax2.is_sharing_x_axes() + assert not ax2.is_sharing_y_axes() + + # Testing sharing + ax1.share_x_axes(ax2) + + sharedx = ax1._shared_x_axes + assert ax2 in sharedx + + sharedx = ax2._shared_x_axes + assert ax1 in sharedx + + ax2.share_y_axes(ax1) + + sharedy = ax1._shared_y_axes + assert ax2 in sharedy + + sharedy = ax2._shared_y_axes + assert ax1 in sharedy + + assert ax1.is_sharing_x_axes() + assert ax1.is_sharing_y_axes() + + assert ax2.is_sharing_x_axes() + assert ax2.is_sharing_y_axes() + + +def test_share_unshare_3d_axes(): + fig = plt.figure() + ax1 = fig.add_subplot(211, projection="3d") + ax2 = fig.add_subplot(212, projection="3d", + sharex=ax1, + sharey=ax1, + sharez=ax1) + + # Testing unsharing + ax1.unshare_axes() + assert ax2._sharex is None + assert ax2._sharey is None + assert ax2._sharez is None + + sharedx = ax2._shared_x_axes + sharedy = ax2._shared_y_axes + sharedz = ax2._shared_z_axes + + assert ax1 not in sharedx + assert ax1 not in sharedy + assert ax1 not in sharedz + + sharedx = ax1._shared_x_axes + sharedy = ax1._shared_y_axes + sharedz = ax1._shared_z_axes + + assert ax2 not in sharedx + assert ax2 not in sharedy + assert ax2 not in sharedz + + # Testing sharing + + # x axis + ax1.share_x_axes(ax2) + + sharedx = ax1._shared_x_axes + assert ax2 in sharedx + + sharedx = ax2._shared_x_axes + assert ax1 in sharedx + + # y axis + ax2.share_y_axes(ax1) + + sharedy = ax1._shared_y_axes + assert ax2 in sharedy + + sharedy = ax2._shared_y_axes + assert ax1 in sharedy + + # z axis + ax1.share_z_axes(ax2) + + sharedz = ax2._shared_z_axes + assert ax1 in sharedz + + sharedz = ax1._shared_z_axes + assert ax2 in sharedz diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 719dc7356fc8..66d461e7d6f1 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -147,6 +147,45 @@ def test_polar(): plt.draw() +def test_sharing_axes(): + fig = plt.figure() + ax1 = fig.add_subplot(211) + ax2 = fig.add_subplot(212, sharex=ax1, sharey=ax1) + + pf = pickle.dumps(fig) + fig1 = pickle.loads(pf) + + ax = fig1.axes + + assert ax[0] in ax[1]._shared_x_axes + assert ax[0] in ax[1]._shared_y_axes + + assert ax[1] in ax[0]._shared_x_axes + assert ax[1] in ax[0]._shared_y_axes + + +def test_sharing_3d_axes(): + fig = plt.figure() + ax1 = fig.add_subplot(211, projection="3d") + ax2 = fig.add_subplot(212, projection="3d", + sharex=ax1, + sharey=ax1, + sharez=ax1) + + pf = pickle.dumps(fig) + fig1 = pickle.loads(pf) + + ax = fig1.axes + + assert ax[0] in ax[1]._shared_x_axes + assert ax[0] in ax[1]._shared_y_axes + assert ax[0] in ax[1]._shared_z_axes + + assert ax[1] in ax[0]._shared_x_axes + assert ax[1] in ax[0]._shared_y_axes + assert ax[1] in ax[0]._shared_z_axes + + class TransformBlob(object): def __init__(self): self.identity = mtransforms.IdentityTransform() diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 16bb0ebec65e..f37602dbb3dd 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -13,6 +13,8 @@ from collections import defaultdict import math import warnings +import copy +from weakref import WeakSet import numpy as np @@ -44,7 +46,6 @@ class Axes3D(Axes): 3D axes object. """ name = '3d' - _shared_z_axes = cbook.Grouper() def __init__( self, fig, rect=None, *args, @@ -90,10 +91,11 @@ def __init__( self.view_init(self.initial_elev, self.initial_azim) self._ready = 0 + self._shared_z_axes = WeakSet([self]) self._sharez = sharez + if sharez is not None: - self._shared_z_axes.join(self, sharez) - self._adjustable = 'datalim' + self.share_z_axes(sharez) super().__init__(fig, rect, frameon=True, *args, **kwargs) # Disable drawing of axes by base class @@ -125,6 +127,17 @@ def __init__( self.figure.add_axes(self) + def __getstate__(self): + state = super(Axes3D, self).__getstate__() + state['_shared_z_axes'] = list(self._shared_z_axes) + + return state + + def __setstate__(self, state): + self.__dict__ = state + + self._shared_z_axes = WeakSet(state['_shared_z_axes']) + def set_axis_off(self): self._axis3don = False self.stale = True @@ -640,7 +653,7 @@ def set_xlim3d(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.xy_viewLim.intervalx, emit=False, auto=auto) @@ -698,7 +711,7 @@ def set_ylim3d(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.xy_viewLim.intervaly, emit=False, auto=auto) @@ -756,7 +769,7 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, if emit: self.callbacks.process('zlim_changed', self) # Call all of the other y-axes that are shared with this one - for other in self._shared_z_axes.get_siblings(self): + for other in self._shared_z_axes: if other is not self: other.set_zlim(self.zz_viewLim.intervalx, emit=False, auto=auto) @@ -1105,12 +1118,14 @@ def cla(self): super().cla() self.zaxis.cla() - if self._sharez is not None: - self.zaxis.major = self._sharez.zaxis.major - self.zaxis.minor = self._sharez.zaxis.minor - z0, z1 = self._sharez.get_zlim() + sharez = self._sharez + + if sharez is not None: + self.zaxis.major = sharez.zaxis.major + self.zaxis.minor = sharez.zaxis.minor + z0, z1 = sharez.get_zlim() self.set_zlim(z0, z1, emit=False, auto=None) - self.zaxis._set_scale(self._sharez.zaxis.get_scale()) + self.zaxis._set_scale(sharez.zaxis.get_scale()) else: self.zaxis._set_scale('linear') try: @@ -2885,6 +2900,60 @@ def permutation_matrices(n): return polygons + def unshare_z_axes(self, axes=None): + """ + Unshare z axis. + + Parameters + ---------- + axes: Axes + Axes to unshare, if related. None will unshare itself. + """ + if axes is None or axes is self: + children = self._unshare_axes('_shared_z_axes', '_sharez') + for ax in children: + self._copy_axis_major_minor(ax.zaxis) + self._copy_axis_major_minor(self.zaxis) + elif axes in self._shared_z_axes: + axes.unshare_z_axes() + + def unshare_axes(self, axes=None): + """ + Unshare x, y and z axes. + + Parameters + ---------- + axes: Axes + Axes to unshare, if related. None will unshare itself. + """ + self.unshare_x_axes(axes) + self.unshare_y_axes(axes) + self.unshare_z_axes(axes) + + def share_z_axes(self, axes): + """ + Share z axis. + + Parameters + ---------- + axes: Axes + Axes to share. + """ + self._share_axes(axes, '_shared_z_axes') + + def share_axes(self, axes): + """ + Share x, y, z axes. + + Parameters + ---------- + axes: Axes + Axes to share. + """ + self.share_x_axes(axes) + self.share_y_axes(axes) + self.share_z_axes(axes) + def get_test_data(delta=0.05): ''' From df83b529c5ea256ea13a997d54ee3058b71a43f6 Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Wed, 28 Feb 2018 07:07:16 +0100 Subject: [PATCH 2/7] Fix test related to sharing --- lib/matplotlib/axes/_base.py | 5 ++--- lib/matplotlib/tests/test_subplots.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 3c1481e8d9a3..225d1e44dead 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1289,8 +1289,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: @@ -4325,7 +4324,7 @@ def _share_axes(self, axes, shared_axes): for ax in shared: setattr(ax, shared_axes, shared) - ax._adjustable = 'datalim' + # ax._adjustable = 'datalim' def share_x_axes(self, axes): """ diff --git a/lib/matplotlib/tests/test_subplots.py b/lib/matplotlib/tests/test_subplots.py index 9a1a5e7f7a8d..76ef9ecca089 100644 --- a/lib/matplotlib/tests/test_subplots.py +++ b/lib/matplotlib/tests/test_subplots.py @@ -19,8 +19,10 @@ def check_shared(axs, x_shared, y_shared): enumerate(zip("xy", [x_shared, y_shared]))): if i2 <= i1: continue + share = getattr(ax1, "_shared_{}_axes".format(name)) + share = ax2 in share assert \ - (getattr(axs[0], "_shared_{}_axes".format(name)).joined(ax1, ax2) + (share == shared[i1, i2]), \ "axes %i and %i incorrectly %ssharing %s axis" % ( i1, i2, "not " if shared[i1, i2] else "", name) From 6817e547690e3fe05086db988edf7d55b7e4809d Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Sun, 11 Mar 2018 00:17:38 +0100 Subject: [PATCH 3/7] Update api changes. Delete api change file in old api folder. --- .../{api_changes => next_api_changes}/2017-12-06-KL.rst | 2 +- .../remove_old_share_axes_after_creation.rst | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) rename doc/api/{api_changes => next_api_changes}/2017-12-06-KL.rst (93%) create mode 100644 doc/api/next_api_changes/remove_old_share_axes_after_creation.rst diff --git a/doc/api/api_changes/2017-12-06-KL.rst b/doc/api/next_api_changes/2017-12-06-KL.rst similarity index 93% rename from doc/api/api_changes/2017-12-06-KL.rst rename to doc/api/next_api_changes/2017-12-06-KL.rst index 118d77310836..1321454ebbff 100644 --- a/doc/api/api_changes/2017-12-06-KL.rst +++ b/doc/api/next_api_changes/2017-12-06-KL.rst @@ -1,5 +1,5 @@ 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. diff --git a/doc/api/next_api_changes/remove_old_share_axes_after_creation.rst b/doc/api/next_api_changes/remove_old_share_axes_after_creation.rst new file mode 100644 index 000000000000..067533834e4b --- /dev/null +++ b/doc/api/next_api_changes/remove_old_share_axes_after_creation.rst @@ -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. From fec300740731d981cf4e400726bdfd1b99402bfa Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Wed, 14 Mar 2018 05:07:21 +0100 Subject: [PATCH 4/7] Simplify unshare code. get shared return a shallow copy. --- lib/matplotlib/axes/_base.py | 80 +++++++++--------------------- lib/mpl_toolkits/mplot3d/axes3d.py | 38 ++++---------- 2 files changed, 33 insertions(+), 85 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 225d1e44dead..6b99b33f4842 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4235,11 +4235,11 @@ def twiny(self): def get_shared_x_axes(self): """Return a copy of the shared axes Weakset object for x axes""" - return self._shared_x_axes + return WeakSet(self._shared_x_axes) def get_shared_y_axes(self): """Return a copy of the shared axes Weakset object for y axes""" - return self._shared_y_axes + return WeakSet(self._shared_y_axes) def is_sharing_x_axes(self): return len(self.get_shared_x_axes()) > 1 @@ -4247,19 +4247,21 @@ def is_sharing_x_axes(self): def is_sharing_y_axes(self): return len(self.get_shared_y_axes()) > 1 - def _unshare_axes(self, shared_axes, parent): - children = [] + def _unshare_axes(self, shared_axes): + for ax in getattr(self, "_shared_{}_axes".format(shared_axes)): + parent = getattr(ax, "_share{}".format(shared_axes)) - for ax in getattr(self, shared_axes): - if getattr(ax, parent) is self: - setattr(ax, parent, None) - children.append(ax) + 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).remove(self) - setattr(self, shared_axes, WeakSet([self])) - setattr(self, parent, None) + 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) - return children + self._copy_axis_major_minor( + getattr(self, "{}axis".format(shared_axes))) @staticmethod def _copy_axis_major_minor(axis): @@ -4272,51 +4274,18 @@ def _copy_axis_major_minor(axis): axis.major.set_axis(axis) axis.minor.set_axis(axis) - def unshare_x_axes(self, axes=None): - """ - Unshare x axis. - - Parameters - ---------- - axes: Axes - Axes to unshare, if related. None will unshare itself. - """ - if axes is None or axes is self: - children = self._unshare_axes('_shared_x_axes', '_sharex') - for ax in children: - self._copy_axis_major_minor(ax.xaxis) - self._copy_axis_major_minor(self.xaxis) - elif axes in self._shared_x_axes: - axes.unshare_x_axes() - - def unshare_y_axes(self, axes=None): - """ - Unshare y axis. - - Parameters - ---------- - axes: Axes - Axes to unshare, if related. None will unshare itself. - """ - if axes is None or axes is self: - children = self._unshare_axes('_shared_y_axes', '_sharey') - for ax in children: - self._copy_axis_major_minor(ax.yaxis) - self._copy_axis_major_minor(self.yaxis) - elif axes in self._shared_y_axes: - axes.unshare_y_axes() + def unshare_x_axes(self): + """ Unshare x axis. """ + self._unshare_axes("x") - def unshare_axes(self, axes=None): - """ - Unshare both x and y axes. + def unshare_y_axes(self): + """ Unshare y axis. """ + self._unshare_axes("y") - Parameters - ---------- - axes: Axes - Axes to unshare, if related. None will unshare itself. - """ - self.unshare_x_axes(axes) - self.unshare_y_axes(axes) + 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): shared = getattr(self, shared_axes) @@ -4324,7 +4293,6 @@ def _share_axes(self, axes, shared_axes): for ax in shared: setattr(ax, shared_axes, shared) - # ax._adjustable = 'datalim' def share_x_axes(self, axes): """ diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index f37602dbb3dd..790c255f6ddc 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2900,35 +2900,15 @@ def permutation_matrices(n): return polygons - def unshare_z_axes(self, axes=None): - """ - Unshare z axis. - - Parameters - ---------- - axes: Axes - Axes to unshare, if related. None will unshare itself. - """ - if axes is None or axes is self: - children = self._unshare_axes('_shared_z_axes', '_sharez') - for ax in children: - self._copy_axis_major_minor(ax.zaxis) - self._copy_axis_major_minor(self.zaxis) - elif axes in self._shared_z_axes: - axes.unshare_z_axes() - - def unshare_axes(self, axes=None): - """ - Unshare x, y and z axes. - - Parameters - ---------- - axes: Axes - Axes to unshare, if related. None will unshare itself. - """ - self.unshare_x_axes(axes) - self.unshare_y_axes(axes) - self.unshare_z_axes(axes) + def unshare_z_axes(self): + """ Unshare z axis. """ + self._unshare_axes("z") + + def unshare_axes(self): + """ Unshare x, y and z axes. """ + self.unshare_x_axes() + self.unshare_y_axes() + self.unshare_z_axes() def share_z_axes(self, axes): """ From 4febb39c5b8c40d5be8b14995185118d732a47ee Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Wed, 14 Mar 2018 11:50:17 +0100 Subject: [PATCH 5/7] Simplify share code. --- lib/matplotlib/axes/_base.py | 14 +++++++++----- lib/mpl_toolkits/mplot3d/axes3d.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6b99b33f4842..00936bac03aa 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4288,11 +4288,15 @@ def unshare_axes(self): self.unshare_y_axes() def _share_axes(self, axes, shared_axes): - shared = getattr(self, shared_axes) - shared |= getattr(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, shared) + setattr(ax, "_shared_{}_axes".format(shared_axes), shared) def share_x_axes(self, axes): """ @@ -4303,7 +4307,7 @@ def share_x_axes(self, axes): axes: Axes Axes to share. """ - self._share_axes(axes, '_shared_x_axes') + self._share_axes(axes, 'x') def share_y_axes(self, axes): """ @@ -4314,7 +4318,7 @@ def share_y_axes(self, axes): axes: Axes Axes to share. """ - self._share_axes(axes, '_shared_y_axes') + self._share_axes(axes, 'y') def share_axes(self, axes): """ diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 790c255f6ddc..e71dce9a1af3 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2919,7 +2919,7 @@ def share_z_axes(self, axes): axes: Axes Axes to share. """ - self._share_axes(axes, '_shared_z_axes') + self._share_axes(axes, 'z') def share_axes(self, axes): """ From 765ce7720561100c7a7595a28d58b4a941e65f55 Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Wed, 14 Mar 2018 12:05:50 +0100 Subject: [PATCH 6/7] Fix example unshare code. --- examples/mplot3d/share_unshare_3d_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mplot3d/share_unshare_3d_axes.py b/examples/mplot3d/share_unshare_3d_axes.py index 04112e6acfc5..6568ae025292 100644 --- a/examples/mplot3d/share_unshare_3d_axes.py +++ b/examples/mplot3d/share_unshare_3d_axes.py @@ -33,7 +33,7 @@ ax2 = fig.add_subplot(313, projection='3d', sharex=ax) ax2.plot(x, y, z) -ax2.unshare_x_axes(ax) +ax2.unshare_x_axes() ax2.share_z_axes(ax) plt.show() From 8d3acc3c74dabf4c7cc74eb7be811f1496c24950 Mon Sep 17 00:00:00 2001 From: Kjell Le Date: Tue, 5 Jun 2018 17:25:29 +0200 Subject: [PATCH 7/7] Fix rebase error pep8 is a girl --- .../2017-12-05_share_unshare_axes_after_creation.rst | 8 ++++++-- lib/matplotlib/axes/_base.py | 1 - lib/mpl_toolkits/mplot3d/axes3d.py | 3 --- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst b/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst index 78814ff3b91c..74f88b1762b1 100644 --- a/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst +++ b/doc/users/next_whats_new/2017-12-05_share_unshare_axes_after_creation.rst @@ -1,8 +1,12 @@ Share and unshare `axes` after creation --------------------------------------- -`~.Axes` have `~.Axes.unshare_x_axes`, `~.Axes.unshare_y_axes`, `~.Axes.unshare_z_axes` and `~.Axes.unshare_axes` methods to unshare axes. -Similiar there are `~.Axes.share_x_axes`, `~.Axes.share_y_axes`, `~.Axes.share_z_axes` and `~.Axes.share_axes` methods to share axes. +`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. \ No newline at end of file diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 00936bac03aa..95cfdd03533b 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -999,7 +999,6 @@ def cla(self): self.ignore_existing_data_limits = True self.callbacks = cbook.CallbackRegistry() - if self._sharex is not None: # major and minor are axis.Ticker class instances with # locator and formatter attributes diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index e71dce9a1af3..937300620d1d 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -541,7 +541,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, _tight = self._tight = bool(tight) if scalex and self._autoscaleXon: - self._shared_x_axes.clean() x0, x1 = self.xy_dataLim.intervalx xlocator = self.xaxis.get_major_locator() try: @@ -558,7 +557,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self.set_xbound(x0, x1) if scaley and self._autoscaleYon: - self._shared_y_axes.clean() y0, y1 = self.xy_dataLim.intervaly ylocator = self.yaxis.get_major_locator() try: @@ -575,7 +573,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self.set_ybound(y0, y1) if scalez and self._autoscaleZon: - self._shared_z_axes.clean() z0, z1 = self.zz_dataLim.intervalx zlocator = self.zaxis.get_major_locator() try: