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

Skip to content

Commit 034c251

Browse files
committed
Take into account aspect ratio in square state
1 parent 11f2a23 commit 034c251

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
@@ -161,6 +161,7 @@ def onselect(epress, erelease):
161161
tool.add_default_state('move')
162162
tool.add_default_state('square')
163163
tool.add_default_state('center')
164+
tool.add_default_state('data_coordinates')
164165

165166

166167
@pytest.mark.parametrize('use_default_state', [True, False])
@@ -377,6 +378,42 @@ def onselect(epress, erelease):
377378
ydata_new, extents[3] - ydiff)
378379

379380

381+
def test_rectangle_resize_square_center_aspect():
382+
ax = get_ax()
383+
ax.set_aspect(0.8)
384+
385+
def onselect(epress, erelease):
386+
pass
387+
388+
tool = widgets.RectangleSelector(ax, onselect, interactive=True)
389+
# Create rectangle
390+
_resize_rectangle(tool, 70, 65, 120, 115)
391+
tool.add_default_state('square')
392+
tool.add_default_state('center')
393+
assert tool.extents == (70.0, 120.0, 65.0, 115.0)
394+
395+
# resize E handle
396+
extents = tool.extents
397+
xdata, ydata = extents[1], extents[3]
398+
xdiff = 10
399+
xdata_new, ydata_new = xdata + xdiff, ydata
400+
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
401+
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
402+
46.25, 133.75])
403+
404+
# use data coordinates
405+
do_event(tool, 'on_key_press', key='d')
406+
# resize E handle
407+
extents = tool.extents
408+
xdata, ydata, width = extents[1], extents[3], extents[1] - extents[0]
409+
xdiff, ycenter = 10, extents[2] + (extents[3] - extents[2]) / 2
410+
xdata_new, ydata_new = xdata + xdiff, ydata
411+
ychange = width / 2 + xdiff
412+
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
413+
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
414+
ycenter - ychange, ycenter + ychange])
415+
416+
380417
def test_ellipse():
381418
"""For ellipse, test out the key modifiers"""
382419
ax = get_ax()
@@ -415,7 +452,7 @@ def onselect(epress, erelease):
415452
do_event(tool, 'on_key_release', xdata=10, ydata=10, button=1,
416453
key='shift')
417454
extents = [int(e) for e in tool.extents]
418-
assert extents == [10, 35, 10, 34]
455+
assert extents == [10, 35, 10, 35]
419456

420457
# create a square from center
421458
do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1,
@@ -426,7 +463,7 @@ def onselect(epress, erelease):
426463
do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1,
427464
key='ctrl+shift')
428465
extents = [int(e) for e in tool.extents]
429-
assert extents == [70, 129, 70, 130]
466+
assert extents == [70, 130, 70, 130]
430467

431468
assert tool.geometry.shape == (2, 73)
432469
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
@@ -1985,6 +1986,12 @@ def on_key_press(self, event):
19851986
artist.set_visible(False)
19861987
self.update()
19871988
return
1989+
if key == 'd' and key in self._state_modifier_keys.values():
1990+
modifier = 'data_coordinates'
1991+
if modifier in self._default_state:
1992+
self._default_state.remove(modifier)
1993+
else:
1994+
self.add_default_state(modifier)
19881995
for (state, modifier) in self._state_modifier_keys.items():
19891996
if modifier in key:
19901997
self._state.add(state)
@@ -2152,7 +2159,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
21522159
if state_modifier_keys is None:
21532160
state_modifier_keys = dict(clear='escape',
21542161
square='not-applicable',
2155-
center='not-applicable')
2162+
center='not-applicable',
2163+
data_coordinates='not-applicable')
21562164
super().__init__(ax, onselect, useblit=useblit, button=button,
21572165
state_modifier_keys=state_modifier_keys)
21582166

@@ -2713,8 +2721,12 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
27132721
- "clear": Clear the current shape, default: "escape".
27142722
- "square": Makes the shape square, default: "shift".
27152723
- "center": change the shape around its center, default: "ctrl".
2724+
- "data_coordinates": define if data or figure coordinates should be
2725+
used to define the square shape, default: "d"
27162726
2717-
"square" and "center" can be combined.
2727+
"square" and "center" can be combined. The square shape can be defined
2728+
in data or figure coordinates as determined by the ``data_coordinates``
2729+
modifier, which can be enable and disable by pressing the 'd' key.
27182730
27192731
drag_from_anywhere : bool, default: False
27202732
If `True`, the widget can be moved by clicking anywhere within
@@ -2947,61 +2959,75 @@ def _onmove(self, event):
29472959
"""Motion notify event handler."""
29482960

29492961
state = self._state | self._default_state
2962+
2963+
dx = event.xdata - self._eventpress.xdata
2964+
dy = event.ydata - self._eventpress.ydata
2965+
refmax = None
2966+
if 'data_coordinates' in state:
2967+
aspect_ratio = 1
2968+
refx, refy = dx, dy
2969+
else:
2970+
figure_size = self.ax.get_figure().get_size_inches()
2971+
ll, ur = self.ax.get_position() * figure_size
2972+
width, height = ur - ll
2973+
aspect_ratio = height / width * self.ax.get_data_ratio()
2974+
refx = event.xdata / (self._eventpress.xdata + 1e-6)
2975+
refy = event.ydata / (self._eventpress.ydata + 1e-6)
2976+
29502977
# resize an existing shape
29512978
if self._active_handle and self._active_handle != 'C':
29522979
x0, x1, y0, y1 = self._extents_on_press
29532980
sizepress = [x1 - x0, y1 - y0]
29542981
center = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2]
2955-
dx = event.xdata - self._eventpress.xdata
2956-
dy = event.ydata - self._eventpress.ydata
2957-
2958-
# change sign of relative changes to simplify calculation
2959-
# Switch variables so that only x1 and/or y1 are updated on move
2960-
x_factor = y_factor = 1
2961-
if 'W' in self._active_handle:
2962-
x_factor *= -1
2963-
dx *= x_factor
2964-
x0 = x1
2965-
if 'S' in self._active_handle:
2966-
y_factor *= -1
2967-
dy *= y_factor
2968-
y0 = y1
29692982

