Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/release/next_whats_new/twin_axes_zorder.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Twin Axes ``delta_zorder``
--------------------------

`~matplotlib.axes.Axes.twinx` and `~matplotlib.axes.Axes.twiny` now accept a
*delta_zorder* keyword argument, a relative offset added to the original Axes'
zorder, to control whether the twin Axes is drawn in front of, or behind, the
original Axes. For example, pass ``delta_zorder=-1`` to draw a twin Axes
behind the main Axes.

In addition, Matplotlib now automatically manages background patch visibility
for each group of twinned Axes so that only the bottom-most Axes in the group
has a visible background patch (respecting ``frameon``).
44 changes: 44 additions & 0 deletions galleries/examples/subplots_axes_and_figures/twin_axes_zorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
===========================
Twin Axes with delta_zorder
===========================

`~matplotlib.axes.Axes.twinx` and `~matplotlib.axes.Axes.twiny` accept a
*delta_zorder* keyword argument (a relative offset added to the original Axes'
zorder) that controls whether the twin Axes is drawn in front of or behind the
original Axes.

Matplotlib also automatically manages background patch visibility for twinned
Axes groups so that only the bottom-most Axes has a visible background patch
(respecting ``frameon``). This avoids the background of a higher-zorder twin
Axes covering artists drawn on the underlying Axes.
"""

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 400)
y_main = np.sin(x)
y_twin = 0.4 * np.cos(x) + 0.6

fig, ax = plt.subplots()

# Put the twin Axes behind the original Axes (relative to the original zorder).
ax2 = ax.twinx(delta_zorder=-1)

# Draw something broad on the twin Axes so that the stacking is obvious.
ax2.fill_between(x, 0, y_twin, color="C1", alpha=0.35, label="twin fill")
ax2.plot(x, y_twin, color="C1", lw=6, alpha=0.8)

# Draw overlapping artists on the main Axes; they appear on top.
ax.scatter(x[::8], y_main[::8], s=35, color="C0", edgecolor="k", linewidth=0.5,
zorder=3, label="main scatter")
ax.plot(x, y_main, color="C0", lw=4)

ax.set_xlabel("x")
ax.set_ylabel("main y")
ax2.set_ylabel("twin y")
ax.set_title("Twin Axes drawn behind the main Axes using delta_zorder")

fig.tight_layout()
plt.show()
56 changes: 48 additions & 8 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,10 @@ def __str__(self):
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
type(self).__name__, self._position.bounds)

def set_zorder(self, level):
super().set_zorder(level)
self._update_twinned_axes_patch_visibility()

def __init__(self, fig,
*args,
facecolor=None, # defaults to rc axes.facecolor
Expand Down Expand Up @@ -3324,6 +3328,7 @@ def set_frame_on(self, b):
b : bool
"""
self._frameon = b
self._update_twinned_axes_patch_visibility()
self.stale = True

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

def _make_twin_axes(self, *args, **kwargs):
def _update_twinned_axes_patch_visibility(self):
"""
Update patch visibility for a group of twinned Axes.

Only the bottom-most Axes in the group (lowest zorder, breaking ties by
creation/insertion order) has a visible background patch.
"""
if self not in self._twinned_axes:
return
twinned = list(self._twinned_axes.get_siblings(self))
if not twinned:
return
fig = self.get_figure(root=False)
fig_axes = fig.axes if fig is not None else []
insertion_order = {ax: idx for idx, ax in enumerate(fig_axes)}

twinned.sort(
key=lambda ax: (ax.get_zorder(), insertion_order.get(ax, len(fig_axes)))
)
bottom = twinned[0]
for ax in twinned:
if ax.patch is not None:
ax.patch.set_visible((ax is bottom) and ax.get_frame_on())

def _make_twin_axes(self, *args, delta_zorder=0.0, **kwargs):
"""Make a twinx Axes of self. This is used for twinx and twiny."""
if 'sharex' in kwargs and 'sharey' in kwargs:
# The following line is added in v2.2 to avoid breaking Seaborn,
Expand All @@ -4722,12 +4751,13 @@ def _make_twin_axes(self, *args, **kwargs):
[0, 0, 1, 1], self.transAxes))
self.set_adjustable('datalim')
twin.set_adjustable('datalim')
twin.set_zorder(self.zorder)
twin.set_zorder(self.get_zorder() + delta_zorder)

self._twinned_axes.join(self, twin)
self._update_twinned_axes_patch_visibility()
return twin

