Skip to content

Allow sharex/y after axes creation. #15287

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
Apr 8, 2020
Merged
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
7 changes: 5 additions & 2 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,8 @@ Adding Artists
Axes.add_table


Twinning
========
Twinning and sharing
====================

.. autosummary::
:toctree: _as_gen
Expand All @@ -458,6 +458,9 @@ Twinning
Axes.twinx
Axes.twiny

Axes.sharex
Axes.sharey

Axes.get_shared_x_axes
Axes.get_shared_y_axes

Expand Down
8 changes: 8 additions & 0 deletions doc/users/next_whats_new/2020-01-24-lateshare.rst
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions examples/subplots_axes_and_figures/subplots_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# """"""""""
Expand Down
79 changes: 50 additions & 29 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

This docstring could be more explicit and non circular. For instance the verb to share would indicate that self shares with other, whereas the exact opposite happens. Further passing sharex=other calls this function so I don’t think it’s very helpful to say that this function is the same as that and expect the user to ferret out the behaviour

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"self shares with other, whereas the exact opposite happens"
What do you mean by "opposite"? Isn't sharing symmetrical? (well perhaps not deep in the implementation details, but from the user PoV it is)

Copy link
Member

Choose a reason for hiding this comment

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

I meant in everyday English the transitive verb “to share” means one person shares Something with another

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you propose an alternate wording here?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, the main point is that saying ax.sharex is "Equivalent to passing sharex=other when constructing..." is a circular doc, because when you construct, you call ax.sharex. This function ideally would define what ax.sharex does explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now the docs of sharex (as kwargs to the Axes constructor) are

        sharex, sharey : `~.axes.Axes`, optional
            The x or y `~.matplotlib.axis` is shared with the x or
            y axis in the input `~.axes.Axes`.

which is not much better :/ I agree this can be improved, but is arguably a separate issue?

Copy link
Member

Choose a reason for hiding this comment

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

The docstring defines what the function does for the user, and one of our main problems with "share" is that people don't know what it means and even in the history of the code base different definitions have popped up. Right now I'm not 100% sure what this PR changed and I think it should be defined clearly, in plain English, what calling this function will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ax0 = (some axes)
ax1 = fig.add_subplot(...)
ax1.sharex(ax0)

is equivalent to

ax0 = (some axes)
ax1 = fig.add_subplot(..., sharex=ax0)

Now it is true that what the latter does is not really well documented either, but the doc available for ax1.sharex(ax0) is, well, exactly the same.

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__()
Expand All @@ -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())

Expand Down Expand Up @@ -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)

Expand Down