1717
1818import matplotlib as mpl
1919from matplotlib import docstring
20- from . import _api , backend_tools , cbook , colors , ticker
20+ from . import _api , backend_tools , cbook , colors , ticker , transforms
2121from .lines import Line2D
2222from .patches import Circle , Rectangle , Ellipse
2323from .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