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

Skip to content

Commit 9350a02

Browse files
committed
Implement rotation selector
1 parent 403972b commit 9350a02

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
@@ -510,11 +510,10 @@ def onselect(epress, erelease):
510510
'markeredgecolor': 'b'})
511511
tool.extents = (100, 150, 100, 150)
512512

513-
assert tool.corners == (
514-
(100, 150, 150, 100), (100, 100, 150, 150))
513+
assert_allclose(tool.corners, ((100, 150, 150, 100), (100, 100, 150, 150)))
515514
assert tool.extents == (100, 150, 100, 150)
516-
assert tool.edge_centers == (
517-
(100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150))
515+
assert_allclose(tool.edge_centers,
516+
((100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150)))
518517
assert tool.extents == (100, 150, 100, 150)
519518

520519
# 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
@@ -1932,8 +1934,9 @@ def press(self, event):
19321934
key = event.key or ''
19331935
key = key.replace('ctrl', 'control')
19341936
# move state is locked in on a button press
1935-
if key == self._state_modifier_keys['move']:
1936-
self._state.add('move')
1937+
for action in ['move']:
1938+
if key == self._state_modifier_keys[action]:
1939+
self._state.add(action)
19371940
self._press(event)
19381941
return True
19391942
return False
@@ -1985,14 +1988,15 @@ def on_key_press(self, event):
19851988
artist.set_visible(False)
19861989
self.update()
19871990
return
1988-
if key == 'd' and key in self._state_modifier_keys.values():
1989-
modifier = 'data_coordinates'
1990-
if modifier in self._default_state:
1991-
self._default_state.remove(modifier)
1992-
else:
1993-
self.add_default_state(modifier)
1991+
for state in ['rotate', 'data_coordinates']:
1992+
if key == self._state_modifier_keys[state]:
1993+
if state in self._default_state:
1994+
self._default_state.remove(state)
1995+
else:
1996+
self.add_default_state(state)
19941997
for (state, modifier) in self._state_modifier_keys.items():
1995-
if modifier in key:
1998+
# Multiple keys are string concatenated using '+'
1999+
if modifier in key.split('+'):
19962000
self._state.add(state)
19972001
self._on_key_press(event)
19982002

@@ -2193,7 +2197,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
21932197
state_modifier_keys = dict(clear='escape',
21942198
square='not-applicable',
21952199
center='not-applicable',
2196-
data_coordinates='not-applicable')
2200+
data_coordinates='not-applicable',
2201+
rotate='not-applicable')
21972202
super().__init__(ax, onselect, useblit=useblit, button=button,
21982203
state_modifier_keys=state_modifier_keys)
21992204

