Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4187919

Browse files
committed
ENH: Give control whether twinx() or twiny() overlays the main axis (#31122)
1 parent cc6cead commit 4187919

5 files changed

Lines changed: 156 additions & 11 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Twin Axes ``delta_zorder``
2+
--------------------------
3+
4+
`~matplotlib.axes.Axes.twinx` and `~matplotlib.axes.Axes.twiny` now accept a
5+
*delta_zorder* keyword argument, a relative offset added to the original Axes'
6+
zorder, to control whether the twin Axes is drawn in front of, or behind, the
7+
original Axes. For example, pass ``delta_zorder=-1`` to easily draw a twin Axes
8+
behind the main Axes.
9+
10+
In addition, Matplotlib now automatically manages background patch visibility
11+
for each group of twinned Axes so that only the bottom-most Axes in the group
12+
has a visible background patch (respecting ``frameon``).
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
===========================
3+
Twin Axes with delta_zorder
4+
===========================
5+
6+
`~matplotlib.axes.Axes.twinx` and `~matplotlib.axes.Axes.twiny` accept a
7+
*delta_zorder* keyword argument (a relative offset added to the original Axes'
8+
zorder) that controls whether the twin Axes is drawn in front of or behind the
9+
original Axes.
10+
11+
Matplotlib also automatically manages background patch visibility for twinned
12+
Axes groups so that only the bottom-most Axes has a visible background patch
13+
(respecting ``frameon``). This avoids the background of a higher-zorder twin
14+
Axes covering artists drawn on the underlying Axes.
15+
"""
16+
17+
import matplotlib.pyplot as plt
18+
import numpy as np
19+
20+
x = np.linspace(0, 10, 400)
21+
y_main = np.sin(x)
22+
y_twin = 0.4 * np.cos(x) + 0.6
23+
24+
fig, ax = plt.subplots()
25+
26+
# Put the twin Axes behind the original Axes (relative to the original zorder).
27+
ax2 = ax.twinx(delta_zorder=-1)
28+
29+
# Draw something broad on the twin Axes so that the stacking is obvious.
30+
ax2.fill_between(x, 0, y_twin, color="C1", alpha=0.35, label="twin fill")
31+
ax2.plot(x, y_twin, color="C1", lw=6, alpha=0.8)
32+
33+
# Draw overlapping artists on the main Axes; they appear on top.
34+
ax.scatter(x[::8], y_main[::8], s=35, color="C0", edgecolor="k", linewidth=0.5,
35+
zorder=3, label="main scatter")
36+
ax.plot(x, y_main, color="C0", lw=4)
37+
38+
ax.set_xlabel("x")
39+
ax.set_ylabel("main y")
40+
ax2.set_ylabel("twin y")
41+
ax.set_title("Twin Axes drawn behind the main Axes using delta_zorder")
42+
43+
fig.tight_layout()
44+
plt.show()

lib/matplotlib/axes/_base.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,10 @@ def __str__(self):
614614
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
615615
type(self).__name__, self._position.bounds)
616616

617+
def set_zorder(self, level):
618+
super().set_zorder(level)
619+
self._update_twinned_axes_patch_visibility()
620+
617621
def __init__(self, fig,
618622
*args,
619623
facecolor=None, # defaults to rc axes.facecolor
@@ -3324,6 +3328,7 @@ def set_frame_on(self, b):
33243328
b : bool
33253329
"""
33263330
self._frameon = b
3331+
self._update_twinned_axes_patch_visibility()
33273332
self.stale = True
33283333

33293334
def get_axisbelow(self):
@@ -4705,7 +4710,32 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True,
47054710
return mtransforms.Bbox.union(
47064711
[b for b in bb if b.width != 0 or b.height != 0])
47074712

4708-
def _make_twin_axes(self, *args, **kwargs):
4713+
def _update_twinned_axes_patch_visibility(self):
4714+
"""
4715+
Update patch visibility for a group of twinned Axes.
4716+
4717+
Only the bottom-most Axes in the group (lowest zorder, breaking ties by
4718+
creation/insertion order) has a visible background patch.
4719+
"""
4720+
if self not in self._twinned_axes:
4721+
return
4722+
twinned = list(self._twinned_axes.get_siblings(self))
4723+
if not twinned:
4724+
return
4725+
fig = self.get_figure(root=False)
4726+
fig_axes = fig.axes if fig is not None else []
4727+
insertion_order = {ax: idx for idx, ax in enumerate(fig_axes)}
4728+
4729+
twinned.sort(
4730+
key=lambda ax: (ax.get_zorder(), insertion_order.get(ax, len(fig_axes)))
4731+
)
4732+
bottom = twinned[0]
4733+
for ax in twinned:
4734+
patch = getattr(ax, "patch", None)
4735+
if patch is not None:
4736+
patch.set_visible((ax is bottom) and ax.get_frame_on())
4737+
4738+
def _make_twin_axes(self, *args, delta_zorder=0.0, **kwargs):
47094739
"""Make a twinx Axes of self. This is used for twinx and twiny."""
47104740
if 'sharex' in kwargs and 'sharey' in kwargs:
47114741
# The following line is added in v2.2 to avoid breaking Seaborn,
@@ -4722,7 +4752,7 @@ def _make_twin_axes(self, *args, **kwargs):
47224752
[0, 0, 1, 1], self.transAxes))
47234753
self.set_adjustable('datalim')
47244754
twin.set_adjustable('datalim')
4725-
twin.set_zorder(self.zorder)
4755+
twin.set_zorder(self.get_zorder() + delta_zorder)
47264756

