Skip to content
Prev Previous commit
Let @rotation.setter rotate around center, too; update selector tests
  • Loading branch information
dhomeier committed Jan 28, 2025
commit 27c1c11f2e9743295ac469f461e7d88420ed9f99
35 changes: 27 additions & 8 deletions lib/matplotlib/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,11 @@ def test_rectangle_resize_square_center(ax):
def test_selector_rotate(ax, selector_class):
ax.set_aspect(1)
tool = selector_class(ax, interactive=True)
corners = np.array([[100, 130, 130, 100], [100, 100, 140, 140]])
# Draw rectangle
click_and_drag(tool, start=(100, 100), end=(130, 140))
assert_allclose(tool.extents, (100, 130, 100, 140))
assert_allclose(tool.corners, corners, atol=1e-6)
assert len(tool._state) == 0

# Rotate anticlockwise using top-right corner
Expand All @@ -474,20 +476,37 @@ def test_selector_rotate(ax, selector_class):
# Extents change as the selector remains rigid in display coordinates
assert_allclose(tool.extents, (110.10, 119.90, 95.49, 144.51), atol=0.01)
assert_allclose(tool.rotation, 25.56, atol=0.01)
angle = tool.rotation
corners_dragged = tool.corners

# Check corners on returning to unrotated position, then rotate back with @setter
tool.rotation = 0
assert tool.rotation == 0
assert_allclose(tool.corners, corners, atol=1e-6)

tool.rotation = angle
assert_allclose(tool.rotation, 25.56, atol=0.01)
assert_allclose(tool.corners, corners_dragged, atol=1e-6)
assert_allclose(tool.extents, (110.10, 119.90, 95.49, 144.51), atol=0.01)

tool.rotation = 45
assert tool.rotation == 45
# Corners should move
# Corners should move again
assert_allclose(tool.corners,
np.array([[110.10, 131.31, 103.03, 81.81],
[95.49, 116.70, 144.98, 123.77]]), atol=0.01)
np.array([[118.54, 139.75, 111.46, 90.25],
[95.25, 116.46, 144.75, 123.54]]), atol=0.01)

# Scale using top-right corner
click_and_drag(tool, start=(110, 145), end=(110, 160))
assert_allclose(tool.extents, (110, 110, 145, 160), atol=0.01)
# Rescale using top-right corner
click_and_drag(tool, start=(111, 145), end=(111, 160))
assert_allclose(tool.corners,
np.array([[118.54, 147.25, 111.46, 82.75],
[95.25, 123.96, 159.75, 131.04]]), atol=0.01)
assert_allclose(tool.extents, (118.54, 111.46, 95.25, 159.75), atol=0.01)

if selector_class == widgets.RectangleSelector:
assert tool._selection_artist.rotation_point == 'xy'
with pytest.raises(ValueError):
tool._selection_artist.rotation_point = 'unvalid_value'
tool._selection_artist.rotation_point = 'invalid_value'


def test_rectangle_add_remove_set(ax):
Expand Down Expand Up @@ -570,7 +589,7 @@ def test_selector_handles(ax, selector_class):
"""Test geometry of Rectangle/Ellipse Selector [bounding box]"""
tool = selector_class(ax, grab_range=10, interactive=True,
handle_props={'markerfacecolor': 'r',
'markeredgecolor': 'b'})
'markeredgecolor': 'b'})
tool.extents = (100, 150, 100, 150)

assert_allclose(tool.corners, ((100, 150, 150, 100), (100, 100, 150, 150)))
Expand Down
12 changes: 10 additions & 2 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1539,7 +1539,7 @@
Return the corners of the ellipse, moving anti-clockwise from
the center of the left-hand edge before rotation.
"""
return self.get_patch_transform().transform(

Check warning on line 1542 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L1542

Added line #L1542 was not covered by tests
[(-1, 0), (0, -1), (1, 0), (0, 1)])


Expand Down Expand Up @@ -2913,17 +2913,17 @@
Return a named tuple containing all geometry state attributes
(position, size, orientation) in display coordinates.
"""
return _RectState(

Check warning on line 2916 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L2916

Added line #L2916 was not covered by tests
self._x0, self._y0, self._width, self._height, self._rotation)

