diff --git a/doc/api/next_api_changes/deprecations/20113-EP.rst b/doc/api/next_api_changes/deprecations/20113-EP.rst new file mode 100644 index 000000000000..fe39447632c6 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20113-EP.rst @@ -0,0 +1,21 @@ +SpanSelector +~~~~~~~~~~~~ +``span_stays`` is deprecated, use ``interactive`` argument instead +Several `~matplotlib.widgets.SpanSelector` class internals have been privatized +and deprecated: +- ``pressv`` +- ``prev`` +- ``rect`` +- ``rectprops`` +- ``active_handle`` +- ``span_stays`` + + +Several `~matplotlib.widgets.RectangleSelector` and +`~matplotlib.widgets.EllipseSelector` class internals have been privatized and +deprecated: +- ``to_draw`` +- ``drawtype`` +- ``rectprops`` +- ``active_handle`` +- ``interactive`` diff --git a/doc/users/next_whats_new/widget_dragging.rst b/doc/users/next_whats_new/widget_dragging.rst index 3766c680341d..174a9ced77d4 100644 --- a/doc/users/next_whats_new/widget_dragging.rst +++ b/doc/users/next_whats_new/widget_dragging.rst @@ -1,9 +1,12 @@ Dragging selectors ------------------ -The `~matplotlib.widgets.RectangleSelector` and -`~matplotlib.widgets.EllipseSelector` have a new keyword argument, +The `~matplotlib.widgets.SpanSelector`, `~matplotlib.widgets.RectangleSelector` +and `~matplotlib.widgets.EllipseSelector` have a new keyword argument, *drag_from_anywhere*, which when set to `True` allows you to click and drag from anywhere inside the selector to move it. Previously it was only possible to move it by either activating the move modifier button, or clicking on the central handle. + +The size of the `~matplotlib.widgets.SpanSelector` can now be changed using +the edge handles. diff --git a/examples/widgets/span_selector.py b/examples/widgets/span_selector.py index 8392be667cfd..a9e1058ca232 100644 --- a/examples/widgets/span_selector.py +++ b/examples/widgets/span_selector.py @@ -16,7 +16,7 @@ fig, (ax1, ax2) = plt.subplots(2, figsize=(8, 6)) x = np.arange(0.0, 5.0, 0.01) -y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x)) +y = np.sin(2 * np.pi * x) + 0.5 * np.random.randn(len(x)) ax1.plot(x, y) ax1.set_ylim(-2, 2) @@ -37,7 +37,8 @@ def onselect(xmin, xmax): line2.set_data(region_x, region_y) ax2.set_xlim(region_x[0], region_x[-1]) ax2.set_ylim(region_y.min(), region_y.max()) - fig.canvas.draw() + fig.canvas.draw_idle() + ############################################################################# # .. note:: @@ -47,8 +48,15 @@ def onselect(xmin, xmax): # -span = SpanSelector(ax1, onselect, 'horizontal', useblit=True, - rectprops=dict(alpha=0.5, facecolor='tab:blue')) +span = SpanSelector( + ax1, + onselect, + "horizontal", + useblit=True, + rectprops=dict(alpha=0.5, facecolor="tab:blue"), + interactive=True, + drag_from_anywhere=True +) # Set useblit=True on most backends for enhanced performance. diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index f2ac7749d6ea..f12f6d72a817 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -195,11 +195,11 @@ def check_span(*args, **kwargs): def onselect(vmin, vmax): ax._got_onselect = True assert vmin == 100 - assert vmax == 150 + assert vmax == 199 def onmove(vmin, vmax): assert vmin == 100 - assert vmax == 125 + assert vmax == 199 ax._got_on_move = True if 'onmove_callback' in kwargs: @@ -207,8 +207,9 @@ def onmove(vmin, vmax): tool = widgets.SpanSelector(ax, onselect, *args, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) - do_event(tool, 'onmove', xdata=125, ydata=125, button=1) - do_event(tool, 'release', xdata=150, ydata=150, button=1) + # move outside of axis + do_event(tool, 'onmove', xdata=199, ydata=199, button=1) + do_event(tool, 'release', xdata=250, ydata=250, button=1) assert ax._got_onselect @@ -222,6 +223,85 @@ def test_span_selector(): check_span('horizontal', rectprops=dict(fill=True)) +@pytest.mark.parametrize('drag_from_anywhere', [True, False]) +def test_span_selector_drag(drag_from_anywhere): + ax = get_ax() + + def onselect(epress, erelease): + pass + + # Create span + tool = widgets.SpanSelector(ax, onselect, 'horizontal', interactive=True, + drag_from_anywhere=drag_from_anywhere) + do_event(tool, 'press', xdata=10, ydata=10, button=1) + do_event(tool, 'onmove', xdata=100, ydata=120, button=1) + do_event(tool, 'release', xdata=100, ydata=120, button=1) + assert tool.extents == (10, 100) + # Drag inside span + # + # If drag_from_anywhere == True, this will move the span by 10, + # giving new value extents = 20, 110 + # + # If drag_from_anywhere == False, this will create a new span with + # value extents = 25, 35 + do_event(tool, 'press', xdata=25, ydata=15, button=1) + do_event(tool, 'onmove', xdata=35, ydata=25, button=1) + do_event(tool, 'release', xdata=35, ydata=25, button=1) + if drag_from_anywhere: + assert tool.extents == (20, 110) + else: + assert tool.extents == (25, 35) + + # Check that in both cases, dragging outside the span draws a new span + do_event(tool, 'press', xdata=175, ydata=185, button=1) + do_event(tool, 'onmove', xdata=185, ydata=195, button=1) + do_event(tool, 'release', xdata=185, ydata=195, button=1) + assert tool.extents == (175, 185) + + +def test_span_selector_direction(): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.SpanSelector(ax, onselect, 'horizontal', interactive=True) + assert tool.direction == 'horizontal' + assert tool._edge_handles.direction == 'horizontal' + + with pytest.raises(ValueError): + tool = widgets.SpanSelector(ax, onselect, 'invalid_direction') + + tool.direction = 'vertical' + assert tool.direction == 'vertical' + assert tool._edge_handles.direction == 'vertical' + + with pytest.raises(ValueError): + tool.direction = 'invalid_string' + + +def test_tool_line_handle(): + ax = get_ax() + + positions = [20, 30, 50] + + tool_line_handle = widgets.ToolLineHandles(ax, positions, 'horizontal', + useblit=False) + + for artist in tool_line_handle.artists: + assert not artist.get_animated() + assert not artist.get_visible() + + tool_line_handle.set_visible(True) + tool_line_handle.set_animated(True) + + for artist in tool_line_handle.artists: + assert artist.get_animated() + assert artist.get_visible() + + assert tool_line_handle.positions == positions + + def check_lasso_selector(**kwargs): ax = get_ax() diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 7d2243f6c553..8dbe5d2f2707 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1777,7 +1777,7 @@ def ignore(self, event): def update(self): """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" - if not self.ax.get_visible(): + if not self.ax.get_visible() or self.ax.figure._cachedRenderer is None: return False if self.useblit: if self.background is not None: @@ -1923,8 +1923,9 @@ class SpanSelector(_SelectorWidget): direction : {"horizontal", "vertical"} The direction along which to draw the span selector. - minspan : float, default: None - If selection is less than *minspan*, do not call *onselect*. + minspan : float, default: 0 + If selection is less than or equal to *minspan*, do not call + *onselect*. useblit : bool, default: False If True, use the backend-dependent blitting features for faster @@ -1938,10 +1939,28 @@ class SpanSelector(_SelectorWidget): span_stays : bool, default: False If True, the span stays visible after the mouse is released. + Deprecated, use interactive instead. + + interactive : bool, default: False + Whether to draw a set of handles that allow interaction with the + widget after it is drawn. button : `.MouseButton` or list of `.MouseButton` The mouse buttons which activate the span selector. + line_props : dict, default: None + Line properties with which the interactive line are drawn. Only used + when *interactive* is True. See `matplotlib.lines.Line2D` for details + on valid properties. + + maxdist : float, default: 10 + Distance in pixels within which the interactive tool handles can be + activated. + + drag_from_anywhere : bool, optional + If `True`, the widget can be moved by clicking anywhere within + its bounds. + Examples -------- >>> import matplotlib.pyplot as plt @@ -1958,9 +1977,11 @@ class SpanSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/span_selector` """ - def __init__(self, ax, onselect, direction, minspan=None, useblit=False, - rectprops=None, onmove_callback=None, span_stays=False, - button=None): + @_api.rename_parameter("3.5", "span_stays", "interactive") + def __init__(self, ax, onselect, direction, minspan=0, useblit=False, + rectprops=None, onmove_callback=None, interactive=False, + button=None, line_props=None, maxdist=10, + drag_from_anywhere=False): super().__init__(ax, onselect, useblit=useblit, button=button) @@ -1969,24 +1990,56 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, rectprops['animated'] = self.useblit - _api.check_in_list(['horizontal', 'vertical'], direction=direction) self.direction = direction - self.rect = None - self.pressv = None + self._rect = None + self.visible = True + self._extents_on_press = None + + # self._pressv is deprecated and we don't use it internally anymore + # but we maintain it until it is removed + self._pressv = None - self.rectprops = rectprops + self._rectprops = rectprops self.onmove_callback = onmove_callback self.minspan = minspan - self.span_stays = span_stays - # Needed when dragging out of axes - self.prev = (0, 0) + self.maxdist = maxdist + self._interactive = interactive + self.drag_from_anywhere = drag_from_anywhere # Reset canvas so that `new_axes` connects events. self.canvas = None + self.artists = [] self.new_axes(ax) + # Setup handles + props = dict(color=rectprops.get('facecolor', 'r')) + props.update(cbook.normalize_kwargs(line_props, Line2D._alias_map)) + + if self._interactive: + self._edge_order = ['min', 'max'] + self._setup_edge_handle(props) + + self._active_handle = None + + # prev attribute is deprecated but we still need to maintain it + self._prev = (0, 0) + + rect = _api.deprecate_privatize_attribute("3.5") + + rectprops = _api.deprecate_privatize_attribute("3.5") + + active_handle = _api.deprecate_privatize_attribute("3.5") + + pressv = _api.deprecate_privatize_attribute("3.5") + + span_stays = _api.deprecated("3.5")( + property(lambda self: self._interactive) + ) + + prev = _api.deprecate_privatize_attribute("3.5") + def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" self.ax = ax @@ -2003,117 +2056,300 @@ def new_axes(self, ax): else: trans = ax.get_yaxis_transform() w, h = 1, 0 - self.rect = Rectangle((0, 0), w, h, - transform=trans, - visible=False, - **self.rectprops) - if self.span_stays: - self.stay_rect = Rectangle((0, 0), w, h, - transform=trans, - visible=False, - **self.rectprops) - self.stay_rect.set_animated(False) - self.ax.add_patch(self.stay_rect) - - self.ax.add_patch(self.rect) - self.artists = [self.rect] + self._rect = Rectangle((0, 0), w, h, + transform=trans, + visible=False, + **self._rectprops) + + self.ax.add_patch(self._rect) + if len(self.artists) > 0: + self.artists[0] = self._rect + else: + self.artists.append(self._rect) - def ignore(self, event): - # docstring inherited - return super().ignore(event) or not self.visible + def _setup_edge_handle(self, props): + self._edge_handles = ToolLineHandles(self.ax, self.extents, + direction=self.direction, + line_props=props, + useblit=self.useblit) + self.artists.extend([line for line in self._edge_handles.artists]) def _press(self, event): """Button press event handler.""" - self.rect.set_visible(self.visible) - if self.span_stays: - self.stay_rect.set_visible(False) - # really force a draw so that the stay rect is not in - # the blit background - if self.useblit: - self.canvas.draw() - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - self.pressv = xdata + if self._interactive and self._rect.get_visible(): + self._set_active_handle(event) else: - self.pressv = ydata + self._active_handle = None - self._set_span_xy(event) - return False + if self._active_handle is None or not self._interactive: + # Clear previous rectangle before drawing new rectangle. + self.update() - def _release(self, event): - """Button release event handler.""" - if self.pressv is None: - return + v = event.xdata if self.direction == 'horizontal' else event.ydata + # self._pressv and self._prev are deprecated but we still need to + # maintain them + self._pressv = v + self._prev = self._get_data(event) - self.rect.set_visible(False) + if self._active_handle is None: + # when the press event outside the span, we initially set the + # visibility to False and extents to (v, v) + # update will be called when setting the extents + self.visible = False + self.extents = v, v + # We need to set the visibility back, so the span selector will be + # drawn when necessary (span width > 0) + self.visible = True + else: + self.set_visible(True) - if self.span_stays: - self.stay_rect.set_x(self.rect.get_x()) - self.stay_rect.set_y(self.rect.get_y()) - self.stay_rect.set_width(self.rect.get_width()) - self.stay_rect.set_height(self.rect.get_height()) - self.stay_rect.set_visible(True) + return False - self.canvas.draw_idle() - vmin = self.pressv - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - vmax = xdata or self.prev[0] + @property + def direction(self): + """Direction of the span selector: 'vertical' or 'horizontal'.""" + return self._direction + + @direction.setter + def direction(self, direction): + """Set the direction of the span selector.""" + _api.check_in_list(['horizontal', 'vertical'], direction=direction) + if hasattr(self, '_direction') and direction != self._direction: + # remove previous artists + self._rect.remove() + if self._interactive: + self._edge_handles.remove() + for artist in self._edge_handles.artists: + self.artists.remove(artist) + self._direction = direction + self.new_axes(self.ax) + if self._interactive: + self._setup_edge_handle(self._edge_handles._line_props) else: - vmax = ydata or self.prev[1] + self._direction = direction - if vmin > vmax: - vmin, vmax = vmax, vmin + def _release(self, event): + """Button release event handler.""" + if not self._interactive: + self._rect.set_visible(False) + + vmin, vmax = self.extents span = vmax - vmin - if self.minspan is not None and span < self.minspan: + if span <= self.minspan: + self.set_visible(False) + self.update() return + self.onselect(vmin, vmax) - self.pressv = None + self.update() + + # self._pressv is deprecated but we still need to maintain it + self._pressv = None + return False def _onmove(self, event): """Motion notify event handler.""" - if self.pressv is None: - return - self._set_span_xy(event) + # self._prev are deprecated but we still need to maintain it + self._prev = self._get_data(event) - if self.onmove_callback is not None: - vmin = self.pressv - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - vmax = xdata or self.prev[0] - else: - vmax = ydata or self.prev[1] + v = event.xdata if self.direction == 'horizontal' else event.ydata + if self.direction == 'horizontal': + vpress = self.eventpress.xdata + else: + vpress = self.eventpress.ydata + # move existing span + # When "dragging from anywhere", `self._active_handle` is set to 'C' + # (match notation used in the RectangleSelector) + if self._active_handle == 'C' and self._extents_on_press is not None: + vmin, vmax = self._extents_on_press + dv = v - vpress + vmin += dv + vmax += dv + + # resize an existing shape + elif self._active_handle and self._active_handle != 'C': + vmin, vmax = self._extents_on_press + if self._active_handle == 'min': + vmin = v + else: + vmax = v + # new shape + else: + vmin, vmax = vpress, v if vmin > vmax: vmin, vmax = vmax, vmin + + self.extents = vmin, vmax + + if self.onmove_callback is not None: self.onmove_callback(vmin, vmax) - self.update() return False - def _set_span_xy(self, event): - """Set the span coordinates.""" - x, y = self._get_data(event) - if x is None: - return + def _draw_shape(self, vmin, vmax): + if vmin > vmax: + vmin, vmax = vmax, vmin + if self.direction == 'horizontal': + self._rect.set_x(vmin) + self._rect.set_width(vmax - vmin) + else: + self._rect.set_y(vmin) + self._rect.set_height(vmax - vmin) + + def _set_active_handle(self, event): + """Set active handle based on the location of the mouse event.""" + # Note: event.xdata/ydata in data coordinates, event.x/y in pixels + e_idx, e_dist = self._edge_handles.closest(event.x, event.y) - self.prev = x, y + # Prioritise center handle over other handles + # Use 'C' to match the notation used in the RectangleSelector + if 'move' in self.state: + self._active_handle = 'C' + elif e_dist > self.maxdist: + # Not close to any handles + self._active_handle = None + if self.drag_from_anywhere and self._contains(event): + # Check if we've clicked inside the region + self._active_handle = 'C' + self._extents_on_press = self.extents + else: + self._active_handle = None + return + else: + # Closest to an edge handle + self._active_handle = self._edge_order[e_idx] + + # Save coordinates of rectangle at the start of handle movement. + self._extents_on_press = self.extents + + def _contains(self, event): + """Return True if event is within the patch.""" + return self._rect.contains(event, radius=0)[0] + + @property + def extents(self): + """Return extents of the span selector.""" if self.direction == 'horizontal': - v = x + vmin = self._rect.get_x() + vmax = vmin + self._rect.get_width() else: - v = y + vmin = self._rect.get_y() + vmax = vmin + self._rect.get_height() + return vmin, vmax + + @extents.setter + def extents(self, extents): + # Update displayed shape + self._draw_shape(*extents) + if self._interactive: + # Update displayed handles + self._edge_handles.set_data(self.extents) + self.set_visible(self.visible) + self.update() + + +class ToolLineHandles: + """ + Control handles for canvas tools. + + Parameters + ---------- + ax : `matplotlib.axes.Axes` + Matplotlib axes where tool handles are displayed. + positions : 1D array + Positions of handles in data coordinates. + direction : {"horizontal", "vertical"} + Direction of handles, either 'vertical' or 'horizontal' + line_props : dict + Additional line properties. See `matplotlib.lines.Line2D`. + """ + + def __init__(self, ax, positions, direction, line_props=None, + useblit=True): + self.ax = ax + + _api.check_in_list(['horizontal', 'vertical'], direction=direction) + self._direction = direction - minv, maxv = v, self.pressv - if minv > maxv: - minv, maxv = maxv, minv + if line_props is None: + line_props = {} + line_props.update({'visible': False, 'animated': useblit}) + + line_fun = ax.axvline if self.direction == 'horizontal' else ax.axhline + self._line_props = line_props + + self.artists = [line_fun(p, **line_props) for p in positions] + + @property + def positions(self): + """Positions of the handle in data coordinates.""" + method = 'get_xdata' if self.direction == 'horizontal' else 'get_ydata' + return [getattr(line, method)()[0] for line in self.artists] + + @property + def direction(self): + """Direction of the handle: 'vertical' or 'horizontal'.""" + return self._direction + + def set_data(self, positions): + """ + Set x or y positions of handles, depending if the lines are vertical + of horizontal. + + Parameters + ---------- + positions : tuple of length 2 + Set the positions of the handle in data coordinates + """ + method = 'set_xdata' if self.direction == 'horizontal' else 'set_ydata' + for line, p in zip(self.artists, positions): + getattr(line, method)([p, p]) + + def set_visible(self, value): + """Set the visibility state of the handles artist.""" + for artist in self.artists: + artist.set_visible(value) + + def set_animated(self, value): + """Set the animated state of the handles artist.""" + for artist in self.artists: + artist.set_animated(value) + + def remove(self): + """Remove the handles artist from the figure.""" + for artist in self.artists: + artist.remove() + + def closest(self, x, y): + """ + Return index and pixel distance to closest handle. + + Parameters + ---------- + x, y : float + x, y position from which the distance will be calculated to + determinate the closest handle + + Returns + ------- + index, distance : index of the handle and its distance from + position x, y + """ if self.direction == 'horizontal': - self.rect.set_x(minv) - self.rect.set_width(maxv - minv) + p_pts = np.array([ + self.ax.transData.transform((p, 0))[0] for p in self.positions + ]) + dist = abs(p_pts - x) else: - self.rect.set_y(minv) - self.rect.set_height(maxv - minv) + p_pts = np.array([ + self.ax.transData.transform((0, p))[1] for p in self.positions + ]) + dist = abs(p_pts - y) + index = np.argmin(dist) + return index, dist[index] class ToolHandles: @@ -2274,9 +2510,9 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) super().__init__(ax, onselect, useblit=useblit, button=button, state_modifier_keys=state_modifier_keys) - self.to_draw = None + self._to_draw = None self.visible = True - self.interactive = interactive + self._interactive = interactive self.drag_from_anywhere = drag_from_anywhere if drawtype == 'none': # draw a line but make it invisible @@ -2293,11 +2529,11 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) rectprops = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) rectprops['animated'] = self.useblit - self.rectprops = rectprops - self.visible = self.rectprops.pop('visible', self.visible) - self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False, - **self.rectprops) - self.ax.add_patch(self.to_draw) + _rectprops = rectprops + self.visible = _rectprops.pop('visible', self.visible) + self._to_draw = self._shape_klass((0, 0), 0, 1, visible=False, + **_rectprops) + self.ax.add_patch(self._to_draw) if drawtype == 'line': _api.warn_deprecated( "3.5", message="Support for drawtype='line' is deprecated " @@ -2308,16 +2544,16 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) linewidth=2, alpha=0.5) lineprops['animated'] = self.useblit self.lineprops = lineprops - self.to_draw = Line2D([0, 0], [0, 0], visible=False, - **self.lineprops) - self.ax.add_line(self.to_draw) + self._to_draw = Line2D([0, 0], [0, 0], visible=False, + **self.lineprops) + self.ax.add_line(self._to_draw) self.minspanx = minspanx self.minspany = minspany _api.check_in_list(['data', 'pixels'], spancoords=spancoords) self.spancoords = spancoords - self.drawtype = drawtype + self._drawtype = drawtype self.maxdist = maxdist @@ -2342,41 +2578,53 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) marker_props=props, useblit=self.useblit) - self.active_handle = None + self._active_handle = None - self.artists = [self.to_draw, self._center_handle.artist, + self.artists = [self._to_draw, self._center_handle.artist, self._corner_handles.artist, self._edge_handles.artist] - if not self.interactive: - self.artists = [self.to_draw] + if not self._interactive: + self.artists = [self._to_draw] self._extents_on_press = None + to_draw = _api.deprecate_privatize_attribute("3.5") + + drawtype = _api.deprecate_privatize_attribute("3.5") + + active_handle = _api.deprecate_privatize_attribute("3.5") + + interactive = _api.deprecate_privatize_attribute("3.5") + def _press(self, event): """Button press event handler.""" # make the drawn box/line visible get the click-coordinates, # button, ... - if self.interactive and self.to_draw.get_visible(): + if self._interactive and self._to_draw.get_visible(): self._set_active_handle(event) else: - self.active_handle = None + self._active_handle = None - if self.active_handle is None or not self.interactive: + if self._active_handle is None or not self._interactive: # Clear previous rectangle before drawing new rectangle. self.update() - if not self.interactive: + if self._active_handle is None: x = event.xdata y = event.ydata + self.visible = False self.extents = x, x, y, y + self.visible = True + else: + self.set_visible(True) - self.set_visible(self.visible) + return False def _release(self, event): """Button release event handler.""" - if not self.interactive: - self.to_draw.set_visible(False) + if not self._interactive: + self._to_draw.set_visible(False) # update the eventpress and eventrelease with the resulting extents x0, x1, y0, y1 = self.extents @@ -2402,7 +2650,7 @@ def _release(self, event): spancoords=self.spancoords) # check if drawn distance (if it exists) is not too small in # either x or y-direction - if (self.drawtype != 'none' + if (self._drawtype != 'none' and (self.minspanx is not None and spanx < self.minspanx or self.minspany is not None and spany < self.minspany)): for artist in self.artists: @@ -2419,15 +2667,15 @@ def _release(self, event): def _onmove(self, event): """Motion notify event handler.""" # resize an existing shape - if self.active_handle and self.active_handle != 'C': + if self._active_handle and self._active_handle != 'C': x0, x1, y0, y1 = self._extents_on_press - if self.active_handle in ['E', 'W'] + self._corner_order: + if self._active_handle in ['E', 'W'] + self._corner_order: x1 = event.xdata - if self.active_handle in ['N', 'S'] + self._corner_order: + if self._active_handle in ['N', 'S'] + self._corner_order: y1 = event.ydata # move existing shape - elif (('move' in self.state or self.active_handle == 'C' or + elif (('move' in self.state or self._active_handle == 'C' or (self.drag_from_anywhere and self._contains(event))) and self._extents_on_press is not None): x0, x1, y0, y1 = self._extents_on_press @@ -2474,14 +2722,14 @@ def _onmove(self, event): @property def _rect_bbox(self): - if self.drawtype == 'box': - x0 = self.to_draw.get_x() - y0 = self.to_draw.get_y() - width = self.to_draw.get_width() - height = self.to_draw.get_height() + if self._drawtype == 'box': + x0 = self._to_draw.get_x() + y0 = self._to_draw.get_y() + width = self._to_draw.get_width() + height = self._to_draw.get_height() return x0, y0, width, height else: - x, y = self.to_draw.get_data() + x, y = self._to_draw.get_data() x0, x1 = min(x), max(x) y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0 @@ -2521,7 +2769,7 @@ def extents(self): @extents.setter def extents(self, extents): # Update displayed shape - self.draw_shape(extents) + self._draw_shape(extents) # Update displayed handles self._corner_handles.set_data(*self.corners) self._edge_handles.set_data(*self.edge_centers) @@ -2529,7 +2777,9 @@ def extents(self, extents): self.set_visible(self.visible) self.update() - def draw_shape(self, extents): + draw_shape = _api.deprecate_privatize_attribute('3.5') + + def _draw_shape(self, extents): x0, x1, y0, y1 = extents xmin, xmax = sorted([x0, x1]) ymin, ymax = sorted([y0, y1]) @@ -2541,14 +2791,14 @@ def draw_shape(self, extents): xmax = min(xmax, xlim[1]) ymax = min(ymax, ylim[1]) - if self.drawtype == 'box': - self.to_draw.set_x(xmin) - self.to_draw.set_y(ymin) - self.to_draw.set_width(xmax - xmin) - self.to_draw.set_height(ymax - ymin) + if self._drawtype == 'box': + self._to_draw.set_x(xmin) + self._to_draw.set_y(ymin) + self._to_draw.set_width(xmax - xmin) + self._to_draw.set_height(ymax - ymin) - elif self.drawtype == 'line': - self.to_draw.set_data([xmin, xmax], [ymin, ymax]) + elif self._drawtype == 'line': + self._to_draw.set_data([xmin, xmax], [ymin, ymax]) def _set_active_handle(self, event): """Set active handle based on the location of the mouse event.""" @@ -2558,40 +2808,40 @@ def _set_active_handle(self, event): m_idx, m_dist = self._center_handle.closest(event.x, event.y) if 'move' in self.state: - self.active_handle = 'C' + self._active_handle = 'C' self._extents_on_press = self.extents # Set active handle as closest handle, if mouse click is close enough. elif m_dist < self.maxdist * 2: # Prioritise center handle over other handles - self.active_handle = 'C' + self._active_handle = 'C' elif c_dist > self.maxdist and e_dist > self.maxdist: # Not close to any handles if self.drag_from_anywhere and self._contains(event): # Check if we've clicked inside the region - self.active_handle = 'C' + self._active_handle = 'C' self._extents_on_press = self.extents else: - self.active_handle = None + self._active_handle = None return elif c_dist < e_dist: # Closest to a corner handle - self.active_handle = self._corner_order[c_idx] + self._active_handle = self._corner_order[c_idx] else: # Closest to an edge handle - self.active_handle = self._edge_order[e_idx] + self._active_handle = self._edge_order[e_idx] # Save coordinates of rectangle at the start of handle movement. x0, x1, y0, y1 = self.extents # Switch variables so that only x1 and/or y1 are updated on move. - if self.active_handle in ['W', 'SW', 'NW']: + if self._active_handle in ['W', 'SW', 'NW']: x0, x1 = x1, event.xdata - if self.active_handle in ['N', 'NW', 'NE']: + if self._active_handle in ['N', 'NW', 'NE']: y0, y1 = y1, event.ydata self._extents_on_press = x0, x1, y0, y1 def _contains(self, event): """Return True if event is within the patch.""" - return self.to_draw.contains(event, radius=0)[0] + return self._to_draw.contains(event, radius=0)[0] @property def geometry(self): @@ -2602,12 +2852,12 @@ def geometry(self): of the four corners of the rectangle starting and ending in the top left corner. """ - if hasattr(self.to_draw, 'get_verts'): + if hasattr(self._to_draw, 'get_verts'): xfm = self.ax.transData.inverted() - y, x = xfm.transform(self.to_draw.get_verts()).T + y, x = xfm.transform(self._to_draw.get_verts()).T return np.array([x, y]) else: - return np.array(self.to_draw.get_data()) + return np.array(self._to_draw.get_data()) class EllipseSelector(RectangleSelector): @@ -2647,8 +2897,9 @@ def toggle_selector(event): plt.show() """ _shape_klass = Ellipse + draw_shape = _api.deprecate_privatize_attribute('3.5') - def draw_shape(self, extents): + def _draw_shape(self, extents): x0, x1, y0, y1 = extents xmin, xmax = sorted([x0, x1]) ymin, ymax = sorted([y0, y1]) @@ -2656,25 +2907,25 @@ def draw_shape(self, extents): a = (xmax - xmin) / 2. b = (ymax - ymin) / 2. - if self.drawtype == 'box': - self.to_draw.center = center - self.to_draw.width = 2 * a - self.to_draw.height = 2 * b + if self._drawtype == 'box': + self._to_draw.center = center + self._to_draw.width = 2 * a + self._to_draw.height = 2 * b else: rad = np.deg2rad(np.arange(31) * 12) x = a * np.cos(rad) + center[0] y = b * np.sin(rad) + center[1] - self.to_draw.set_data(x, y) + self._to_draw.set_data(x, y) @property def _rect_bbox(self): - if self.drawtype == 'box': - x, y = self.to_draw.center - width = self.to_draw.width - height = self.to_draw.height + if self._drawtype == 'box': + x, y = self._to_draw.center + width = self._to_draw.width + height = self._to_draw.height return x - width / 2., y - height / 2., width, height else: - x, y = self.to_draw.get_data() + x, y = self._to_draw.get_data() x0, x1 = min(x), max(x) y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0