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

Skip to content

Commit 778e711

Browse files
committed
Take into account aspect ratio in square state
1 parent ba764bd commit 778e711

File tree

2 files changed

+113
-54
lines changed

2 files changed

+113
-54
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 & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,8 @@ def __init__(self, ax, onselect, useblit=False, button=None,
17961796
self.connect_default_events()
17971797

17981798
self._state_modifier_keys = dict(move=' ', clear='escape',
1799-
square='shift', center='control')
1799+
square='shift', center='control',
1800+
data_coordinates='d')
18001801
self._state_modifier_keys.update(state_modifier_keys or {})
18011802

18021803
self.background = None
@@ -1984,6 +1985,12 @@ def on_key_press(self, event):
19841985
artist.set_visible(False)
19851986
self.update()
19861987
return
1988+
if key == 'd' and key in self._state_modifier_keys.values():
1989+
modifier = 'data_coordinates'
1990+
if modifier in self._default_state:
1991+
self._default_state.remove(modifier)
1992+
else:
1993+
self.add_default_state(modifier)
19871994
for (state, modifier) in self._state_modifier_keys.items():
19881995
if modifier in key:
19891996
self._state.add(state)
@@ -2185,7 +2192,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
21852192
if state_modifier_keys is None:
21862193
state_modifier_keys = dict(clear='escape',
21872194
square='not-applicable',
2188-
center='not-applicable')
2195+
center='not-applicable',
2196+
data_coordinates='not-applicable')
21892197
super().__init__(ax, onselect, useblit=useblit, button=button,
21902198
state_modifier_keys=state_modifier_keys)
21912199

@@ -2754,8 +2762,12 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
27542762
- "clear": Clear the current shape, default: "escape".
27552763
- "square": Makes the shape square, default: "shift".
27562764
- "center": change the shape around its center, default: "ctrl".
2765+
- "data_coordinates": define if data or figure coordinates should be
2766+
used to define the square shape, default: "d"
27572767
2758-
"square" and "center" can be combined.
2768+
"square" and "center" can be combined. The square shape can be defined
2769+
in data or figure coordinates as determined by the ``data_coordinates``
2770+
modifier, which can be enable and disable by pressing the 'd' key.
27592771
27602772
drag_from_anywhere : bool, default: False
27612773
If `True`, the widget can be moved by clicking anywhere within
@@ -2990,61 +3002,75 @@ def _onmove(self, event):
29903002
"""Motion notify event handler."""
29913003

29923004
state = self._state | self._default_state
3005+
3006+
dx = event.xdata - self._eventpress.xdata
3007+
dy = event.ydata - self._eventpress.ydata
3008+
refmax = None
3009+
if 'data_coordinates' in state:
3010+
aspect_ratio = 1
3011+
refx, refy = dx, dy
3012+
else:
3013+
figure_size = self.ax.get_figure().get_size_inches()
3014+
ll, ur = self.ax.get_position() * figure_size
3015+
width, height = ur - ll
3016+
aspect_ratio = height / width * self.ax.get_data_ratio()
3017+
refx = event.xdata / (self._eventpress.xdata + 1e-6)
3018+
refy = event.ydata / (self._eventpress.ydata + 1e-6)
3019+
29933020
# resize an existing shape
29943021
if self._active_handle and self._active_handle != 'C':
29953022
x0, x1, y0, y1 = self._extents_on_press
29963023
sizepress = [x1 - x0, y1 - y0]
29973024
center = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2]
2998-
dx = event.xdata - self._eventpress.xdata
2999-
dy = event.ydata - self._eventpress.ydata
3000-
3001-
# change sign of relative changes to simplify calculation
3002-
# Switch variables so that only x1 and/or y1 are updated on move
3003-
x_factor = y_factor = 1
3004-
if 'W' in self._active_handle:
3005-
x_factor *= -1
3006-
dx *= x_factor
3007-
x0 = x1
3008-
if 'S' in self._active_handle:
3009-
y_factor *= -1
3010-
dy *= y_factor
3011-
y0 = y1
30123025

