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

Skip to content

Commit c67be76

Browse files
committed
Fix shape rectangle when using rotation and the axes aspect ratio != 1
1 parent 2e09fa7 commit c67be76

File tree

4 files changed

+106
-55
lines changed

4 files changed

+106
-55
lines changed

lib/matplotlib/axes/_axes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8149,3 +8149,10 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
81498149
tricontourf = mtri.tricontourf
81508150
tripcolor = mtri.tripcolor
81518151
triplot = mtri.triplot
8152+
8153+
def _get_aspect_ratio(self):
8154+
"""Convenience method to calculate aspect ratio of the axes."""
8155+
figure_size = self.get_figure().get_size_inches()
8156+
ll, ur = self.get_position() * figure_size
8157+
width, height = ur - ll
8158+
return height / width * self.get_data_ratio()

lib/matplotlib/patches.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,8 @@ def __init__(self, xy, width, height, angle=0.0,
740740
self._height = height
741741
self.angle = float(angle)
742742
self.rotate_around_center = rotate_around_center
743+
# Required for RectangleSelector with axes aspect ratio != 1
744+
self._aspect_ratio_correction = 1.0
743745
self._convert_units() # Validate the inputs.
744746

745747
def get_path(self):
@@ -765,9 +767,13 @@ def get_patch_transform(self):
765767
rotation_point = bbox.x0 + width / 2., bbox.y0 + height / 2.
766768
else:
767769
rotation_point = bbox.x0, bbox.y0
768-
return (transforms.BboxTransformTo(bbox)
769-
+ transforms.Affine2D().rotate_deg_around(
770-
*rotation_point, self.angle))
770+
return transforms.BboxTransformTo(bbox) \
771+
+ transforms.Affine2D() \
772+
.translate(-rotation_point[0], -rotation_point[1]) \
773+
.scale(1, self._aspect_ratio_correction) \
774+
.rotate_deg(self.angle) \
775+
.scale(1, 1 / self._aspect_ratio_correction) \
776+
.translate(*rotation_point)
771777

772778
def get_x(self):
773779
"""Return the left coordinate of the rectangle."""
@@ -1525,6 +1531,8 @@ def __init__(self, xy, width, height, angle=0, **kwargs):
15251531
self._width, self._height = width, height
15261532
self._angle = angle
15271533
self._path = Path.unit_circle()
1534+
# Required for EllipseSelector with axes aspect ratio != 1
1535+
self._aspect_ratio_correction = 1.0
15281536
# Note: This cannot be calculated until this is added to an Axes
15291537
self._patch_transform = transforms.IdentityTransform()
15301538

@@ -1542,8 +1550,9 @@ def _recompute_transform(self):
15421550
width = self.convert_xunits(self._width)
15431551
height = self.convert_yunits(self._height)
15441552
self._patch_transform = transforms.Affine2D() \
1545-
.scale(width * 0.5, height * 0.5) \
1553+
.scale(width * 0.5, height * 0.5 * self._aspect_ratio_correction) \
15461554
.rotate_deg(self.angle) \
1555+
.scale(1, 1 / self._aspect_ratio_correction) \
15471556
.translate(*center)
15481557

15491558
def get_path(self):

lib/matplotlib/tests/test_widgets.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -322,61 +322,61 @@ def onselect(epress, erelease):
322322
_resize_rectangle(tool, 70, 65, 120, 115)
323323
tool.add_default_state('square')
324324
tool.add_default_state('center')
325-
assert tool.extents == (70.0, 120.0, 65.0, 115.0)
325+
assert_allclose(tool.extents, (70.0, 120.0, 65.0, 115.0))
326326

327327
# resize NE handle
328328
extents = tool.extents
329329
xdata, ydata = extents[1], extents[3]
330330
xdiff, ydiff = 10, 5
331331
xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
332332
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
333-
assert tool.extents == (extents[0] - xdiff, xdata_new,
334-
extents[2] - xdiff, extents[3] + xdiff)
333+
assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new,
334+
extents[2] - xdiff, extents[3] + xdiff))
335335

336336
# resize E handle
337337
extents = tool.extents
338338
xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
339339
xdiff = 10
340340
xdata_new, ydata_new = xdata + xdiff, ydata
341341
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
342-
assert tool.extents == (extents[0] - xdiff, xdata_new,
343-
extents[2] - xdiff, extents[3] + xdiff)
342+
assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new,
343+
extents[2] - xdiff, extents[3] + xdiff))
344344

