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

Skip to content

Commit dfc0d05

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

5 files changed

Lines changed: 168 additions & 41 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: 52 additions & 22 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
@@ -2470,20 +2474,6 @@ def _add_text(self, txt):
24702474
self.stale = True
24712475
return txt
24722476

2473-
def _point_in_data_domain(self, x, y):
2474-
"""
2475-
Check if the data point (x, y) is within the valid domain of the axes
2476-
scales.
2477-
2478-
Returns False if the point is outside the data range
2479-
(e.g. negative coordinates with a log scale).
2480-
"""
2481-
for val, axis in zip([x, y], self._axis_map.values()):
2482-
vmin, vmax = axis.limit_range_for_scale(val, val)
2483-
if vmin != val or vmax != val:
2484-
return False
2485-
return True
2486-
24872477
def _update_line_limits(self, line):
24882478
"""
24892479
Figures out the data limit of the given line, updating `.Axes.dataLim`.
@@ -3316,6 +3306,7 @@ def set_frame_on(self, b):
33163306
b : bool
33173307
"""
33183308
self._frameon = b
3309+
self._update_twinned_axes_patch_visibility()
33193310
self.stale = True
33203311

33213312
def get_axisbelow(self):
@@ -4683,7 +4674,31 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True,
46834674
return mtransforms.Bbox.union(
46844675
[b for b in bb if b.width != 0 or b.height != 0])
46854676

4686-
def _make_twin_axes(self, *args, **kwargs):
4677+
def _update_twinned_axes_patch_visibility(self):
4678+
"""
4679+
Update patch visibility for a group of twinned Axes.
4680+
4681+
Only the bottom-most Axes in the group (lowest zorder, breaking ties by
4682+
creation/insertion order) has a visible background patch.
4683+
"""
4684+
if self not in self._twinned_axes:
4685+
return
4686+
twinned = list(self._twinned_axes.get_siblings(self))
4687+
if not twinned:
4688+
return
4689+
fig = self.get_figure(root=False)
4690+
fig_axes = fig.axes if fig is not None else []
4691+
insertion_order = {ax: idx for idx, ax in enumerate(fig_axes)}
4692+
4693+
twinned.sort(key=lambda ax: (ax.get_zorder(),
4694+
insertion_order.get(ax, len(fig_axes))))
4695+
bottom = twinned[0]
4696+
for ax in twinned:
4697+
patch = getattr(ax, "patch", None)
4698+
if patch is not None:
4699+
patch.set_visible((ax is bottom) and ax.get_frame_on())
4700+
4701+
def _make_twin_axes(self, *args, delta_zorder=None, **kwargs):
46874702
"""Make a twinx Axes of self. This is used for twinx and twiny."""
46884703
if 'sharex' in kwargs and 'sharey' in kwargs:
46894704
# The following line is added in v2.2 to avoid breaking Seaborn,
@@ -4700,12 +4715,15 @@ def _make_twin_axes(self, *args, **kwargs):
47004715
[0, 0, 1, 1], self.transAxes))
47014716
self.set_adjustable('datalim')
47024717
twin.set_adjustable('datalim')
4703-
twin.set_zorder(self.zorder)
4718+
original_zorder = self.get_zorder()
4719+
twin.set_zorder(original_zorder if delta_zorder is None
4720+
else original_zorder + delta_zorder)
47044721

47054722
self._twinned_axes.join(self, twin)
4723+
self._update_twinned_axes_patch_visibility()
47064724
return twin
47074725

4708-
def twinx(self, axes_class=None, **kwargs):
4726+
def twinx(self, axes_class=None, *, delta_zorder=None, **kwargs):
47094727
"""
47104728
Create a twin Axes sharing the xaxis.
47114729
@@ -4726,6 +4744,12 @@ def twinx(self, axes_class=None, **kwargs):
47264744
47274745
.. versionadded:: 3.11
47284746
4747+
delta_zorder : float, optional
4748+
A zorder offset for the twin Axes, relative to the original Axes.
4749+
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
4750+
By default (*delta_zorder* is None), the twin has the same zorder
4751+
as the original Axes.
4752+
47294753
kwargs : dict
47304754
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.
47314755
@@ -4743,18 +4767,18 @@ def twinx(self, axes_class=None, **kwargs):
47434767
"""
47444768
if axes_class:
47454769
kwargs["axes_class"] = axes_class
4746-
ax2 = self._make_twin_axes(sharex=self, **kwargs)
4770+
ax2 = self._make_twin_axes(sharex=self, delta_zorder=delta_zorder,
4771+
**kwargs)
47474772
ax2.yaxis.tick_right()
47484773
ax2.yaxis.set_label_position('right')
47494774
ax2.yaxis.set_offset_position('right')
47504775
ax2.set_autoscalex_on(self.get_autoscalex_on())
47514776
self.yaxis.tick_left()
47524777
ax2.xaxis.set_visible(False)
4753-
ax2.patch.set_visible(False)
47544778
ax2.xaxis.units = self.xaxis.units
47554779
return ax2
47564780