30133026
# from center
30143027
if 'center' in state:
30153028
if 'square' in state:
3016-
if self._active_handle in ['E', 'W']:
3017-
# using E, W handle we need to update dy accordingly
3018-
dy = dx
3019-
elif self._active_handle in ['S', 'N']:
3020-
# using S, N handle, we need to update dx accordingly
3021-
dx = dy
3029+
# when using a corner, find which reference to use
3030+
if self._active_handle in self._corner_order:
3031+
refmax = max(refx, refy, key=abs)
3032+
if self._active_handle in ['E', 'W'] or refmax == refx:
3033+
dw = event.xdata - center[0]
3034+
dh = dw / aspect_ratio
30223035
else:
3023-
dx = dy = max(dx, dy, key=abs)
3024-
3025-
dw = sizepress[0] / 2 + dx
3026-
dh = sizepress[1] / 2 + dy
3027-
3028-
if 'square' not in state:
3036+
dh = event.ydata - center[1]
3037+
dw = dh * aspect_ratio
3038+
else:
3039+
dw = sizepress[0] / 2
3040+
dh = sizepress[1] / 2
30293041
# cancel changes in perpendicular direction
3030-
if self._active_handle in ['E', 'W']:
3031-
dh = sizepress[1] / 2
3032-
if self._active_handle in ['N', 'S']:
3033-
dw = sizepress[0] / 2
3042+
if self._active_handle in ['E', 'W'] + self._corner_order:
3043+
dw = abs(event.xdata - center[0])
3044+
if self._active_handle in ['N', 'S'] + self._corner_order:
3045+
dh = abs(event.ydata - center[1])
30343046

30353047
x0, x1, y0, y1 = (center[0] - dw, center[0] + dw,
30363048
center[1] - dh, center[1] + dh)
30373049

30383050
else:
3051+
# change sign of relative changes to simplify calculation
3052+
# Switch variables so that x1 and/or y1 are updated on move
3053+
x_factor = y_factor = 1
3054+
if 'W' in self._active_handle:
3055+
x0 = x1
3056+
x_factor *= -1
3057+
if 'S' in self._active_handle:
3058+
y0 = y1
3059+
y_factor *= -1
3060+
if self._active_handle in ['E', 'W'] + self._corner_order:
3061+
x1 = event.xdata
3062+
if self._active_handle in ['N', 'S'] + self._corner_order:
3063+
y1 = event.ydata
30393064
if 'square' in state:
3040-
dx = dy = max(dx, dy, key=abs)
3041-
x1 = x0 + x_factor * (dx + sizepress[0])
3042-
y1 = y0 + y_factor * (dy + sizepress[1])
3043-
else:
3044-
if self._active_handle in ['E', 'W'] + self._corner_order:
3045-
x1 = event.xdata
3046-
if self._active_handle in ['N', 'S'] + self._corner_order:
3047-
y1 = event.ydata
3065+
# when using a corner, find which reference to use
3066+
if self._active_handle in self._corner_order:
3067+
refmax = max(refx, refy, key=abs)
3068+
if self._active_handle in ['E', 'W'] or refmax == refx:
3069+
sign = np.sign(event.ydata - y0)
3070+
y1 = y0 + sign * abs(x1 - x0) / aspect_ratio
3071+
else:
3072+
sign = np.sign(event.xdata - x0)
3073+
x1 = x0 + sign * abs(y1 - y0) * aspect_ratio
30483074

30493075
# move existing shape
30503076
elif self._active_handle == 'C':
@@ -3063,21 +3089,16 @@ def _onmove(self, event):
30633089
if self.ignore_event_outside and self._selection_completed:
30643090
return
30653091
center = [self._eventpress.xdata, self._eventpress.ydata]
3066-
center_pix = [self._eventpress.x, self._eventpress.y]
30673092
dx = (event.xdata - center[0]) / 2.
30683093
dy = (event.ydata - center[1]) / 2.
30693094

30703095
# square shape
30713096
if 'square' in state:
3072-
dx_pix = abs(event.x - center_pix[0])
3073-
dy_pix = abs(event.y - center_pix[1])
3074-
if not dx_pix:
3075-
return
3076-
maxd = max(abs(dx_pix), abs(dy_pix))
3077-
if abs(dx_pix) < maxd:
3078-
dx *= maxd / (abs(dx_pix) + 1e-6)
3079-
if abs(dy_pix) < maxd:
3080-
dy *= maxd / (abs(dy_pix) + 1e-6)
3097+
refmax = max(refx, refy, key=abs)
3098+
if refmax == refx:
3099+
dy = dx / aspect_ratio
3100+
else:
3101+
dx = dy * aspect_ratio
30813102

30823103
# from center
30833104
if 'center' in state:
@@ -3435,7 +3456,8 @@ def __init__(self, ax, onselect, useblit=False,
34353456
state_modifier_keys = dict(clear='escape', move_vertex='control',
34363457
move_all='shift', move='not-applicable',
34373458
square='not-applicable',
3438-
center='not-applicable')
3459+
center='not-applicable',
3460+
data_coordinates='not-applicable')
34393461
super().__init__(ax, onselect, useblit=useblit,
34403462
state_modifier_keys=state_modifier_keys)
34413463

0 commit comments

Comments
 (0)