345345
# resize E handle negative diff
346346
extents = tool.extents
347347
xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
348348
xdiff = -20
349349
xdata_new, ydata_new = xdata + xdiff, ydata
350350
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
351-
assert tool.extents == (extents[0] - xdiff, xdata_new,
352-
extents[2] - xdiff, extents[3] + xdiff)
351+
assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new,
352+
extents[2] - xdiff, extents[3] + xdiff))
353353

354354
# resize W handle
355355
extents = tool.extents
356356
xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
357357
xdiff = 5
358358
xdata_new, ydata_new = xdata + xdiff, ydata
359359
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
360-
assert tool.extents == (xdata_new, extents[1] - xdiff,
361-
extents[2] + xdiff, extents[3] - xdiff)
360+
assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff,
361+
extents[2] + xdiff, extents[3] - xdiff))
362362

363363
# resize W handle negative diff
364364
extents = tool.extents
365365
xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
366366
xdiff = -25
367367
xdata_new, ydata_new = xdata + xdiff, ydata
368368
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
369-
assert tool.extents == (xdata_new, extents[1] - xdiff,
370-
extents[2] + xdiff, extents[3] - xdiff)
369+
assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff,
370+
extents[2] + xdiff, extents[3] - xdiff))
371371

372372
# resize SW handle
373373
extents = tool.extents
374374
xdata, ydata = extents[0], extents[2]
375375
xdiff, ydiff = 20, 25
376376
xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
377377
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
378-
assert tool.extents == (extents[0] + ydiff, extents[1] - ydiff,
379-
ydata_new, extents[3] - ydiff)
378+
assert_allclose(tool.extents, (extents[0] + ydiff, extents[1] - ydiff,
379+
ydata_new, extents[3] - ydiff))
380380

381381

382382
@pytest.mark.parametrize('selector_class',
@@ -393,26 +393,35 @@ def onselect(epress, erelease):
393393
do_event(tool, 'onmove', xdata=130, ydata=140)
394394
do_event(tool, 'release', xdata=130, ydata=140)
395395
assert tool.extents == (100, 130, 100, 140)
396+
assert len(tool._default_state) == 0
397+
assert len(tool._state) == 0
396398

397399
# Rotate anticlockwise using top-right corner
398400
do_event(tool, 'on_key_press', key='r')
401+
assert tool._default_state == set(['rotate'])
402+
assert len(tool._state) == 0
399403
do_event(tool, 'press', xdata=130, ydata=140)
400-
do_event(tool, 'onmove', xdata=110, ydata=145)
401-
do_event(tool, 'release', xdata=110, ydata=145)
404+
do_event(tool, 'onmove', xdata=120, ydata=145)
405+
do_event(tool, 'release', xdata=120, ydata=145)
402406
do_event(tool, 'on_key_press', key='r')
407+
assert len(tool._default_state) == 0
408+
assert len(tool._state) == 0
403409
# Extents shouldn't change (as shape of rectangle hasn't changed)
404410
assert tool.extents == (100, 130, 100, 140)
411+
assert_allclose(tool.rotation, 25.56, atol=0.01)
412+
tool.rotation = 45
413+
assert tool.rotation == 45
405414
# Corners should move
406415
# The third corner is at (100, 145)
407416
assert_allclose(tool.corners,
408-
np.array([[119.9, 139.9, 110.1, 90.1],
409-
[95.4, 117.8, 144.5, 122.2]]), atol=0.1)
417+
np.array([[118.53, 139.75, 111.46, 90.25],
418+
[95.25, 116.46, 144.75, 123.54]]), atol=0.01)
410419

411420
# Scale using top-right corner
412421
do_event(tool, 'press', xdata=110, ydata=145)
413422
do_event(tool, 'onmove', xdata=110, ydata=160)
414423
do_event(tool, 'release', xdata=110, ydata=160)
415-
assert_allclose(tool.extents, (100, 141.5, 100, 150.4), atol=0.1)
424+
assert_allclose(tool.extents, (100, 139.75, 100, 151.82), atol=0.01)
416425

417426

418427
def test_rectangle_resize_square_center_aspect():
@@ -434,6 +443,7 @@ def onselect(epress, erelease):
434443
xdata, ydata = extents[1], extents[3]
435444
xdiff = 10
436445
xdata_new, ydata_new = xdata + xdiff, ydata
446+
ychange = xdiff * 1 / tool._aspect_ratio_correction
437447
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
438448
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
439449
46.25, 133.75])

