Skip to content

Commit 707844b

Browse files
committed
Add option of bounding box for PolygonSelector
Update polygon every time bounding box is moved Allow box customisation Remove del Prevent rotation on polygon bounding box Fix line length Cleanup transform logic Don't use dicts as keyword defaults Minor doc fixes
1 parent a35921c commit 707844b

File tree

4 files changed

+115
-7
lines changed

4 files changed

+115
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
PolygonSelector bounding boxes
2+
------------------------------
3+
`~matplotlib.widgets.PolygonSelector` now has a *draw_box* argument, which
4+
when set to `True` will draw a bounding box around the polygon once it is
5+
complete. The bounding box can be resized and moved, allowing the points of
6+
the polygon to be easily resized.

examples/widgets/polygon_selector_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def __init__(self, ax, collection, alpha_other=0.3):
5050
elif len(self.fc) == 1:
5151
self.fc = np.tile(self.fc, (self.Npts, 1))
5252

53-
self.poly = PolygonSelector(ax, self.onselect)
53+
self.poly = PolygonSelector(ax, self.onselect, draw_box=True)
5454
self.ind = []
5555

5656
def onselect(self, verts):

lib/matplotlib/lines.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,12 @@ def set_picker(self, p):
617617
self.pickradius = p
618618
self._picker = p
619619

620+
def get_bbox(self):
621+
"""Get the bounding box of this line."""
622+
bbox = Bbox([[0, 0], [0, 0]])
623+
bbox.update_from_data_xy(self.get_xydata())
624+
return bbox
625+
620626
def get_window_extent(self, renderer):
621627
bbox = Bbox([[0, 0], [0, 0]])
622628
trans_data_to_xy = self.get_transform().transform

