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

Skip to content

Commit 0287455

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

5 files changed

Lines changed: 186 additions & 37 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: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -612,21 +612,28 @@ class _AxesBase(martist.Artist):
612612

613613
def __str__(self):
614614
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
615-
type(self).__name__, self._position.bounds)
616-
617-
def __init__(self, fig,
618-
*args,
619-
facecolor=None, # defaults to rc axes.facecolor
620-
frameon=True,
621-
sharex=None, # use Axes instance's xaxis info
622-
sharey=None, # use Axes instance's yaxis info
623-
label='',
624-
xscale=None,
625-
yscale=None,
626-
box_aspect=None,
627-
forward_navigation_events="auto",
628-
**kwargs
629-
):
615+
type(self).__name__, self._position.bounds
616+
)
617+
618+
def set_zorder(self, level):
619+
super().set_zorder(level)
620+
self._update_twinned_axes_patch_visibility()
621+
622+
def __init__(
623+
self,
624+
fig,
625+
*args,
626+
facecolor=None, # defaults to rc axes.facecolor
627+
frameon=True,
628+
sharex=None, # use Axes instance's xaxis info
629+
sharey=None, # use Axes instance's yaxis info
630+
label="",
631+
xscale=None,
632+
yscale=None,
633+
box_aspect=None,
634+
forward_navigation_events="auto",
635+
**kwargs,
636+
):
630637
"""
631638
Build an Axes in a figure.
632639
@@ -3316,6 +3323,7 @@ def set_frame_on(self, b):
33163323
b : bool
33173324
"""
33183325
self._frameon = b
3326+
self._update_twinned_axes_patch_visibility()
33193327
self.stale = True
33203328

33213329
def get_axisbelow(self):
@@ -4680,12 +4688,36 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True,
46804688
bbox = a.get_tightbbox(renderer)
46814689
if bbox is not None and bbox._is_finite():
46824690
bb.append(bbox)
4683-
return mtransforms.Bbox.union(
4684-
[b for b in bb if b.width != 0 or b.height != 0])
4691+
return mtransforms.Bbox.union([b for b in bb if b.width != 0 or b.height != 0])
46854692

4686-
def _make_twin_axes(self, *args, **kwargs):
4693+
def _update_twinned_axes_patch_visibility(self):
4694+
"""
4695+
Update patch visibility for a group of twinned Axes.
4696+
4697+
Only the bottom-most Axes in the group (lowest zorder, breaking ties by
4698+
creation/insertion order) has a visible background patch.
4699+
"""
4700+
if self not in self._twinned_axes:
4701+
return
4702+
twinned = list(self._twinned_axes.get_siblings(self))
4703+
if not twinned:
4704+
return
4705+
fig = self.get_figure(root=False)
4706+
fig_axes = fig.axes if fig is not None else []
4707+
insertion_order = {ax: idx for idx, ax in enumerate(fig_axes)}
4708+
4709+
twinned.sort(
4710+
key=lambda ax: (ax.get_zorder(), insertion_order.get(ax, len(fig_axes)))
4711+
)
4712+
bottom = twinned[0]
4713+
for ax in twinned:
4714+
patch = getattr(ax, "patch", None)
4715+
if patch is not None:
4716+
patch.set_visible((ax is bottom) and ax.get_frame_on())
4717+
4718+
def _make_twin_axes(self, *args, delta_zorder=0.0, **kwargs):
46874719
"""Make a twinx Axes of self. This is used for twinx and twiny."""
4688-
if 'sharex' in kwargs and 'sharey' in kwargs:
4720+
if "sharex" in kwargs and "sharey" in kwargs:
46894721
# The following line is added in v2.2 to avoid breaking Seaborn,
46904722
# which currently uses this internal API.
46914723
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
@@ -4695,17 +4727,20 @@ def _make_twin_axes(self, *args, **kwargs):
46954727
twin = self.get_figure(root=False).add_subplot(ss, *args, **kwargs)
46964728
else:
46974729
twin = self.get_figure(root=False).add_axes(
4698-
self.get_position(True), *args, **kwargs,
4699-
axes_locator=_TransformedBoundsLocator(
4700-
[0, 0, 1, 1], self.transAxes))
4701-
self.set_adjustable('datalim')
4702-
twin.set_adjustable('datalim')
4703-
twin.set_zorder(self.zorder)
4730+
self.get_position(True),
4731+
*args,
4732+
**kwargs,
4733+
axes_locator=_TransformedBoundsLocator([0, 0, 1, 1], self.transAxes),
4734+
)
4735+
self.set_adjustable("datalim")
4736+
twin.set_adjustable("datalim")
4737+
twin.set_zorder(self.get_zorder() + delta_zorder)
47044738

47054739
self._twinned_axes.join(self, twin)
4740+
self._update_twinned_axes_patch_visibility()
47064741
return twin
47074742