lib/matplotlib/widgets.py

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1935,9 +1935,8 @@ def press(self, event):
19351935
key = event.key or ''
19361936
key = key.replace('ctrl', 'control')
19371937
# move state is locked in on a button press
1938-
for state in ['move']:
1939-
if key == self._state_modifier_keys[state]:
1940-
self._state.add(state)
1938+
if key == self._state_modifier_keys['move']:
1939+
self._state.add('move')
19411940
self._press(event)
19421941
return True
19431942
return False
@@ -1990,14 +1989,10 @@ def on_key_press(self, event):
19901989
self.update()
19911990
return
19921991
for (state, modifier) in self._state_modifier_keys.items():
1993-
if modifier in key.split('+'):
1994-
# rotate and data_coordinates are enable/disable
1995-
# on key press
1996-
if (state in ['rotate', 'data_coordinates'] and
1997-
state in self._state):
1998-
self._state.discard(state)
1999-
else:
2000-
self._state.add(state)
1992+
# 'rotate' and 'data_coordinates' are added in _default_state
1993+
if (modifier in key.split('+') and
1994+
state not in ['rotate', 'data_coordinates']):
1995+
self._state.add(state)
20011996
self._on_key_press(event)
20021997

20031998
def _on_key_press(self, event):
@@ -2007,9 +2002,9 @@ def on_key_release(self, event):
20072002
"""Key release event handler and validator."""
20082003
if self.active:
20092004
key = event.key or ''
2005+
key = key.replace('ctrl', 'control')
20102006
for (state, modifier) in self._state_modifier_keys.items():
2011-
if (modifier in key.split('+') and
2012-
state not in ['rotate', 'data_coordinates']):
2007+
if modifier in key.split('+'):
20132008
self._state.discard(state)
20142009
self._on_key_release(event)
20152010