@_geometry_state.setter
def _geometry_state(self, state):
self._x0 = state.x0
self._y0 = state.y0
self._width = state.width
self._height = state.height
self._rotation = state.rotation
self._update_selection_artist()

Check warning on line 2926 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L2921-L2926

Added lines #L2921 - L2926 were not covered by tests

@property
def positions(self):
Expand Down Expand Up @@ -3277,7 +3277,7 @@

def _on_resize(self, event):
# Callback for an Axes resize
self._update_handles()

Check warning on line 3280 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3280

Added line #L3280 was not covered by tests

@property
def _geometry_state(self):
Expand Down Expand Up @@ -3362,7 +3362,7 @@
spanx = self._width
spany = self._height
else:
_api.check_in_list(['data', 'pixels'], spancoords=self.spancoords)

Check warning on line 3365 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3365

Added line #L3365 was not covered by tests
# check if drawn distance (if it exists) is not too small in
# either x or y-direction
if abs(spanx) <= self.minspanx or abs(spany) <= self.minspany:
Expand Down Expand Up @@ -3458,7 +3458,7 @@
else:
hw = hh = abs(event.y - center[1])
if self._use_data_coordinates:
hw /= self.ax._get_aspect_ratio()

Check warning on line 3461 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3461

Added line #L3461 was not covered by tests

else:
hw = size_on_press[0] / 2
Expand Down Expand Up @@ -3495,7 +3495,7 @@
if self._active_handle in ['E', 'W']:
height = width
elif self._active_handle in ['N', 'S']:
width = height

Check warning on line 3498 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3498

Added line #L3498 was not covered by tests
elif self._active_handle == 'NE':
width = height = max(event.x - x0, event.y - y0)
elif self._active_handle == 'SW':
Expand All @@ -3507,14 +3507,14 @@
width = height = new_wh
elif self._active_handle == 'SE':
# Keep x0, y0 + height a fixed point
new_wh = max(event.x - x0, y0 + height - event.y)
y0 += height - new_wh
width = height = new_wh

Check warning on line 3512 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3510-L3512

Added lines #L3510 - L3512 were not covered by tests
elif self._active_handle == 'NW':
# Keep x0 + width, y0 a fixed point
new_wh = max(x0 + width - event.x, event.y - y0)
x0 += width - new_wh
width = height = new_wh

Check warning on line 3517 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3515-L3517

Added lines #L3515 - L3517 were not covered by tests

# Transform back into de-rotated display coordinates
new_geom_state.x0, new_geom_state.y0 = t.inverted().transform(
Expand Down Expand Up @@ -3557,7 +3557,7 @@
sign = np.sign(dx) or 1
dx = sign * abs(dy)
if self._use_data_coordinates:
dx /= self.ax._get_aspect_ratio()

Check warning on line 3560 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3560

Added line #L3560 was not covered by tests

# from center
if 'center' in state:
Expand Down Expand Up @@ -3636,13 +3636,21 @@
@property
def rotation(self):
"""
Rotation in degrees.
Rotation in degrees (around center).
"""
return np.rad2deg(self._rotation)

@rotation.setter
def rotation(self, value):
self._rotation = np.deg2rad(value)
new_angle = np.deg2rad(value)
rot = new_angle - self._rotation
xc, yc = self._selection_artist.get_center()
new_geom_state = copy.copy(self._geometry_state)
new_geom_state.rotation = new_angle
# Transform the rectangle corner xy so we are rotating about the center.
new_geom_state.xy = Affine2D().rotate_around(xc, yc, rot).transform(
self._geometry_state.xy)
self._geometry_state = new_geom_state
self._update_selection_artist()

def _clip_to_axes(self, x, y):
Expand All @@ -3664,10 +3672,10 @@
# display min/max are in the correct order.

if min_lim[0] > max_lim[0]:
min_lim[0], max_lim[0] = max_lim[0], min_lim[0]

Check warning on line 3675 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3675

Added line #L3675 was not covered by tests

if min_lim[1] > max_lim[1]:
min_lim[1], max_lim[1] = max_lim[1], min_lim[1]

Check warning on line 3678 in lib/matplotlib/widgets.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/widgets.py#L3678

Added line #L3678 was not covered by tests

x = np.clip(x, min_lim[0], max_lim[0])
y = np.clip(y, min_lim[1], max_lim[1])
Expand Down