47274757
self._twinned_axes.join(self, twin)
47284758

@@ -4739,9 +4769,10 @@ def _make_twin_axes(self, *args, **kwargs):
47394769
twin._set_position(self.get_position(original=True), which="original")
47404770
twin._set_position(self.get_position(original=False), which="active")
47414771

4772+
self._update_twinned_axes_patch_visibility()
47424773
return twin
47434774

4744-
def twinx(self, axes_class=None, **kwargs):
4775+
def twinx(self, axes_class=None, *, delta_zorder=0.0, **kwargs):
47454776
"""
47464777
Create a twin Axes sharing the xaxis.
47474778
@@ -4762,6 +4793,12 @@ def twinx(self, axes_class=None, **kwargs):
47624793
47634794
.. versionadded:: 3.11
47644795
4796+
delta_zorder : float, default: 0
4797+
A zorder offset for the twin Axes, relative to the original Axes.
4798+
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
4799+
By default (*delta_zorder* is 0), the twin has the same zorder as
4800+
the original Axes.
4801+
47654802
kwargs : dict
47664803
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.
47674804
@@ -4779,18 +4816,17 @@ def twinx(self, axes_class=None, **kwargs):
47794816
"""
47804817
if axes_class:
47814818
kwargs["axes_class"] = axes_class
4782-
ax2 = self._make_twin_axes(sharex=self, **kwargs)
4819+
ax2 = self._make_twin_axes(sharex=self, delta_zorder=delta_zorder, **kwargs)
47834820
ax2.yaxis.tick_right()
47844821
ax2.yaxis.set_label_position('right')
47854822
ax2.yaxis.set_offset_position('right')
47864823
ax2.set_autoscalex_on(self.get_autoscalex_on())
47874824
self.yaxis.tick_left()
47884825
ax2.xaxis.set_visible(False)
4789-
ax2.patch.set_visible(False)
47904826
ax2.xaxis.units = self.xaxis.units
47914827
return ax2
47924828

