8000 Fix shape rectangle when using rotation and the axes aspect ratio != 1 · matplotlib/matplotlib@762a2f0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 762a2f0

Browse files
committed
Fix shape rectangle when using rotation and the axes aspect ratio != 1
1 parent db55921 commit 762a2f0

File tree

4 files changed

+72
-31
lines changed

4 files changed

+72
-31
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: 14 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,14 @@ 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)
777+
771778

772779
def get_x(self):
773780
"""Return the left coordinate of the rectangle."""
@@ -1525,6 +1532,8 @@ def __init__(self, xy, width, height, angle=0, **kwargs):
15251532
self._width, self._height = width, height
15261533
self._angle = angle
15271534
self._path = Path.unit_circle()
1535+
# Required for EllipseSelector with axes aspect ratio != 1
1536+
self._aspect_ratio_correction = 1.0
15281537
# Note: This cannot be calculated until this is added to an Axes
15291538
self._patch_transform = transforms.IdentityTransform()
15301539

@@ -1542,8 +1551,9 @@ def _recompute_transform(self):
15421551
width = self.convert_xunits(self._width)
15431552
height = self.convert_yunits(self._height)
15441553
self._patch_transform = transforms.Affine2D() \
1545-
.scale(width * 0.5, height * 0.5) \
1554+
.scale(width * 0.5, height * 0.5 * self._aspect_ratio_correction) \
15461555
.rotate_deg(self.angle) \
1556+
.scale(1, 1 / self._aspect_ratio_correction) \
15471557
.translate(*center)
15481558

15491559
def get_path(self):

lib/matplotlib/tests/test_widgets.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,19 @@ 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)
400404
do_event(tool, 'onmove', xdata=110, ydata=145)
401405
do_event(tool, 'release', xdata=110, 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)
405411
# Corners should move
@@ -436,7 +442,7 @@ def onselect(epress, erelease):
436442
xdata_new, ydata_new = xdata + xdiff, ydata
437443
_resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new)
438444
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
439-
46.25, 133.75])
445+
55, 125])
440446

441447
# use data coordinates
442448
do_event(tool, 'on_key_press', key='d')

lib/matplotlib/widgets.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,14 +1990,10 @@ def on_key_press(self, event):
19901990
self.update()
19911991
return
19921992
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)
1993+
# 'rotate' and 'data_coordinates' are added in _default_state
1994+
if (modifier in key.split('+') and
1995+
state not in ['rotate', 'data_coordinates']):
1996+
self._state.add(state)
20011997
self._on_key_press(event)
20021998

20031999
def _on_key_press(self, event):
@@ -2007,9 +2003,9 @@ def on_key_release(self, event):
20072003
"""Key release event handler and validator."""
20082004
if self.active:
20092005
key = event.key or ''
2006+
key = key.replace('ctrl', 'control')
20102007
for (state, modifier) in self._state_modifier_keys.items():
2011-
if (modifier in key.split('+') and
2012-
state not in ['rotate', 'data_coordinates']):
2008+
if modifier in key.split('+'):
20132009
self._state.discard(state)
20142010
self._on_key_release(event)
20152011

@@ -2795,7 +2791,8 @@ def __init__(self, ax, onselect, drawtype='box',
27952791
self._interactive = interactive
27962792
self.drag_from_anywhere = drag_from_anywhere
27972793
self.ignore_event_outside = ignore_event_outside
2798-
self._rotation = 0
2794+
self._rotation = 0.0
2795+
self._aspect_ratio_correction = 1.0
27992796

28002797
if drawtype == 'none': # draw a line but make it invisible
28012798
_api.warn_deprecated(
@@ -2814,6 +2811,7 @@ def __init__(self, ax, onselect, drawtype='box',
28142811
_props = props
28152812
self.visible = _props.pop('visible', self.visible)
28162813
self._to_draw = self._init_shape(**_props)
2814+
self._set_aspect_ratio_correction()
28172815
self.ax.add_patch(self._to_draw)
28182816
if drawtype == 'line':
28192817
_api.warn_deprecated(
@@ -2909,6 +2907,7 @@ def _press(self, event):
29092907
self.set_visible(True)
29102908

29112909
self._extents_on_press = self.extents
2910+
self._rotation_on_press = self._rotation
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 = 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
30213016
else:
30223017
dh = event.ydata - center[1]
3023-
dw = dh * aspect_ratio
3018+
dw = dh
30243019
else:
30253020
dw = sizepress[0] / 2
30263021
dh = sizepress[1] / 2
@@ -3053,10 +3048,10 @@ 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)
30573052
else:
30583053
sign = np.sign(event.xdata - x0)
3059-
x1 = x0 + sign * abs(y1 - y0) * aspect_ratio
3054+
x1 = x0 + sign * abs(y1 - y0)
30603055

30613056
# move existing shape
30623057
elif self._active_handle == 'C':
@@ -3083,9 +3078,9 @@ def _onmove(self, event):
30833078
if 'square' in state:
30843079
refmax = max(refx, refy, key=abs)
30853080
if refmax == refx:
3086-
dy = dx / aspect_ratio
3081+
dy = dx
30873082
else:
3088-
dx = dy * aspect_ratio
3083+
dx = dy
30893084

30903085
# from center
30913086
if 'center' in state:
@@ -3102,6 +3097,18 @@ def _onmove(self, event):
31023097

31033098
self.extents = x0, x1, y0, y1
31043099

3100+
def _on_key_press(self, event):
3101+
key = event.key or ''
3102+
key = key.replace('ctrl', 'control')
3103+
for (state, modifier) in self._state_modifier_keys.items():
3104+
if modifier in key.split('+'):
3105+
if state in ['rotate', 'data_coordinates']:
3106+
if state in self._default_state:
3107+
self._default_state.discard(state)
3108+
else:
3109+
self._default_state.add(state)
3110+
self._set_aspect_ratio_correction()
3111+
31053112
@property
31063113
def _rect_bbox(self):
31073114
if self._drawtype == 'box':
@@ -3112,8 +3119,19 @@ def _rect_bbox(self):
31123119
y0, y1 = min(y), max(y)
31133120
return x0, y0, x1 - x0, y1 - y0
31143121

3122+
def _set_aspect_ratio_correction(self):
3123+
if 'data_coordinates' in self._state | self._default_state:
3124+
self._aspect_ratio_correction = 1
3125+
else:
3126+
self._aspect_ratio_correction = self.ax._get_aspect_ratio()
3127+
self._to_draw._aspect_ratio_correction = self._aspect_ratio_correction
3128+
31153129
def _get_rotation_transform(self):
3116-
return Affine2D().rotate_around(*self.center, self._rotation)
3130+
return Affine2D().translate(-self.center[0], -self.center[1]) \
3131+
.scale(1, self._aspect_ratio_correction) \
3132+
.rotate(self._rotation) \
3133+
.scale(1, 1 / self._aspect_ratio_correction) \
3134+
.translate(*self.center)
31173135

31183136
@property
31193137
def corners(self):
@@ -3285,7 +3303,7 @@ def _draw_shape(self, extents):
32853303
self._to_draw.center = center
32863304
self._to_draw.width = 2 * a
32873305
self._to_draw.height = 2 * b
3288-
self._to_draw.set_angle(self.rotation)
3306+
self._to_draw.angle = self.rotation
32893307
else:
32903308
rad = np.deg2rad(np.arange(31) * 12)
32913309
x = a * np.cos(rad) + center[0]

0 commit comments

Comments
 (0)
0