@@ -2795,7 +2790,8 @@ def __init__(self, ax, onselect, drawtype='box',
27952790
self._interactive = interactive
27962791
self.drag_from_anywhere = drag_from_anywhere
27972792
self.ignore_event_outside = ignore_event_outside
2798-
self._rotation = 0
2793+
self._rotation = 0.0
2794+
self._aspect_ratio_correction = 1.0
27992795

28002796
if drawtype == 'none': # draw a line but make it invisible
28012797
_api.warn_deprecated(
@@ -2814,6 +2810,7 @@ def __init__(self, ax, onselect, drawtype='box',
28142810
_props = props
28152811
self.visible = _props.pop('visible', self.visible)
28162812
self._to_draw = self._init_shape(**_props)
2813+
self._set_aspect_ratio_correction()
28172814
self.ax.add_patch(self._to_draw)
28182815
if drawtype == 'line':
28192816
_api.warn_deprecated(
@@ -2909,6 +2906,8 @@ def _press(self, event):
29092906
self.set_visible(True)
29102907

29112908
self._extents_on_press = self.extents
2909+
self._rotation_on_press = self._rotation
2910+
self._set_aspect_ratio_correction()
29122911

29132912
return False
29142913

@@ -2984,13 +2983,8 @@ def _onmove(self, event):
29842983
dy = event.ydata - eventpress.ydata
29852984
refmax = None
29862985
if 'data_coordinates' in state:
2987-
aspect_ratio = 1
29882986
refx, refy = dx, dy
29892987
else:
2990-
figure_size = self.ax.get_figure().get_size_inches()
2991-
ll, ur = self.ax.get_position() * figure_size
2992-
width, height = ur - ll
2993-
aspect_ratio = height / width * self.ax.get_data_ratio()
29942988
refx = event.xdata / (eventpress.xdata + 1e-6)
29952989
refy = event.ydata / (eventpress.ydata + 1e-6)
29962990

@@ -3001,8 +2995,9 @@ def _onmove(self, event):
30012995
a = np.array([eventpress.xdata, eventpress.ydata])
30022996
b = np.array(self.center)
30032997
c = np.array([event.xdata, event.ydata])
3004-
self._rotation = (np.arctan2(c[1]-b[1], c[0]-b[0]) -
3005-
np.arctan2(a[1]-b[1], a[0]-b[0]))
2998+
angle = (np.arctan2(c[1]-b[1], c[0]-b[0]) -
2999+
np.arctan2(a[1]-b[1], a[0]-b[0]))
3000+
self.rotation = np.rad2deg(self._rotation_on_press + angle)
30063001

30073002
# resize an existing shape
30083003
elif self._active_handle and self._active_handle != 'C':
@@ -3017,10 +3012,10 @@ def _onmove(self, event):
30173012
refmax = max(refx, refy, key=abs)
30183013
if self._active_handle in ['E', 'W'] or refmax == refx:
30193014
dw = event.xdata - center[0]
3020-
dh = dw / aspect_ratio
3015+
dh = dw / self._aspect_ratio_correction
30213016
else:
30223017
dh = event.ydata - center[1]
3023-
dw = dh * aspect_ratio
3018+
dw = dh * self._aspect_ratio_correction
30243019
else:
30253020
dw = sizepress[0] / 2
30263021
dh = sizepress[1] / 2
@@ -3053,10 +3048,12 @@ def _onmove(self, event):
30533048
refmax = max(refx, refy, key=abs)
30543049
if self._active_handle in ['E', 'W'] or refmax == refx:
30553050
sign = np.sign(event.ydata - y0)
3056-
y1 = y0 + sign * abs(x1 - x0) / aspect_ratio
3051+
y1 = y0 + sign * abs(x1 - x0) / \
3052+
self._aspect_ratio_correction
30573053
else:
30583054
sign = np.sign(event.xdata - x0)
3059-
x1 = x0 + sign * abs(y1 - y0) * aspect_ratio
3055+
x1 = x0 + sign * abs(y1 - y0) * \
3056+
self._aspect_ratio_correction
30603057

30613058
# move existing shape
30623059
elif self._active_handle == 'C':
@@ -3083,9 +3080,9 @@ def _onmove(self, event):
30833080
if 'square' in state:
30843081
refmax = max(refx, refy, key=abs)
30853082
if refmax == refx:
3086-
dy = dx / aspect_ratio
3083+
dy = np.sign(dy) * abs(dx) / self._aspect_ratio_correction
30873084
else:
3088-
dx = dy * aspect_ratio
3085+
dx = np.sign(dx) * abs(dy) * self._aspect_ratio_correction
30893086

30903087
# from center
30913088
if 'center' in state:
@@ -3102,6 +3099,18 @@ def _onmove(self, event):
31023099

31033100
self.extents = x0, x1, y0, y1
31043101

3102+
def _on_key_press(self, event):
3103+
key = event.key or ''
3104+
key = key.replace('ctrl', 'control')
3105+
for (state, modifier) in self._state_modifier_keys.items():
3106+
if modifier in key.split('+'):
3107+
if state in ['rotate', 'data_coordinates']:
3108+
if state in self._default_state:
3109+
self._default_state.discard(state)
3110+
else:
3111+
self._default_state.add(state)
3112+
self._set_aspect_ratio_correction()
3113+
31053114
@property
31063115
def _rect_bbox(self):
31073116
if self._drawtype == 'box':
@@ -3112,8 +3121,21 @@ def _rect_bbox(self):
31123121
y0, y1 = min(y), max(y)
31133122
return x0, y0, x1 - x0, y1 - y0
31143123

3124+
def _set_aspect_ratio_correction(self):
3125+
aspect_ratio = self.ax._get_aspect_ratio()
3126+
self._to_draw._aspect_ratio_correction = aspect_ratio
3127+
if 'data_coordinates' in self._state | self._default_state:
3128+
self._aspect_ratio_correction = 1
3129+
else:
3130+
self._aspect_ratio_correction = aspect_ratio
3131+
31153132
def _get_rotation_transform(self):
3116-
return Affine2D().rotate_around(*self.center, self._rotation)
3133+
aspect_ratio = self.ax._get_aspect_ratio()
3134+
return Affine2D().translate(-self.center[0], -self.center[1]) \
3135+
.scale(1, aspect_ratio) \
3136+
.rotate(self._rotation) \
3137+
.scale(1, 1 / aspect_ratio) \
3138+
.translate(*self.center)
31173139

31183140
@property
31193141
def corners(self):
@@ -3167,14 +3189,17 @@ def extents(self, extents):
31673189

31683190
@property
31693191
def rotation(self):
3170-
"""Rotation in degree."""
3192+
"""Rotation in degree in interval [0, 45]."""
31713193
return np.rad2deg(self._rotation)
31723194

31733195
@rotation.setter
31743196
def rotation(self, value):
3175-
self._rotation = np.deg2rad(value)
3176-
# call extents setter to draw shape and update handles positions
3177-
self.extents = self.extents
3197+
# Restrict to a limited range of rotation [0, 45] to avoid changing
3198+
# order of handles
3199+
if 0 <= value and value <= 45:
3200+
self._rotation = np.deg2rad(value)
3201+
# call extents setter to draw shape and update handles positions
3202+
self.extents = self.extents
31783203

31793204
draw_shape = _api.deprecate_privatize_attribute('3.5')
31803205

@@ -3285,7 +3310,7 @@ def _draw_shape(self, extents):
32853310
self._to_draw.center = center
32863311
self._to_draw.width = 2 * a
32873312
self._to_draw.height = 2 * b
3288-
self._to_draw.set_angle(self.rotation)
3313+
self._to_draw.angle = self.rotation
32893314
else:
32903315
rad = np.deg2rad(np.arange(31) * 12)
32913316
x = a * np.cos(rad) + center[0]

0 commit comments

Comments
 (0)