Skip to content

Commit a761873

Browse files
committed
ENH: Added share_tickers parameter to axes._AxesBase.twinx/y
Added copy constructor to axis.Ticker
1 parent b9c3b64 commit a761873

File tree

3 files changed

+120
-34
lines changed

3 files changed

+120
-34
lines changed

lib/matplotlib/axes/_base.py

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,13 @@ def __init__(self, fig, rect,
470470
to share the x-axis with
471471
*sharey* an class:`~matplotlib.axes.Axes` instance
472472
to share the y-axis with
473+
*share_tickers* [ *True* | *False* ] whether the major and
474+
minor `Formatter` and `Locator` instances
475+
are always shared (if `True`) or can be set
476+
independently (if `False`) between this set
477+
of axes and `sharex` and `sharey`. This
478+
argument has no meaning if neither `sharex`
479+
nor `sharey` are set. Defaults to `True`.
473480
*title* the title string
474481
*visible* [ *True* | *False* ] whether the axes is
475482
visible
@@ -499,6 +506,10 @@ def __init__(self, fig, rect,
499506
self.set_anchor('C')
500507
self._sharex = sharex
501508
self._sharey = sharey
509+
# share_tickers is only used as a modifier for sharex/y. It
510+
# should not remain in kwargs by the time kwargs updates the
511+
# instance dictionary.
512+
self._share_tickers = kwargs.pop('share_tickers', True)
502513
if sharex is not None:
503514
self._shared_x_axes.join(self, sharex)
504515
if sharex._adjustable == 'box':
@@ -974,50 +985,52 @@ def cla(self):
974985
self.callbacks = cbook.CallbackRegistry()
975986

976987
if self._sharex is not None:
977-
# major and minor are class instances with
978-
# locator and formatter attributes
979-
self.xaxis.major = self._sharex.xaxis.major
980-
self.xaxis.minor = self._sharex.xaxis.minor
988+
# The tickers need to exist but can be empty until after the
989+
# call to Axis._set_scale since they will be overwritten
990+
# anyway
991+
self.xaxis.major = maxis.Ticker()
992+
self.xaxis.minor = maxis.Ticker()
993+
994+
# Copy the axis limits
981995
x0, x1 = self._sharex.get_xlim()
982996
self.set_xlim(x0, x1, emit=False, auto=None)
983997

984-
# Save the current formatter/locator so we don't lose it
985-
majf = self._sharex.xaxis.get_major_formatter()
986-
minf = self._sharex.xaxis.get_minor_formatter()
987-
majl = self._sharex.xaxis.get_major_locator()
988-
minl = self._sharex.xaxis.get_minor_locator()
989-
990998
# This overwrites the current formatter/locator
991999
self.xaxis._set_scale(self._sharex.xaxis.get_scale())
9921000

993-
# Reset the formatter/locator
994-
self.xaxis.set_major_formatter(majf)
995-
self.xaxis.set_minor_formatter(minf)
996-
self.xaxis.set_major_locator(majl)
997-
self.xaxis.set_minor_locator(minl)
1001+
# Reset the formatter/locator. Axis handle gets marked as
1002+
# stale in previous line, no need to repeat.
1003+
if self._share_tickers:
1004+
self.xaxis.major = self._sharex.xaxis.major
1005+
self.xaxis.minor = self._sharex.xaxis.minor
1006+
else:
1007+
self.xaxis.major = maxis.Ticker(self._sharex.xaxis.major)
1008+
self.xaxis.minor = maxis.Ticker(self._sharex.xaxis.minor)
9981009
else:
9991010
self.xaxis._set_scale('linear')
10001011

10011012
if self._sharey is not None:
1002-
self.yaxis.major = self._sharey.yaxis.major
1003-
self.yaxis.minor = self._sharey.yaxis.minor
1013+
# The tickers need to exist but can be empty until after the
1014+
# call to Axis._set_scale since they will be overwritten
1015+
# anyway
1016+
self.yaxis.major = maxis.Ticker()
1017+
self.yaxis.minor = maxis.Ticker()
1018+
1019+
# Copy the axis limits
10041020
y0, y1 = self._sharey.get_ylim()
10051021
self.set_ylim(y0, y1, emit=False, auto=None)
10061022

1007-
# Save the current formatter/locator so we don't lose it
1008-
majf = self._sharey.yaxis.get_major_formatter()
1009-
minf = self._sharey.yaxis.get_minor_formatter()
1010-
majl = self._sharey.yaxis.get_major_locator()
1011-
minl = self._sharey.yaxis.get_minor_locator()
1012-
10131023
# This overwrites the current formatter/locator
10141024
self.yaxis._set_scale(self._sharey.yaxis.get_scale())
10151025

1016-
# Reset the formatter/locator
1017-
self.yaxis.set_major_formatter(majf)
1018-
self.yaxis.set_minor_formatter(minf)
1019-
self.yaxis.set_major_locator(majl)
1020-
self.yaxis.set_minor_locator(minl)
1026+
# Reset the formatter/locator. Axis handle gets marked as
1027+
# stale in previous line, no need to repeat.
1028+
if self._share_tickers:
1029+
self.yaxis.major = self._sharey.yaxis.major
1030+
self.yaxis.minor = self._sharey.yaxis.minor
1031+
else:
1032+
self.yaxis.major = maxis.Ticker(self._sharey.yaxis.major)
1033+
self.yaxis.minor = maxis.Ticker(self._sharey.yaxis.minor)
10211034
else:
10221035
self.yaxis._set_scale('linear')
10231036

@@ -3894,21 +3907,27 @@ def _make_twin_axes(self, *kl, **kwargs):
38943907
ax2 = self.figure.add_axes(self.get_position(True), *kl, **kwargs)
38953908
return ax2
38963909

3897-
def twinx(self):
3910+
def twinx(self, share_tickers=True):
38983911
"""
38993912
Create a twin Axes sharing the xaxis
39003913
3901-
create a twin of Axes for generating a plot with a sharex
3914+
Create a twin of Axes for generating a plot with a sharex
39023915
x-axis but independent y axis. The y-axis of self will have
39033916
ticks on left and the returned axes will have ticks on the
39043917
right. To ensure tick marks of both axis align, see
39053918
:class:`~matplotlib.ticker.LinearLocator`
39063919
3920+
`share_tickers` determines if the shared axis will always have
3921+
the same major and minor `Formatter` and `Locator` objects as
3922+
this one. This is usually desirable since the axes overlap.
3923+
However, if one of the axes is shifted so that they are both
3924+
visible, it may be useful to set this parameter to ``False``.
3925+
39073926
.. note::
39083927
For those who are 'picking' artists while using twinx, pick
39093928
events are only called for the artists in the top-most axes.
39103929
"""
3911-
ax2 = self._make_twin_axes(sharex=self)
3930+
ax2 = self._make_twin_axes(sharex=self, share_tickers=share_tickers)
39123931
ax2.yaxis.tick_right()
39133932
ax2.yaxis.set_label_position('right')
39143933
ax2.yaxis.set_offset_position('right')
@@ -3917,21 +3936,27 @@ def twinx(self):
39173936
ax2.patch.set_visible(False)
39183937
return ax2
39193938

3920-
def twiny(self):
3939+
def twiny(self, share_tickers=True):
39213940
"""
39223941
Create a twin Axes sharing the yaxis
39233942
3924-
create a twin of Axes for generating a plot with a shared
3943+
Create a twin of Axes for generating a plot with a shared
39253944
y-axis but independent x axis. The x-axis of self will have
39263945
ticks on bottom and the returned axes will have ticks on the
39273946
top.
39283947
3948+
`share_tickers` determines if the shared axis will always have
3949+
the same major and minor `Formatter` and `Locator` objects as
3950+
this one. This is usually desirable since the axes overlap.
3951+
However, if one of the axes is shifted so that they are both
3952+
visible, it may be useful to set this parameter to ``False``.
3953+
39293954
.. note::
39303955
For those who are 'picking' artists while using twiny, pick
39313956
events are only called for the artists in the top-most axes.
39323957
"""
39333958

3934-
ax2 = self._make_twin_axes(sharey=self)
3959+
ax2 = self._make_twin_axes(sharey=self, share_tickers=share_tickers)
39353960
ax2.xaxis.tick_top()
39363961
ax2.xaxis.set_label_position('top')
39373962
self.xaxis.tick_bottom()

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 __init__(self, ticker=None):
608+
"""
609+
Constructor that optionally allows a copy to be made.
610+
"""
611+
if ticker:
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
@@ -26,6 +26,7 @@
2626
import matplotlib.markers as mmarkers
2727
import matplotlib.patches as mpatches
2828
import matplotlib.colors as mcolors
29+
import matplotlib.ticker as mticker
2930
from numpy.testing import assert_allclose, assert_array_equal
3031
from matplotlib.cbook import IgnoredKeywordWarning
3132
import matplotlib.colors as mcolors
@@ -144,6 +145,58 @@ def test_twinx_cla():
144145
assert ax.patch.get_visible()
145146
assert ax.yaxis.get_visible()
146147

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

148201
@image_comparison(baseline_images=["minorticks_on_rcParams_both"], extensions=['png'])
149202
def test_minorticks_on_rcParams_both():

0 commit comments

Comments
 (0)