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

Skip to content

Commit d7ebfb8

Browse files
committed
Implement rotation selector
1 parent f6a5d99 commit d7ebfb8

File tree

3 files changed

+84
-34
lines changed

3 files changed

+84
-34
lines changed

lib/matplotlib/patches.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,8 @@ def __str__(self):
709709
return fmt % pars
710710

711711
@docstring.dedent_interpd
712-
def __init__(self, xy, width, height, angle=0.0, **kwargs):
712+
def __init__(self, xy, width, height, angle=0.0,
713+
rotate_around_center=False, **kwargs):
713714
"""
714715
Parameters
715716
----------
@@ -720,7 +721,12 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs):
720721
height : float
721722
Rectangle height.
722723
angle : float, default: 0
723-
Rotation in degrees anti-clockwise about *xy*.
724+
Rotation in degrees anti-clockwise about *xy* if
725+
*rotate_around_center* if False, otherwise rotate around the
726+
center of the rectangle
727+
rotate_around_center : bool, default: False
728+
If True, the rotation is performed around the center of the
729+
rectangle.
724730
725731
Other Parameters
726732
----------------
@@ -733,6 +739,7 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs):
733739
self._width = width
734740
self._height = height
735741
self.angle = float(angle)
742+
self.rotate_around_center = rotate_around_center
736743
self._convert_units() # Validate the inputs.
737744

738745
def get_path(self):
@@ -753,9 +760,14 @@ def get_patch_transform(self):
753760
# important to call the accessor method and not directly access the
754761
# transformation member variable.
755762
bbox = self.get_bbox()
763+
if self.rotate_around_center:
764+
width, height = bbox.x1 - bbox.x0, bbox.y1 - bbox.y0
765+
rotation_point = bbox.x0 + width / 2., bbox.y0 + height / 2.
766+
else:
767+
rotation_point = bbox.x0, bbox.y0
756768
return (transforms.BboxTransformTo(bbox)
757769
+ transforms.Affine2D().rotate_deg_around(
758-
bbox.x0, bbox.y0, self.angle))
770+
*rotation_point, self.angle))
759771

760772
def get_x(self):
761773
"""Return the left coordinate of the rectangle."""

lib/matplotlib/tests/test_widgets.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -482,11 +482,10 @@ def onselect(epress, erelease):
482482
'markeredgecolor': 'b'})
483483
tool.extents = (100, 150, 100, 150)
484484

485-
assert tool.corners == (
486-
(100, 150, 150, 100), (100, 100, 150, 150))
485+
assert_allclose(tool.corners, ((100, 150, 150, 100), (100, 100, 150, 150)))
487486
assert tool.extents == (100, 150, 100, 150)
488-
assert tool.edge_centers == (
489-
(100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150))
487+
assert_allclose(tool.edge_centers,
488+
((100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150)))
490489
assert tool.extents == (100, 150, 100, 150)
491490

492491
# grab a corner and move it

lib/matplotlib/widgets.py

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from . import _api, backend_tools, cbook, colors, ticker
2121
from .lines import Line2D
2222
from .patches import Circle, Rectangle, Ellipse
23+
from .transforms import Affine2D
2324

2425

2526
class LockDraw:
@@ -1797,7 +1798,8 @@ def __init__(self, ax, onselect, useblit=False, button=None,
17971798

17981799
self._state_modifier_keys = dict(move=' ', clear='escape',
17991800
square='shift', center='control',
1800-
data_coordinates='d')
1801+
data_coordinates='d',
1802+
rotate='r')
18011803
self._state_modifier_keys.update(state_modifier_keys or {})
18021804

18031805
self.background = None
@@ -1933,8 +1935,9 @@ def press(self, event):
19331935
key = event.key or ''
19341936
key = key.replace('ctrl', 'control')
19351937
# move state is locked in on a button press
1936-
if key == self._state_modifier_keys['move']:
1937-
self._state.add('move')
1938+
for action in ['move']:
1939+
if key == self._state_modifier_keys[action]:
1940+
self._state.add(action)
19381941
self._press(event)
19391942
return True
19401943
return False
@@ -1986,14 +1989,15 @@ def on_key_press(self, event):
19861989
artist.set_visible(False)
19871990
self.update()
19881991
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)
1992+
for state in ['rotate', 'data_coordinates']:
1993+
if key == self._state_modifier_keys[state]:
1994+
if state in self._default_state:
1995+
self._default_state.remove(state)
1996+
else:
1997+
self.add_default_state(state)
19951998
for (state, modifier) in self._state_modifier_keys.items():
1996-
if modifier in key:
1999+
# Multiple keys are string concatenated using '+'
2000+
if modifier in key.split('+'):
19972001
self._state.add(state)
19982002
self._on_key_press(event)
19992003

@@ -2160,7 +2164,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
21602164
state_modifier_keys = dict(clear='escape',
21612165
square='not-applicable',
21622166
center='not-applicable',
2163-
data_coordinates='not-applicable')
2167+
data_coordinates='not-applicable',
2168+
rotate='not-applicable')
21642169
super().__init__(ax, onselect, useblit=useblit, button=button,
21652170
state_modifier_keys=state_modifier_keys)
21662171

@@ -2723,6 +2728,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
27232728
- "center": change the shape around its center, default: "ctrl".
27242729
- "data_coordinates": define if data or figure coordinates should be
27252730
used to define the square shape, default: "d"
2731+
- "rotate": Rotate the shape around its center, default: "r".
27262732
27272733
"square" and "center" can be combined. The square shape can be defined
27282734
in data or figure coordinates as determined by the ``data_coordinates``
@@ -2770,8 +2776,6 @@ class RectangleSelector(_SelectorWidget):
27702776
See also: :doc:`/gallery/widgets/rectangle_selector`
27712777
"""
27722778

