From 0b555d828698928f7012b15c03a633f116f47f4a Mon Sep 17 00:00:00 2001 From: Joseph Fox-Rabinovitz Date: Mon, 28 Nov 2016 17:49:56 -0500 Subject: [PATCH 1/4] ENH: Added share_tickers parameter to axes._AxesBase.twinx/y Added copy constructor to axis.Ticker --- lib/matplotlib/axes/_base.py | 118 ++++++++++++++++++++++++------ lib/matplotlib/axis.py | 8 ++ lib/matplotlib/tests/test_axes.py | 53 ++++++++++++++ 3 files changed, 156 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index b771474c585b..d08e6d52da3b 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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 @@ -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) @@ -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: @@ -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: @@ -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: @@ -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 ------- @@ -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') @@ -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 ------- @@ -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()) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index b3a7ffcc3ca3..d9ec72ec744b 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -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): """ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 5a3a3e5d4253..90175887dc2e 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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) @@ -181,6 +182,58 @@ def test_twinx_cla(): assert ax.patch.get_visible() assert ax.yaxis.get_visible() +@cleanup +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() + + 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 + + # Verify that for share_tickers=True, the formatters and locators + # are identical no matter what + 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() + + # Make some more twinned axes to play with (with share_tickers=False) + 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 + 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() + @image_comparison(baseline_images=['twin_autoscale'], extensions=['png']) def test_twinx_axis_scales(): From 6e69ca8667b26722a382313ee2fd4a2923783536 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Thu, 5 Apr 2018 12:57:33 +0100 Subject: [PATCH 2/4] Remove cleanup --- lib/matplotlib/tests/test_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 90175887dc2e..08633cfd92e4 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -182,7 +182,7 @@ def test_twinx_cla(): assert ax.patch.get_visible() assert ax.yaxis.get_visible() -@cleanup + def test_twin_xy_sharing(): fig, ax = plt.subplots() From a67ffb4261b03234de30722bbae5044ea2b2cabb Mon Sep 17 00:00:00 2001 From: David Stansby Date: Thu, 5 Apr 2018 13:01:45 +0100 Subject: [PATCH 3/4] Add a couple more tests --- lib/matplotlib/tests/test_axes.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 08633cfd92e4..c9bf8d9f1927 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -191,20 +191,20 @@ def test_twin_xy_sharing(): 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 - # Verify that for share_tickers=True, the formatters and locators - # are identical no matter what + # 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() - # Make some more twinned axes to play with (with share_tickers=False) + # 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() @@ -228,11 +228,16 @@ def test_twin_xy_sharing(): # 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 (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']) From de1ea7f0a86d4d2622bfc3262df1a74ed2f5261d Mon Sep 17 00:00:00 2001 From: David Stansby Date: Thu, 5 Apr 2018 13:05:36 +0100 Subject: [PATCH 4/4] Added what's new --- doc/users/next_whats_new/shared_tickers.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/users/next_whats_new/shared_tickers.rst diff --git a/doc/users/next_whats_new/shared_tickers.rst b/doc/users/next_whats_new/shared_tickers.rst new file mode 100644 index 000000000000..816c2974d5dd --- /dev/null +++ b/doc/users/next_whats_new/shared_tickers.rst @@ -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()`. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy