Skip to content

Commit a6de72c

Browse files
committed
Take into account aspect ratio in square state
1 parent 3a1bde9 commit a6de72c

File tree

2 files changed

+113
-57
lines changed

2 files changed

+113
-57
lines changed

lib/matplotlib/tests/test_widgets.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def onselect(epress, erelease):
189189
tool.add_default_state('move')
190190
tool.add_default_state('square')
191191
tool.add_default_state('center')
192+
tool.add_default_state('data_coordinates')
192193

193194

194195
@pytest.mark.parametrize('use_default_state', [True, False])
@@ -405,6 +406,42 @@ def onselect(epress, erelease):
405406
ydata_new, extents[3] - ydiff)
406407

407408

409+
def test_rectangle_resize_square_center_aspect():
410+
ax = get_ax()
411+
ax.set_aspect(0.8)
412+
413+
def onselect(epress, erelease):
414+
pass
415+
416+
tool = widgets.RectangleSelector(ax, onselect, interactive=True)
417+
# Create rectangle
418+
_resize_rectangle(tool, 70, 65, 120, 115)
419+
tool.add_default_state('square')
420+
tool.add_default_state('center')
421+
assert tool.extents == (70.0, 120.0, 65.0, 115.0)
422+
423+
# resize E handle
424+
extents = tool.extents
425+
xdata, ydata = extents[1], extents[3]
426+
xdiff = 10
427+
xdata_new, ydata_new = xdata + xdiff, ydata
428+
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
429+
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
430+
46.25, 133.75])
431+
432+
# use data coordinates
433+
do_event(tool, 'on_key_press', key='d')
434+
# resize E handle
435+
extents = tool.extents
436+
xdata, ydata, width = extents[1], extents[3], extents[1] - extents[0]
437+
xdiff, ycenter = 10, extents[2] + (extents[3] - extents[2]) / 2
438+
xdata_new, ydata_new = xdata + xdiff, ydata
439+
ychange = width / 2 + xdiff
440+
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
441+
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
442+
ycenter - ychange, ycenter + ychange])
443+
444+
408445
def test_ellipse():
409446
"""For ellipse, test out the key modifiers"""
410447
ax = get_ax()
@@ -443,7 +480,7 @@ def onselect(epress, erelease):
443480
do_event(tool, 'on_key_release', xdata=10, ydata=10, button=1,
444481
key='shift')
445482
extents = [int(e) for e in tool.extents]
446-
assert extents == [10, 35, 10, 34]
483+
assert extents == [10, 35, 10, 35]
447484

448485
# create a square from center
449486
do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1,
@@ -454,7 +491,7 @@ def onselect(epress, erelease):
454491
do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1,
455492
key='ctrl+shift')
456493
extents = [int(e) for e in tool.extents]
457-
assert extents == [70, 129, 70, 130]
494+
assert extents == [70, 130, 70, 130]
458495

459496
assert tool.geometry.shape == (2, 73)
460497
assert_allclose(tool.geometry[:, 0], [70., 100])

lib/matplotlib/widgets.py

Lines changed: 74 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,7 +1810,8 @@ def __init__(self, ax, onselect, useblit=False, button=None,
18101810
self.connect_default_events()
18111811

18121812
self._state_modifier_keys = dict(move=' ', clear='escape',
1813-
square='shift', center='control')
1813+
square='shift', center='control',
1814+
data_coordinates='d')
18141815
self._state_modifier_keys.update(state_modifier_keys or {})
18151816

18161817
self.background = None
@@ -1996,6 +1997,12 @@ def on_key_press(self, event):
19961997
if key == self._state_modifier_keys['clear']:
19971998
self.clear()
19981999
return
2000+
if key == 'd' and key in self._state_modifier_keys.values():
2001+
modifier = 'data_coordinates'
2002+
if modifier in self._default_state:
2003+
self._default_state.remove(modifier)
2004+
else:
2005+
self.add_default_state(modifier)
19992006
for (state, modifier) in self._state_modifier_keys.items():
20002007
if modifier in key:
20012008
self._state.add(state)
@@ -2204,7 +2211,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
22042211
if state_modifier_keys is None:
22052212
state_modifier_keys = dict(clear='escape',
22062213
square='not-applicable',
2207-
center='not-applicable')
2214+
center='not-applicable',
2215+
data_coordinates='not-applicable')
22082216
super().__init__(ax, onselect, useblit=useblit, button=button,
22092217
state_modifier_keys=state_modifier_keys)
22102218