4708-
def twinx(self, axes_class=None, **kwargs):
4743+
def twinx(self, axes_class=None, *, delta_zorder=0.0, **kwargs):
47094744
"""
47104745
Create a twin Axes sharing the xaxis.
47114746
@@ -4726,6 +4761,12 @@ def twinx(self, axes_class=None, **kwargs):
47264761
47274762
.. versionadded:: 3.11
47284763
4764+
delta_zorder : float, default: 0
4765+
A zorder offset for the twin Axes, relative to the original Axes.
4766+
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
4767+
By default (*delta_zorder* is 0), the twin has the same zorder as
4768+
the original Axes.
4769+
47294770
kwargs : dict
47304771
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.
47314772
@@ -4743,18 +4784,17 @@ def twinx(self, axes_class=None, **kwargs):
47434784
"""
47444785
if axes_class:
47454786
kwargs["axes_class"] = axes_class
4746-
ax2 = self._make_twin_axes(sharex=self, **kwargs)
4787+
ax2 = self._make_twin_axes(sharex=self, delta_zorder=delta_zorder, **kwargs)
47474788
ax2.yaxis.tick_right()
4748-
ax2.yaxis.set_label_position('right')
4749-
ax2.yaxis.set_offset_position('right')
4789+
ax2.yaxis.set_label_position("right")
4790+
ax2.yaxis.set_offset_position("right")
47504791
ax2.set_autoscalex_on(self.get_autoscalex_on())
47514792
self.yaxis.tick_left()
47524793
ax2.xaxis.set_visible(False)
4753-
ax2.patch.set_visible(False)
47544794
ax2.xaxis.units = self.xaxis.units
47554795
return ax2
47564796

4757-
def twiny(self, axes_class=None, **kwargs):
4797+
def twiny(self, axes_class=None, *, delta_zorder=0.0, **kwargs):
47584798
"""
47594799
Create a twin Axes sharing the yaxis.
47604800
@@ -4775,6 +4815,12 @@ def twiny(self, axes_class=None, **kwargs):
47754815
47764816
.. versionadded:: 3.11
47774817
4818+
delta_zorder : float, default: 0
4819+
A zorder offset for the twin Axes, relative to the original Axes.
4820+
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
4821+
By default (*delta_zorder* is 0), the twin has the same zorder as
4822+
the original Axes.
4823+
47784824
kwargs : dict
47794825
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.
47804826
@@ -4792,13 +4838,12 @@ def twiny(self, axes_class=None, **kwargs):
47924838
"""
47934839
if axes_class:
47944840
kwargs["axes_class"] = axes_class
4795-
ax2 = self._make_twin_axes(sharey=self, **kwargs)
4841+
ax2 = self._make_twin_axes(sharey=self, delta_zorder=delta_zorder, **kwargs)
47964842
ax2.xaxis.tick_top()
4797-
ax2.xaxis.set_label_position('top')
4843+
ax2.xaxis.set_label_position("top")
47984844
ax2.set_autoscaley_on(self.get_autoscaley_on())
47994845
self.xaxis.tick_bottom()
48004846
ax2.yaxis.set_visible(False)
4801-
ax2.patch.set_visible(False)
48024847
ax2.yaxis.units = self.yaxis.units
48034848
return ax2
48044849

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
@@ -8081,6 +8081,50 @@ def test_twinning_default_axes_class():
80818081
assert type(twiny) is Axes
80828082

80838083

8084+
def test_twinning_patch_visibility_default():
8085+
_, ax = plt.subplots()
8086+
ax2 = ax.twinx()
8087+
assert ax.patch.get_visible()
8088+
assert not ax2.patch.get_visible()
8089+
8090+
8091+
def test_twinning_patch_visibility_respects_delta_zorder():
8092+
_, ax = plt.subplots()
8093+
ax2 = ax.twinx(delta_zorder=-1)
8094+
assert ax2.get_zorder() == ax.get_zorder() - 1
8095+
assert ax2.patch.get_visible()
8096+
assert not ax.patch.get_visible()
8097+
8098+
8099+
def test_twinning_patch_visibility_multiple_twins_same_zorder():
8100+
_, ax = plt.subplots()
8101+
ax2 = ax.twinx()
8102+
ax3 = ax.twinx()
8103+
assert ax.patch.get_visible()
8104+
assert not ax2.patch.get_visible()
8105+
assert not ax3.patch.get_visible()
8106+
8107+
8108+
def test_twinning_patch_visibility_updates_for_new_bottom():
8109+
_, ax = plt.subplots()
8110+
ax2 = ax.twinx()
8111+
ax3 = ax.twinx(delta_zorder=-1)
8112+
assert ax3.patch.get_visible()
8113+
assert not ax2.patch.get_visible()
8114+
assert not ax.patch.get_visible()
8115+
8116+
8117+
def test_twinning_patch_visibility_updates_after_set_zorder():
8118+
_, ax = plt.subplots()
8119+
ax2 = ax.twinx()
8120+
assert ax.patch.get_visible()
8121+
assert not ax2.patch.get_visible()
8122+
8123+
ax2.set_zorder(ax.get_zorder() - 1)
8124+
assert ax2.patch.get_visible()
8125+
assert not ax.patch.get_visible()
8126+
8127+
80848128
def test_zero_linewidth():
80858129
# Check that setting a zero linewidth doesn't error
80868130
plt.plot([0, 1], [0, 1], ls='--', lw=0)

0 commit comments

Comments
 (0)