Skip to content

Commit 202f91b

Browse files
committed
ENH: Added share_tickers parameter to axes._AxesBase.twinx/y
Added copy constructor to axis.Ticker
1 parent 70831bd commit 202f91b

File tree

3 files changed

+154
-44
lines changed

3 files changed

+154
-44
lines changed

lib/matplotlib/axes/_base.py

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,13 @@ def __init__(self, fig, rect,
462462
to share the x-axis with
463463
*sharey* an class:`~matplotlib.axes.Axes` instance
464464
to share the y-axis with
465+
*share_tickers* [ *True* | *False* ] whether the major and
466+
minor `Formatter` and `Locator` instances
467+
are always shared (if `True`) or can be set
468+
independently (if `False`) between this set
469+
of axes and `sharex` and `sharey`. This
470+
argument has no meaning if neither `sharex`
471+
nor `sharey` are set. Defaults to `True`.
465472
*title* the title string
466473
*visible* [ *True* | *False* ] whether the axes is
467474
visible
@@ -476,6 +483,14 @@ def __init__(self, fig, rect,
476483
*yticklabels* sequence of strings
477484
*yticks* sequence of floats
478485
================ =========================================
486+
487+
.. warning::
488+
489+
Setting `share_tickers` to `False` and changing the
490+
`Locator`s of a shared axis may not play with autoscaling.
491+
Autoscaling may need to access the `Locator` object of the
492+
base axis. Normally, with `share_tickers=True`, the axes
493+
are guaranteed to share a `Locator` instance.
479494
""" % {'scale': ' | '.join(
480495
[repr(x) for x in mscale.get_scale_names()])}
481496
martist.Artist.__init__(self)
@@ -493,6 +508,10 @@ def __init__(self, fig, rect,
493508
self.set_anchor('C')
494509
self._sharex = sharex
495510
self._sharey = sharey
511+
# share_tickers is only used as a modifier for sharex/y. It
512+
# should not remain in kwargs by the time kwargs updates the
513+
# instance dictionary.
514+
self._share_tickers = kwargs.pop('share_tickers', True)
496515
if sharex is not None:
497516
self._shared_x_axes.join(self, sharex)
498517
if sharex._adjustable == 'box':
@@ -968,50 +987,52 @@ def cla(self):
968987
self.callbacks = cbook.CallbackRegistry()
969988

970989
if self._sharex is not None:
971-
# major and minor are class instances with
972-
# locator and formatter attributes
973-
self.xaxis.major = self._sharex.xaxis.major
974-
self.xaxis.minor = self._sharex.xaxis.minor
990+
# The tickers need to exist but can be empty until after the
991+
# call to Axis._set_scale since they will be overwritten
992+
# anyway
993+
self.xaxis.major = maxis.Ticker()
994+
self.xaxis.minor = maxis.Ticker()
995+
996+
# Copy the axis limits
975997
x0, x1 = self._sharex.get_xlim()
976998
self.set_xlim(x0, x1, emit=False, auto=None)
977999

978-
# Save the current formatter/locator so we don't lose it
979-
majf = self._sharex.xaxis.get_major_formatter()
980-
minf = self._sharex.xaxis.get_minor_formatter()
981-
majl = self._sharex.xaxis.get_major_locator()
982-
minl = self._sharex.xaxis.get_minor_locator()
983-
9841000
# This overwrites the current formatter/locator
9851001
self.xaxis._set_scale(self._sharex.xaxis.get_scale())
9861002

987-
# Reset the formatter/locator
988-
self.xaxis.set_major_formatter(majf)
989-
self.xaxis.set_minor_formatter(minf)
990-
self.xaxis.set_major_locator(majl)
991-
self.xaxis.set_minor_locator(minl)
1003+
# Reset the formatter/locator. Axis handle gets marked as
1004+
# stale in previous line, no need to repeat.
1005+
if self._share_tickers:
1006+
self.xaxis.major = self._sharex.xaxis.major
1007+
self.xaxis.minor = self._sharex.xaxis.minor
1008+
else:
1009+
self.xaxis.major.update_from(self._sharex.xaxis.major)
1010+
self.xaxis.minor.update_from(self._sharex.xaxis.minor)
9921011
else:
9931012
self.xaxis._set_scale('linear')
9941013

9951014
if self._sharey is not None:
996-
self.yaxis.major = self._sharey.yaxis.major
997-
self.yaxis.minor = self._sharey.yaxis.minor
1015+
# The tickers need to exist but can be empty until after the
1016+
# call to Axis._set_scale since they will be overwritten
1017+
# anyway
1018+
self.yaxis.major = maxis.Ticker()
1019+
self.yaxis.minor = maxis.Ticker()
1020+
1021+
# Copy the axis limits
9981022
y0, y1 = self._sharey.get_ylim()
9991023
self.set_ylim(y0, y1, emit=False, auto=None)
10001024

1001-
# Save the current formatter/locator so we don't lose it
1002-
majf = self._sharey.yaxis.get_major_formatter()
1003-
minf = self._sharey.yaxis.get_minor_formatter()
1004-
majl = self._sharey.yaxis.get_major_locator()
1005-
minl = self._sharey.yaxis.get_minor_locator()
1006-
10071025
# This overwrites the current formatter/locator
10081026
self.yaxis._set_scale(self._sharey.yaxis.get_scale())
10091027

1010-
# Reset the formatter/locator
1011-
self.yaxis.set_major_formatter(majf)
1012-
self.yaxis.set_minor_formatter(minf)
1013-
self.yaxis.set_major_locator(majl)
1014-
self.yaxis.set_minor_locator(minl)
1028+
# Reset the formatter/locator. Axis handle gets marked as
1029+
# stale in previous line, no need to repeat.
1030+
if self._share_tickers:
1031+
self.yaxis.major = self._sharey.yaxis.major
1032+
self.yaxis.minor = self._sharey.yaxis.minor
1033+
else:
1034+
self.yaxis.major.update_from(self._sharey.yaxis.major)
1035+
self.yaxis.minor.update_from(self._sharey.yaxis.minor)
10151036
else:
10161037
self.yaxis._set_scale('linear')
10171038

@@ -3898,15 +3919,29 @@ def _make_twin_axes(self, *kl, **kwargs):
38983919
ax2 = self.figure.add_axes(self.get_position(True), *kl, **kwargs)
38993920
return ax2
39003921

3901-
def twinx(self):
3922+
def twinx(self, share_tickers=True):
39023923
"""
3903-
Create a twin Axes sharing the xaxis
3924+
Create a twin Axes sharing the xaxis.
39043925
3905-
Create a new Axes instance with an invisible x-axis and an independent
3906-
y-axis positioned opposite to the original one (i.e. at right). The
3907-
x-axis autoscale setting will be inherited from the original Axes.
3908-
To ensure that the tick marks of both y-axes align, see
3909-
`~matplotlib.ticker.LinearLocator`
3926+
Create a new Axes instance with an invisible x-axis and an
3927+
independent y-axis positioned opposite to the original one (i.e.
3928+
at right). The x-axis autoscale setting will be inherited from
3929+
the original Axes. To ensure that the tick marks of both y-axes
3930+
align, see :class:`matplotlib.ticker.LinearLocator`.
3931+
3932+
`share_tickers` determines if the shared axis will always have
3933+
the same major and minor `Formatter` and `Locator` objects as
3934+
this one. This is usually desirable since the axes overlap.
3935+
However, if one of the axes is shifted so that they are both
3936+
visible, it may be useful to set this parameter to ``False``.
3937+
3938+
.. warning::
3939+
3940+
Setting `share_tickers` to `False` and modifying the
3941+
`Locator` of either axis may cause problems with
3942+
autoscaling. Autoscaling may require access to the
3943+
`Locator`, so the behavior will be undefined if the base and
3944+
twinned axis do not share a `Locator` instance.
39103945
39113946
Returns
39123947
-------
@@ -3918,7 +3953,7 @@ def twinx(self):
39183953
For those who are 'picking' artists while using twinx, pick
39193954
events are only called for the artists in the top-most axes.
39203955
"""
3921-
ax2 = self._make_twin_axes(sharex=self)
3956+
ax2 = self._make_twin_axes(sharex=self, share_tickers=share_tickers)
39223957
ax2.yaxis.tick_right()
39233958
ax2.yaxis.set_label_position('right')
39243959
ax2.yaxis.set_offset_position('right')
@@ -3928,15 +3963,29 @@ def twinx(self):
39283963
ax2.patch.set_visible(False)
39293964
return ax2
39303965

3931-
def twiny(self):
3966+
def twiny(self, share_tickers=True):
39323967
"""
3933-
Create a twin Axes sharing the yaxis
3968+
Create a twin Axes sharing the yaxis.
3969+
3970+
Create a new Axes instance with an invisible y-axis and an
3971+
independent x-axis positioned opposite to the original one (i.e.
3972+
at top). The y-axis autoscale setting will be inherited from the
3973+
original Axes. To ensure that the tick marks of both x-axes
3974+
align, see :class:`matplotlib.ticker.LinearLocator`
3975+
3976+
`share_tickers` determines if the shared axis will always have
3977+
the same major and minor `Formatter` and `Locator` objects as
3978+
this one. This is usually desirable since the axes overlap.
3979+
However, if one of the axes is shifted so that they are both
3980+
visible, it may be useful to set this parameter to ``False``.
3981+
3982+
.. warning::
39343983
3935-
Create a new Axes instance with an invisible y-axis and an independent
3936-
x-axis positioned opposite to the original one (i.e. at top). The
3937-
y-axis autoscale setting will be inherited from the original Axes.
3938-
To ensure that the tick marks of both x-axes align, see
3939-
`~matplotlib.ticker.LinearLocator`
3984+
Setting `share_tickers` to `False` and modifying the
3985+
`Locator` of either axis may cause problems with
3986+
autoscaling. Autoscaling may require access to the
3987+
`Locator`, so the behavior will be undefined if the base and
3988+
twinned axis do not share a `Locator` instance.
39403989
39413990
Returns
39423991
-------
@@ -3948,7 +3997,7 @@ def twiny(self):
39483997
For those who are 'picking' artists while using twiny, pick
39493998
events are only called for the artists in the top-most axes.
39503999
"""
3951-
ax2 = self._make_twin_axes(sharey=self)
4000+
ax2 = self._make_twin_axes(sharey=self, share_tickers=share_tickers)
39524001
ax2.xaxis.tick_top()
39534002
ax2.xaxis.set_label_position('top')
39544003
ax2.set_autoscaley_on(self.get_autoscaley_on())

lib/matplotlib/axis.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,14 @@ class Ticker(object):
604604
locator = None
605605
formatter = None
606606

607+
def update_from(self, ticker):
608+
"""
609+
Copies the formatter and locator of another ticker into this
610+
one.
611+
"""
612+
self.locator = ticker.locator
613+
self.formatter = ticker.formatter
614+
607615

608616
class Axis(artist.Artist):
609617
"""

lib/matplotlib/tests/test_axes.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import matplotlib.markers as mmarkers
2626
import matplotlib.patches as mpatches
2727
import matplotlib.colors as mcolors
28+
import matplotlib.ticker as mticker
2829
from numpy.testing import assert_allclose, assert_array_equal
2930
from matplotlib.cbook import IgnoredKeywordWarning
3031
import matplotlib.colors as mcolors
@@ -143,6 +144,58 @@ def test_twinx_cla():
143144
assert ax.patch.get_visible()
144145
assert ax.yaxis.get_visible()
145146

147+
@cleanup
148+
def test_twin_xy_sharing():
149+
fig, ax = plt.subplots()
150+
151+
# Make some twinned axes to play with (with share_tickers=True)
152+
ax2 = ax.twinx()
153+
ax3 = ax2.twiny()
154+
plt.draw()
155+
156+
assert ax.xaxis.major is ax2.xaxis.major
157+
assert ax.xaxis.minor is ax2.xaxis.minor
158+
assert ax2.yaxis.major is ax3.yaxis.major
159+
assert ax2.yaxis.minor is ax3.yaxis.minor
160+
161+
# Verify that for share_tickers=True, the formatters and locators
162+
# are identical no matter what
163+
ax2.xaxis.set_major_formatter(mticker.PercentFormatter())
164+
ax3.yaxis.set_major_locator(mticker.MaxNLocator())
165+
166+
assert ax.xaxis.get_major_formatter() is ax2.xaxis.get_major_formatter()
167+
assert ax2.yaxis.get_major_locator() is ax3.yaxis.get_major_locator()
168+
169+
# Make some more twinned axes to play with (with share_tickers=False)
170+
ax4 = ax.twinx(share_tickers=False)
171+
ax5 = ax2.twiny(share_tickers=False)
172+
plt.draw()
173+
174+
assert ax4 is not ax2
175+
assert ax5 is not ax3
176+
177+
assert ax.xaxis.major is not ax4.xaxis.major
178+
assert ax.xaxis.minor is not ax4.xaxis.minor
179+
assert ax.xaxis.get_major_formatter() is ax4.xaxis.get_major_formatter()
180+
assert ax.xaxis.get_minor_formatter() is ax4.xaxis.get_minor_formatter()
181+
assert ax.xaxis.get_major_locator() is ax4.xaxis.get_major_locator()
182+
assert ax.xaxis.get_minor_locator() is ax4.xaxis.get_minor_locator()
183+
184+
assert ax2.yaxis.major is not ax5.yaxis.major
185+
assert ax2.yaxis.minor is not ax5.yaxis.minor
186+
assert ax2.yaxis.get_major_formatter() is ax5.yaxis.get_major_formatter()
187+
assert ax2.yaxis.get_minor_formatter() is ax5.yaxis.get_minor_formatter()
188+
assert ax2.yaxis.get_major_locator() is ax5.yaxis.get_major_locator()
189+
assert ax2.yaxis.get_minor_locator() is ax5.yaxis.get_minor_locator()
190+
191+
# Verify that for share_tickers=False, the formatters and locators
192+
# can be changed independently
193+
ax4.xaxis.set_minor_formatter(mticker.PercentFormatter())
194+
ax5.yaxis.set_minor_locator(mticker.MaxNLocator())
195+
196+
assert ax.xaxis.get_minor_formatter() is not ax4.xaxis.get_minor_formatter()
197+
assert ax2.yaxis.get_minor_locator() is not ax4.yaxis.get_minor_locator()
198+
146199

147200
@image_comparison(baseline_images=['twin_autoscale'], extensions=['png'])
148201
def test_twinx_axis_scales():

0 commit comments

Comments
 (0)