From afa875f2be2c7e9ae3e6cbf529f0dd417b960a27 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sun, 25 Apr 2021 18:21:05 +0100 Subject: [PATCH 01/35] Add interactive option to SpanSelector to change it interactively after release. Deprecate span_stays --- lib/matplotlib/widgets.py | 207 +++++++++++++++++++++++++++++++------- 1 file changed, 173 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 7d2243f6c553..442e3633d9dd 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1933,12 +1933,23 @@ class SpanSelector(_SelectorWidget): rectprops : dict, default: None Dictionary of `matplotlib.patches.Patch` properties. + maxdist : float, default: 10 + Distance in pixels within which the interactive tool handles can be + activated. + + marker_props : dict + Properties with which the interactive handles are drawn. + onmove_callback : func(min, max), min/max are floats, default: None Called on mouse move while the span is being selected. span_stays : bool, default: False If True, the span stays visible after the mouse is released. + 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. @@ -1959,7 +1970,8 @@ class SpanSelector(_SelectorWidget): """ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, - rectprops=None, onmove_callback=None, span_stays=False, + rectprops=None, maxdist=10, marker_props=None, + onmove_callback=None, span_stays=False, interactive=False, button=None): super().__init__(ax, onselect, useblit=useblit, button=button) @@ -1973,12 +1985,19 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.direction = direction self.rect = None + self.visible = True self.pressv = None self.rectprops = rectprops self.onmove_callback = onmove_callback self.minspan = minspan - self.span_stays = span_stays + + self.maxdist = maxdist + # Deprecate `span_stays` in favour of interactive to be consistent + # with Rectangle, etc. + if span_stays: + interactive = True + self.interactive = interactive # Needed when dragging out of axes self.prev = (0, 0) @@ -1987,6 +2006,30 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.canvas = None self.new_axes(ax) + # Setup handles + if rectprops is None: + props = dict(markeredgecolor='r') + else: + props = dict(markeredgecolor=rectprops.get('edgecolor', 'r')) + props.update(cbook.normalize_kwargs(marker_props, Line2D._alias_map)) + + self._edge_order = ['min', 'max'] + xe, ye = self.edge_centers + self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', + marker_props=props, + useblit=self.useblit) + + xc, yc = self.center + self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', + marker_props=props, + useblit=self.useblit) + + self.active_handle = None + + if self.interactive: + self.artists.extend([self._center_handle.artist, + self._edge_handles.artist]) + def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" self.ax = ax @@ -2007,13 +2050,6 @@ def new_axes(self, ax): 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] @@ -2024,20 +2060,26 @@ def ignore(self, event): 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() + if self.interactive and self.rect.get_visible(): + self._set_active_handle(event) + else: + self.active_handle = None + xdata, ydata = self._get_data(event) if self.direction == 'horizontal': self.pressv = xdata else: self.pressv = ydata + self._extents_on_press = self.extents self._set_span_xy(event) + + if self.active_handle is None or not self.interactive: + # Clear previous rectangle before drawing new rectangle. + self.update() + + self.set_visible(self.visible) + return False def _release(self, event): @@ -2045,16 +2087,9 @@ def _release(self, event): if self.pressv is None: return - self.rect.set_visible(False) - - 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) + if not self.interactive: + self.rect.set_visible(False) - self.canvas.draw_idle() vmin = self.pressv xdata, ydata = self._get_data(event) if self.direction == 'horizontal': @@ -2068,6 +2103,8 @@ def _release(self, event): if self.minspan is not None and span < self.minspan: return self.onselect(vmin, vmax) + self.update() + self.pressv = None return False @@ -2076,9 +2113,29 @@ def _onmove(self, event): if self.pressv is None: return - self._set_span_xy(event) + vmin, vmax = self._extents_on_press + # move existing span + if self.active_handle == 'C' and self._extents_on_press is not None: + if self.direction == 'horizontal': + dv = event.xdata - self.eventpress.xdata + else: + dv = event.ydata - self.eventpress.ydata + vmin += dv + vmax += dv + + # resize an existing shape + elif self.active_handle and self.active_handle != 'C': + if self.direction == 'horizontal': + v = event.xdata + else: + v = event.ydata + if self.active_handle == 'min': + vmin = v + else: + vmax = v + else: + self._set_span_xy(event) - if self.onmove_callback is not None: vmin = self.pressv xdata, ydata = self._get_data(event) if self.direction == 'horizontal': @@ -2088,9 +2145,13 @@ def _onmove(self, event): if vmin > vmax: vmin, vmax = vmax, vmin + + if self.onmove_callback is not None: self.onmove_callback(vmin, vmax) + self.extents = vmin, vmax self.update() + return False def _set_span_xy(self, event): @@ -2105,15 +2166,93 @@ def _set_span_xy(self, event): else: v = y - minv, maxv = v, self.pressv - if minv > maxv: - minv, maxv = maxv, minv + values = v, self.pressv + self.draw_shape(*values) + + def draw_shape(self, vmin, vmax): + if vmin > vmax: + vmin, vmax = vmax, vmin if self.direction == 'horizontal': - self.rect.set_x(minv) - self.rect.set_width(maxv - minv) + self.rect.set_x(vmin) + self.rect.set_width(vmax - vmin) else: - self.rect.set_y(minv) - self.rect.set_height(maxv - minv) + 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) + m_idx, m_dist = self._center_handle.closest(event.x, event.y) + + # Prioritise center handle over other handles + if 'move' in self.state or m_dist < self.maxdist * 2: + self.active_handle = 'C' + elif e_dist > self.maxdist: + # Not close to any handles + 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 + + @property + def vmin(self): + """ Get the start span coordinate.""" + if self.direction == 'horizontal': + vmin = self.rect.get_x() + else: + vmin = self.rect.get_y() + return vmin + + @property + def vmax(self): + """ Get the end span coordinate.""" + if self.direction == 'horizontal': + vmax = self.vmin + self.rect.get_width() + else: + vmax = self.vmin + self.rect.get_height() + return vmax + + @property + def _mid_position_orthogonal(self): + if self.direction == 'horizontal': + axis_limits = self.ax.get_ylim() + # p = self.rect.get_height() / 2 + else: + axis_limits = self.ax.get_ylim() + # p = self.rect.get_width() / 2 + return (axis_limits[1] - axis_limits[0]) / 2 + + @property + def edge_centers(self): + """Midpoint of rectangle edges.""" + p = self._mid_position_orthogonal + return (self.vmin, self.vmax), (p, p) + + @property + def center(self): + """Center of rectangle.""" + p = self._mid_position_orthogonal + return (self.vmin + (self.vmax - self.vmin) / 2, ), (p, ) + + @property + def extents(self): + """Return (vmin, vmax).""" + return self.vmin, self.vmax + + @extents.setter + def extents(self, extents): + # Update displayed shape + self.draw_shape(*extents) + # Update displayed handles + self._edge_handles.set_data(*self.edge_centers) + self._center_handle.set_data(*self.center) + self.set_visible(self.visible) + self.update() class ToolHandles: From 6ebf76a88e6e5bfb145e894a4769aea9d9fc832c Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 10:30:54 +0100 Subject: [PATCH 02/35] Add drag_from_anywhere functionality. --- lib/matplotlib/widgets.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 442e3633d9dd..f625d3c9af21 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1953,6 +1953,10 @@ class SpanSelector(_SelectorWidget): button : `.MouseButton` or list of `.MouseButton` The mouse buttons which activate the span selector. + drag_from_anywhere : bool, optional + If `True`, the widget can be moved by clicking anywhere within + its bounds. + Examples -------- >>> import matplotlib.pyplot as plt @@ -1972,7 +1976,7 @@ class SpanSelector(_SelectorWidget): def __init__(self, ax, onselect, direction, minspan=None, useblit=False, rectprops=None, maxdist=10, marker_props=None, onmove_callback=None, span_stays=False, interactive=False, - button=None): + button=None, drag_from_anywhere=False): super().__init__(ax, onselect, useblit=useblit, button=button) @@ -1998,6 +2002,7 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, if span_stays: interactive = True self.interactive = interactive + self.drag_from_anywhere = drag_from_anywhere # Needed when dragging out of axes self.prev = (0, 0) @@ -2115,6 +2120,8 @@ def _onmove(self, event): vmin, vmax = self._extents_on_press # move existing span + # When "dragging from anywhere", the `self.active_handle` is set to 'C' + # in _set_active_handle if self.active_handle == 'C' and self._extents_on_press is not None: if self.direction == 'horizontal': dv = event.xdata - self.eventpress.xdata @@ -2191,7 +2198,13 @@ def _set_active_handle(self, event): elif e_dist > self.maxdist: # Not close to any handles self.active_handle = None - return + 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] @@ -2199,6 +2212,10 @@ def _set_active_handle(self, event): # 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 vmin(self): """ Get the start span coordinate.""" From 01319f16e7b2ccf3f47a013bd0772a8a9d43a62a Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 15:12:04 +0100 Subject: [PATCH 03/35] Add test for dragging span selector. --- lib/matplotlib/tests/test_widgets.py | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index f2ac7749d6ea..c2cbd818b365 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -222,6 +222,42 @@ 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.vmin, tool.vmax) == (10, 100) + # Drag inside span + # + # If drag_from_anywhere == True, this will move the span by 10, + # giving new value vmin, vmax = 20, 110 + # + # If drag_from_anywhere == False, this will create a new span with + # value vmin, vmax = 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.vmin, tool.vmax) == (20, 110) + else: + assert (tool.vmin, tool.vmax) == (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.vmin, tool.vmax) == (175, 185) + + def check_lasso_selector(**kwargs): ax = get_ax() From cc1d69a25b7acecf13d8d08ae6b17d81f6dc4f00 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 17:48:12 +0100 Subject: [PATCH 04/35] Use line handlers for span selector --- lib/matplotlib/widgets.py | 112 +++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f625d3c9af21..c25519388d8a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2019,21 +2019,15 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, props.update(cbook.normalize_kwargs(marker_props, Line2D._alias_map)) self._edge_order = ['min', 'max'] - xe, ye = self.edge_centers - self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', - marker_props=props, - useblit=self.useblit) - - xc, yc = self.center - self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', - marker_props=props, - useblit=self.useblit) + self._edge_handles = ToolLineHandles(self.ax, self.extents, + direction=direction, + marker_props=props, + useblit=self.useblit) self.active_handle = None if self.interactive: - self.artists.extend([self._center_handle.artist, - self._edge_handles.artist]) + self.artists.extend([line for line in self._edge_handles.artists]) def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" @@ -2190,10 +2184,9 @@ 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) - m_idx, m_dist = self._center_handle.closest(event.x, event.y) # Prioritise center handle over other handles - if 'move' in self.state or m_dist < self.maxdist * 2: + if 'move' in self.state: self.active_handle = 'C' elif e_dist > self.maxdist: # Not close to any handles @@ -2234,28 +2227,6 @@ def vmax(self): vmax = self.vmin + self.rect.get_height() return vmax - @property - def _mid_position_orthogonal(self): - if self.direction == 'horizontal': - axis_limits = self.ax.get_ylim() - # p = self.rect.get_height() / 2 - else: - axis_limits = self.ax.get_ylim() - # p = self.rect.get_width() / 2 - return (axis_limits[1] - axis_limits[0]) / 2 - - @property - def edge_centers(self): - """Midpoint of rectangle edges.""" - p = self._mid_position_orthogonal - return (self.vmin, self.vmax), (p, p) - - @property - def center(self): - """Center of rectangle.""" - p = self._mid_position_orthogonal - return (self.vmin + (self.vmax - self.vmin) / 2, ), (p, ) - @property def extents(self): """Return (vmin, vmax).""" @@ -2266,12 +2237,79 @@ def extents(self, extents): # Update displayed shape self.draw_shape(*extents) # Update displayed handles - self._edge_handles.set_data(*self.edge_centers) - self._center_handle.set_data(*self.center) + 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 control handles. + marker : str + Shape of marker used to display handle. See `matplotlib.pyplot.plot`. + marker_props : dict + Additional marker properties. See `matplotlib.lines.Line2D`. + """ + + def __init__(self, ax, positions, direction, marker_props=None, + useblit=True): + self.ax = ax + props = {'linestyle': 'none', 'alpha': 1, 'visible': False, + 'label': '_nolegend_', + **cbook.normalize_kwargs(marker_props, Line2D._alias_map)} + self._markers = [] + self.direction = direction + + for p in positions: + if self.direction == 'horizontal': + l = ax.axvline(p) + else: + l = ax.axhline(p) + l.set_visible(False) + self._markers.append(l) + + self.artists = self._markers + + @property + def positions(self): + method = 'get_xdata' if self.direction == 'horizontal' else 'get_ydata' + return [getattr(line, method)() for line in self._markers] + + def set_data(self, positions): + """Set x positions of handles.""" + method = 'set_xdata' if self.direction == 'horizontal' else 'set_ydata' + for line, p in zip(self._markers, positions): + getattr(line, method)(p) + + def set_visible(self, val): + self._markers.set_visible(val) + + def set_animated(self, val): + self._markers.set_animated(val) + + def closest(self, x, y): + """Return index and pixel distance to closest handle.""" + if self.direction == 'horizontal': + p_pts = np.array([ + self.ax.transData.transform((p, 0))[0] for p in self.positions + ]) + dist = abs(p_pts - x) + else: + p_pts = np.array([ + self.ax.transData.transform((0, p))[1] for p in self.positions + ]) + dist = abs(p_pts - y) + min_index = np.argmin(dist) + return min_index, dist[min_index] + + class ToolHandles: """ Control handles for canvas tools. From 7d374619d3703a7ced7d7014d09ad8f971ab6f02 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 18:12:40 +0100 Subject: [PATCH 05/35] Set handle properties. --- lib/matplotlib/widgets.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index c25519388d8a..b9b11fffca59 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2012,10 +2012,7 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.new_axes(ax) # Setup handles - if rectprops is None: - props = dict(markeredgecolor='r') - else: - props = dict(markeredgecolor=rectprops.get('edgecolor', 'r')) + props = dict(color=rectprops.get('facecolor', 'r')) props.update(cbook.normalize_kwargs(marker_props, Line2D._alias_map)) self._edge_order = ['min', 'max'] @@ -2261,19 +2258,12 @@ class ToolLineHandles: def __init__(self, ax, positions, direction, marker_props=None, useblit=True): self.ax = ax - props = {'linestyle': 'none', 'alpha': 1, 'visible': False, - 'label': '_nolegend_', - **cbook.normalize_kwargs(marker_props, Line2D._alias_map)} self._markers = [] self.direction = direction + marker_props.update({'visible':False}) - for p in positions: - if self.direction == 'horizontal': - l = ax.axvline(p) - else: - l = ax.axhline(p) - l.set_visible(False) - self._markers.append(l) + line_func = ax.axvline if self.direction == 'horizontal' else ax.axhline + self._markers = [line_func(p, **marker_props) for p in positions] self.artists = self._markers From db230699a80a0b247457f1f6650966893ff6b98a Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 18:44:12 +0100 Subject: [PATCH 06/35] Update docstring. --- lib/matplotlib/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index b9b11fffca59..85c1fa8738c9 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1945,6 +1945,7 @@ 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 From db8b587339815a70d8f0c27d8a3b47dbd4c4ac64 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 19:37:39 +0100 Subject: [PATCH 07/35] Tidy up. --- lib/matplotlib/widgets.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 85c1fa8738c9..f7e36a51643e 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1974,7 +1974,7 @@ class SpanSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/span_selector` """ - def __init__(self, ax, onselect, direction, minspan=None, useblit=False, + def __init__(self, ax, onselect, direction, minspan=0, useblit=False, rectprops=None, maxdist=10, marker_props=None, onmove_callback=None, span_stays=False, interactive=False, button=None, drag_from_anywhere=False): @@ -2051,10 +2051,6 @@ def new_axes(self, ax): self.ax.add_patch(self.rect) self.artists = [self.rect] - def ignore(self, event): - # docstring inherited - return super().ignore(event) or not self.visible - def _press(self, event): """Button press event handler.""" if self.interactive and self.rect.get_visible(): @@ -2097,8 +2093,11 @@ def _release(self, event): if vmin > vmax: vmin, vmax = vmax, vmin span = vmax - vmin - if self.minspan is not None and span < self.minspan: + if self.minspan is not None and span <= self.minspan: + self.set_visible(False) + self.update() return + self.onselect(vmin, vmax) self.update() @@ -2113,7 +2112,7 @@ def _onmove(self, event): vmin, vmax = self._extents_on_press # move existing span # When "dragging from anywhere", the `self.active_handle` is set to 'C' - # in _set_active_handle + # in _set_active_handle (match notation used in the RectangleSelector) if self.active_handle == 'C' and self._extents_on_press is not None: if self.direction == 'horizontal': dv = event.xdata - self.eventpress.xdata @@ -2124,10 +2123,7 @@ def _onmove(self, event): # resize an existing shape elif self.active_handle and self.active_handle != 'C': - if self.direction == 'horizontal': - v = event.xdata - else: - v = event.ydata + v = event.xdata if self.direction == 'horizontal' else event.ydata if self.active_handle == 'min': vmin = v else: @@ -2149,7 +2145,6 @@ def _onmove(self, event): self.onmove_callback(vmin, vmax) self.extents = vmin, vmax - self.update() return False @@ -2160,10 +2155,7 @@ def _set_span_xy(self, event): return self.prev = x, y - if self.direction == 'horizontal': - v = x - else: - v = y + v = x if self.direction == 'horizontal' else y values = v, self.pressv self.draw_shape(*values) @@ -2184,6 +2176,7 @@ def _set_active_handle(self, event): e_idx, e_dist = self._edge_handles.closest(event.x, event.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: @@ -2236,7 +2229,6 @@ def extents(self, extents): self.draw_shape(*extents) # Update displayed handles self._edge_handles.set_data(self.extents) - self.set_visible(self.visible) self.update() @@ -2261,7 +2253,7 @@ def __init__(self, ax, positions, direction, marker_props=None, self.ax = ax self._markers = [] self.direction = direction - marker_props.update({'visible':False}) + marker_props.update({'visible':False, 'animated':useblit}) line_func = ax.axvline if self.direction == 'horizontal' else ax.axhline self._markers = [line_func(p, **marker_props) for p in positions] From 369ff9d232395afce3de5f3ec22f4dc25c600fa1 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 28 Apr 2021 20:01:59 +0100 Subject: [PATCH 08/35] Add API deprecation. --- lib/matplotlib/widgets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f7e36a51643e..230644bd681b 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1974,6 +1974,7 @@ class SpanSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/span_selector` """ + @_api.delete_parameter("3.5", "span_stays") def __init__(self, ax, onselect, direction, minspan=0, useblit=False, rectprops=None, maxdist=10, marker_props=None, onmove_callback=None, span_stays=False, interactive=False, @@ -2002,6 +2003,11 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, # with Rectangle, etc. if span_stays: interactive = True + _api.warn_deprecated( + "3.5", message="Support for span_stays=True is deprecated " + "since %(since)s and will be removed " + "%(removal)s." + "Use interactive=True instead.") self.interactive = interactive self.drag_from_anywhere = drag_from_anywhere @@ -2093,7 +2099,7 @@ def _release(self, event): if vmin > vmax: vmin, vmax = vmax, vmin span = vmax - vmin - if self.minspan is not None and span <= self.minspan: + if span < self.minspan: self.set_visible(False) self.update() return From a3c321c77c335733981f1d8dbaab36b10b814092 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 29 Apr 2021 17:33:11 +0100 Subject: [PATCH 09/35] Make SpanSelector consistent with RectangleSelector to further improve its interactivity. --- lib/matplotlib/widgets.py | 77 ++++++++++----------------------------- 1 file changed, 20 insertions(+), 57 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 230644bd681b..a5b39e0ba93c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1992,7 +1992,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.rect = None self.visible = True - self.pressv = None + self._extents_on_press = None self.rectprops = rectprops self.onmove_callback = onmove_callback @@ -2011,9 +2011,6 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.interactive = interactive self.drag_from_anywhere = drag_from_anywhere - # Needed when dragging out of axes - self.prev = (0, 0) - # Reset canvas so that `new_axes` connects events. self.canvas = None self.new_axes(ax) @@ -2064,40 +2061,24 @@ def _press(self, event): else: self.active_handle = None - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - self.pressv = xdata - else: - self.pressv = ydata - self._extents_on_press = self.extents - - self._set_span_xy(event) - if self.active_handle is None or not self.interactive: # Clear previous rectangle before drawing new rectangle. self.update() + if not self.interactive: + v = event.xdata if self.direction == 'horizontal' else event.ydata + self.extents = v, v + self.set_visible(self.visible) return False def _release(self, event): """Button release event handler.""" - if self.pressv is None: - return - if not self.interactive: self.rect.set_visible(False) - 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] - - if vmin > vmax: - vmin, vmax = vmax, vmin + vmin, vmax = self.extents span = vmax - vmin if span < self.minspan: self.set_visible(False) @@ -2107,65 +2088,46 @@ def _release(self, event): self.onselect(vmin, vmax) self.update() - self.pressv = None return False def _onmove(self, event): """Motion notify event handler.""" - if self.pressv is None: - return - vmin, vmax = self._extents_on_press + 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", the `self.active_handle` is set to 'C' # in _set_active_handle (match notation used in the RectangleSelector) if self.active_handle == 'C' and self._extents_on_press is not None: - if self.direction == 'horizontal': - dv = event.xdata - self.eventpress.xdata - else: - dv = event.ydata - self.eventpress.ydata + 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': - v = event.xdata if self.direction == 'horizontal' else event.ydata + vmin, vmax = self._extents_on_press if self.active_handle == 'min': vmin = v else: vmax = v + # new shape else: - self._set_span_xy(event) - - 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] - + 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.extents = vmin, vmax - return False - def _set_span_xy(self, event): - """Set the span coordinates.""" - x, y = self._get_data(event) - if x is None: - return - - self.prev = x, y - v = x if self.direction == 'horizontal' else y - - values = v, self.pressv - self.draw_shape(*values) - def draw_shape(self, vmin, vmax): if vmin > vmax: vmin, vmax = vmax, vmin @@ -2235,6 +2197,7 @@ def extents(self, extents): self.draw_shape(*extents) # Update displayed handles self._edge_handles.set_data(self.extents) + self.set_visible(self.visible) self.update() From 349c134a8db4c0d7bd83b66129789f7f59d949b1 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 29 Apr 2021 18:06:25 +0100 Subject: [PATCH 10/35] Test move outside of axis. --- lib/matplotlib/tests/test_widgets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index c2cbd818b365..7e0861ebe6ae 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 From 52c36667bff9604deafc0aa06ff0429f59fbba81 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 29 Apr 2021 18:46:44 +0100 Subject: [PATCH 11/35] flake8 fixes. --- lib/matplotlib/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a5b39e0ba93c..cb5afe14c4fe 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2170,7 +2170,7 @@ def _contains(self, event): @property def vmin(self): - """ Get the start span coordinate.""" + """Get the start span coordinate.""" if self.direction == 'horizontal': vmin = self.rect.get_x() else: @@ -2179,7 +2179,7 @@ def vmin(self): @property def vmax(self): - """ Get the end span coordinate.""" + """Get the end span coordinate.""" if self.direction == 'horizontal': vmax = self.vmin + self.rect.get_width() else: @@ -2222,10 +2222,10 @@ def __init__(self, ax, positions, direction, marker_props=None, self.ax = ax self._markers = [] self.direction = direction - marker_props.update({'visible':False, 'animated':useblit}) + marker_props.update({'visible': False, 'animated': useblit}) - line_func = ax.axvline if self.direction == 'horizontal' else ax.axhline - self._markers = [line_func(p, **marker_props) for p in positions] + line_fun = ax.axvline if self.direction == 'horizontal' else ax.axhline + self._markers = [line_fun(p, **marker_props) for p in positions] self.artists = self._markers From bc3808b0c2a45faf34ed05e5958b7bb2509c31b2 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 12 May 2021 19:51:19 +0100 Subject: [PATCH 12/35] Use @_api.rename_parameter instead of @_api.delete_parameter to rename `span_stays` to `interactive`. --- lib/matplotlib/widgets.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index cb5afe14c4fe..ff55046014f8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1974,10 +1974,10 @@ class SpanSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/span_selector` """ - @_api.delete_parameter("3.5", "span_stays") + @_api.rename_parameter("3.5", "span_stays", "interactive") def __init__(self, ax, onselect, direction, minspan=0, useblit=False, rectprops=None, maxdist=10, marker_props=None, - onmove_callback=None, span_stays=False, interactive=False, + onmove_callback=None, interactive=False, button=None, drag_from_anywhere=False): super().__init__(ax, onselect, useblit=useblit, button=button) @@ -1999,15 +1999,6 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.minspan = minspan self.maxdist = maxdist - # Deprecate `span_stays` in favour of interactive to be consistent - # with Rectangle, etc. - if span_stays: - interactive = True - _api.warn_deprecated( - "3.5", message="Support for span_stays=True is deprecated " - "since %(since)s and will be removed " - "%(removal)s." - "Use interactive=True instead.") self.interactive = interactive self.drag_from_anywhere = drag_from_anywhere From 1e774e0dbf3d2d504169936fec2605541fe9519d Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 12 May 2021 19:59:38 +0100 Subject: [PATCH 13/35] Allow setting extents before the SpanSelector/RectangleSelector/EllipseSelector have been drawn. --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index ff55046014f8..74196791bdf4 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: From 4bf2d095ec1efe80c3a2b5af218370fb084a15c7 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 12 May 2021 20:15:30 +0100 Subject: [PATCH 14/35] Privatize draw_shape for SpanSelector, RectangleSelector and EllipseSelector. --- lib/matplotlib/widgets.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 74196791bdf4..c91b4fb05226 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2119,7 +2119,7 @@ def _onmove(self, event): return False - def draw_shape(self, vmin, vmax): + def _draw_shape(self, vmin, vmax): if vmin > vmax: vmin, vmax = vmax, vmin if self.direction == 'horizontal': @@ -2185,7 +2185,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._edge_handles.set_data(self.extents) self.set_visible(self.visible) @@ -2658,7 +2658,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) @@ -2666,7 +2666,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]) @@ -2784,8 +2786,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]) From bb3c1ae98f72894eb5a29298dcf145f09cbeff61 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 19 May 2021 16:05:44 +0100 Subject: [PATCH 15/35] Deprecate SpanSelector.pressv --- lib/matplotlib/widgets.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index c91b4fb05226..8077ab817be4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1994,6 +1994,10 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, 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.onmove_callback = onmove_callback self.minspan = minspan @@ -2056,14 +2060,26 @@ def _press(self, event): # Clear previous rectangle before drawing new rectangle. self.update() + v = event.xdata if self.direction == 'horizontal' else event.ydata + # self._pressv is deprecated but we still need to maintain it + self._pressv = v if not self.interactive: - v = event.xdata if self.direction == 'horizontal' else event.ydata self.extents = v, v self.set_visible(self.visible) return False + @_api.deprecated("3.5") + @property + def pressv(self): + return self._pressv + + @_api.deprecated("3.5") + @pressv.setter + def pressv(self, value): + self._pressv = value + def _release(self, event): """Button release event handler.""" if not self.interactive: @@ -2079,6 +2095,9 @@ def _release(self, event): self.onselect(vmin, vmax) self.update() + # self._pressv is deprecated but we still need to maintain it + self._pressv = None + return False def _onmove(self, event): From 512231868f57e183216a56abde126b61a00c0752 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 19 May 2021 19:04:53 +0100 Subject: [PATCH 16/35] Reset arguments order in SpanSelector --- lib/matplotlib/widgets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 8077ab817be4..de391e8fdbb3 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1933,10 +1933,6 @@ class SpanSelector(_SelectorWidget): rectprops : dict, default: None Dictionary of `matplotlib.patches.Patch` properties. - maxdist : float, default: 10 - Distance in pixels within which the interactive tool handles can be - activated. - marker_props : dict Properties with which the interactive handles are drawn. @@ -1954,6 +1950,10 @@ class SpanSelector(_SelectorWidget): button : `.MouseButton` or list of `.MouseButton` The mouse buttons which activate the span selector. + 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. @@ -1976,9 +1976,9 @@ class SpanSelector(_SelectorWidget): @_api.rename_parameter("3.5", "span_stays", "interactive") def __init__(self, ax, onselect, direction, minspan=0, useblit=False, - rectprops=None, maxdist=10, marker_props=None, - onmove_callback=None, interactive=False, - button=None, drag_from_anywhere=False): + rectprops=None, marker_props=None, onmove_callback=None, + interactive=False, button=None, + maxdist=10, drag_from_anywhere=False): super().__init__(ax, onselect, useblit=useblit, button=button) From aa023f463b200db244337c032f32d5851aaf265a Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 3 Jun 2021 22:23:12 +0100 Subject: [PATCH 17/35] Improve docstring, argument name, fix `set_animated`, `set_visible` in ToolLineHandles and add tests --- lib/matplotlib/tests/test_widgets.py | 22 ++++++ lib/matplotlib/widgets.py | 102 +++++++++++++++++++-------- 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 7e0861ebe6ae..0ca40de753fb 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -259,6 +259,28 @@ def onselect(epress, erelease): assert (tool.vmin, tool.vmax) == (175, 185) +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 de391e8fdbb3..06f6cf44a40a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1933,9 +1933,6 @@ class SpanSelector(_SelectorWidget): rectprops : dict, default: None Dictionary of `matplotlib.patches.Patch` properties. - marker_props : dict - Properties with which the interactive handles are drawn. - onmove_callback : func(min, max), min/max are floats, default: None Called on mouse move while the span is being selected. @@ -1950,6 +1947,11 @@ class SpanSelector(_SelectorWidget): 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. @@ -1976,9 +1978,9 @@ class SpanSelector(_SelectorWidget): @_api.rename_parameter("3.5", "span_stays", "interactive") def __init__(self, ax, onselect, direction, minspan=0, useblit=False, - rectprops=None, marker_props=None, onmove_callback=None, - interactive=False, button=None, - maxdist=10, drag_from_anywhere=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) @@ -1988,7 +1990,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, rectprops['animated'] = self.useblit _api.check_in_list(['horizontal', 'vertical'], direction=direction) - self.direction = direction + self._direction = direction self.rect = None self.visible = True @@ -2012,12 +2014,12 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, # Setup handles props = dict(color=rectprops.get('facecolor', 'r')) - props.update(cbook.normalize_kwargs(marker_props, Line2D._alias_map)) + props.update(cbook.normalize_kwargs(line_props, Line2D._alias_map)) self._edge_order = ['min', 'max'] self._edge_handles = ToolLineHandles(self.ax, self.extents, direction=direction, - marker_props=props, + line_props=props, useblit=self.useblit) self.active_handle = None @@ -2080,6 +2082,11 @@ def pressv(self): def pressv(self, value): self._pressv = value + @property + def direction(self): + """Direction of the span selector: 'vertical' or 'horizontal'.""" + return self._direction + def _release(self, event): """Button release event handler.""" if not self.interactive: @@ -2220,44 +2227,77 @@ class ToolLineHandles: ax : `matplotlib.axes.Axes` Matplotlib axes where tool handles are displayed. positions : 1D array - Positions of control handles. - marker : str - Shape of marker used to display handle. See `matplotlib.pyplot.plot`. - marker_props : dict - Additional marker properties. See `matplotlib.lines.Line2D`. + 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, marker_props=None, + def __init__(self, ax, positions, direction, line_props=None, useblit=True): self.ax = ax - self._markers = [] - self.direction = direction - marker_props.update({'visible': False, 'animated': useblit}) + + _api.check_in_list(['horizontal', 'vertical'], direction=direction) + self._direction = direction + + 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._markers = [line_fun(p, **marker_props) for p in positions] - self.artists = self._markers + 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)() for line in self._markers] + 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 positions of handles.""" + """ + Set x positions of handles + + 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._markers, positions): - getattr(line, method)(p) + for line, p in zip(self.artists, positions): + getattr(line, method)([p, p]) - def set_visible(self, val): - self._markers.set_visible(val) + def set_visible(self, value): + """Set the visibility state of the handles artist.""" + for m in self.artists: + m.set_visible(value) - def set_animated(self, val): - self._markers.set_animated(val) + def set_animated(self, value): + """Set the animated state of the handles artist.""" + for m in self.artists: + m.set_animated(value) def closest(self, x, y): - """Return index and pixel distance to closest handle.""" + """ + 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': p_pts = np.array([ self.ax.transData.transform((p, 0))[0] for p in self.positions @@ -2268,8 +2308,8 @@ def closest(self, x, y): self.ax.transData.transform((0, p))[1] for p in self.positions ]) dist = abs(p_pts - y) - min_index = np.argmin(dist) - return min_index, dist[min_index] + index = np.argmin(dist) + return index, dist[index] class ToolHandles: From 03d1414f401e6ee091373017d48e8ca23698020e Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 3 Jun 2021 22:34:56 +0100 Subject: [PATCH 18/35] Make rect and rectprops private (with deprecation) --- lib/matplotlib/widgets.py | 123 +++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 06f6cf44a40a..b0f77ae10609 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1992,7 +1992,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, _api.check_in_list(['horizontal', 'vertical'], direction=direction) self._direction = direction - self.rect = None + self._rect = None self.visible = True self._extents_on_press = None @@ -2000,7 +2000,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, # 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 @@ -2043,17 +2043,17 @@ 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) + self._rect = Rectangle((0, 0), w, h, + transform=trans, + visible=False, + **self._rectprops) - self.ax.add_patch(self.rect) - self.artists = [self.rect] + self.ax.add_patch(self._rect) + self.artists = [self._rect] def _press(self, event): """Button press event handler.""" - if self.interactive and self.rect.get_visible(): + if self.interactive and self._rect.get_visible(): self._set_active_handle(event) else: self.active_handle = None @@ -2072,6 +2072,11 @@ def _press(self, event): return False + @_api.deprecated("3.5") + @property + def rect(self): + return self._rect + @_api.deprecated("3.5") @property def pressv(self): @@ -2082,6 +2087,11 @@ def pressv(self): def pressv(self, value): self._pressv = value + @_api.deprecated("3.5") + @property + def rectprops(self): + return self._rectprops + @property def direction(self): """Direction of the span selector: 'vertical' or 'horizontal'.""" @@ -2090,7 +2100,7 @@ def direction(self): def _release(self, event): """Button release event handler.""" if not self.interactive: - self.rect.set_visible(False) + self._rect.set_visible(False) vmin, vmax = self.extents span = vmax - vmin @@ -2149,11 +2159,11 @@ 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) + self._rect.set_x(vmin) + self._rect.set_width(vmax - vmin) else: - self.rect.set_y(vmin) - self.rect.set_height(vmax - vmin) + 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.""" @@ -2183,24 +2193,24 @@ def _set_active_handle(self, event): def _contains(self, event): """Return True if event is within the patch.""" - return self.rect.contains(event, radius=0)[0] + return self._rect.contains(event, radius=0)[0] @property def vmin(self): """Get the start span coordinate.""" if self.direction == 'horizontal': - vmin = self.rect.get_x() + vmin = self._rect.get_x() else: - vmin = self.rect.get_y() + vmin = self._rect.get_y() return vmin @property def vmax(self): """Get the end span coordinate.""" if self.direction == 'horizontal': - vmax = self.vmin + self.rect.get_width() + vmax = self.vmin + self._rect.get_width() else: - vmax = self.vmin + self.rect.get_height() + vmax = self.vmin + self._rect.get_height() return vmax @property @@ -2470,7 +2480,7 @@ 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.drag_from_anywhere = drag_from_anywhere @@ -2489,11 +2499,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 " @@ -2504,9 +2514,9 @@ 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 @@ -2540,20 +2550,25 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) 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] + self.artists = [self._to_draw] self._extents_on_press = None + @_api.deprecated("3.5") + @property + def to_draw(self): + return self._to_draw + 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 @@ -2572,7 +2587,7 @@ def _press(self, event): def _release(self, event): """Button release event handler.""" if not self.interactive: - self.to_draw.set_visible(False) + self._to_draw.set_visible(False) # update the eventpress and eventrelease with the resulting extents x0, x1, y0, y1 = self.extents @@ -2671,13 +2686,13 @@ 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() + 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 @@ -2740,13 +2755,13 @@ def _draw_shape(self, extents): 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) + 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]) + 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.""" @@ -2789,7 +2804,7 @@ def _set_active_handle(self, event): 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): @@ -2800,12 +2815,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): @@ -2856,24 +2871,24 @@ def _draw_shape(self, extents): 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 + 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 + 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 From 524f2b79315ec3cc05a262c722cf2d2708c665f5 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 3 Jun 2021 22:39:31 +0100 Subject: [PATCH 19/35] Remove vmin, vmax of span selector, use extents instead --- lib/matplotlib/tests/test_widgets.py | 12 ++++++------ lib/matplotlib/widgets.py | 22 +++++----------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0ca40de753fb..94ac9327c358 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -236,27 +236,27 @@ def onselect(epress, erelease): 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.vmin, tool.vmax) == (10, 100) + assert tool.extents == (10, 100) # Drag inside span # # If drag_from_anywhere == True, this will move the span by 10, - # giving new value vmin, vmax = 20, 110 + # giving new value extents = 20, 110 # # If drag_from_anywhere == False, this will create a new span with - # value vmin, vmax = 25, 35 + # value vmin, vmaxextents = 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.vmin, tool.vmax) == (20, 110) + assert tool.extents == (20, 110) else: - assert (tool.vmin, tool.vmax) == (25, 35) + 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.vmin, tool.vmax) == (175, 185) + assert tool.extents == (175, 185) def test_tool_line_handle(): diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index b0f77ae10609..b52feef6d708 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2196,27 +2196,15 @@ def _contains(self, event): return self._rect.contains(event, radius=0)[0] @property - def vmin(self): - """Get the start span coordinate.""" + def extents(self): + """Return extents of the span selector.""" if self.direction == 'horizontal': vmin = self._rect.get_x() + vmax = vmin + self._rect.get_width() else: vmin = self._rect.get_y() - return vmin - - @property - def vmax(self): - """Get the end span coordinate.""" - if self.direction == 'horizontal': - vmax = self.vmin + self._rect.get_width() - else: - vmax = self.vmin + self._rect.get_height() - return vmax - - @property - def extents(self): - """Return (vmin, vmax).""" - return self.vmin, self.vmax + vmax = vmin + self._rect.get_height() + return vmin, vmax @extents.setter def extents(self, extents): From 5587bb428216e24dc39850f0fe8448845834d3fd Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Fri, 4 Jun 2021 10:09:45 +0100 Subject: [PATCH 20/35] Privatize more attributes, which are not expected to be used by users. --- lib/matplotlib/widgets.py | 105 +++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index b52feef6d708..0b1e384de31a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2022,11 +2022,19 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, line_props=props, useblit=self.useblit) - self.active_handle = None + self._active_handle = None if self.interactive: self.artists.extend([line for line in self._edge_handles.artists]) + 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") + def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" self.ax = ax @@ -2056,9 +2064,9 @@ def _press(self, event): if self.interactive and self._rect.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() @@ -2072,26 +2080,6 @@ def _press(self, event): return False - @_api.deprecated("3.5") - @property - def rect(self): - return self._rect - - @_api.deprecated("3.5") - @property - def pressv(self): - return self._pressv - - @_api.deprecated("3.5") - @pressv.setter - def pressv(self, value): - self._pressv = value - - @_api.deprecated("3.5") - @property - def rectprops(self): - return self._rectprops - @property def direction(self): """Direction of the span selector: 'vertical' or 'horizontal'.""" @@ -2127,18 +2115,18 @@ def _onmove(self, event): vpress = self.eventpress.ydata # move existing span - # When "dragging from anywhere", the `self.active_handle` is set to 'C' + # When "dragging from anywhere", the `self._active_handle` is set to 'C' # in _set_active_handle (match notation used in the RectangleSelector) - if self.active_handle == 'C' and self._extents_on_press is not None: + 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': + elif self._active_handle and self._active_handle != 'C': vmin, vmax = self._extents_on_press - if self.active_handle == 'min': + if self._active_handle == 'min': vmin = v else: vmax = v @@ -2173,20 +2161,20 @@ def _set_active_handle(self, event): # 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' + self._active_handle = 'C' elif e_dist > self.maxdist: # Not close to any handles - self.active_handle = None + 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._active_handle = 'C' self._extents_on_press = self.extents else: - self.active_handle = None + self._active_handle = None return 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. self._extents_on_press = self.extents @@ -2511,7 +2499,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) _api.check_in_list(['data', 'pixels'], spancoords=spancoords) self.spancoords = spancoords - self.drawtype = drawtype + self._drawtype = drawtype self.maxdist = maxdist @@ -2536,7 +2524,7 @@ 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._corner_handles.artist, @@ -2547,10 +2535,11 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) self._extents_on_press = None - @_api.deprecated("3.5") - @property - def to_draw(self): - return self._to_draw + to_draw = _api.deprecate_privatize_attribute("3.5") + + drawtype = _api.deprecate_privatize_attribute("3.5") + + active_handle = _api.deprecate_privatize_attribute("3.5") def _press(self, event): """Button press event handler.""" @@ -2559,9 +2548,9 @@ def _press(self, event): 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() @@ -2601,7 +2590,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: @@ -2618,15 +2607,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 @@ -2673,7 +2662,7 @@ def _onmove(self, event): @property def _rect_bbox(self): - if self.drawtype == 'box': + if self._drawtype == 'box': x0 = self._to_draw.get_x() y0 = self._to_draw.get_y() width = self._to_draw.get_width() @@ -2742,13 +2731,13 @@ def _draw_shape(self, extents): xmax = min(xmax, xlim[1]) ymax = min(ymax, ylim[1]) - if self.drawtype == 'box': + 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': + elif self._drawtype == 'line': self._to_draw.set_data([xmin, xmax], [ymin, ymax]) def _set_active_handle(self, event): @@ -2759,34 +2748,34 @@ 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 @@ -2858,7 +2847,7 @@ def _draw_shape(self, extents): a = (xmax - xmin) / 2. b = (ymax - ymin) / 2. - if self.drawtype == 'box': + if self._drawtype == 'box': self._to_draw.center = center self._to_draw.width = 2 * a self._to_draw.height = 2 * b @@ -2870,7 +2859,7 @@ def _draw_shape(self, extents): @property def _rect_bbox(self): - if self.drawtype == 'box': + if self._drawtype == 'box': x, y = self._to_draw.center width = self._to_draw.width height = self._to_draw.height From fb7bdd7f9fc8c249089de28be832f97ef0ede7f2 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Fri, 4 Jun 2021 10:29:46 +0100 Subject: [PATCH 21/35] Fix flake8 compliance --- lib/matplotlib/widgets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 0b1e384de31a..2b17f68477ea 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2115,8 +2115,8 @@ def _onmove(self, event): vpress = self.eventpress.ydata # move existing span - # When "dragging from anywhere", the `self._active_handle` is set to 'C' - # in _set_active_handle (match notation used in the RectangleSelector) + # 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 @@ -2261,13 +2261,13 @@ def set_data(self, positions): def set_visible(self, value): """Set the visibility state of the handles artist.""" - for m in self.artists: - m.set_visible(value) + for artist in self.artists: + artist.set_visible(value) def set_animated(self, value): """Set the animated state of the handles artist.""" - for m in self.artists: - m.set_animated(value) + for artist in self.artists: + artist.set_animated(value) def closest(self, x, y): """ From f10ab5c571481ee9a7499c9f09b7d724118179ae Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jun 2021 20:54:43 -0400 Subject: [PATCH 22/35] MNT: restore clearing selection with 0-width selection --- lib/matplotlib/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 2b17f68477ea..9be91d9289c0 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2073,10 +2073,10 @@ def _press(self, event): v = event.xdata if self.direction == 'horizontal' else event.ydata # self._pressv is deprecated but we still need to maintain it self._pressv = v - if not self.interactive: + if self._active_handle is None: self.extents = v, v - self.set_visible(self.visible) + self.set_visible(True) return False @@ -2092,7 +2092,7 @@ def _release(self, event): vmin, vmax = self.extents span = vmax - vmin - if span < self.minspan: + if span <= self.minspan: self.set_visible(False) self.update() return From 472072b8efcb396988bf7c35709477116ea56375 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jun 2021 20:55:09 -0400 Subject: [PATCH 23/35] DOC: show off more keyword arguments in span selector demo --- examples/widgets/span_selector.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/widgets/span_selector.py b/examples/widgets/span_selector.py index 8392be667cfd..5e8ffb9ee3b6 100644 --- a/examples/widgets/span_selector.py +++ b/examples/widgets/span_selector.py @@ -16,14 +16,14 @@ 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) ax1.set_title('Press left mouse button and drag ' 'to select a region in the top graph') -line2, = ax2.plot([], []) +(line2,) = ax2.plot([], []) def onselect(xmin, xmax): @@ -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. From ab9eebfbd6164873ec1e3b7274da2017b845db06 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Fri, 18 Jun 2021 19:13:52 +0100 Subject: [PATCH 24/35] Avoid calling update when clearing the span selector with 0-width selection --- lib/matplotlib/widgets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9be91d9289c0..65bccad0db61 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2074,7 +2074,10 @@ def _press(self, event): # self._pressv is deprecated but we still need to maintain it self._pressv = v if self._active_handle is None: - self.extents = v, v + # when the press event outside the span, we initially set the + # extents to (v, v) and _onmove or _release will follow up + # use _draw_shape instead of extents to avoid calling update + self._draw_shape(v, v) self.set_visible(True) From bbf115645a8b64dae5c53dbf90f8e83ab16a6aaa Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Fri, 18 Jun 2021 19:42:57 +0100 Subject: [PATCH 25/35] Add new features and API changes entries --- .../next_api_changes/deprecations/20113-EP.rst | 18 ++++++++++++++++++ doc/users/next_whats_new/widget_dragging.rst | 7 +++++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/20113-EP.rst 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..715683104032 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20113-EP.rst @@ -0,0 +1,18 @@ +SpanSelector +~~~~~~~~~~~~ +``span_stays`` is deprecated, use ``interactive`` argument instead +Several `~matplotlib.widgets.SpanSelector` class internals have been privatized +and deprecated: +- ``pressv`` +- ``rect`` +- ``rectprops`` +- ``active_handle`` + + +Several `~matplotlib.widgets.RectangleSelector` and +`~matplotlib.widgets.EllipseSelector` class internals have been privatized and +deprecated: +- ``to_draw`` +- ``drawtype`` +- ``rectprops`` +- ``active_handle`` 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. From 3f72e1742bef8685b38e632210130b02964ce6fe Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 09:57:37 +0100 Subject: [PATCH 26/35] Fix typo and docstring --- lib/matplotlib/tests/test_widgets.py | 2 +- lib/matplotlib/widgets.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 94ac9327c358..5e0dc388b9b6 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -243,7 +243,7 @@ def onselect(epress, erelease): # giving new value extents = 20, 110 # # If drag_from_anywhere == False, this will create a new span with - # value vmin, vmaxextents = 25, 35 + # 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) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 65bccad0db61..2ab560eb9c09 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1923,8 +1923,8 @@ 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 egal to *minspan*, do not call *onselect*. useblit : bool, default: False If True, use the backend-dependent blitting features for faster @@ -2251,7 +2251,8 @@ def direction(self): def set_data(self, positions): """ - Set x positions of handles + Set x or y positions of handles, depending if the lines are vertical + of horizontal. Parameters ---------- From 8eeea0290f24c3231a06f5d4c257f7b031cefbf0 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 10:06:40 +0100 Subject: [PATCH 27/35] Privatize interactive in RectangleSelector and EllipseSelector --- .../deprecations/20113-EP.rst | 1 + lib/matplotlib/widgets.py | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/doc/api/next_api_changes/deprecations/20113-EP.rst b/doc/api/next_api_changes/deprecations/20113-EP.rst index 715683104032..4281e9985dbd 100644 --- a/doc/api/next_api_changes/deprecations/20113-EP.rst +++ b/doc/api/next_api_changes/deprecations/20113-EP.rst @@ -16,3 +16,4 @@ deprecated: - ``drawtype`` - ``rectprops`` - ``active_handle`` +- ``interactive`` diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 2ab560eb9c09..cb578ed9b746 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2005,7 +2005,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.minspan = minspan self.maxdist = maxdist - self.interactive = interactive + self._interactive = interactive self.drag_from_anywhere = drag_from_anywhere # Reset canvas so that `new_axes` connects events. @@ -2024,7 +2024,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self._active_handle = None - if self.interactive: + if self._interactive: self.artists.extend([line for line in self._edge_handles.artists]) rect = _api.deprecate_privatize_attribute("3.5") @@ -2061,12 +2061,12 @@ def new_axes(self, ax): def _press(self, event): """Button press event handler.""" - if self.interactive and self._rect.get_visible(): + if self._interactive and self._rect.get_visible(): self._set_active_handle(event) else: 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() @@ -2090,7 +2090,7 @@ def direction(self): def _release(self, event): """Button release event handler.""" - if not self.interactive: + if not self._interactive: self._rect.set_visible(False) vmin, vmax = self.extents @@ -2462,7 +2462,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) 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 @@ -2534,7 +2534,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) self._corner_handles.artist, self._edge_handles.artist] - if not self.interactive: + if not self._interactive: self.artists = [self._to_draw] self._extents_on_press = None @@ -2545,20 +2545,22 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) 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 - 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 not self._interactive: x = event.xdata y = event.ydata self.extents = x, x, y, y @@ -2567,7 +2569,7 @@ def _press(self, event): def _release(self, event): """Button release event handler.""" - if not self.interactive: + if not self._interactive: self._to_draw.set_visible(False) # update the eventpress and eventrelease with the resulting extents From 6266fcf34d098ac56745d737d6bfabbe6a0cbb4e Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 11:17:48 +0100 Subject: [PATCH 28/35] Fix clearing span without blitting. --- lib/matplotlib/widgets.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index cb578ed9b746..e3ab38b0fc29 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2073,13 +2073,18 @@ def _press(self, event): v = event.xdata if self.direction == 'horizontal' else event.ydata # self._pressv is deprecated but we still need to maintain it self._pressv = v + if self._active_handle is None: # when the press event outside the span, we initially set the - # extents to (v, v) and _onmove or _release will follow up - # use _draw_shape instead of extents to avoid calling update - self._draw_shape(v, v) - - self.set_visible(True) + # 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) return False From ff07b6e321335b0c175758d54a847ee38e811a8a Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 11:22:36 +0100 Subject: [PATCH 29/35] Clear RectangleSelector and EllipseSelector when clicking outside the selector consistently with SpanSelector --- lib/matplotlib/widgets.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e3ab38b0fc29..01aaaad3a363 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2565,12 +2565,16 @@ def _press(self, event): # 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.""" From 9b98eec58bdda61847f2ce71c7ad0ba3fef237d8 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 11:45:03 +0100 Subject: [PATCH 30/35] Deprecate `SpanSelector.span_stays` attributes --- doc/api/next_api_changes/deprecations/20113-EP.rst | 1 + examples/widgets/span_selector.py | 2 +- lib/matplotlib/widgets.py | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/api/next_api_changes/deprecations/20113-EP.rst b/doc/api/next_api_changes/deprecations/20113-EP.rst index 4281e9985dbd..d59c72bbf1aa 100644 --- a/doc/api/next_api_changes/deprecations/20113-EP.rst +++ b/doc/api/next_api_changes/deprecations/20113-EP.rst @@ -7,6 +7,7 @@ and deprecated: - ``rect`` - ``rectprops`` - ``active_handle`` +- ``span_stays`` Several `~matplotlib.widgets.RectangleSelector` and diff --git a/examples/widgets/span_selector.py b/examples/widgets/span_selector.py index 5e8ffb9ee3b6..a9e1058ca232 100644 --- a/examples/widgets/span_selector.py +++ b/examples/widgets/span_selector.py @@ -23,7 +23,7 @@ ax1.set_title('Press left mouse button and drag ' 'to select a region in the top graph') -(line2,) = ax2.plot([], []) +line2, = ax2.plot([], []) def onselect(xmin, xmax): diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 01aaaad3a363..a9f32fc2746a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2035,6 +2035,10 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, pressv = _api.deprecate_privatize_attribute("3.5") + span_stays = _api.deprecated("3.5")( + property(lambda self: self._interactive) + ) + def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" self.ax = ax From d123ebf85c94e4fed7612dd6b4ceb3848c4a3f57 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 11:57:39 +0100 Subject: [PATCH 31/35] Deprecate `SpanSelector.prev` attribute --- doc/api/next_api_changes/deprecations/20113-EP.rst | 1 + lib/matplotlib/widgets.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/api/next_api_changes/deprecations/20113-EP.rst b/doc/api/next_api_changes/deprecations/20113-EP.rst index d59c72bbf1aa..fe39447632c6 100644 --- a/doc/api/next_api_changes/deprecations/20113-EP.rst +++ b/doc/api/next_api_changes/deprecations/20113-EP.rst @@ -4,6 +4,7 @@ SpanSelector Several `~matplotlib.widgets.SpanSelector` class internals have been privatized and deprecated: - ``pressv`` +- ``prev`` - ``rect`` - ``rectprops`` - ``active_handle`` diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a9f32fc2746a..ca9e75aa43da 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2027,6 +2027,9 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, if self._interactive: self.artists.extend([line for line in self._edge_handles.artists]) + # prev attritube 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") @@ -2039,6 +2042,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, property(lambda self: self._interactive) ) + prev = _api.deprecated("3.5")(property(lambda self: self._prev)) + def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" self.ax = ax @@ -2075,8 +2080,10 @@ def _press(self, event): self.update() v = event.xdata if self.direction == 'horizontal' else event.ydata - # self._pressv is deprecated but we still need to maintain it + # self._pressv and self._prev are deprecated but we still need to + # maintain them self._pressv = v + self._prev = self._get_data(event) if self._active_handle is None: # when the press event outside the span, we initially set the @@ -2120,6 +2127,9 @@ def _release(self, event): def _onmove(self, event): """Motion notify event handler.""" + # self._prev are deprecated but we still need to maintain it + self._prev = self._get_data(event) + v = event.xdata if self.direction == 'horizontal' else event.ydata if self.direction == 'horizontal': vpress = self.eventpress.xdata From f64e98bc7c4a08210f603261a48555e4930173b2 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 19 Jun 2021 12:53:47 +0100 Subject: [PATCH 32/35] Allow changing direction of span selector --- lib/matplotlib/widgets.py | 52 ++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index ca9e75aa43da..f71c2b7fe5eb 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1989,7 +1989,6 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, rectprops['animated'] = self.useblit - _api.check_in_list(['horizontal', 'vertical'], direction=direction) self._direction = direction self._rect = None @@ -2010,23 +2009,19 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, # 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)) - self._edge_order = ['min', 'max'] - self._edge_handles = ToolLineHandles(self.ax, self.extents, - direction=direction, - line_props=props, - useblit=self.useblit) + if self._interactive: + self._edge_order = ['min', 'max'] + self._setup_edge_handle(props) self._active_handle = None - if self._interactive: - self.artists.extend([line for line in self._edge_handles.artists]) - # prev attritube is deprecated but we still need to maintain it self._prev = (0, 0) @@ -2066,7 +2061,17 @@ def new_axes(self, ax): **self._rectprops) self.ax.add_patch(self._rect) - self.artists = [self._rect] + if len(self.artists) > 0: + self.artists[0] = self._rect + else: + self.artists.append(self._rect) + + 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.""" @@ -2104,6 +2109,22 @@ 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 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) + def _release(self, event): """Button release event handler.""" if not self._interactive: @@ -2220,8 +2241,9 @@ def extents(self): def extents(self, extents): # Update displayed shape self._draw_shape(*extents) - # Update displayed handles - self._edge_handles.set_data(self.extents) + if self._interactive: + # Update displayed handles + self._edge_handles.set_data(self.extents) self.set_visible(self.visible) self.update() @@ -2254,6 +2276,7 @@ def __init__(self, ax, positions, direction, line_props=None, 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] @@ -2292,6 +2315,11 @@ def set_animated(self, value): 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. From 7abf57c6dbd03cffa42f07168c9dacce5a9d8ff3 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 29 Jun 2021 10:44:43 +0100 Subject: [PATCH 33/35] Fix typos --- lib/matplotlib/widgets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f71c2b7fe5eb..939ea19ff7a8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1924,7 +1924,8 @@ class SpanSelector(_SelectorWidget): The direction along which to draw the span selector. minspan : float, default: 0 - If selection is less than or egal to *minspan*, do not call *onselect*. + 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 @@ -1949,7 +1950,7 @@ class SpanSelector(_SelectorWidget): 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 + when *interactive* is True. See `matplotlib.lines.Line2D` for details on valid properties. maxdist : float, default: 10 @@ -2022,7 +2023,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self._active_handle = None - # prev attritube is deprecated but we still need to maintain it + # prev attribute is deprecated but we still need to maintain it self._prev = (0, 0) rect = _api.deprecate_privatize_attribute("3.5") From e280ed9fe2a6fbacc716a42c6f86a78017a1069a Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 30 Jun 2021 07:14:06 +0100 Subject: [PATCH 34/35] Use deprecate_privatize_attribute in favour of deprecate to simplify code --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 939ea19ff7a8..22ec3259f685 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2038,7 +2038,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, property(lambda self: self._interactive) ) - prev = _api.deprecated("3.5")(property(lambda self: self._prev)) + prev = _api.deprecate_privatize_attribute("3.5") def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" From 92d1705e076355862e3c60fbb7aa93c26875bd38 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 30 Jun 2021 09:35:50 +0100 Subject: [PATCH 35/35] Use the property setter of direction when initialising a SpanSelector and add test. --- lib/matplotlib/tests/test_widgets.py | 21 +++++++++++++++++++++ lib/matplotlib/widgets.py | 6 ++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 5e0dc388b9b6..f12f6d72a817 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -259,6 +259,27 @@ def onselect(epress, erelease): 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() diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 22ec3259f685..8dbe5d2f2707 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1990,7 +1990,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, rectprops['animated'] = self.useblit - self._direction = direction + self.direction = direction self._rect = None self.visible = True @@ -2114,7 +2114,7 @@ def direction(self): def direction(self, direction): """Set the direction of the span selector.""" _api.check_in_list(['horizontal', 'vertical'], direction=direction) - if direction != self._direction: + if hasattr(self, '_direction') and direction != self._direction: # remove previous artists self._rect.remove() if self._interactive: @@ -2125,6 +2125,8 @@ def direction(self, direction): self.new_axes(self.ax) if self._interactive: self._setup_edge_handle(self._edge_handles._line_props) + else: + self._direction = direction def _release(self, event): """Button release event handler."""