4793-
def twiny(self, axes_class=None, **kwargs):
4829+
def twiny(self, axes_class=None, *, delta_zorder=0.0, **kwargs):
47944830
"""
47954831
Create a twin Axes sharing the yaxis.
47964832
@@ -4811,6 +4847,12 @@ def twiny(self, axes_class=None, **kwargs):
48114847
48124848
.. versionadded:: 3.11
48134849
4850+
delta_zorder : float, default: 0
4851+
A zorder offset for the twin Axes, relative to the original Axes.
4852+
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
4853+
By default (*delta_zorder* is 0), the twin has the same zorder as
4854+
the original Axes.
4855+
48144856
kwargs : dict
48154857
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.
48164858
@@ -4828,13 +4870,12 @@ def twiny(self, axes_class=None, **kwargs):
48284870
"""
48294871
if axes_class:
48304872
kwargs["axes_class"] = axes_class
4831-
ax2 = self._make_twin_axes(sharey=self, **kwargs)
4873+
ax2 = self._make_twin_axes(sharey=self, delta_zorder=delta_zorder, **kwargs)
48324874
ax2.xaxis.tick_top()
48334875
ax2.xaxis.set_label_position('top')
48344876
ax2.set_autoscaley_on(self.get_autoscaley_on())
48354877
self.xaxis.tick_bottom()
48364878
ax2.yaxis.set_visible(False)
4837-
ax2.patch.set_visible(False)
48384879
ax2.yaxis.units = self.yaxis.units
48394880
return ax2
48404881

lib/matplotlib/axes/_base.pyi

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,14 @@ class _AxesBase(martist.Artist):
384384
*,
385385
call_axes_locator: bool = ...,
386386
bbox_extra_artists: Sequence[Artist] | None = ...,
387-
for_layout_only: bool = ...
387+
for_layout_only: bool = ...,
388388
) -> Bbox | None: ...
389-
def twinx(self, axes_class: Axes | None = ..., **kwargs) -> Axes: ...
390-
def twiny(self, axes_class: Axes | None = ..., **kwargs) -> Axes: ...
389+
def twinx(
390+
self, axes_class: Axes | None = ..., *, delta_zorder: float = ..., **kwargs
391+
) -> Axes: ...
392+
def twiny(
393+
self, axes_class: Axes | None = ..., *, delta_zorder: float = ..., **kwargs
394+
) -> Axes: ...
391395
@classmethod
392396
def get_shared_x_axes(cls) -> cbook.GrouperView: ...
393397
@classmethod

lib/matplotlib/tests/test_axes.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8100,6 +8100,50 @@ def test_twinning_default_axes_class():
81008100
assert type(twiny) is Axes
81018101

81028102

8103+
def test_twinning_patch_visibility_default():
8104+
_, ax = plt.subplots()
8105+
ax2 = ax.twinx()
8106+
assert ax.patch.get_visible()
8107+
assert not ax2.patch.get_visible()
8108+
8109+
8110+
def test_twinning_patch_visibility_respects_delta_zorder():
8111+
_, ax = plt.subplots()
8112+
ax2 = ax.twinx(delta_zorder=-1)
8113+
assert ax2.get_zorder() == ax.get_zorder() - 1
8114+
assert ax2.patch.get_visible()
8115+
assert not ax.patch.get_visible()
8116+
8117+
8118+
def test_twinning_patch_visibility_multiple_twins_same_zorder():
8119+
_, ax = plt.subplots()
8120+
ax2 = ax.twinx()
8121+
ax3 = ax.twinx()
8122+
assert ax.patch.get_visible()
8123+
assert not ax2.patch.get_visible()
8124+
assert not ax3.patch.get_visible()
8125+
8126+
8127+
def test_twinning_patch_visibility_updates_for_new_bottom():
8128+
_, ax = plt.subplots()
8129+
ax2 = ax.twinx()
8130+
ax3 = ax.twinx(delta_zorder=-1)
8131+
assert ax3.patch.get_visible()
8132+
assert not ax2.patch.get_visible()
8133+
assert not ax.patch.get_visible()
8134+
8135+
8136+
def test_twinning_patch_visibility_updates_after_set_zorder():
8137+
_, ax = plt.subplots()
8138+
ax2 = ax.twinx()
8139+
assert ax.patch.get_visible()
8140+
assert not ax2.patch.get_visible()
8141+
8142+
ax2.set_zorder(ax.get_zorder() - 1)
8143+
assert ax2.patch.get_visible()
8144+
assert not ax.patch.get_visible()
8145+
8146+
81038147
@mpl.style.context('mpl20')
81048148
@check_figures_equal()
81058149
def test_stairs_fill_zero_linewidth(fig_test, fig_ref):

0 commit comments

Comments
 (0)