From 233a05d476c8c23a75de442d0f2e54ba86586ea3 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 20 Jul 2021 10:47:54 +0100 Subject: [PATCH 1/8] Protect `Selector.artists` attribute --- lib/matplotlib/widgets.py | 212 ++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 101 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 2b8b1d4a8f16..d6e3140f7d8e 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1800,7 +1800,8 @@ def __init__(self, ax, onselect, useblit=False, button=None, self.state_modifier_keys.update(state_modifier_keys or {}) self.background = None - self.artists = [] + # The first item of this list is the main artist (rectangle, line, etc) + self._artists = [] if isinstance(button, Integral): self.validButtons = [button] @@ -2009,6 +2010,14 @@ def set_visible(self, visible): for artist in self.artists: artist.set_visible(visible) + def add_artist(self, artist): + """Add artist to the selector.""" + self._artists.append(artist) + + @property + def artists(self): + return tuple(self._artists) + class SpanSelector(_SelectorWidget): """ @@ -2114,7 +2123,6 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.direction = direction - self._rect = None self.visible = True self._extents_on_press = None @@ -2133,7 +2141,6 @@ 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 @@ -2185,16 +2192,16 @@ 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._props) + rect_artist = Rectangle((0, 0), w, h, + transform=trans, + visible=False, + **self._props) - self.ax.add_patch(self._rect) + self.ax.add_patch(rect_artist) if len(self.artists) > 0: - self.artists[0] = self._rect + self._artists[0] = rect_artist else: - self.artists.append(self._rect) + self.add_artist(rect_artist) def _setup_edge_handle(self, props): # Define initial position using the axis bounds to keep the same bounds @@ -2206,7 +2213,7 @@ def _setup_edge_handle(self, props): direction=self.direction, line_props=props, useblit=self.useblit) - self.artists.extend([line for line in self._edge_handles.artists]) + self._artists.extend([line for line in self._edge_handles._artists]) def _set_cursor(self, enabled): """Update the canvas cursor based on direction of the selector.""" @@ -2228,7 +2235,7 @@ def connect_default_events(self): def _press(self, event): """Button press event handler.""" self._set_cursor(True) - if self._interactive and self._rect.get_visible(): + if self._interactive and self.artists[0].get_visible(): self._set_active_handle(event) else: self._active_handle = None @@ -2268,11 +2275,11 @@ def direction(self, direction): _api.check_in_list(['horizontal', 'vertical'], direction=direction) if hasattr(self, '_direction') and direction != self._direction: # remove previous artists - self._rect.remove() + self._artists[0].remove() if self._interactive: self._edge_handles.remove() - for artist in self._edge_handles.artists: - self.artists.remove(artist) + for artist in self._edge_handles._artists: + self._artists.remove(artist) self._direction = direction self.new_axes(self.ax) if self._interactive: @@ -2287,7 +2294,7 @@ def _release(self, event): self._pressv = None if not self._interactive: - self._rect.set_visible(False) + self.artists[0].set_visible(False) if (self._active_handle is None and self._selection_completed and self.ignore_event_outside): @@ -2375,11 +2382,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.artists[0].set_x(vmin) + self.artists[0].set_width(vmax - vmin) else: - self._rect.set_y(vmin) - self._rect.set_height(vmax - vmin) + self.artists[0].set_y(vmin) + self.artists[0].set_height(vmax - vmin) def _set_active_handle(self, event): """Set active handle based on the location of the mouse event.""" @@ -2409,17 +2416,17 @@ 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.artists[0].contains(event, radius=0)[0] @property def extents(self): """Return extents of the span selector.""" if self.direction == 'horizontal': - vmin = self._rect.get_x() - vmax = vmin + self._rect.get_width() + vmin = self.artists[0].get_x() + vmax = vmin + self.artists[0].get_width() else: - vmin = self._rect.get_y() - vmax = vmin + self._rect.get_height() + vmin = self.artists[0].get_y() + vmax = vmin + self.artists[0].get_height() return vmin, vmax @extents.setter @@ -2466,7 +2473,11 @@ def __init__(self, ax, positions, direction, line_props=None, 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] + self._artists = [line_fun(p, **line_props) for p in positions] + + @property + def artists(self): + return tuple(self._artists) @property def positions(self): @@ -2505,7 +2516,7 @@ def set_animated(self, value): def remove(self): """Remove the handles artist from the figure.""" - for artist in self.artists: + for artist in self._artists: artist.remove() def closest(self, x, y): @@ -2726,7 +2737,6 @@ def __init__(self, ax, onselect, drawtype='box', super().__init__(ax, onselect, useblit=useblit, button=button, state_modifier_keys=state_modifier_keys) - self._to_draw = None self.visible = True self._interactive = interactive self.drag_from_anywhere = drag_from_anywhere @@ -2748,9 +2758,8 @@ def __init__(self, ax, onselect, drawtype='box', props['animated'] = self.useblit _props = props self.visible = _props.pop('visible', self.visible) - self._to_draw = self._shape_klass((0, 0), 0, 1, visible=False, - **_props) - self.ax.add_patch(self._to_draw) + to_draw = self._shape_klass((0, 0), 0, 1, visible=False, **_props) + self.ax.add_patch(to_draw) if drawtype == 'line': _api.warn_deprecated( "3.5", message="Support for drawtype='line' is deprecated " @@ -2761,9 +2770,10 @@ def __init__(self, ax, onselect, drawtype='box', 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) + to_draw = Line2D([0, 0], [0, 0], visible=False, **self.lineprops) + self.ax.add_line(to_draw) + + self._artists = [to_draw] self.minspanx = minspanx self.minspany = minspany @@ -2774,35 +2784,33 @@ def __init__(self, ax, onselect, drawtype='box', self.grab_range = grab_range - handle_props = { - 'markeredgecolor': (props or {}).get('edgecolor', 'black'), - **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} - - self._corner_order = ['NW', 'NE', 'SE', 'SW'] - xc, yc = self.corners - self._corner_handles = ToolHandles(self.ax, xc, yc, - marker_props=handle_props, - useblit=self.useblit) - - self._edge_order = ['W', 'N', 'E', 'S'] - xe, ye = self.edge_centers - self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', - marker_props=handle_props, - useblit=self.useblit) - - xc, yc = self.center - self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', - marker_props=handle_props, - useblit=self.useblit) + if self._interactive: + handle_props = { + 'markeredgecolor': (props or {}).get('edgecolor', 'black'), + **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} + + self._corner_order = ['NW', 'NE', 'SE', 'SW'] + xc, yc = self.corners + self._corner_handles = ToolHandles(self.ax, xc, yc, + marker_props=handle_props, + useblit=self.useblit) + + self._edge_order = ['W', 'N', 'E', 'S'] + xe, ye = self.edge_centers + self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', + marker_props=handle_props, + useblit=self.useblit) - self._active_handle = None + xc, yc = self.center + self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', + marker_props=handle_props, + useblit=self.useblit) - self.artists = [self._to_draw, self._center_handle.artist, - self._corner_handles.artist, - self._edge_handles.artist] + self._active_handle = None - if not self._interactive: - self.artists = [self._to_draw] + self._artists.extend([self._center_handle.artist, + self._corner_handles.artist, + self._edge_handles.artist]) self._extents_on_press = None @@ -2822,7 +2830,7 @@ 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.artists[0].get_visible(): self._set_active_handle(event) else: self._active_handle = None @@ -2845,7 +2853,7 @@ def _press(self, event): def _release(self, event): """Button release event handler.""" if not self._interactive: - self._to_draw.set_visible(False) + self.artists[0].set_visible(False) if (self._active_handle is None and self._selection_completed and self.ignore_event_outside): @@ -2955,13 +2963,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.artists[0].get_x() + y0 = self.artists[0].get_y() + width = self.artists[0].get_width() + height = self.artists[0].get_height() return x0, y0, width, height else: - x, y = self._to_draw.get_data() + x, y = self.artists[0].get_data() x0, x1 = min(x), max(x) y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0 @@ -3002,10 +3010,11 @@ def extents(self): def extents(self, extents): # Update displayed shape self._draw_shape(extents) - # Update displayed handles - self._corner_handles.set_data(*self.corners) - self._edge_handles.set_data(*self.edge_centers) - self._center_handle.set_data(*self.center) + if self._interactive: + # Update displayed handles + self._corner_handles.set_data(*self.corners) + self._edge_handles.set_data(*self.edge_centers) + self._center_handle.set_data(*self.center) self.set_visible(self.visible) self.update() @@ -3024,13 +3033,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.artists[0].set_x(xmin) + self.artists[0].set_y(ymin) + self.artists[0].set_width(xmax - xmin) + self.artists[0].set_height(ymax - ymin) elif self._drawtype == 'line': - self._to_draw.set_data([xmin, xmax], [ymin, ymax]) + self.artists[0].set_data([xmin, xmax], [ymin, ymax]) def _set_active_handle(self, event): """Set active handle based on the location of the mouse event.""" @@ -3074,7 +3083,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.artists[0].contains(event, radius=0)[0] @property def geometry(self): @@ -3085,12 +3094,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.artists[0], 'get_verts'): xfm = self.ax.transData.inverted() - y, x = xfm.transform(self._to_draw.get_verts()).T + y, x = xfm.transform(self.artists[0].get_verts()).T return np.array([x, y]) else: - return np.array(self._to_draw.get_data()) + return np.array(self.artists[0].get_data()) @docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( @@ -3124,24 +3133,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.artists[0].center = center + self.artists[0].width = 2 * a + self.artists[0].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.artists[0].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.artists[0].center + width = self.artists[0].width + height = self.artists[0].height return x - width / 2., y - height / 2., width, height else: - x, y = self._to_draw.get_data() + x, y = self.artists[0].get_data() x0, x1 = min(x), max(x) y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0 @@ -3196,9 +3205,9 @@ def __init__(self, ax, onselect=None, useblit=True, props=None, props = dict() # self.useblit may be != useblit, if the canvas doesn't support blit. props.update(animated=self.useblit, visible=False) - self.line = Line2D([], [], **props) - self.ax.add_line(self.line) - self.artists = [self.line] + line = Line2D([], [], **props) + self.ax.add_line(line) + self.add_artist(line) @_api.deprecated("3.5", alternative="press") def onpress(self, event): @@ -3206,7 +3215,7 @@ def onpress(self, event): def _press(self, event): self.verts = [self._get_data(event)] - self.line.set_visible(True) + self.artists[0].set_visible(True) @_api.deprecated("3.5", alternative="release") def onrelease(self, event): @@ -3216,15 +3225,16 @@ def _release(self, event): if self.verts is not None: self.verts.append(self._get_data(event)) self.onselect(self.verts) - self.line.set_data([[], []]) - self.line.set_visible(False) + self.artists[0].set_data([[], []]) + self.artists[0].set_visible(False) self.verts = None def _onmove(self, event): if self.verts is None: return self.verts.append(self._get_data(event)) - self.line.set_data(list(zip(*self.verts))) + self.artists[0].set_data(list(zip(*self.verts))) + self.update() @@ -3312,8 +3322,8 @@ def __init__(self, ax, onselect, useblit=False, if props is None: props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) props['animated'] = self.useblit - self.line = Line2D(self._xs, self._ys, **props) - self.ax.add_line(self.line) + line = Line2D(self._xs, self._ys, **props) + self.ax.add_line(line) if handle_props is None: handle_props = dict(markeredgecolor='k', @@ -3325,7 +3335,7 @@ def __init__(self, ax, onselect, useblit=False, self._active_handle_idx = -1 self.grab_range = grab_range - self.artists = [self.line, self._polygon_handles.artist] + self._artists = [line, self._polygon_handles.artist] self.set_visible(True) vertex_select_radius = _api.deprecated("3.5", name="vertex_select_radius", @@ -3437,8 +3447,8 @@ def _onmove(self, event): # Position pending vertex. else: # Calculate distance to the start vertex. - x0, y0 = self.line.get_transform().transform((self._xs[0], - self._ys[0])) + x0, y0 = self.artists[0].get_transform().transform((self._xs[0], + self._ys[0])) v0_dist = np.hypot(x0 - event.x, y0 - event.y) # Lock on to the start vertex if near it and ready to complete. if len(self._xs) > 3 and v0_dist < self.grab_range: @@ -3478,7 +3488,7 @@ def _on_key_release(self, event): def _draw_polygon(self): """Redraw the polygon based on the new vertex positions.""" - self.line.set_data(self._xs, self._ys) + self.artists[0].set_data(self._xs, self._ys) # Only show one tool handle at the start and end vertex of the polygon # if the polygon is completed or the user is locked on to the start # vertex. From 9d5f71f8d45fc46d945fbbd9e7204a957696c7c9 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 20 Jul 2021 11:42:07 +0100 Subject: [PATCH 2/8] Add `set_props` and `set_handle_props` to selectors. --- lib/matplotlib/tests/test_widgets.py | 103 ++++++++++++++++++++++++++- lib/matplotlib/widgets.py | 53 +++++++++++--- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index e6df7cfb68b8..c5ace9ce8543 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -90,6 +90,36 @@ def onselect(epress, erelease): assert tool.center == (180, 190) +def test_rectangle_selector_set_props_handle_props(): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect, interactive=True, + props=dict(facecolor='b', alpha=0.2), + handle_props=dict(alpha=0.5)) + # Create rectangle + do_event(tool, 'press', xdata=0, ydata=10, button=1) + do_event(tool, 'onmove', xdata=100, ydata=120, button=1) + do_event(tool, 'release', xdata=100, ydata=120, button=1) + + artist = tool.artists[0] + assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) + props = dict(facecolor='r', alpha=0.3) + tool.set_props(**props) + assert artist.get_facecolor() == mcolors.to_rgba(*props.values()) + + for artist in tool._handles_artists: + assert artist.get_markeredgecolor() == 'black' + assert artist.get_alpha() == 0.5 + handle_props = dict(markeredgecolor='r', alpha=0.3) + tool.set_handle_props(**handle_props) + for artist in tool._handles_artists: + assert artist.get_markeredgecolor() == 'r' + assert artist.get_alpha() == 0.3 + + def test_ellipse(): """For ellipse, test out the key modifiers""" ax = get_ax() @@ -185,9 +215,9 @@ def onselect(epress, erelease): # Check that marker_props worked. assert mcolors.same_color( - tool._corner_handles.artist.get_markerfacecolor(), 'r') + tool._corner_handles.artists[0].get_markerfacecolor(), 'r') assert mcolors.same_color( - tool._corner_handles.artist.get_markeredgecolor(), 'b') + tool._corner_handles.artists[0].get_markeredgecolor(), 'b') @pytest.mark.parametrize('interactive', [True, False]) @@ -404,6 +434,36 @@ def onselect(*args): tool.direction = 'invalid_string' +def test_span_selector_set_props_handle_props(): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.SpanSelector(ax, onselect, 'horizontal', interactive=True, + props=dict(facecolor='b', alpha=0.2), + handle_props=dict(alpha=0.5)) + # Create rectangle + do_event(tool, 'press', xdata=0, ydata=10, button=1) + do_event(tool, 'onmove', xdata=100, ydata=120, button=1) + do_event(tool, 'release', xdata=100, ydata=120, button=1) + + artist = tool.artists[0] + assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) + props = dict(facecolor='r', alpha=0.3) + tool.set_props(**props) + assert artist.get_facecolor() == mcolors.to_rgba(*props.values()) + + for artist in tool._handles_artists: + assert artist.get_color() == 'b' + assert artist.get_alpha() == 0.5 + handle_props = dict(color='r', alpha=0.3) + tool.set_handle_props(**handle_props) + for artist in tool._handles_artists: + assert artist.get_color() == 'r' + assert artist.get_alpha() == 0.3 + + def test_tool_line_handle(): ax = get_ax() @@ -781,6 +841,45 @@ def test_polygon_selector(): check_polygon_selector(event_sequence, expected_result, 1) +def test_polygon_selector_set_props_handle_props(): + ax = get_ax() + + ax._selections_count = 0 + + def onselect(vertices): + ax._selections_count += 1 + ax._current_result = vertices + + tool = widgets.PolygonSelector(ax, onselect, + props=dict(color='b', alpha=0.2), + handle_props=dict(alpha=0.5)) + + event_sequence = (polygon_place_vertex(50, 50) + + polygon_place_vertex(150, 50) + + polygon_place_vertex(50, 150) + + polygon_place_vertex(50, 50)) + + for (etype, event_args) in event_sequence: + do_event(tool, etype, **event_args) + + artist = tool.artists[0] + assert artist.get_color() == 'b' + assert artist.get_alpha() == 0.2 + props = dict(color='r', alpha=0.3) + tool.set_props(**props) + assert artist.get_color() == props['color'] + assert artist.get_alpha() == props['alpha'] + + for artist in tool._handles_artists: + assert artist.get_color() == 'b' + assert artist.get_alpha() == 0.5 + handle_props = dict(color='r', alpha=0.3) + tool.set_handle_props(**handle_props) + for artist in tool._handles_artists: + assert artist.get_color() == 'r' + assert artist.get_alpha() == 0.3 + + @pytest.mark.parametrize( "horizOn, vertOn", [(True, True), (True, False), (False, True)], diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index d6e3140f7d8e..1371f170c436 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2016,8 +2016,31 @@ def add_artist(self, artist): @property def artists(self): + """Tuple of the artists of the selector""" return tuple(self._artists) + def set_props(self, **props): + """ + Set the properties of the selector artist. See the `props` argument + in the selector docstring to know which properties are supported. + """ + self.artists[0].set(**props) + self.update() + self._props = props + + def set_handle_props(self, **handle_props): + """ + Set the properties of the handles selector artist. See the + `handle_props` argument in the selector docstring to know which + properties are supported. + """ + if not hasattr(self, '_handles_artists'): + raise NotImplementedError("This selector doesn't have handles.") + for handle in self._handles_artists: + handle.set(**handle_props) + self.update() + self._handle_props = handle_props + class SpanSelector(_SelectorWidget): """ @@ -2150,7 +2173,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, if self._interactive: self._edge_order = ['min', 'max'] - self._setup_edge_handle(handle_props) + self._setup_edge_handles(handle_props) self._active_handle = None @@ -2203,7 +2226,7 @@ def new_axes(self, ax): else: self.add_artist(rect_artist) - def _setup_edge_handle(self, props): + def _setup_edge_handles(self, props): # Define initial position using the axis bounds to keep the same bounds if self.direction == 'horizontal': positions = self.ax.get_xbound() @@ -2283,7 +2306,7 @@ def direction(self, direction): self._direction = direction self.new_axes(self.ax) if self._interactive: - self._setup_edge_handle(self._edge_handles._line_props) + self._setup_edge_handles(self._edge_handles._line_props) else: self._direction = direction @@ -2439,6 +2462,10 @@ def extents(self, extents): self.set_visible(self.visible) self.update() + @property + def _handles_artists(self): + return self._edge_handles.artists + class ToolLineHandles: """ @@ -2575,7 +2602,6 @@ def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True): **cbook.normalize_kwargs(marker_props, Line2D._alias_map)} self._markers = Line2D(x, y, animated=useblit, **props) self.ax.add_line(self._markers) - self.artist = self._markers @property def x(self): @@ -2585,6 +2611,10 @@ def x(self): def y(self): return self._markers.get_ydata() + @property + def artists(self): + return (self._markers, ) + def set_data(self, pts, y=None): """Set x and y positions of handles.""" if y is not None: @@ -2808,9 +2838,7 @@ def __init__(self, ax, onselect, drawtype='box', self._active_handle = None - self._artists.extend([self._center_handle.artist, - self._corner_handles.artist, - self._edge_handles.artist]) + self._artists.extend(self._handles_artists) self._extents_on_press = None @@ -2826,6 +2854,11 @@ def __init__(self, ax, onselect, drawtype='box', property(lambda self: self.grab_range, lambda self, value: setattr(self, "grab_range", value))) + @property + def _handles_artists(self): + return (self._center_handle.artists + self._corner_handles.artists + + self._edge_handles.artists) + def _press(self, event): """Button press event handler.""" # make the drawn box/line visible get the click-coordinates, @@ -3335,7 +3368,7 @@ def __init__(self, ax, onselect, useblit=False, self._active_handle_idx = -1 self.grab_range = grab_range - self._artists = [line, self._polygon_handles.artist] + self._artists = [line] + list(self._handles_artists) self.set_visible(True) vertex_select_radius = _api.deprecated("3.5", name="vertex_select_radius", @@ -3348,6 +3381,10 @@ def __init__(self, ax, onselect, useblit=False, def _nverts(self): return len(self._xs) + @property + def _handles_artists(self): + return self._polygon_handles.artists + def _remove_vertex(self, i): """Remove vertex with index i.""" if (self._nverts > 2 and From 0c06766a941c7e09caafc43a2b3bc8b5f6ff388b Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 27 Jul 2021 11:04:53 +0100 Subject: [PATCH 3/8] Fix deprecated attribute and add new feature entry --- .../setting_artists_properties_selector.rst | 5 +++++ lib/matplotlib/widgets.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 doc/users/next_whats_new/setting_artists_properties_selector.rst diff --git a/doc/users/next_whats_new/setting_artists_properties_selector.rst b/doc/users/next_whats_new/setting_artists_properties_selector.rst new file mode 100644 index 000000000000..dbf1a4333f0e --- /dev/null +++ b/doc/users/next_whats_new/setting_artists_properties_selector.rst @@ -0,0 +1,5 @@ +Setting artist properties of selectors +-------------------------------------- + +The artist properties of selectors can be changed using the ``set_props`` and +``set_handle_props`` methods. diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 1371f170c436..78527274f656 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2180,7 +2180,9 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, # prev attribute is deprecated but we still need to maintain it self._prev = (0, 0) - rect = _api.deprecate_privatize_attribute("3.5") + rect = _api.deprecated("3.5")( + property(lambda self: self.artists[0]) + ) rectprops = _api.deprecated("3.5")( property(lambda self: self._props) @@ -2842,7 +2844,9 @@ def __init__(self, ax, onselect, drawtype='box', self._extents_on_press = None - to_draw = _api.deprecate_privatize_attribute("3.5") + to_draw = _api.deprecated("3.5")( + property(lambda self: self.artists[0]) + ) drawtype = _api.deprecate_privatize_attribute("3.5") @@ -3371,6 +3375,10 @@ def __init__(self, ax, onselect, useblit=False, self._artists = [line] + list(self._handles_artists) self.set_visible(True) + line = _api.deprecated("3.5")( + property(lambda self: self.artists[0]) + ) + vertex_select_radius = _api.deprecated("3.5", name="vertex_select_radius", alternative="grab_range")( property(lambda self: self.grab_range, From b6e4e88d8ee8f6254a75d658f914cce40993f053 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 5 Aug 2021 09:30:44 +0100 Subject: [PATCH 4/8] Simplify access to selector artists --- lib/matplotlib/tests/test_widgets.py | 6 +- lib/matplotlib/widgets.py | 182 +++++++++++++-------------- 2 files changed, 93 insertions(+), 95 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index c5ace9ce8543..abb3cef06050 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -104,7 +104,7 @@ def onselect(epress, erelease): do_event(tool, 'onmove', xdata=100, ydata=120, button=1) do_event(tool, 'release', xdata=100, ydata=120, button=1) - artist = tool.artists[0] + artist = tool._selection_artist assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) props = dict(facecolor='r', alpha=0.3) tool.set_props(**props) @@ -448,7 +448,7 @@ def onselect(epress, erelease): do_event(tool, 'onmove', xdata=100, ydata=120, button=1) do_event(tool, 'release', xdata=100, ydata=120, button=1) - artist = tool.artists[0] + artist = tool._selection_artist assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) props = dict(facecolor='r', alpha=0.3) tool.set_props(**props) @@ -862,7 +862,7 @@ def onselect(vertices): for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) - artist = tool.artists[0] + artist = tool._selection_artist assert artist.get_color() == 'b' assert artist.get_alpha() == 0.2 props = dict(color='r', alpha=0.3) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 78527274f656..bcbcfd765c1c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1800,8 +1800,6 @@ def __init__(self, ax, onselect, useblit=False, button=None, self.state_modifier_keys.update(state_modifier_keys or {}) self.background = None - # The first item of this list is the main artist (rectangle, line, etc) - self._artists = [] if isinstance(button, Integral): self.validButtons = [button] @@ -2010,23 +2008,23 @@ def set_visible(self, visible): for artist in self.artists: artist.set_visible(visible) - def add_artist(self, artist): - """Add artist to the selector.""" - self._artists.append(artist) - @property def artists(self): - """Tuple of the artists of the selector""" - return tuple(self._artists) + """Tuple of the artists of the selector.""" + if getattr(self, '_handles_artists', None) is not None: + return (self._selection_artist, ) + self._handles_artists + else: + return (self._selection_artist, ) def set_props(self, **props): """ Set the properties of the selector artist. See the `props` argument in the selector docstring to know which properties are supported. """ - self.artists[0].set(**props) - self.update() - self._props = props + self._selection_artist.set(**props) + if self.useblit: + self.update() + self._props.update(props) def set_handle_props(self, **handle_props): """ @@ -2038,8 +2036,9 @@ def set_handle_props(self, **handle_props): raise NotImplementedError("This selector doesn't have handles.") for handle in self._handles_artists: handle.set(**handle_props) - self.update() - self._handle_props = handle_props + if self.useblit: + self.update() + self._handle_props.update(handle_props) class SpanSelector(_SelectorWidget): @@ -2159,6 +2158,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.grab_range = grab_range self._interactive = interactive + self._edge_handles = None self.drag_from_anywhere = drag_from_anywhere self.ignore_event_outside = ignore_event_outside @@ -2167,13 +2167,13 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self.new_axes(ax) # Setup handles - handle_props = { + self._handle_props = { 'color': props.get('facecolor', 'r'), **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} if self._interactive: self._edge_order = ['min', 'max'] - self._setup_edge_handles(handle_props) + self._setup_edge_handles(self._handle_props) self._active_handle = None @@ -2181,7 +2181,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, self._prev = (0, 0) rect = _api.deprecated("3.5")( - property(lambda self: self.artists[0]) + property(lambda self: self._selection_artist) ) rectprops = _api.deprecated("3.5")( @@ -2223,10 +2223,7 @@ def new_axes(self, ax): **self._props) self.ax.add_patch(rect_artist) - if len(self.artists) > 0: - self._artists[0] = rect_artist - else: - self.add_artist(rect_artist) + self._selection_artist = rect_artist def _setup_edge_handles(self, props): # Define initial position using the axis bounds to keep the same bounds @@ -2238,7 +2235,11 @@ def _setup_edge_handles(self, props): direction=self.direction, line_props=props, useblit=self.useblit) - self._artists.extend([line for line in self._edge_handles._artists]) + + @property + def _handles_artists(self): + if self._edge_handles is not None: + return self._edge_handles.artists def _set_cursor(self, enabled): """Update the canvas cursor based on direction of the selector.""" @@ -2260,7 +2261,7 @@ def connect_default_events(self): def _press(self, event): """Button press event handler.""" self._set_cursor(True) - if self._interactive and self.artists[0].get_visible(): + if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) else: self._active_handle = None @@ -2300,15 +2301,13 @@ def direction(self, direction): _api.check_in_list(['horizontal', 'vertical'], direction=direction) if hasattr(self, '_direction') and direction != self._direction: # remove previous artists - self._artists[0].remove() + self._selection_artist.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_handles(self._edge_handles._line_props) + self._setup_edge_handles(self._handle_props) else: self._direction = direction @@ -2319,7 +2318,7 @@ def _release(self, event): self._pressv = None if not self._interactive: - self.artists[0].set_visible(False) + self._selection_artist.set_visible(False) if (self._active_handle is None and self._selection_completed and self.ignore_event_outside): @@ -2407,11 +2406,11 @@ def _draw_shape(self, vmin, vmax): if vmin > vmax: vmin, vmax = vmax, vmin if self.direction == 'horizontal': - self.artists[0].set_x(vmin) - self.artists[0].set_width(vmax - vmin) + self._selection_artist.set_x(vmin) + self._selection_artist.set_width(vmax - vmin) else: - self.artists[0].set_y(vmin) - self.artists[0].set_height(vmax - vmin) + self._selection_artist.set_y(vmin) + self._selection_artist.set_height(vmax - vmin) def _set_active_handle(self, event): """Set active handle based on the location of the mouse event.""" @@ -2441,17 +2440,17 @@ def _set_active_handle(self, event): def _contains(self, event): """Return True if event is within the patch.""" - return self.artists[0].contains(event, radius=0)[0] + return self._selection_artist.contains(event, radius=0)[0] @property def extents(self): """Return extents of the span selector.""" if self.direction == 'horizontal': - vmin = self.artists[0].get_x() - vmax = vmin + self.artists[0].get_width() + vmin = self._selection_artist.get_x() + vmax = vmin + self._selection_artist.get_width() else: - vmin = self.artists[0].get_y() - vmax = vmin + self.artists[0].get_height() + vmin = self._selection_artist.get_y() + vmax = vmin + self._selection_artist.get_height() return vmin, vmax @extents.setter @@ -2464,10 +2463,6 @@ def extents(self, extents): self.set_visible(self.visible) self.update() - @property - def _handles_artists(self): - return self._edge_handles.artists - class ToolLineHandles: """ @@ -2500,7 +2495,6 @@ 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] @@ -2788,9 +2782,10 @@ def __init__(self, ax, onselect, drawtype='box', props = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) props['animated'] = self.useblit - _props = props - self.visible = _props.pop('visible', self.visible) - to_draw = self._shape_klass((0, 0), 0, 1, visible=False, **_props) + self.visible = props.pop('visible', self.visible) + self._props = props + to_draw = self._shape_klass((0, 0), 0, 1, visible=False, + **self._props) self.ax.add_patch(to_draw) if drawtype == 'line': _api.warn_deprecated( @@ -2801,11 +2796,11 @@ def __init__(self, ax, onselect, drawtype='box', lineprops = dict(color='black', linestyle='-', linewidth=2, alpha=0.5) lineprops['animated'] = self.useblit - self.lineprops = lineprops - to_draw = Line2D([0, 0], [0, 0], visible=False, **self.lineprops) + self._props = lineprops + to_draw = Line2D([0, 0], [0, 0], visible=False, **self._props) self.ax.add_line(to_draw) - self._artists = [to_draw] + self._selection_artist = to_draw self.minspanx = minspanx self.minspany = minspany @@ -2817,35 +2812,34 @@ def __init__(self, ax, onselect, drawtype='box', self.grab_range = grab_range if self._interactive: - handle_props = { - 'markeredgecolor': (props or {}).get('edgecolor', 'black'), + self._handle_props = { + 'markeredgecolor': (self._props or {}).get( + 'edgecolor', 'black'), **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} self._corner_order = ['NW', 'NE', 'SE', 'SW'] xc, yc = self.corners self._corner_handles = ToolHandles(self.ax, xc, yc, - marker_props=handle_props, + marker_props=self._handle_props, useblit=self.useblit) self._edge_order = ['W', 'N', 'E', 'S'] xe, ye = self.edge_centers self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', - marker_props=handle_props, + marker_props=self._handle_props, useblit=self.useblit) xc, yc = self.center self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', - marker_props=handle_props, + marker_props=self._handle_props, useblit=self.useblit) self._active_handle = None - self._artists.extend(self._handles_artists) - self._extents_on_press = None to_draw = _api.deprecated("3.5")( - property(lambda self: self.artists[0]) + property(lambda self: self._selection_artist) ) drawtype = _api.deprecate_privatize_attribute("3.5") @@ -2867,7 +2861,7 @@ def _press(self, event): """Button press event handler.""" # make the drawn box/line visible get the click-coordinates, # button, ... - if self._interactive and self.artists[0].get_visible(): + if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) else: self._active_handle = None @@ -2890,7 +2884,7 @@ def _press(self, event): def _release(self, event): """Button release event handler.""" if not self._interactive: - self.artists[0].set_visible(False) + self._selection_artist.set_visible(False) if (self._active_handle is None and self._selection_completed and self.ignore_event_outside): @@ -3000,13 +2994,13 @@ def _onmove(self, event): @property def _rect_bbox(self): if self._drawtype == 'box': - x0 = self.artists[0].get_x() - y0 = self.artists[0].get_y() - width = self.artists[0].get_width() - height = self.artists[0].get_height() + x0 = self._selection_artist.get_x() + y0 = self._selection_artist.get_y() + width = self._selection_artist.get_width() + height = self._selection_artist.get_height() return x0, y0, width, height else: - x, y = self.artists[0].get_data() + x, y = self._selection_artist.get_data() x0, x1 = min(x), max(x) y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0 @@ -3070,13 +3064,13 @@ def _draw_shape(self, extents): ymax = min(ymax, ylim[1]) if self._drawtype == 'box': - self.artists[0].set_x(xmin) - self.artists[0].set_y(ymin) - self.artists[0].set_width(xmax - xmin) - self.artists[0].set_height(ymax - ymin) + self._selection_artist.set_x(xmin) + self._selection_artist.set_y(ymin) + self._selection_artist.set_width(xmax - xmin) + self._selection_artist.set_height(ymax - ymin) elif self._drawtype == 'line': - self.artists[0].set_data([xmin, xmax], [ymin, ymax]) + self._selection_artist.set_data([xmin, xmax], [ymin, ymax]) def _set_active_handle(self, event): """Set active handle based on the location of the mouse event.""" @@ -3120,7 +3114,7 @@ def _set_active_handle(self, event): def _contains(self, event): """Return True if event is within the patch.""" - return self.artists[0].contains(event, radius=0)[0] + return self._selection_artist.contains(event, radius=0)[0] @property def geometry(self): @@ -3131,12 +3125,12 @@ def geometry(self): of the four corners of the rectangle starting and ending in the top left corner. """ - if hasattr(self.artists[0], 'get_verts'): + if hasattr(self._selection_artist, 'get_verts'): xfm = self.ax.transData.inverted() - y, x = xfm.transform(self.artists[0].get_verts()).T + y, x = xfm.transform(self._selection_artist.get_verts()).T return np.array([x, y]) else: - return np.array(self.artists[0].get_data()) + return np.array(self._selection_artist.get_data()) @docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( @@ -3170,24 +3164,24 @@ def _draw_shape(self, extents): b = (ymax - ymin) / 2. if self._drawtype == 'box': - self.artists[0].center = center - self.artists[0].width = 2 * a - self.artists[0].height = 2 * b + self._selection_artist.center = center + self._selection_artist.width = 2 * a + self._selection_artist.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.artists[0].set_data(x, y) + self._selection_artist.set_data(x, y) @property def _rect_bbox(self): if self._drawtype == 'box': - x, y = self.artists[0].center - width = self.artists[0].width - height = self.artists[0].height + x, y = self._selection_artist.center + width = self._selection_artist.width + height = self._selection_artist.height return x - width / 2., y - height / 2., width, height else: - x, y = self.artists[0].get_data() + x, y = self._selection_artist.get_data() x0, x1 = min(x), max(x) y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0 @@ -3244,7 +3238,7 @@ def __init__(self, ax, onselect=None, useblit=True, props=None, props.update(animated=self.useblit, visible=False) line = Line2D([], [], **props) self.ax.add_line(line) - self.add_artist(line) + self._selection_artist = line @_api.deprecated("3.5", alternative="press") def onpress(self, event): @@ -3252,7 +3246,7 @@ def onpress(self, event): def _press(self, event): self.verts = [self._get_data(event)] - self.artists[0].set_visible(True) + self._selection_artist.set_visible(True) @_api.deprecated("3.5", alternative="release") def onrelease(self, event): @@ -3262,15 +3256,15 @@ def _release(self, event): if self.verts is not None: self.verts.append(self._get_data(event)) self.onselect(self.verts) - self.artists[0].set_data([[], []]) - self.artists[0].set_visible(False) + self._selection_artist.set_data([[], []]) + self._selection_artist.set_visible(False) self.verts = None def _onmove(self, event): if self.verts is None: return self.verts.append(self._get_data(event)) - self.artists[0].set_data(list(zip(*self.verts))) + self._selection_artist.set_data(list(zip(*self.verts))) self.update() @@ -3359,24 +3353,26 @@ def __init__(self, ax, onselect, useblit=False, if props is None: props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) props['animated'] = self.useblit - line = Line2D(self._xs, self._ys, **props) + self._props = props + line = Line2D(self._xs, self._ys, **self._props) self.ax.add_line(line) + self._selection_artist = line if handle_props is None: handle_props = dict(markeredgecolor='k', - markerfacecolor=props.get('color', 'k')) + markerfacecolor=self._props.get('color', 'k')) + self._handle_props = handle_props self._polygon_handles = ToolHandles(self.ax, self._xs, self._ys, useblit=self.useblit, - marker_props=handle_props) + marker_props=self._handle_props) self._active_handle_idx = -1 self.grab_range = grab_range - self._artists = [line] + list(self._handles_artists) self.set_visible(True) line = _api.deprecated("3.5")( - property(lambda self: self.artists[0]) + property(lambda self: self._selection_artist) ) vertex_select_radius = _api.deprecated("3.5", name="vertex_select_radius", @@ -3492,8 +3488,10 @@ def _onmove(self, event): # Position pending vertex. else: # Calculate distance to the start vertex. - x0, y0 = self.artists[0].get_transform().transform((self._xs[0], - self._ys[0])) + x0, y0 = self._selection_artist.get_transform().transform( + (self._xs[0], + self._ys[0]) + ) v0_dist = np.hypot(x0 - event.x, y0 - event.y) # Lock on to the start vertex if near it and ready to complete. if len(self._xs) > 3 and v0_dist < self.grab_range: @@ -3533,7 +3531,7 @@ def _on_key_release(self, event): def _draw_polygon(self): """Redraw the polygon based on the new vertex positions.""" - self.artists[0].set_data(self._xs, self._ys) + self._selection_artist.set_data(self._xs, self._ys) # Only show one tool handle at the start and end vertex of the polygon # if the polygon is completed or the user is locked on to the start # vertex. From a871ca646181a7ab3ceefe96be2686f208ee92bb Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 5 Aug 2021 16:17:34 +0100 Subject: [PATCH 5/8] Normalise kwargs when setting selector properties --- lib/matplotlib/widgets.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index bcbcfd765c1c..e5950b5ef5ea 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2021,7 +2021,11 @@ def set_props(self, **props): Set the properties of the selector artist. See the `props` argument in the selector docstring to know which properties are supported. """ - self._selection_artist.set(**props) + artist = self._selection_artist + alias_map = getattr(artist, '_alias_map', None) + if alias_map: + props = cbook.normalize_kwargs(props, alias_map) + artist.set(**props) if self.useblit: self.update() self._props.update(props) @@ -2034,6 +2038,11 @@ def set_handle_props(self, **handle_props): """ if not hasattr(self, '_handles_artists'): raise NotImplementedError("This selector doesn't have handles.") + + artist = self._handles_artists[0] + alias_map = getattr(artist, '_alias_map', None) + if alias_map: + handle_props = cbook.normalize_kwargs(handle_props, alias_map) for handle in self._handles_artists: handle.set(**handle_props) if self.useblit: From 768c5839f39a7383619465235c0bf4ba685df74f Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 21 Aug 2021 11:52:53 +0100 Subject: [PATCH 6/8] Apply suggestions from code review: improve normalisation kwargs Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/widgets.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e5950b5ef5ea..a40bbe2a151d 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2022,9 +2022,7 @@ def set_props(self, **props): in the selector docstring to know which properties are supported. """ artist = self._selection_artist - alias_map = getattr(artist, '_alias_map', None) - if alias_map: - props = cbook.normalize_kwargs(props, alias_map) + props = cbook.normalize_kwargs(props, artist) artist.set(**props) if self.useblit: self.update() @@ -2040,9 +2038,7 @@ def set_handle_props(self, **handle_props): raise NotImplementedError("This selector doesn't have handles.") artist = self._handles_artists[0] - alias_map = getattr(artist, '_alias_map', None) - if alias_map: - handle_props = cbook.normalize_kwargs(handle_props, alias_map) + handle_props = cbook.normalize_kwargs(handle_props, artist) for handle in self._handles_artists: handle.set(**handle_props) if self.useblit: @@ -2178,7 +2174,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False, # Setup handles self._handle_props = { 'color': props.get('facecolor', 'r'), - **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} + **cbook.normalize_kwargs(handle_props, Line2D)} if self._interactive: self._edge_order = ['min', 'max'] @@ -2824,7 +2820,7 @@ def __init__(self, ax, onselect, drawtype='box', self._handle_props = { 'markeredgecolor': (self._props or {}).get( 'edgecolor', 'black'), - **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} + **cbook.normalize_kwargs(handle_props, Line2D)} self._corner_order = ['NW', 'NE', 'SE', 'SW'] xc, yc = self.corners @@ -2863,8 +2859,8 @@ def __init__(self, ax, onselect, drawtype='box', @property def _handles_artists(self): - return (self._center_handle.artists + self._corner_handles.artists + - self._edge_handles.artists) + return (*self._center_handle.artists, *self._corner_handles.artists, + *self._edge_handles.artists) def _press(self, event): """Button press event handler.""" @@ -3498,8 +3494,7 @@ def _onmove(self, event): else: # Calculate distance to the start vertex. x0, y0 = self._selection_artist.get_transform().transform( - (self._xs[0], - self._ys[0]) + (self._xs[0], self._ys[0]) ) v0_dist = np.hypot(x0 - event.x, y0 - event.y) # Lock on to the start vertex if near it and ready to complete. From cee023ee9bb470d9fe281583daa28b1da56ca5c5 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 21 Aug 2021 12:01:12 +0100 Subject: [PATCH 7/8] Add api change note for the deprecation of line attribute of PolygonSelector. --- doc/api/next_api_changes/deprecations/20693-EP.rst | 4 ++++ lib/matplotlib/widgets.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/api/next_api_changes/deprecations/20693-EP.rst diff --git a/doc/api/next_api_changes/deprecations/20693-EP.rst b/doc/api/next_api_changes/deprecations/20693-EP.rst new file mode 100644 index 000000000000..7d63e27fdd4f --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20693-EP.rst @@ -0,0 +1,4 @@ +PolygonSelector +^^^^^^^^^^^^^^^ +The *line* attribute is deprecated. If you want to change the selector +artist properties, use the ``set_props`` or ``set_handle_props`` methods. diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a40bbe2a151d..825c6ca0878d 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2012,7 +2012,7 @@ def set_visible(self, visible): def artists(self): """Tuple of the artists of the selector.""" if getattr(self, '_handles_artists', None) is not None: - return (self._selection_artist, ) + self._handles_artists + return (self._selection_artist, *self._handles_artists) else: return (self._selection_artist, ) From 893e7cf1cf38e914906757f6807270863f7e2cab Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 24 Aug 2021 15:57:05 +0100 Subject: [PATCH 8/8] Simplify syntax and improve readability Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/tests/test_widgets.py | 26 ++++++++++---------------- lib/matplotlib/widgets.py | 8 ++++---- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index abb3cef06050..36e2209c7fd2 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -106,15 +106,13 @@ def onselect(epress, erelease): artist = tool._selection_artist assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) - props = dict(facecolor='r', alpha=0.3) - tool.set_props(**props) - assert artist.get_facecolor() == mcolors.to_rgba(*props.values()) + tool.set_props(facecolor='r', alpha=0.3) + assert artist.get_facecolor() == mcolors.to_rgba('r', alpha=0.3) for artist in tool._handles_artists: assert artist.get_markeredgecolor() == 'black' assert artist.get_alpha() == 0.5 - handle_props = dict(markeredgecolor='r', alpha=0.3) - tool.set_handle_props(**handle_props) + tool.set_handle_props(markeredgecolor='r', alpha=0.3) for artist in tool._handles_artists: assert artist.get_markeredgecolor() == 'r' assert artist.get_alpha() == 0.3 @@ -450,15 +448,13 @@ def onselect(epress, erelease): artist = tool._selection_artist assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) - props = dict(facecolor='r', alpha=0.3) - tool.set_props(**props) - assert artist.get_facecolor() == mcolors.to_rgba(*props.values()) + tool.set_props(facecolor='r', alpha=0.3) + assert artist.get_facecolor() == mcolors.to_rgba('r', alpha=0.3) for artist in tool._handles_artists: assert artist.get_color() == 'b' assert artist.get_alpha() == 0.5 - handle_props = dict(color='r', alpha=0.3) - tool.set_handle_props(**handle_props) + tool.set_handle_props(color='r', alpha=0.3) for artist in tool._handles_artists: assert artist.get_color() == 'r' assert artist.get_alpha() == 0.3 @@ -865,16 +861,14 @@ def onselect(vertices): artist = tool._selection_artist assert artist.get_color() == 'b' assert artist.get_alpha() == 0.2 - props = dict(color='r', alpha=0.3) - tool.set_props(**props) - assert artist.get_color() == props['color'] - assert artist.get_alpha() == props['alpha'] + tool.set_props(color='r', alpha=0.3) + assert artist.get_color() == 'r' + assert artist.get_alpha() == 0.3 for artist in tool._handles_artists: assert artist.get_color() == 'b' assert artist.get_alpha() == 0.5 - handle_props = dict(color='r', alpha=0.3) - tool.set_handle_props(**handle_props) + tool.set_handle_props(color='r', alpha=0.3) for artist in tool._handles_artists: assert artist.get_color() == 'r' assert artist.get_alpha() == 0.3 diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 825c6ca0878d..48c096032017 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2011,10 +2011,8 @@ def set_visible(self, visible): @property def artists(self): """Tuple of the artists of the selector.""" - if getattr(self, '_handles_artists', None) is not None: - return (self._selection_artist, *self._handles_artists) - else: - return (self._selection_artist, ) + handles_artists = getattr(self, '_handles_artists', ()) + return (self._selection_artist,) + handles_artists def set_props(self, **props): """ @@ -2245,6 +2243,8 @@ def _setup_edge_handles(self, props): def _handles_artists(self): if self._edge_handles is not None: return self._edge_handles.artists + else: + return () def _set_cursor(self, enabled): """Update the canvas cursor based on direction of the selector."""