From 83c2d7b6b567b8d4006671ce862bf804f3b50426 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 18 Sep 2019 12:39:11 +0200 Subject: [PATCH] Allow sharex/y after axes creation. This intentionally does not allow unsharing or changing the shared axes, as there are bigger questions on the API there. The API is named `Axes.sharex()` to allow for a later `Axes.unsharex()`, though (`set_sharex()` would be fine, but `set_unsharex()`? or perhaps `set_sharex(None)`, though). An example use case is creating a grid with `subplots()`, but with custom sharing relationships between the subplots -- e.g., sharex/sharey across all, except the first row of axes which only shares x with their column and the first column which only shares y with their lines. --- doc/api/axes_api.rst | 7 +- .../next_whats_new/2020-01-24-lateshare.rst | 8 ++ .../subplots_demo.py | 17 ++++ lib/matplotlib/axes/_base.py | 79 ++++++++++++------- 4 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 doc/users/next_whats_new/2020-01-24-lateshare.rst diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 84817c6d6bba..4e7e4174ffd3 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -447,8 +447,8 @@ Adding Artists Axes.add_table -Twinning -======== +Twinning and sharing +==================== .. autosummary:: :toctree: _as_gen @@ -458,6 +458,9 @@ Twinning Axes.twinx Axes.twiny + Axes.sharex + Axes.sharey + Axes.get_shared_x_axes Axes.get_shared_y_axes diff --git a/doc/users/next_whats_new/2020-01-24-lateshare.rst b/doc/users/next_whats_new/2020-01-24-lateshare.rst new file mode 100644 index 000000000000..fdb17c85a01a --- /dev/null +++ b/doc/users/next_whats_new/2020-01-24-lateshare.rst @@ -0,0 +1,8 @@ +`.Axes.sharex`, `.Axes.sharey` +------------------------------ +These new methods allow sharing axes *immediately* after creating them. For +example, they can be used to selectively link some axes created all together +using `~.Figure.subplots`. + +Note that they may *not* be used to share axes after any operation (e.g., +drawing) has occurred on them. diff --git a/examples/subplots_axes_and_figures/subplots_demo.py b/examples/subplots_axes_and_figures/subplots_demo.py index e4434808864c..e0bf7fe68521 100644 --- a/examples/subplots_axes_and_figures/subplots_demo.py +++ b/examples/subplots_axes_and_figures/subplots_demo.py @@ -176,6 +176,23 @@ for ax in axs.flat: ax.label_outer() +############################################################################### +# If you want a more complex sharing structure, you can first create the +# grid of axes with no sharing, and then call `.axes.Axes.sharex` or +# `.axes.Axes.sharey` to add sharing info a posteriori. + +fig, axs = plt.subplots(2, 2) +axs[0, 0].plot(x, y) +axs[0, 0].set_title("main") +axs[1, 0].plot(x, y**2) +axs[1, 0].set_title("shares x with main") +axs[1, 0].sharex(axs[0, 0]) +axs[0, 1].plot(x + 1, y + 1) +axs[0, 1].set_title("unrelated") +axs[1, 1].plot(x + 2, y + 2) +axs[1, 1].set_title("also unrelated") +fig.tight_layout() + ############################################################################### # Polar axes # """""""""" diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6d8eb4f8ebc6..23222d885298 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -487,16 +487,8 @@ def __init__(self, fig, rect, self._anchor = 'C' self._stale_viewlim_x = False self._stale_viewlim_y = False - self._sharex = sharex - self._sharey = sharey - if sharex is not None: - if not isinstance(sharex, _AxesBase): - raise TypeError('sharex must be an axes, not a bool') - self._shared_x_axes.join(self, sharex) - if sharey is not None: - if not isinstance(sharey, _AxesBase): - raise TypeError('sharey must be an axes, not a bool') - self._shared_y_axes.join(self, sharey) + self._sharex = None + self._sharey = None self.set_label(label) self.set_figure(fig) self.set_box_aspect(box_aspect) @@ -515,6 +507,11 @@ def __init__(self, fig, rect, self._rasterization_zorder = None self.cla() + if sharex is not None: + self.sharex(sharex) + if sharey is not None: + self.sharey(sharey) + # funcs used to format x and y - fall back on major formatters self.fmt_xdata = None self.fmt_ydata = None @@ -1008,6 +1005,44 @@ def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): return OrderedDict((side, mspines.Spine.linear_spine(self, side)) for side in ['left', 'right', 'bottom', 'top']) + def sharex(self, other): + """ + Share the x-axis with *other*. + + This is equivalent to passing ``sharex=other`` when constructing the + axes, and cannot be used if the x-axis is already being shared with + another axes. + """ + cbook._check_isinstance(_AxesBase, other=other) + if self._sharex is not None and other is not self._sharex: + raise ValueError("x-axis is already shared") + self._shared_x_axes.join(self, other) + self._sharex = other + self.xaxis.major = other.xaxis.major # Ticker instances holding + self.xaxis.minor = other.xaxis.minor # locator and formatter. + x0, x1 = other.get_xlim() + self.set_xlim(x0, x1, emit=False, auto=other.get_autoscalex_on()) + self.xaxis._scale = other.xaxis._scale + + def sharey(self, other): + """ + Share the y-axis with *other*. + + This is equivalent to passing ``sharey=other`` when constructing the + axes, and cannot be used if the y-axis is already being shared with + another axes. + """ + cbook._check_isinstance(_AxesBase, other=other) + if self._sharey is not None and other is not self._sharey: + raise ValueError("y-axis is already shared") + self._shared_y_axes.join(self, other) + self._sharey = other + self.yaxis.major = other.yaxis.major # Ticker instances holding + self.yaxis.minor = other.yaxis.minor # locator and formatter. + y0, y1 = other.get_ylim() + self.set_ylim(y0, y1, emit=False, auto=other.get_autoscaley_on()) + self.yaxis._scale = other.yaxis._scale + def cla(self): """Clear the current axes.""" # Note: this is called by Axes.__init__() @@ -1031,38 +1066,25 @@ def cla(self): self.callbacks = cbook.CallbackRegistry() if self._sharex is not None: - # major and minor are axis.Ticker class instances with - # 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=self._sharex.get_autoscalex_on()) - self.xaxis._scale = self._sharex.xaxis._scale + self.sharex(self._sharex) else: self.xaxis._set_scale('linear') try: self.set_xlim(0, 1) except TypeError: pass - if self._sharey is not None: - self.yaxis.major = self._sharey.yaxis.major - self.yaxis.minor = self._sharey.yaxis.minor - y0, y1 = self._sharey.get_ylim() - self.set_ylim(y0, y1, emit=False, - auto=self._sharey.get_autoscaley_on()) - self.yaxis._scale = self._sharey.yaxis._scale + self.sharey(self._sharey) else: self.yaxis._set_scale('linear') try: self.set_ylim(0, 1) except TypeError: pass + # update the minor locator for x and y axis based on rcParams if mpl.rcParams['xtick.minor.visible']: self.xaxis.set_minor_locator(mticker.AutoMinorLocator()) - if mpl.rcParams['ytick.minor.visible']: self.yaxis.set_minor_locator(mticker.AutoMinorLocator()) @@ -1144,11 +1166,10 @@ def cla(self): self._shared_x_axes.clean() self._shared_y_axes.clean() - if self._sharex: + if self._sharex is not None: self.xaxis.set_visible(xaxis_visible) self.patch.set_visible(patch_visible) - - if self._sharey: + if self._sharey is not None: self.yaxis.set_visible(yaxis_visible) self.patch.set_visible(patch_visible)