@@ -2778,8 +2786,12 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
27782786
- "clear": Clear the current shape, default: "escape".
27792787
- "square": Make the shape square, default: "shift".
27802788
- "center": change the shape around its center, default: "ctrl".
2789+
- "data_coordinates": define if data or figure coordinates should be
2790+
used to define the square shape, default: "d"
27812791
2782-
"square" and "center" can be combined.
2792+
"square" and "center" can be combined. The square shape can be defined
2793+
in data or figure coordinates as determined by the ``data_coordinates``
2794+
modifier, which can be enable and disable by pressing the 'd' key.
27832795
27842796
drag_from_anywhere : bool, default: False
27852797
If `True`, the widget can be moved by clicking anywhere within
@@ -3014,64 +3026,75 @@ def _onmove(self, event):
30143026
"""Motion notify event handler."""
30153027

30163028
state = self._state | self._default_state
3029+
3030+
dx = event.xdata - self._eventpress.xdata
3031+
dy = event.ydata - self._eventpress.ydata
3032+
refmax = None
3033+
if 'data_coordinates' in state:
3034+
aspect_ratio = 1
3035+
refx, refy = dx, dy
3036+
else:
3037+
figure_size = self.ax.get_figure().get_size_inches()
3038+
ll, ur = self.ax.get_position() * figure_size
3039+
width, height = ur - ll
3040+
aspect_ratio = height / width * self.ax.get_data_ratio()
3041+
refx = event.xdata / (self._eventpress.xdata + 1e-6)
3042+
refy = event.ydata / (self._eventpress.ydata + 1e-6)
3043+
30173044
# resize an existing shape
30183045
if self._active_handle and self._active_handle != 'C':
30193046
x0, x1, y0, y1 = self._extents_on_press
30203047
size_on_press = [x1 - x0, y1 - y0]
30213048
center = [x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2]
3022-
dx = event.xdata - self._eventpress.xdata
3023-
dy = event.ydata - self._eventpress.ydata
3024-
3025-
# change sign of relative changes to simplify calculation
3026-
# Switch variables so that only x1 and/or y1 are updated on move
3027-
x_factor = y_factor = 1
3028-
if 'W' in self._active_handle:
3029-
x_factor *= -1
3030-
dx *= x_factor
3031-
x0 = x1
3032-
if 'S' in self._active_handle:
3033-
y_factor *= -1
3034-
dy *= y_factor
3035-
y0 = y1
30363049

30373050
# Keeping the center fixed
30383051
if 'center' in state:
30393052
if 'square' in state:
3040-
# Force the same change in dx and dy
3041-
if self._active_handle in ['E', 'W']:
3042-
# using E, W handle we need to update dy accordingly
3043-
dy = dx
3044-
elif self._active_handle in ['S', 'N']:
3045-
# using S, N handle, we need to update dx accordingly
3046-
dx = dy
3053+
# when using a corner, find which reference to use
3054+
if self._active_handle in self._corner_order:
3055+
refmax = max(refx, refy, key=abs)
3056+
if self._active_handle in ['E', 'W'] or refmax == refx:
3057+
hw = event.xdata - center[0]
3058+
hh = hw / aspect_ratio
30473059
else:
3048-
dx = dy = max(dx, dy, key=abs)
3049-
3050-
# new half-width and half-height
3051-
hw = size_on_press[0] / 2 + dx
3052-
hh = size_on_press[1] / 2 + dy
3053-
3054-
if 'square' not in state:
3060+
hh = event.ydata - center[1]
3061+
hw = hh * aspect_ratio
3062+
else:
3063+
hw = size_on_press[0] / 2
3064+
hh = size_on_press[1] / 2
30553065
# cancel changes in perpendicular direction
3056-
if self._active_handle in ['E', 'W']:
3057-
hh = size_on_press[1] / 2
3058-
if self._active_handle in ['N', 'S']:
3059-
hw = size_on_press[0] / 2
3066+
if self._active_handle in ['E', 'W'] + self._corner_order:
3067+
hw = abs(event.xdata - center[0])
3068+
if self._active_handle in ['N', 'S'] + self._corner_order:
3069+
hh = abs(event.ydata - center[1])
30603070