4757-
def twiny(self, axes_class=None, **kwargs):
4781+
def twiny(self, axes_class=None, *, delta_zorder=None, **kwargs):
47584782
"""
47594783
Create a twin Axes sharing the yaxis.
47604784
@@ -4775,6 +4799,12 @@ def twiny(self, axes_class=None, **kwargs):
47754799
47764800
.. versionadded:: 3.11
47774801
4802+
delta_zorder : float, optional
4803+
A zorder offset for the twin Axes, relative to the original Axes.
4804+
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
4805+
By default (*delta_zorder* is None), the twin has the same zorder
4806+
as the original Axes.
4807+
47784808
kwargs : dict
47794809
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.
47804810
@@ -4792,13 +4822,13 @@ def twiny(self, axes_class=None, **kwargs):
47924822
"""
47934823
if axes_class:
47944824
kwargs["axes_class"] = axes_class
4795-
ax2 = self._make_twin_axes(sharey=self, **kwargs)
4825+
ax2 = self._make_twin_axes(sharey=self, delta_zorder=delta_zorder,
4826+
**kwargs)
47964827
ax2.xaxis.tick_top()
47974828
ax2.xaxis.set_label_position('top')
47984829
ax2.set_autoscaley_on(self.get_autoscaley_on())
47994830
self.xaxis.tick_bottom()
48004831
ax2.yaxis.set_visible(False)
4801-
ax2.patch.set_visible(False)
48024832
ax2.yaxis.units = self.yaxis.units
48034833
return ax2
48044834

lib/matplotlib/axes/_base.pyi

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,20 @@ class _AxesBase(martist.Artist):
386386
bbox_extra_artists: Sequence[Artist] | None = ...,
387387
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,
391+
axes_class: Axes | None = ...,
392+
*,
393+
delta_zorder: float | None = ...,
394+
**kwargs
395+
) -> Axes: ...
396+
def twiny(
397+
self,
398+
axes_class: Axes | None = ...,
399+
*,
400+
delta_zorder: float | None = ...,
401+
**kwargs
402+
) -> Axes: ...
391403
@classmethod
392404
def get_shared_x_axes(cls) -> cbook.GrouperView: ...
393405
@classmethod

lib/matplotlib/tests/test_axes.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,21 +2461,6 @@ def test_hist_log_barstacked():
24612461
assert axs[0].get_ylim() == axs[1].get_ylim()
24622462

24632463

2464-
def test_hist_timedelta_raises():
2465-
import numpy as np
2466-
import matplotlib.pyplot as plt
2467-
2468-
fig, ax = plt.subplots()
2469-
2470-
arr_np = np.array([1, 2, 5, 7], dtype="timedelta64[D]")
2471-
with pytest.raises(TypeError, match="does not currently support timedelta inputs"):
2472-
ax.hist(arr_np)
2473-
2474-
arr_py = [datetime.timedelta(seconds=i) for i in range(5)]
2475-
with pytest.raises(TypeError, match="does not currently support timedelta inputs"):
2476-
ax.hist(arr_py)
2477-
2478-
24792464
@image_comparison(['hist_bar_empty.png'], remove_text=True)
24802465
def test_hist_bar_empty():
24812466
# From #3886: creating hist from empty dataset raises ValueError
@@ -8081,6 +8066,50 @@ def test_twinning_default_axes_class():
80818066
assert type(twiny) is Axes
80828067

80838068

8069+
def test_twinning_patch_visibility_default():
8070+
_, ax = plt.subplots()
8071+
ax2 = ax.twinx()
8072+
assert ax.patch.get_visible()
8073+
assert not ax2.patch.get_visible()
8074+
8075+
8076+
def test_twinning_patch_visibility_respects_zorder():
8077+
_, ax = plt.subplots()
8078+
ax2 = ax.twinx(delta_zorder=-1)
8079+
assert ax2.get_zorder() == ax.get_zorder() - 1
8080+
assert ax2.patch.get_visible()
8081+
assert not ax.patch.get_visible()
8082+
8083+
8084+
def test_twinning_patch_visibility_multiple_twins_same_zorder():
8085+
_, ax = plt.subplots()
8086+
ax2 = ax.twinx()
8087+
ax3 = ax.twinx()
8088+
assert ax.patch.get_visible()
8089+
assert not ax2.patch.get_visible()
8090+
assert not ax3.patch.get_visible()
8091+
8092+
8093+
def test_twinning_patch_visibility_updates_for_new_bottom():
8094+
_, ax = plt.subplots()
8095+
ax2 = ax.twinx(delta_zorder=-1)
8096+
ax3 = ax.twinx(delta_zorder=-2)
8097+
assert ax3.patch.get_visible()
8098+
assert not ax2.patch.get_visible()
8099+
assert not ax.patch.get_visible()
8100+
8101+
8102+
def test_twinning_patch_visibility_updates_after_set_zorder():
8103+
_, ax = plt.subplots()
8104+
ax2 = ax.twinx()
8105+
assert ax.patch.get_visible()
8106+
assert not ax2.patch.get_visible()
8107+
8108+
ax2.set_zorder(ax.get_zorder() - 1)
8109+
assert ax2.patch.get_visible()
8110+
assert not ax.patch.get_visible()
8111+
8112+
80848113
def test_zero_linewidth():
80858114
# Check that setting a zero linewidth doesn't error
80868115
plt.plot([0, 1], [0, 1], ls='--', lw=0)
@@ -8530,7 +8559,7 @@ def test_normal_axes():
85308559
]
85318560
for nn, b in enumerate(bbaxis):
85328561
targetbb = mtransforms.Bbox.from_bounds(*target[nn])
8533-
assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=1)
8562+
assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2)
85348563

85358564
target = [
85368565
[150.0, 119.999, 930.0, 11.111],
@@ -8548,7 +8577,7 @@ def test_normal_axes():
85488577

85498578
target = [85.5138, 75.88888, 1021.11, 1017.11]
85508579
targetbb = mtransforms.Bbox.from_bounds(*target)
8551-
assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=1)
8580+
assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=2)
85528581

85538582
# test that get_position roundtrips to get_window_extent
85548583
axbb = ax.get_position().transformed(fig.transFigure).bounds

0 commit comments

Comments
 (0)