29702983
# from center
29712984
if 'center' in state:
29722985
if 'square' in state:
2973-
if self._active_handle in ['E', 'W']:
2974-
# using E, W handle we need to update dy accordingly
2975-
dy = dx
2976-
elif self._active_handle in ['S', 'N']:
2977-
# using S, N handle, we need to update dx accordingly
2978-
dx = dy
2986+
# when using a corner, find which reference to use
2987+
if self._active_handle in self._corner_order:
2988+
refmax = max(refx, refy, key=abs)
2989+
if self._active_handle in ['E', 'W'] or refmax == refx:
2990+
dw = event.xdata - center[0]
2991+
dh = dw / aspect_ratio
29792992
else:
2980-
dx = dy = max(dx, dy, key=abs)
2981-
2982-
dw = sizepress[0] / 2 + dx
2983-
dh = sizepress[1] / 2 + dy
2984-
2985-
if 'square' not in state:
2993+
dh = event.ydata - center[1]
2994+
dw = dh * aspect_ratio
2995+
else:
2996+
dw = sizepress[0] / 2
2997+
dh = sizepress[1] / 2
29862998
# cancel changes in perpendicular direction
2987-
if self._active_handle in ['E', 'W']:
2988-
dh = sizepress[1] / 2
2989-
if self._active_handle in ['N', 'S']:
2990-
dw = sizepress[0] / 2
2999+
if self._active_handle in ['E', 'W'] + self._corner_order:
3000+
dw = abs(event.xdata - center[0])
3001+
if self._active_handle in ['N', 'S'] + self._corner_order:
3002+
dh = abs(event.ydata - center[1])
29913003

29923004
x0, x1, y0, y1 = (center[0] - dw, center[0] + dw,
29933005
center[1] - dh, center[1] + dh)
29943006

29953007
else:
3008+
# change sign of relative changes to simplify calculation
3009+
# Switch variables so that x1 and/or y1 are updated on move
3010+
x_factor = y_factor = 1
3011+
if 'W' in self._active_handle:
3012+
x0 = x1
3013+
x_factor *= -1
3014+
if 'S' in self._active_handle:
3015+
y0 = y1
3016+
y_factor *= -1
3017+
if self._active_handle in ['E', 'W'] + self._corner_order:
3018+
x1 = event.xdata
3019+
if self._active_handle in ['N', 'S'] + self._corner_order:
3020+
y1 = event.ydata
29963021
if 'square' in state:
2997-
dx = dy = max(dx, dy, key=abs)
2998-
x1 = x0 + x_factor * (dx + sizepress[0])
2999-
y1 = y0 + y_factor * (dy + sizepress[1])
3000-
else:
3001-
if self._active_handle in ['E', 'W'] + self._corner_order:
3002-
x1 = event.xdata
3003-
if self._active_handle in ['N', 'S'] + self._corner_order:
3004-
y1 = event.ydata
3022+
# when using a corner, find which reference to use
3023+
if self._active_handle in self._corner_order:
3024+
refmax = max(refx, refy, key=abs)
3025+
if self._active_handle in ['E', 'W'] or refmax == refx:
3026+
sign = np.sign(event.ydata - y0)
3027+
y1 = y0 + sign * abs(x1 - x0) / aspect_ratio
3028+
else:
3029+
sign = np.sign(event.xdata - x0)
3030+
x1 = x0 + sign * abs(y1 - y0) * aspect_ratio
30053031

30063032
# move existing shape
30073033
elif self._active_handle == 'C':
@@ -3020,21 +3046,16 @@ def _onmove(self, event):
30203046
if self.ignore_event_outside and self._selection_completed:
30213047
return
30223048
center = [self._eventpress.xdata, self._eventpress.ydata]
3023-
center_pix = [self._eventpress.x, self._eventpress.y]
30243049
dx = (event.xdata - center[0]) / 2.
30253050
dy = (event.ydata - center[1]) / 2.
30263051

30273052
# square shape
30283053
if 'square' in state:
3029-
dx_pix = abs(event.x - center_pix[0])
3030-
dy_pix = abs(event.y - center_pix[1])
3031-
if not dx_pix:
3032-
return
3033-
maxd = max(abs(dx_pix), abs(dy_pix))
3034-
if abs(dx_pix) < maxd:
3035-
dx *= maxd / (abs(dx_pix) + 1e-6)
3036-
if abs(dy_pix) < maxd:
3037-
dy *= maxd / (abs(dy_pix) + 1e-6)
3054+
refmax = max(refx, refy, key=abs)
3055+
if refmax == refx:
3056+
dy = dx / aspect_ratio
3057+
else:
3058+
dx = dy * aspect_ratio
30383059

30393060
# from center
30403061
if 'center' in state:
@@ -3390,7 +3411,8 @@ def __init__(self, ax, onselect, useblit=False,
33903411
state_modifier_keys = dict(clear='escape', move_vertex='control',
33913412
move_all='shift', move='not-applicable',
33923413
square='not-applicable',
3393-
center='not-applicable')
3414+
center='not-applicable',
3415+
data_coordinates='not-applicable')
33943416
super().__init__(ax, onselect, useblit=useblit,
33953417
state_modifier_keys=state_modifier_keys)
33963418

0 commit comments

Comments
 (0)