@@ -2764,6 +2769,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
27642769
- "center": change the shape around its center, default: "ctrl".
27652770
- "data_coordinates": define if data or figure coordinates should be
27662771
used to define the square shape, default: "d"
2772+
- "rotate": Rotate the shape around its center, default: "r".
27672773
27682774
"square" and "center" can be combined. The square shape can be defined
27692775
in data or figure coordinates as determined by the ``data_coordinates``
@@ -2811,8 +2817,6 @@ class RectangleSelector(_SelectorWidget):
28112817
See also: :doc:`/gallery/widgets/rectangle_selector`
28122818
"""
28132819

2814-
_shape_klass = Rectangle
2815-
28162820
@_api.rename_parameter("3.5", "maxdist", "grab_range")
28172821
@_api.rename_parameter("3.5", "marker_props", "handle_props")
28182822
@_api.rename_parameter("3.5", "rectprops", "props")
@@ -2831,6 +2835,7 @@ def __init__(self, ax, onselect, drawtype='box',
28312835
self._interactive = interactive
28322836
self.drag_from_anywhere = drag_from_anywhere
28332837
self.ignore_event_outside = ignore_event_outside
2838+
self._rotation = 0
28342839

28352840
if drawtype == 'none': # draw a line but make it invisible
28362841
_api.warn_deprecated(
@@ -2848,8 +2853,7 @@ def __init__(self, ax, onselect, drawtype='box',
28482853
props['animated'] = self.useblit
28492854
self.visible = props.pop('visible', self.visible)
28502855
self._props = props
2851-
to_draw = self._shape_klass((0, 0), 0, 1, visible=False,
2852-
**self._props)
2856+
to_draw = self._init_shape(**self._props)
28532857
self.ax.add_patch(to_draw)
28542858
if drawtype == 'line':
28552859
_api.warn_deprecated(
@@ -2921,6 +2925,10 @@ def _handles_artists(self):
29212925
return (*self._center_handle.artists, *self._corner_handles.artists,
29222926
*self._edge_handles.artists)
29232927

2928+
def _init_shape(self, **props):
2929+
return Rectangle((0, 0), 0, 1, visible=False,
2930+
rotate_around_center=True, **props)
2931+
29242932
def _press(self, event):
29252933
"""Button press event handler."""
29262934
# make the drawn box/line visible get the click-coordinates,
@@ -3017,9 +3025,17 @@ def _onmove(self, event):
30173025
refx = event.xdata / (self._eventpress.xdata + 1e-6)
30183026
refy = event.ydata / (self._eventpress.ydata + 1e-6)
30193027

3028+
3029+
x0, x1, y0, y1 = self._extents_on_press
30203030
# resize an existing shape
3021-
if self._active_handle and self._active_handle != 'C':
3022-
x0, x1, y0, y1 = self._extents_on_press
3031+
if 'rotate' in state and self._active_handle in self._corner_order:
3032+
# calculate angle abc
3033+
a = np.array([self._eventpress.xdata, self._eventpress.ydata])
3034+
b = np.array(self.center)
3035+
c = np.array([event.xdata, event.ydata])
3036+
self._rotation = (np.arctan2(c[1]-b[1], c[0]-b[0]) -
3037+
np.arctan2(a[1]-b[1], a[0]-b[0]))
3038+
elif self._active_handle and self._active_handle != 'C':
30233039
sizepress = [x1 - x0, y1 - y0]
30243040
center = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2]
30253041

@@ -3084,6 +3100,7 @@ def _onmove(self, event):
30843100

30853101
# new shape
30863102
else:
3103+
self._rotation = 0
30873104
# Don't create a new rectangle if there is already one when
30883105
# ignore_event_outside=True
30893106
if self.ignore_event_outside and self._selection_completed:
@@ -3118,24 +3135,25 @@ def _onmove(self, event):
31183135
@property
31193136
def _rect_bbox(self):
31203137
if self._drawtype == 'box':
3121-
x0 = self._selection_artist.get_x()
3122-
y0 = self._selection_artist.get_y()
3123-
width = self._selection_artist.get_width()
3124-
height = self._selection_artist.get_height()
3125-
return x0, y0, width, height
3138+
return self._selection_artist.get_bbox().bounds
31263139
else:
31273140
x, y = self._selection_artist.get_data()
31283141
x0, x1 = min(x), max(x)
31293142
y0, y1 = min(y), max(y)
31303143
return x0, y0, x1 - x0, y1 - y0
31313144

3145+
def _get_rotation_transform(self):
3146+
return Affine2D().rotate_around(*self.center, self._rotation)
3147+
31323148
@property
31333149
def corners(self):
31343150
"""Corners of rectangle from lower left, moving clockwise."""
31353151
x0, y0, width, height = self._rect_bbox
31363152
xc = x0, x0 + width, x0 + width, x0
31373153
yc = y0, y0, y0 + height, y0 + height
3138-
return xc, yc
3154+
transform = self._get_rotation_transform()
3155+
coords = transform.transform(np.array([xc, yc]).T).T
3156+
return coords[0], coords[1]
31393157

31403158
@property
31413159
def edge_centers(self):
@@ -3145,7 +3163,9 @@ def edge_centers(self):
31453163
h = height / 2.
31463164
xe = x0, x0 + w, x0 + width, x0 + w
31473165
ye = y0 + h, y0, y0 + h, y0 + height
3148-
return xe, ye
3166+
transform = self._get_rotation_transform()
3167+
coords = transform.transform(np.array([xe, ye]).T).T
3168+
return coords[0], coords[1]
31493169

31503170
@property
31513171
def center(self):
@@ -3155,7 +3175,10 @@ def center(self):
31553175

31563176
@property
31573177
def extents(self):
3158-
"""Return (xmin, xmax, ymin, ymax)."""
3178+
"""
3179+
Return (xmin, xmax, ymin, ymax) as defined by the bounding box before
3180+
rotation.
3181+
"""
31593182
x0, y0, width, height = self._rect_bbox
31603183
xmin, xmax = sorted([x0, x0 + width])
31613184
ymin, ymax = sorted([y0, y0 + height])
@@ -3173,6 +3196,17 @@ def extents(self, extents):
31733196
self.set_visible(self.visible)
31743197
self.update()
31753198

3199+
@property
3200+
def rotation(self):
3201+
"""Rotation in degree."""
3202+
return np.rad2deg(self._rotation)
3203+
3204+
@rotation.setter
3205+
def rotation(self, value):
3206+
self._rotation = np.deg2rad(value)
3207+
# call extents setter to draw shape and update handles positions
3208+
self.extents = self.extents
3209+
31763210
draw_shape = _api.deprecate_privatize_attribute('3.5')
31773211

31783212
def _draw_shape(self, extents):
@@ -3192,6 +3226,7 @@ def _draw_shape(self, extents):
31923226
self._selection_artist.set_y(ymin)
31933227
self._selection_artist.set_width(xmax - xmin)
31943228
self._selection_artist.set_height(ymax - ymin)
3229+
self._selection_artist.set_angle(self.rotation)
31953230

31963231
elif self._drawtype == 'line':
31973232
self._selection_artist.set_data([xmin, xmax], [ymin, ymax])
@@ -3264,9 +3299,11 @@ class EllipseSelector(RectangleSelector):
32643299
:doc:`/gallery/widgets/rectangle_selector`
32653300
"""
32663301

3267-
_shape_klass = Ellipse
32683302
draw_shape = _api.deprecate_privatize_attribute('3.5')
32693303

3304+
def _init_shape(self, **props):
3305+
return Ellipse((0, 0), 0, 1, visible=False, **props)
3306+
32703307
def _draw_shape(self, extents):
32713308
x0, x1, y0, y1 = extents
32723309
xmin, xmax = sorted([x0, x1])
@@ -3279,6 +3316,7 @@ def _draw_shape(self, extents):
32793316
self._selection_artist.center = center
32803317
self._selection_artist.width = 2 * a
32813318
self._selection_artist.height = 2 * b
3319+
self._selection_artist.set_angle(self.rotation)
32823320
else:
32833321
rad = np.deg2rad(np.arange(31) * 12)
32843322
x = a * np.cos(rad) + center[0]
@@ -3457,7 +3495,8 @@ def __init__(self, ax, onselect, useblit=False,
34573495
move_all='shift', move='not-applicable',
34583496
square='not-applicable',
34593497
center='not-applicable',
3460-
data_coordinates='not-applicable')
3498+
data_coordinates='not-applicable',
3499+
rotate='not-applicable')
34613500
super().__init__(ax, onselect, useblit=useblit,
34623501
state_modifier_keys=state_modifier_keys)
34633502

0 commit comments

Comments
 (0)