def twinx(self, axes_class=None, **kwargs):
def twinx(self, axes_class=None, *, delta_zorder=0.0, **kwargs):
"""
Create a twin Axes sharing the xaxis.

Expand All @@ -4748,6 +4778,12 @@ def twinx(self, axes_class=None, **kwargs):

.. versionadded:: 3.11

delta_zorder : float, default: 0
A zorder offset for the twin Axes, relative to the original Axes.
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
By default (*delta_zorder* is 0), the twin has the same zorder as
the original Axes.

kwargs : dict
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.

Expand All @@ -4765,18 +4801,17 @@ def twinx(self, axes_class=None, **kwargs):
"""
if axes_class:
kwargs["axes_class"] = axes_class
ax2 = self._make_twin_axes(sharex=self, **kwargs)
ax2 = self._make_twin_axes(sharex=self, delta_zorder=delta_zorder, **kwargs)
ax2.yaxis.tick_right()
ax2.yaxis.set_label_position('right')
ax2.yaxis.set_offset_position('right')
ax2.set_autoscalex_on(self.get_autoscalex_on())
self.yaxis.tick_left()
ax2.xaxis.set_visible(False)
ax2.patch.set_visible(False)
ax2.xaxis.units = self.xaxis.units
return ax2

def twiny(self, axes_class=None, **kwargs):
def twiny(self, axes_class=None, *, delta_zorder=0.0, **kwargs):
"""
Create a twin Axes sharing the yaxis.

Expand All @@ -4797,6 +4832,12 @@ def twiny(self, axes_class=None, **kwargs):

.. versionadded:: 3.11

delta_zorder : float, default: 0
A zorder offset for the twin Axes, relative to the original Axes.
The twin's zorder is set to ``self.get_zorder() + delta_zorder``.
By default (*delta_zorder* is 0), the twin has the same zorder as
the original Axes.

kwargs : dict
The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`.

Expand All @@ -4814,13 +4855,12 @@ def twiny(self, axes_class=None, **kwargs):
"""
if axes_class:
kwargs["axes_class"] = axes_class
ax2 = self._make_twin_axes(sharey=self, **kwargs)
ax2 = self._make_twin_axes(sharey=self, delta_zorder=delta_zorder, **kwargs)
ax2.xaxis.tick_top()
ax2.xaxis.set_label_position('top')
ax2.set_autoscaley_on(self.get_autoscaley_on())
self.xaxis.tick_bottom()
ax2.yaxis.set_visible(False)
ax2.patch.set_visible(False)
ax2.yaxis.units = self.yaxis.units
return ax2

Expand Down
10 changes: 7 additions & 3 deletions lib/matplotlib/axes/_base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -384,10 +384,14 @@ class _AxesBase(martist.Artist):
*,
call_axes_locator: bool = ...,
bbox_extra_artists: Sequence[Artist] | None = ...,
for_layout_only: bool = ...
for_layout_only: bool = ...,
) -> Bbox | None: ...
def twinx(self, axes_class: Axes | None = ..., **kwargs) -> Axes: ...
def twiny(self, axes_class: Axes | None = ..., **kwargs) -> Axes: ...
def twinx(
self, axes_class: Axes | None = ..., *, delta_zorder: float = ..., **kwargs
) -> Axes: ...
def twiny(
self, axes_class: Axes | None = ..., *, delta_zorder: float = ..., **kwargs
) -> Axes: ...
@classmethod
def get_shared_x_axes(cls) -> cbook.GrouperView: ...
@classmethod
Expand Down
44 changes: 44 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8081,6 +8081,50 @@ def test_twinning_default_axes_class():
assert type(twiny) is Axes


def test_twinning_patch_visibility_default():
_, ax = plt.subplots()
ax2 = ax.twinx()
assert ax.patch.get_visible()
assert not ax2.patch.get_visible()


def test_twinning_patch_visibility_respects_delta_zorder():
_, ax = plt.subplots()
ax2 = ax.twinx(delta_zorder=-1)
assert ax2.get_zorder() == ax.get_zorder() - 1
assert ax2.patch.get_visible()
assert not ax.patch.get_visible()


def test_twinning_patch_visibility_multiple_twins_same_zorder():
_, ax = plt.subplots()
ax2 = ax.twinx()
ax3 = ax.twinx()
assert ax.patch.get_visible()
assert not ax2.patch.get_visible()
assert not ax3.patch.get_visible()


def test_twinning_patch_visibility_updates_for_new_bottom():
_, ax = plt.subplots()
ax2 = ax.twinx()
ax3 = ax.twinx(delta_zorder=-1)
assert ax3.patch.get_visible()
assert not ax2.patch.get_visible()
assert not ax.patch.get_visible()


def test_twinning_patch_visibility_updates_after_set_zorder():
_, ax = plt.subplots()
ax2 = ax.twinx()
assert ax.patch.get_visible()
assert not ax2.patch.get_visible()

ax2.set_zorder(ax.get_zorder() - 1)
assert ax2.patch.get_visible()
assert not ax.patch.get_visible()


@mpl.style.context('mpl20')
@check_figures_equal()
def test_stairs_fill_zero_linewidth(fig_test, fig_ref):
Expand Down
Loading