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
5 changes: 5 additions & 0 deletions doc/release/next_whats_new/single_axis_zoom.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Single Axis Zoom
----------------

Zooming in a single axis (horizontal or vertical) can be done by dragging the
zoom rectangle in one direction only.
39 changes: 32 additions & 7 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2935,6 +2935,17 @@ def draw_rubberband(self, event, x0, y0, x1, y1):
def remove_rubberband(self):
"""Remove the rubberband."""

def draw_whiskers(self, event, x0, y0, x1, y1, ws):
"""
Draw line with whiskers to indicate single axis zoom

We expect that ``x0 == x1`` or ``y0 == y1``. Else nothing will draw
*ws* is the whisker size in pixels.
"""

def remove_whiskers(self):
"""Remove the whiskers."""

def home(self, *args):
"""
Restore the original view.
Expand Down Expand Up @@ -3228,7 +3239,20 @@ def drag_zoom(self, event):
elif key == "y":
x1, x2 = ax.bbox.intervalx

# Single-axis zooms by moving less than 15 pixels
if (abs(event.x - start_xy[0]) < 15) and (abs(event.y - start_xy[1]) > 30):
x1, x2 = ax.bbox.intervalx
whisk = (start_xy[0], y1, start_xy[0], y2)
elif (abs(event.y - start_xy[1]) < 15) and (abs(event.x - start_xy[0]) > 30):
y1, y2 = ax.bbox.intervaly
whisk = (x1, start_xy[1], x2, start_xy[1])
else:
whisk = None
self.remove_whiskers()

self.draw_rubberband(event, x1, y1, x2, y2)
if whisk:
self.draw_whiskers(event, *whisk, ws=30)

def release_zoom(self, event):
"""Callback for mouse button release in zoom to rect mode."""
Expand All @@ -3239,6 +3263,7 @@ def release_zoom(self, event):
# by (pressing and) releasing another mouse button.
self.canvas.mpl_disconnect(self._zoom_info.cid)
self.remove_rubberband()
self.remove_whiskers()

start_x, start_y = self._zoom_info.start_xy
direction = "in" if self._zoom_info.button == 1 else "out"
Expand All @@ -3249,12 +3274,6 @@ def release_zoom(self, event):
key = "x"
elif self._zoom_info.cbar == "vertical":
key = "y"
# Ignore single clicks: 5 pixels is a threshold that allows the user to
# "cancel" a zoom action by zooming by less than 5 pixels.
if ((abs(event.x - start_x) < 5 and key != "y") or
(abs(event.y - start_y) < 5 and key != "x")):
self._cleanup_post_zoom()
return

for i, ax in enumerate(self._zoom_info.axes):
# Detect whether this Axes is twinned with an earlier Axes in the
Expand All @@ -3263,8 +3282,14 @@ def release_zoom(self, event):
for prev in self._zoom_info.axes[:i])
twiny = any(ax.get_shared_y_axes().joined(ax, prev)
for prev in self._zoom_info.axes[:i])
# Handle release of single axis zooms
end_x, end_y = event.x, event.y
if (abs(end_x - start_x) < 15) and (abs(end_y - start_y) > 30):
start_x, end_x = ax.bbox.intervalx
if (abs(end_y - start_y) < 15) and (abs(end_x - start_x) > 30):
start_y, end_y = ax.bbox.intervaly
ax._set_view_from_bbox(
(start_x, start_y, event.x, event.y),
(start_x, start_y, end_x, end_y),
direction, key, twinx, twiny)

self._cleanup_post_zoom()
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/backend_bases.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,10 @@ class NavigationToolbar2:
self, event: Event, x0: float, y0: float, x1: float, y1: float
) -> None: ...
def remove_rubberband(self) -> None: ...
def draw_whiskers(
self, event: Event, x0: float, y0: float, x1: float, y1: float, ws: float
) -> None: ...
def remove_whiskers(self) -> None: ...
def home(self, *args) -> None: ...
def back(self, *args) -> None: ...
def forward(self, *args) -> None: ...
Expand Down
11 changes: 11 additions & 0 deletions lib/matplotlib/backends/_backend_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,20 @@ def draw_rubberband(self, event, x0, y0, x1, y1):
rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
self.canvas._draw_rubberband(rect)

def draw_whiskers(self, event, x0, y0, x1, y1, ws=20):
height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0
x0, y0, x1, y1, ws = [int(val) for val in (x0, y0, x1, y1, ws)]
whisk = (x0, y0, x1, y1)
self.canvas._draw_whiskers(whisk, ws)

def remove_rubberband(self):
self.canvas._draw_rubberband(None)

def remove_whiskers(self):
self.canvas._draw_whiskers(None)

def _update_buttons_checked(self):
for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]:
button = self._gtk_ids.get(name)
Expand Down
51 changes: 50 additions & 1 deletion lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ def filter_destroy(event):

self._rubberband_rect_black = None
self._rubberband_rect_white = None
self._whisker_line = None
self._whisker_cap1 = None
self._whisker_cap2 = None

def _update_device_pixel_ratio(self, event=None):
ratio = None
Expand Down Expand Up @@ -760,11 +763,46 @@ def draw_rubberband(self, event, x0, y0, x1, y1):
y1 = height - y1
self.canvas._rubberband_rect_black = (
self.canvas._tkcanvas.create_rectangle(
x0, y0, x1, y1))
x0, y0, x1, y1, outline='black'))
self.canvas._rubberband_rect_white = (
self.canvas._tkcanvas.create_rectangle(
x0, y0, x1, y1, outline='white', dash=(3, 3)))

def draw_whiskers(self, event, x0, y0, x1, y1, ws=20):
if self.canvas._whisker_line:
self.canvas._tkcanvas.delete(self.canvas._whisker_line)
if self.canvas._whisker_cap1:
self.canvas._tkcanvas.delete(self.canvas._whisker_cap1)
if self.canvas._whisker_cap2:
self.canvas._tkcanvas.delete(self.canvas._whisker_cap2)
height = self.canvas.figure.bbox.height
y0 = height - y0
y1 = height - y1
self.canvas._whisker_line = (
self.canvas._tkcanvas.create_line(x0, y0, x1, y1, fill='black', width=2)
)
if x1 == x0: # vertical line
self.canvas._whisker_cap1 = (
self.canvas._tkcanvas.create_line(
x0 - ws//2, y0, x0 + ws//2, y0, fill='black', width=2)
)
self.canvas._whisker_cap2 = (
self.canvas._tkcanvas.create_line(
x1 - ws//2, y1, x1 + ws//2, y1, fill='black', width=2)
)
elif y1 == y0: # horizontal line
self.canvas._whisker_cap1 = (
self.canvas._tkcanvas.create_line(
x0, y0 - ws//2, x0, y0 + ws//2, fill='black', width=2)
)
self.canvas._whisker_cap2 = (
self.canvas._tkcanvas.create_line(
x1, y1 - ws//2, x1, y1 + ws//2, fill='black', width=2)
)
else: # Don't draw anything
self.canvas._tkcanvas.delete(self.canvas._whisker_line)
self.canvas._whisker_line = None

def remove_rubberband(self):
if self.canvas._rubberband_rect_white:
self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_white)
Expand All @@ -773,6 +811,17 @@ def remove_rubberband(self):
self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_black)
self.canvas._rubberband_rect_black = None

def remove_whiskers(self):
if self.canvas._whisker_line:
self.canvas._tkcanvas.delete(self.canvas._whisker_line)
self.canvas._whisker_line = None
if self.canvas._whisker_cap1:
self.canvas._tkcanvas.delete(self.canvas._whisker_cap1)
self.canvas._whisker_cap1 = None
if self.canvas._whisker_cap2:
self.canvas._tkcanvas.delete(self.canvas._whisker_cap2)
self.canvas._whisker_cap2 = None

def _set_image_for_button(self, button):
"""
Set the image for a button based on its pixel size.
Expand Down
88 changes: 60 additions & 28 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def __init__(self, figure=None):