lib/matplotlib/widgets.py

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import matplotlib as mpl
1919
from matplotlib import docstring
20-
from . import _api, backend_tools, cbook, colors, ticker
20+
from . import _api, backend_tools, cbook, colors, ticker, transforms
2121
from .lines import Line2D
2222
from .patches import Circle, Rectangle, Ellipse
2323
from .transforms import TransformedPatchPath, Affine2D
@@ -2877,6 +2877,11 @@ def __init__(self, ax, onselect, drawtype='box',
28772877
self._rotation = 0.0
28782878
self._aspect_ratio_correction = 1.0
28792879

2880+
# State to allow the option of an interactive selector that can't be
2881+
# interactively drawn. This is used in PolygonSelector as an
2882+
# interactive bounding box to allow the polygon to be easily resized
2883+
self._allow_creation = True
2884+
28802885
if drawtype == 'none': # draw a line but make it invisible
28812886
_api.warn_deprecated(
28822887
"3.5", message="Support for drawtype='none' is deprecated "
@@ -2979,12 +2984,13 @@ def _press(self, event):
29792984
else:
29802985
self._active_handle = None
29812986

2982-
if self._active_handle is None or not self._interactive:
2987+
if ((self._active_handle is None or not self._interactive) and
2988+
self._allow_creation):
29832989
# Clear previous rectangle before drawing new rectangle.
29842990
self.update()
29852991

2986-
if self._active_handle is None and not self.ignore_event_outside:
2987-
# Start drawing a new rectangle
2992+
if (self._active_handle is None and not self.ignore_event_outside and
2993+
self._allow_creation):
29882994
x = event.xdata
29892995
y = event.ydata
29902996
self.visible = False
@@ -3170,7 +3176,8 @@ def _onmove(self, event):
31703176
self._rotation = 0
31713177
# Don't create a new rectangle if there is already one when
31723178
# ignore_event_outside=True
3173-
if self.ignore_event_outside and self._selection_completed:
3179+
if ((self.ignore_event_outside and self._selection_completed) or
3180+
not self._allow_creation):
31743181
return
31753182
center = [eventpress.xdata, eventpress.ydata]
31763183
dx = (event.xdata - center[0]) / 2.
@@ -3569,6 +3576,19 @@ class PolygonSelector(_SelectorWidget):
35693576
A vertex is selected (to complete the polygon or to move a vertex) if
35703577
the mouse click is within *grab_range* pixels of the vertex.
35713578
3579+
draw_box : bool, optional
3580+
If `True`, a bounding box will be drawn around the polygon selector
3581+
once it is complete. This box can be used to move and resize the
3582+
selector.
3583+
3584+
box_handle_props : dict, optional
3585+
Properties to set for the box handles. See the documentation for the
3586+
*handle_props* argument to `RectangleSelector` for more info.
3587+
3588+
box_props : dict, optional
3589+
Properties to set for the box. See the documentation for the *props*
3590+
argument to `RectangleSelector` for more info.
3591+
35723592
Examples
35733593
--------
35743594
:doc:`/gallery/widgets/polygon_selector_demo`
@@ -3584,7 +3604,9 @@ class PolygonSelector(_SelectorWidget):
35843604
@_api.rename_parameter("3.5", "markerprops", "handle_props")
35853605
@_api.rename_parameter("3.5", "vertex_select_radius", "grab_range")
35863606
def __init__(self, ax, onselect, useblit=False,
3587-
props=None, handle_props=None, grab_range=10):
3607+
props=None, handle_props=None, grab_range=10, *,
3608+
draw_box=False, box_handle_props=None,
3609+
box_props=None):
35883610
# The state modifiers 'move', 'square', and 'center' are expected by
35893611
# _SelectorWidget but are not supported by PolygonSelector
35903612
# Note: could not use the existing 'move' state modifier in-place of
@@ -3620,6 +3642,75 @@ def __init__(self, ax, onselect, useblit=False,
36203642
self.grab_range = grab_range
36213643

36223644
self.set_visible(True)
3645+
self._draw_box = draw_box
3646+
self._box = None
3647+
3648+
if box_handle_props is None:
3649+
box_handle_props = {}
3650+
self._box_handle_props = self._handle_props.update(box_handle_props)
3651+
self._box_props = box_props
3652+
3653+
def _get_bbox(self):
3654+
return self._selection_artist.get_bbox()
3655+
3656+
def _add_box(self):
3657+
self._box = RectangleSelector(self.ax,
3658+
onselect=lambda *args, **kwargs: None,
3659+
useblit=self.useblit,
3660+
grab_range=self.grab_range,
3661+
handle_props=self._box_handle_props,
3662+
props=self._box_props,
3663+
interactive=True)
3664+
self._box._state_modifier_keys.pop('rotate')
3665+
self._box.connect_event('motion_notify_event', self._scale_polygon)
3666+
self._update_box()
3667+
# Set state that prevents the RectangleSelector from being created
3668+
# by the user
3669+
self._box._allow_creation = False
3670+
self._box._selection_completed = True
3671+
self._draw_polygon()
3672+
3673+
def _remove_box(self):
3674+
if self._box is not None:
3675+
self._box.set_visible(False)
3676+
self._box = None
3677+
3678+
def _update_box(self):
3679+
# Update selection box extents to the extents of the polygon
3680+
if self._box is not None:
3681+
bbox = self._get_bbox()
3682+
self._box.extents = [bbox.x0, bbox.x1, bbox.y0, bbox.y1]
3683+
# Save a copy
3684+
self._old_box_extents = self._box.extents
3685+
3686+
def _scale_polygon(self, event):
3687+
"""
3688+
Scale the polygon selector points when the bounding box is moved or
3689+
scaled.
3690+
3691+
This is set as a callback on the bounding box RectangleSelector.
3692+
"""
3693+
if not self._selection_completed:
3694+
return
3695+
3696+
if self._old_box_extents == self._box.extents:
3697+
return
3698+
3699+
# Create transform from old box to new box
3700+
x1, y1, w1, h1 = self._box._rect_bbox
3701+
old_bbox = self._get_bbox()
3702+
t = (transforms.Affine2D()
3703+
.translate(-old_bbox.x0, -old_bbox.y0)
3704+
.scale(1 / old_bbox.width, 1 / old_bbox.height)
3705+
.scale(w1, h1)
3706+
.translate(x1, y1))
3707+
3708+
# Update polygon verts
3709+
new_verts = t.transform(np.array(self.verts))
3710+
self._xs = list(np.append(new_verts[:, 0], new_verts[0, 0]))
3711+
self._ys = list(np.append(new_verts[:, 1], new_verts[0, 1]))
3712+
self._draw_polygon()
3713+
self._old_box_extents = self._box.extents
36233714

36243715
line = _api.deprecated("3.5")(
36253716
property(lambda self: self._selection_artist)
@@ -3661,6 +3752,7 @@ def _remove_vertex(self, i):
36613752
# If only one point left, return to incomplete state to let user
36623753
# start drawing again
36633754
self._selection_completed = False
3755+
self._remove_box()
36643756

36653757
def _press(self, event):
36663758
"""Button press event handler."""
@@ -3688,6 +3780,8 @@ def _release(self, event):
36883780
and self._xs[-1] == self._xs[0]
36893781
and self._ys[-1] == self._ys[0]):
36903782
self._selection_completed = True
3783+
if self._draw_box and self._box is None:
3784+
self._add_box()
36913785

36923786
# Place new vertex.
36933787
elif (not self._selection_completed
@@ -3776,11 +3870,13 @@ def _on_key_release(self, event):
37763870
event = self._clean_event(event)
37773871
self._xs, self._ys = [event.xdata], [event.ydata]
37783872
self._selection_completed = False
3873+
self._remove_box()
37793874
self.set_visible(True)
37803875

37813876
def _draw_polygon(self):
37823877
"""Redraw the polygon based on the new vertex positions."""
37833878
self._selection_artist.set_data(self._xs, self._ys)
3879+
self._update_box()
37843880
# Only show one tool handle at the start and end vertex of the polygon
37853881
# if the polygon is completed or the user is locked on to the start
37863882
# vertex.

0 commit comments

Comments
 (0)