Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ae73ddb

Browse filesBrowse files
authored
Merge pull request #27148 from raphaelquast/nested_ax_zoom
Correctly treat pan/zoom events of overlapping axes.
2 parents 3e47cc2 + 5394aff commit ae73ddb
Copy full SHA for ae73ddb

File tree

Expand file treeCollapse file tree

7 files changed

+332
-10
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+332
-10
lines changed

‎doc/api/axes_api.rst

Copy file name to clipboardExpand all lines: doc/api/axes_api.rst
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,9 @@ Interactive
517517
Axes.get_navigate_mode
518518
Axes.set_navigate_mode
519519

520+
Axes.get_forward_navigation_events
521+
Axes.set_forward_navigation_events
522+
520523
Axes.start_pan
521524
Axes.drag_pan
522525
Axes.end_pan
+13Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Correctly treat pan/zoom events of overlapping Axes
2+
---------------------------------------------------
3+
4+
The forwarding of pan/zoom events is now determined by the visibility of the
5+
background-patch (e.g. ``ax.patch.get_visible()``) and by the ``zorder`` of the axes.
6+
7+
- Axes with a visible patch capture the event and do not pass it on to axes below.
8+
Only the Axes with the highest ``zorder`` that contains the event is triggered
9+
(if there are multiple Axes with the same ``zorder``, the last added Axes counts)
10+
- Axes with an invisible patch are also invisible to events and they are passed on to the axes below.
11+
12+
To override the default behavior and explicitly set whether an Axes
13+
should forward navigation events, use `.Axes.set_forward_navigation_events`.
+74Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
===================================
3+
Pan/zoom events of overlapping axes
4+
===================================
5+
6+
Example to illustrate how pan/zoom events of overlapping axes are treated.
7+
8+
9+
The default is the following:
10+
11+
- Axes with a visible patch capture pan/zoom events
12+
- Axes with an invisible patch forward pan/zoom events to axes below
13+
- Shared axes always trigger with their parent axes
14+
(irrespective of the patch visibility)
15+
16+
Note: The visibility of the patch hereby refers to the value of
17+
``ax.patch.get_visible()``. The color and transparency of a
18+
patch have no effect on the treatment of pan/zoom events!
19+
20+
21+
``ax.set_forward_navigation_events(val)`` can be used to override the
22+
default behaviour:
23+
24+
- ``True``: Forward navigation events to axes below.
25+
- ``False``: Execute navigation events only on this axes.
26+
- ``"auto"``: Use the default behaviour.
27+
28+
To disable pan/zoom events completely, use ``ax.set_navigate(False)``
29+
30+
"""
31+
32+
33+
import matplotlib.pyplot as plt
34+
35+
fig = plt.figure(figsize=(11, 6))
36+
fig.suptitle("Showcase for pan/zoom events on overlapping axes.")
37+
38+
ax = fig.add_axes((.05, .05, .9, .9))
39+
ax.patch.set_color(".75")
40+
ax_twin = ax.twinx()
41+
42+
ax1 = fig.add_subplot(221)
43+
ax1_twin = ax1.twinx()
44+
ax1.text(.5, .5,
45+
"Visible patch\n\n"
46+
"Pan/zoom events are NOT\n"
47+
"forwarded to axes below",
48+
ha="center", va="center", transform=ax1.transAxes)
49+
50+
ax11 = fig.add_subplot(223, sharex=ax1, sharey=ax1)
51+
ax11.set_forward_navigation_events(True)
52+
ax11.text(.5, .5,
53+
"Visible patch\n\n"
54+
"Override capture behavior:\n\n"
55+
"ax.set_forward_navigation_events(True)",
56+
ha="center", va="center", transform=ax11.transAxes)
57+
58+
ax2 = fig.add_subplot(222)
59+
ax2_twin = ax2.twinx()
60+
ax2.patch.set_visible(False)
61+
ax2.text(.5, .5,
62+
"Invisible patch\n\n"
63+
"Pan/zoom events are\n"
64+
"forwarded to axes below",
65+
ha="center", va="center", transform=ax2.transAxes)
66+
67+
ax22 = fig.add_subplot(224, sharex=ax2, sharey=ax2)
68+
ax22.patch.set_visible(False)
69+
ax22.set_forward_navigation_events(False)
70+
ax22.text(.5, .5,
71+
"Invisible patch\n\n"
72+
"Override capture behavior:\n\n"
73+
"ax.set_forward_navigation_events(False)",
74+
ha="center", va="center", transform=ax22.transAxes)

‎lib/matplotlib/axes/_base.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_base.py
+39Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def __init__(self, fig,
571571
xscale=None,
572572
yscale=None,
573573
box_aspect=None,
574+
forward_navigation_events="auto",
574575
**kwargs
575576
):
576577
"""
@@ -605,6 +606,11 @@ def __init__(self, fig,
605606
Set a fixed aspect for the Axes box, i.e. the ratio of height to
606607
width. See `~.axes.Axes.set_box_aspect` for details.
607608
609+
forward_navigation_events : bool or "auto", default: "auto"
610+
Control whether pan/zoom events are passed through to Axes below
611+
this one. "auto" is *True* for axes with an invisible patch and
612+
*False* otherwise.
613+
608614
**kwargs
609615
Other optional keyword arguments:
610616
@@ -640,6 +646,7 @@ def __init__(self, fig,
640646
self._adjustable = 'box'
641647
self._anchor = 'C'
642648
self._stale_viewlims = {name: False for name in self._axis_names}
649+
self._forward_navigation_events = forward_navigation_events
643650
self._sharex = sharex
644651
self._sharey = sharey
645652
self.set_label(label)
@@ -4016,6 +4023,11 @@ def set_navigate(self, b):
40164023
Parameters
40174024
----------
40184025
b : bool
4026+
4027+
See Also
4028+
--------
4029+
matplotlib.axes.Axes.set_forward_navigation_events
4030+
40194031
"""
40204032
self._navigate = b
40214033

@@ -4466,6 +4478,8 @@ def _make_twin_axes(self, *args, **kwargs):
44664478
[0, 0, 1, 1], self.transAxes))
44674479
self.set_adjustable('datalim')
44684480
twin.set_adjustable('datalim')
4481+
twin.set_zorder(self.zorder)
4482+
44694483
self._twinned_axes.join(self, twin)
44704484
return twin
44714485

@@ -4612,6 +4626,31 @@ def _label_outer_yaxis(self, *, skip_non_rectangular_axes,
46124626
if self.yaxis.offsetText.get_position()[0] == 1:
46134627
self.yaxis.offsetText.set_visible(False)
46144628

4629+
def set_forward_navigation_events(self, forward):
4630+
"""
4631+
Set how pan/zoom events are forwarded to Axes below this one.
4632+
4633+
Parameters
4634+
----------
4635+
forward : bool or "auto"
4636+
Possible values:
4637+
4638+
- True: Forward events to other axes with lower or equal zorder.
4639+
- False: Events are only executed on this axes.
4640+
- "auto": Default behaviour (*True* for axes with an invisible
4641+
patch and *False* otherwise)
4642+
4643+
See Also
4644+
--------
4645+
matplotlib.axes.Axes.set_navigate
4646+
4647+
"""
4648+
self._forward_navigation_events = forward
4649+
4650+
def get_forward_navigation_events(self):
4651+
"""Get how pan/zoom events are forwarded to Axes below this one."""
4652+
return self._forward_navigation_events
4653+
46154654

46164655
def _draw_rasterized(figure, artists, renderer):
46174656
"""

‎lib/matplotlib/axes/_base.pyi

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_base.pyi
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class _AxesBase(martist.Artist):
7575
xscale: str | ScaleBase | None = ...,
7676
yscale: str | ScaleBase | None = ...,
7777
box_aspect: float | None = ...,
78+
forward_navigation_events: bool | Literal["auto"] = ...,
7879
**kwargs
7980
) -> None: ...
8081
def get_subplotspec(self) -> SubplotSpec | None: ...
@@ -363,6 +364,8 @@ class _AxesBase(martist.Artist):
363364
def can_pan(self) -> bool: ...
364365
def get_navigate(self) -> bool: ...
365366
def set_navigate(self, b: bool) -> None: ...
367+
def get_forward_navigation_events(self) -> bool | Literal["auto"]: ...
368+
def set_forward_navigation_events(self, forward: bool | Literal["auto"]) -> None: ...
366369
def get_navigate_mode(self) -> Literal["PAN", "ZOOM"] | None: ...
367370
def set_navigate_mode(self, b: Literal["PAN", "ZOOM"] | None) -> None: ...
368371
def start_pan(self, x: float, y: float, button: MouseButton) -> None: ...

‎lib/matplotlib/backend_bases.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backend_bases.py
+67-10Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2996,6 +2996,59 @@ def _zoom_pan_handler(self, event):
29962996
elif event.name == "button_release_event":
29972997
self.release_zoom(event)
29982998

2999+
def _start_event_axes_interaction(self, event, *, method):
3000+
3001+
def _ax_filter(ax):
3002+
return (ax.in_axes(event) and
3003+
ax.get_navigate() and
3004+
getattr(ax, f"can_{method}")()
3005+
)
3006+
3007+
def _capture_events(ax):
3008+
f = ax.get_forward_navigation_events()
3009+
if f == "auto": # (capture = patch visibility)
3010+
f = not ax.patch.get_visible()
3011+
return not f
3012+
3013+
# get all relevant axes for the event
3014+
axes = list(filter(_ax_filter, self.canvas.figure.get_axes()))
3015+
3016+
if len(axes) == 0:
3017+
return []
3018+
3019+
if self._nav_stack() is None:
3020+
self.push_current() # Set the home button to this view.
3021+
3022+
# group axes by zorder (reverse to trigger later axes first)
3023+
grps = dict()
3024+
for ax in reversed(axes):
3025+
grps.setdefault(ax.get_zorder(), []).append(ax)
3026+
3027+
axes_to_trigger = []
3028+
# go through zorders in reverse until we hit a capturing axes
3029+
for zorder in sorted(grps, reverse=True):
3030+
for ax in grps[zorder]:
3031+
axes_to_trigger.append(ax)
3032+
# NOTE: shared axes are automatically triggered, but twin-axes not!
3033+
axes_to_trigger.extend(ax._twinned_axes.get_siblings(ax))
3034+
3035+
if _capture_events(ax):
3036+
break # break if we hit a capturing axes
3037+
else:
3038+
# If the inner loop finished without an explicit break,
3039+
# (e.g. no capturing axes was found) continue the
3040+
# outer loop to the next zorder.
3041+
continue
3042+
3043+
# If the inner loop was terminated with an explicit break,
3044+
# terminate the outer loop as well.
3045+
break
3046+
3047+
# avoid duplicated triggers (but keep order of list)
3048+
axes_to_trigger = list(dict.fromkeys(axes_to_trigger))
3049+
3050+
return axes_to_trigger
3051+
29993052
def pan(self, *args):
30003053
"""
30013054
Toggle the pan/zoom tool.
@@ -3021,16 +3074,18 @@ def press_pan(self, event):
30213074
if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT]
30223075
or event.x is None or event.y is None):
30233076
return
3024-
axes = [a for a in self.canvas.figure.get_axes()
3025-
if a.in_axes(event) and a.get_navigate() and a.can_pan()]
3077+
3078+
axes = self._start_event_axes_interaction(event, method="pan")
30263079
if not axes:
30273080
return
3028-
if self._nav_stack() is None:
3029-
self.push_current() # set the home button to this view
3081+
3082+
# call "ax.start_pan(..)" on all relevant axes of an event
30303083
for ax in axes:
30313084
ax.start_pan(event.x, event.y, event.button)
3085+
30323086
self.canvas.mpl_disconnect(self._id_drag)
30333087
id_drag = self.canvas.mpl_connect("motion_notify_event", self.drag_pan)
3088+
30343089
self._pan_info = self._PanInfo(
30353090
button=event.button, axes=axes, cid=id_drag)
30363091

@@ -3076,21 +3131,23 @@ def press_zoom(self, event):
30763131
if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT]
30773132
or event.x is None or event.y is None):
30783133
return
3079-
axes = [a for a in self.canvas.figure.get_axes()
3080-
if a.in_axes(event) and a.get_navigate() and a.can_zoom()]
3134+
3135+
axes = self._start_event_axes_interaction(event, method="zoom")
30813136
if not axes:
30823137
return
3083-
if self._nav_stack() is None:
3084-
self.push_current() # set the home button to this view
3138+
30853139
id_zoom = self.canvas.mpl_connect(
30863140
"motion_notify_event", self.drag_zoom)
3141+
30873142
# A colorbar is one-dimensional, so we extend the zoom rectangle out
30883143
# to the edge of the Axes bbox in the other dimension. To do that we
30893144
# store the orientation of the colorbar for later.
3090-
if hasattr(axes[0], "_colorbar"):
3091-
cbar = axes[0]._colorbar.orientation
3145+
parent_ax = axes[0]
3146+
if hasattr(parent_ax, "_colorbar"):
3147+
cbar = parent_ax._colorbar.orientation
30923148
else:
30933149
cbar = None
3150+
30943151
self._zoom_info = self._ZoomInfo(
30953152
direction="in" if event.button == 1 else "out",
30963153
start_xy=(event.x, event.y), axes=axes, cid=id_zoom, cbar=cbar)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.