30613071
x0, x1, y0, y1 = (center[0] - hw, center[0] + hw,
30623072
center[1] - hh, center[1] + hh)
30633073

30643074
else:
3065-
# Keeping the opposite corner/edge fixed
3075+
# change sign of relative changes to simplify calculation
3076+
# Switch variables so that x1 and/or y1 are updated on move
3077+
x_factor = y_factor = 1
3078+
if 'W' in self._active_handle:
3079+
x0 = x1
3080+
x_factor *= -1
3081+
if 'S' in self._active_handle:
3082+
y0 = y1
3083+
y_factor *= -1
3084+
if self._active_handle in ['E', 'W'] + self._corner_order:
3085+
x1 = event.xdata
3086+
if self._active_handle in ['N', 'S'] + self._corner_order:
3087+
y1 = event.ydata
30663088
if 'square' in state:
3067-
dx = dy = max(dx, dy, key=abs)
3068-
x1 = x0 + x_factor * (dx + size_on_press[0])
3069-
y1 = y0 + y_factor * (dy + size_on_press[1])
3070-
else:
3071-
if self._active_handle in ['E', 'W'] + self._corner_order:
3072-
x1 = event.xdata
3073-
if self._active_handle in ['N', 'S'] + self._corner_order:
3074-
y1 = event.ydata
3089+
# when using a corner, find which reference to use
3090+
if self._active_handle in self._corner_order:
3091+
refmax = max(refx, refy, key=abs)
3092+
if self._active_handle in ['E', 'W'] or refmax == refx:
3093+
sign = np.sign(event.ydata - y0)
3094+
y1 = y0 + sign * abs(x1 - x0) / aspect_ratio
3095+
else:
3096+
sign = np.sign(event.xdata - x0)
3097+
x1 = x0 + sign * abs(y1 - y0) * aspect_ratio
30753098

30763099
# move existing shape
30773100
elif self._active_handle == 'C':
@@ -3090,21 +3113,16 @@ def _onmove(self, event):
30903113
if self.ignore_event_outside and self._selection_completed:
30913114
return
30923115
center = [self._eventpress.xdata, self._eventpress.ydata]
3093-
center_pix = [self._eventpress.x, self._eventpress.y]
30943116
dx = (event.xdata - center[0]) / 2.
30953117
dy = (event.ydata - center[1]) / 2.
30963118

30973119
# square shape
30983120
if 'square' in state:
3099-
dx_pix = abs(event.x - center_pix[0])
3100-
dy_pix = abs(event.y - center_pix[1])
3101-
if not dx_pix:
3102-
return
3103-
maxd = max(abs(dx_pix), abs(dy_pix))
3104-
if abs(dx_pix) < maxd:
3105-
dx *= maxd / (abs(dx_pix) + 1e-6)
3106-
if abs(dy_pix) < maxd:
3107-
dy *= maxd / (abs(dy_pix) + 1e-6)
3121+
refmax = max(refx, refy, key=abs)
3122+
if refmax == refx:
3123+
dy = dx / aspect_ratio
3124+
else:
3125+
dx = dy * aspect_ratio
31083126

31093127
# from center
31103128
if 'center' in state:
@@ -3464,7 +3482,8 @@ def __init__(self, ax, onselect, useblit=False,
34643482
state_modifier_keys = dict(clear='escape', move_vertex='control',
34653483
move_all='shift', move='not-applicable',
34663484
square='not-applicable',
3467-
center='not-applicable')
3485+
center='not-applicable',
3486+
data_coordinates='not-applicable')
34683487
super().__init__(ax, onselect, useblit=useblit,
34693488
state_modifier_keys=state_modifier_keys)
34703489

0 commit comments

Comments
 (0)