Skip to content

Added share_tickers parameter to axes._AxesBase.twinx/y #10960

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 4 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
7 changes: 7 additions & 0 deletions doc/users/next_whats_new/shared_tickers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Two shared axis can have different tick formatters and locators
---------------------------------------------------------------

Previously two shared axis were forced to have the same tick formatter and
tick locator. It is now possible to set shared axis to have different tickers
and formatters using the *share_tickers* keyword argument to `twinx()` and
`twiny()`.
118 changes: 95 additions & 23 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ def __init__(self, fig, rect,
to share the x-axis with
*sharey* an class:`~matplotlib.axes.Axes` instance
to share the y-axis with
*share_tickers* [ *True* | *False* ] whether the major and
minor `Formatter` and `Locator` instances
are always shared (if `True`) or can be set
independently (if `False`) between this set
of axes and `sharex` and `sharey`. This
argument has no meaning if neither `sharex`
nor `sharey` are set. Defaults to `True`.
*title* the title string
*visible* bool, whether the axes is visible
*xlabel* the xlabel
Expand All @@ -461,6 +468,14 @@ def __init__(self, fig, rect,
*yticklabels* sequence of strings
*yticks* sequence of floats
================ =========================================

.. warning::

Setting `share_tickers` to `False` and changing the
`Locator`s of a shared axis may not play with autoscaling.
Autoscaling may need to access the `Locator` object of the
base axis. Normally, with `share_tickers=True`, the axes
are guaranteed to share a `Locator` instance.
""" % {'scale': ' | '.join(
[repr(x) for x in mscale.get_scale_names()])}
martist.Artist.__init__(self)
Expand All @@ -478,6 +493,10 @@ def __init__(self, fig, rect,
self._anchor = 'C'
self._sharex = sharex
self._sharey = sharey
# share_tickers is only used as a modifier for sharex/y. It
# should not remain in kwargs by the time kwargs updates the
# instance dictionary.
self._share_tickers = kwargs.pop('share_tickers', True)
if sharex is not None:
self._shared_x_axes.join(self, sharex)
if sharey is not None:
Expand Down Expand Up @@ -986,15 +1005,27 @@ 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
# The tickers need to exist but can be empty until after the
# call to Axis._set_scale since they will be overwritten
# anyway
self.xaxis.major = maxis.Ticker()
self.xaxis.minor = maxis.Ticker()

# Copy the axis limits
x0, x1 = self._sharex.get_xlim()
self.set_xlim(x0, x1, emit=False,
auto=self._sharex.get_autoscalex_on())
self.xaxis._scale = mscale.scale_factory(
self._sharex.xaxis.get_scale(), self.xaxis)

# Reset the formatter/locator. Axis handle gets marked as
# stale in previous line, no need to repeat.
if self._share_tickers:
self.xaxis.major = self._sharex.xaxis.major
self.xaxis.minor = self._sharex.xaxis.minor
else:
self.xaxis.major.update_from(self._sharex.xaxis.major)
self.xaxis.minor.update_from(self._sharex.xaxis.minor)
else:
self.xaxis._set_scale('linear')
try:
Expand All @@ -1003,13 +1034,27 @@ def cla(self):
pass

if self._sharey is not None:
self.yaxis.major = self._sharey.yaxis.major
self.yaxis.minor = self._sharey.yaxis.minor
# The tickers need to exist but can be empty until after the
# call to Axis._set_scale since they will be overwritten
# anyway
self.yaxis.major = maxis.Ticker()
self.yaxis.minor = maxis.Ticker()

# Copy the axis limits
y0, y1 = self._sharey.get_ylim()
self.set_ylim(y0, y1, emit=False,
auto=self._sharey.get_autoscaley_on())
self.yaxis._scale = mscale.scale_factory(
self._sharey.yaxis.get_scale(), self.yaxis)

# Reset the formatter/locator. Axis handle gets marked as
# stale in previous line, no need to repeat.
if self._share_tickers:
self.yaxis.major = self._sharey.yaxis.major
self.yaxis.minor = self._sharey.yaxis.minor
else:
self.yaxis.major.update_from(self._sharey.yaxis.major)
self.yaxis.minor.update_from(self._sharey.yaxis.minor)
else:
self.yaxis._set_scale('linear')
try:
Expand Down Expand Up @@ -4093,15 +4138,29 @@ def _make_twin_axes(self, *kl, **kwargs):
self._twinned_axes.join(self, ax2)
return ax2

def twinx(self):
def twinx(self, share_tickers=True):
"""
Create a twin Axes sharing the xaxis
Create a twin Axes sharing the xaxis.

Create a new Axes instance with an invisible x-axis and an independent
y-axis positioned opposite to the original one (i.e. at right). The
x-axis autoscale setting will be inherited from the original Axes.
To ensure that the tick marks of both y-axes align, see
`~matplotlib.ticker.LinearLocator`
Create a new Axes instance with an invisible x-axis and an
independent y-axis positioned opposite to the original one (i.e.
at right). The x-axis autoscale setting will be inherited from
the original Axes. To ensure that the tick marks of both y-axes
align, see :class:`matplotlib.ticker.LinearLocator`.

`share_tickers` determines if the shared axis will always have
the same major and minor `Formatter` and `Locator` objects as
this one. This is usually desirable since the axes overlap.
However, if one of the axes is shifted so that they are both
visible, it may be useful to set this parameter to ``False``.

.. warning::

Setting `share_tickers` to `False` and modifying the
`Locator` of either axis may cause problems with
autoscaling. Autoscaling may require access to the
`Locator`, so the behavior will be undefined if the base and
twinned axis do not share a `Locator` instance.

Returns
-------
Expand All @@ -4113,7 +4172,7 @@ def twinx(self):
For those who are 'picking' artists while using twinx, pick
events are only called for the artists in the top-most axes.
"""
ax2 = self._make_twin_axes(sharex=self)
ax2 = self._make_twin_axes(sharex=self, share_tickers=share_tickers)
ax2.yaxis.tick_right()
ax2.yaxis.set_label_position('right')
ax2.yaxis.set_offset_position('right')
Expand All @@ -4123,15 +4182,29 @@ def twinx(self):
ax2.patch.set_visible(False)
return ax2

def twiny(self):
def twiny(self, share_tickers=True):
"""
Create a twin Axes sharing the yaxis
Create a twin Axes sharing the yaxis.

Create a new Axes instance with an invisible y-axis and an
independent x-axis positioned opposite to the original one (i.e.
at top). The y-axis autoscale setting will be inherited from the
original Axes. To ensure that the tick marks of both x-axes
align, see :class:`matplotlib.ticker.LinearLocator`

Create a new Axes instance with an invisible y-axis and an independent
x-axis positioned opposite to the original one (i.e. at top). The
y-axis autoscale setting will be inherited from the original Axes.
To ensure that the tick marks of both x-axes align, see
`~matplotlib.ticker.LinearLocator`
`share_tickers` determines if the shared axis will always have
the same major and minor `Formatter` and `Locator` objects as
this one. This is usually desirable since the axes overlap.
However, if one of the axes is shifted so that they are both
visible, it may be useful to set this parameter to ``False``.

.. warning::

Setting `share_tickers` to `False` and modifying the
`Locator` of either axis may cause problems with
autoscaling. Autoscaling may require access to the
`Locator`, so the behavior will be undefined if the base and
twinned axis do not share a `Locator` instance.

Returns
-------
Expand All @@ -4143,8 +4216,7 @@ def twiny(self):
For those who are 'picking' artists while using twiny, pick
events are only called for the artists in the top-most axes.
"""

ax2 = self._make_twin_axes(sharey=self)
ax2 = self._make_twin_axes(sharey=self, share_tickers=share_tickers)
ax2.xaxis.tick_top()
ax2.xaxis.set_label_position('top')
ax2.set_autoscaley_on(self.get_autoscaley_on())
Expand Down
8 changes: 8 additions & 0 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,14 @@ class Ticker(object):
locator = None
formatter = None

def update_from(self, ticker):
"""
Copies the formatter and locator of another ticker into this
one.
"""
self.locator = ticker.locator
self.formatter = ticker.formatter


class _LazyTickList(object):
"""
Expand Down
58 changes: 58 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import matplotlib.markers as mmarkers
import matplotlib.patches as mpatches
import matplotlib.colors as mcolors
import matplotlib.ticker as mticker
from numpy.testing import assert_allclose, assert_array_equal
from matplotlib.cbook import (
IgnoredKeywordWarning, MatplotlibDeprecationWarning)
Expand Down Expand Up @@ -182,6 +183,63 @@ def test_twinx_cla():
assert ax.yaxis.get_visible()


def test_twin_xy_sharing():
fig, ax = plt.subplots()

# Make some twinned axes to play with (with share_tickers=True)
ax2 = ax.twinx()
ax3 = ax2.twiny()
plt.draw()

# Check that major and minor tickers are the same by default
assert ax.xaxis.major is ax2.xaxis.major
assert ax.xaxis.minor is ax2.xaxis.minor
assert ax2.yaxis.major is ax3.yaxis.major
assert ax2.yaxis.minor is ax3.yaxis.minor

# Check that the tickers remain identical after setting new
# locators and formatters
ax2.xaxis.set_major_formatter(mticker.PercentFormatter())
ax3.yaxis.set_major_locator(mticker.MaxNLocator())
assert ax.xaxis.get_major_formatter() is ax2.xaxis.get_major_formatter()
assert ax2.yaxis.get_major_locator() is ax3.yaxis.get_major_locator()

# Now check that twinned axes with share_tickers=False don't share tickers
ax4 = ax.twinx(share_tickers=False)
ax5 = ax2.twiny(share_tickers=False)
plt.draw()

assert ax4 is not ax2
assert ax5 is not ax3

assert ax.xaxis.major is not ax4.xaxis.major
assert ax.xaxis.minor is not ax4.xaxis.minor
assert ax.xaxis.get_major_formatter() is ax4.xaxis.get_major_formatter()
assert ax.xaxis.get_minor_formatter() is ax4.xaxis.get_minor_formatter()
assert ax.xaxis.get_major_locator() is ax4.xaxis.get_major_locator()
assert ax.xaxis.get_minor_locator() is ax4.xaxis.get_minor_locator()

assert ax2.yaxis.major is not ax5.yaxis.major
assert ax2.yaxis.minor is not ax5.yaxis.minor
assert ax2.yaxis.get_major_formatter() is ax5.yaxis.get_major_formatter()
assert ax2.yaxis.get_minor_formatter() is ax5.yaxis.get_minor_formatter()
assert ax2.yaxis.get_major_locator() is ax5.yaxis.get_major_locator()
assert ax2.yaxis.get_minor_locator() is ax5.yaxis.get_minor_locator()

# Verify that for share_tickers=False, the formatters and locators
# can be changed independently
old_formatter = ax.xaxis.get_minor_formatter()
old_locator = ax2.yaxis.get_minor_locator()
ax4.xaxis.set_minor_formatter(mticker.PercentFormatter())
ax5.yaxis.set_minor_locator(mticker.MaxNLocator())

assert (ax.xaxis.get_minor_formatter() is not
ax4.xaxis.get_minor_formatter())
assert ax2.yaxis.get_minor_locator() is not ax4.yaxis.get_minor_locator()
assert ax.xaxis.get_minor_formatter() is old_formatter
assert ax2.yaxis.get_minor_locator() is old_locator


@image_comparison(baseline_images=['twin_autoscale'], extensions=['png'])
def test_twinx_axis_scales():
x = np.array([0, 0.5, 1])
Expand Down