2773-
_shape_klass = Rectangle
2774-
27752779
@_api.rename_parameter("3.5", "maxdist", "grab_range")
27762780
@_api.rename_parameter("3.5", "marker_props", "handle_props")
27772781
@_api.rename_parameter("3.5", "rectprops", "props")
@@ -2791,6 +2795,7 @@ def __init__(self, ax, onselect, drawtype='box',
27912795
self._interactive = interactive
27922796
self.drag_from_anywhere = drag_from_anywhere
27932797
self.ignore_event_outside = ignore_event_outside
2798+
self._rotation = 0
27942799

27952800
if drawtype == 'none': # draw a line but make it invisible
27962801
_api.warn_deprecated(
@@ -2808,8 +2813,7 @@ def __init__(self, ax, onselect, drawtype='box',
28082813
props['animated'] = self.useblit
28092814
_props = props
28102815
self.visible = _props.pop('visible', self.visible)
2811-
self._to_draw = self._shape_klass((0, 0), 0, 1, visible=False,
2812-
**_props)
2816+
self._to_draw = self._init_shape(**_props)
28132817
self.ax.add_patch(self._to_draw)
28142818
if drawtype == 'line':
28152819
_api.warn_deprecated(
@@ -2878,6 +2882,10 @@ def __init__(self, ax, onselect, drawtype='box',
28782882
property(lambda self: self.grab_range,
28792883
lambda self, value: setattr(self, "grab_range", value)))
28802884

2885+
def _init_shape(self, **props):
2886+
return Rectangle((0, 0), 0, 1, visible=False,
2887+
rotate_around_center=True, **props)
2888+
28812889
def _press(self, event):
28822890
"""Button press event handler."""
28832891
# make the drawn box/line visible get the click-coordinates,
@@ -2974,9 +2982,17 @@ def _onmove(self, event):
29742982
refx = event.xdata / (self._eventpress.xdata + 1e-6)
29752983
refy = event.ydata / (self._eventpress.ydata + 1e-6)
29762984

2985+
2986+
x0, x1, y0, y1 = self._extents_on_press
29772987
# resize an existing shape
2978-
if self._active_handle and self._active_handle != 'C':
2979-
x0, x1, y0, y1 = self._extents_on_press
2988+
if 'rotate' in state and self._active_handle in self._corner_order:
2989+
# calculate angle abc
2990+
a = np.array([self._eventpress.xdata, self._eventpress.ydata])
2991+
b = np.array(self.center)
2992+
c = np.array([event.xdata, event.ydata])
2993+
self._rotation = (np.arctan2(c[1]-b[1], c[0]-b[0]) -
2994+
np.arctan2(a[1]-b[1], a[0]-b[0]))
2995+
elif self._active_handle and self._active_handle != 'C':
29802996
sizepress = [x1 - x0, y1 - y0]
29812997
center = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2]
29822998

@@ -3041,6 +3057,7 @@ def _onmove(self, event):
30413057

30423058
# new shape
30433059
else:
3060+
self._rotation = 0
30443061
# Don't create a new rectangle if there is already one when
30453062
# ignore_event_outside=True
30463063
if self.ignore_event_outside and self._selection_completed:
@@ -3075,24 +3092,25 @@ def _onmove(self, event):
30753092
@property
30763093
def _rect_bbox(self):
30773094
if self._drawtype == 'box':
3078-
x0 = self._to_draw.get_x()
3079-
y0 = self._to_draw.get_y()
3080-
width = self._to_draw.get_width()
3081-
height = self._to_draw.get_height()
3082-
return x0, y0, width, height
3095+
return self._to_draw.get_bbox().bounds
30833096
else:
30843097
x, y = self._to_draw.get_data()
30853098
x0, x1 = min(x), max(x)
30863099
y0, y1 = min(y), max(y)
30873100
return x0, y0, x1 - x0, y1 - y0
30883101

3102+
def _get_rotation_transform(self):
3103+
return Affine2D().rotate_around(*self.center, self._rotation)
3104+
30893105
@property
30903106
def corners(self):
30913107
"""Corners of rectangle from lower left, moving clockwise."""
30923108
x0, y0, width, height = self._rect_bbox
30933109
xc = x0, x0 + width, x0 + width, x0
30943110
yc = y0, y0, y0 + height, y0 + height
3095-
return xc, yc
3111+
transform = self._get_rotation_transform()
3112+
coords = transform.transform(np.array([xc, yc]).T).T
3113+
return coords[0], coords[1]
30963114

30973115
@property
30983116
def edge_centers(self):
@@ -3102,7 +3120,9 @@ def edge_centers(self):
31023120
h = height / 2.
31033121
xe = x0, x0 + w, x0 + width, x0 + w
31043122
ye = y0 + h, y0, y0 + h, y0 + height
3105-
return xe, ye
3123+
transform = self._get_rotation_transform()
3124+
coords = transform.transform(np.array([xe, ye]).T).T
3125+
return coords[0], coords[1]
31063126

31073127
@property
31083128
def center(self):
@@ -3112,7 +3132,10 @@ def center(self):
31123132

31133133
@property
31143134
def extents(self):
3115-
"""Return (xmin, xmax, ymin, ymax)."""
3135+
"""
3136+
Return (xmin, xmax, ymin, ymax) as defined by the bounding box before
3137+
rotation.
3138+
"""
31163139
x0, y0, width, height = self._rect_bbox
31173140
xmin, xmax = sorted([x0, x0 + width])
31183141
ymin, ymax = sorted([y0, y0 + height])
@@ -3129,6 +3152,17 @@ def extents(self, extents):
31293152
self.set_visible(self.visible)
31303153
self.update()
31313154

3155+
@property
3156+
def rotation(self):
3157+
"""Rotation in degree."""
3158+
return np.rad2deg(self._rotation)
3159+
3160+
@rotation.setter
3161+
def rotation(self, value):
3162+
self._rotation = np.deg2rad(value)
3163+
# call extents setter to draw shape and update handles positions
3164+
self.extents = self.extents
3165+
31323166
draw_shape = _api.deprecate_privatize_attribute('3.5')
31333167

31343168
def _draw_shape(self, extents):
@@ -3148,6 +3182,7 @@ def _draw_shape(self, extents):
31483182
self._to_draw.set_y(ymin)
31493183
self._to_draw.set_width(xmax - xmin)
31503184
self._to_draw.set_height(ymax - ymin)
3185+
self._to_draw.set_angle(self.rotation)
31513186

31523187
elif self._drawtype == 'line':
31533188
self._to_draw.set_data([xmin, xmax], [ymin, ymax])
@@ -3220,9 +3255,11 @@ class EllipseSelector(RectangleSelector):
32203255
:doc:`/gallery/widgets/rectangle_selector`
32213256
"""
32223257

3223-
_shape_klass = Ellipse
32243258
draw_shape = _api.deprecate_privatize_attribute('3.5')
32253259

3260+
def _init_shape(self, **props):
3261+
return Ellipse((0, 0), 0, 1, visible=False, **props)
3262+
32263263
def _draw_shape(self, extents):
32273264
x0, x1, y0, y1 = extents
32283265
xmin, xmax = sorted([x0, x1])
@@ -3235,6 +3272,7 @@ def _draw_shape(self, extents):
32353272
self._to_draw.center = center
32363273
self._to_draw.width = 2 * a
32373274
self._to_draw.height = 2 * b
3275+
self._to_draw.set_angle(self.rotation)
32383276
else:
32393277
rad = np.deg2rad(np.arange(31) * 12)
32403278
x = a * np.cos(rad) + center[0]
@@ -3412,7 +3450,8 @@ def __init__(self, ax, onselect, useblit=False,
34123450
move_all='shift', move='not-applicable',
34133451
square='not-applicable',
34143452
center='not-applicable',
3415-
data_coordinates='not-applicable')
3453+
data_coordinates='not-applicable',
3454+
rotate='not-applicable')
34163455
super().__init__(ax, onselect, useblit=useblit,
34173456
state_modifier_keys=state_modifier_keys)
34183457

0 commit comments

Comments
 (0)