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

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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, < 67E6 span class=pl-c1>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)
2B17
0