self._idle_draw_id = 0
self._rubberband_rect = None
self._whiskers = None
self._whisker_size = 20

self.connect('scroll_event', self.scroll_event)
self.connect('button_press_event', self.button_press_event)
Expand Down Expand Up @@ -249,35 +251,65 @@ def _draw_rubberband(self, rect):
# TODO: Only update the rubberband area.
self.queue_draw()

def _post_draw(self, widget, ctx):
if self._rubberband_rect is None:
return
def _draw_whiskers(self, whisk, ws=20):
self._whiskers = whisk # x0, y0, x1, y1
self._whisker_size = ws
self.queue_draw()

x0, y0, w, h = (dim / self.device_pixel_ratio
for dim in self._rubberband_rect)
x1 = x0 + w
y1 = y0 + h

# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)

ctx.set_antialias(1)
ctx.set_line_width(1)
ctx.set_dash((3, 3), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()

ctx.set_dash((3, 3), 3)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()
def _post_draw(self, widget, ctx):
if self._rubberband_rect:

x0, y0, w, h = (dim / self.device_pixel_ratio
for dim in self._rubberband_rect)
x1 = x0 + w
y1 = y0 + h

# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)

ctx.set_antialias(1)
ctx.set_line_width(1)
ctx.set_dash((3, 3), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()

ctx.set_dash((3, 3), 3)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()

if self._whiskers:
x0, y0, x1, y1 = (dim / self.device_pixel_ratio
for dim in self._whiskers)
ws = self._whisker_size / self.device_pixel_ratio

ctx.set_antialias(1)
ctx.set_line_width(2)
ctx.set_dash([], 0)
ctx.set_source_rgb(0, 0, 0)

# main line
ctx.move_to(x0, y0)
ctx.line_to(x1, y1)
if x0 == x1: # vertical line
ctx.move_to(x0 - ws//2, y0)
ctx.line_to(x0 + ws//2, y0)
ctx.move_to(x1 - ws//2, y1)
ctx.line_to(x1 + ws//2, y1)
if y0 == y1: # horizontal line
ctx.move_to(x0, y0 - ws//2)
ctx.line_to(x0, y0 + ws//2)
ctx.move_to(x1, y1 - ws//2)
ctx.line_to(x1, y1 + ws//2)

ctx.stroke()

def on_draw_event(self, widget, ctx):
# to be overwritten by GTK3Agg or GTK3Cairo
Expand Down
Loading
Loading