diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 208c426ba68e..678673b40fec 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -68,7 +68,7 @@ def _contour_labeler_event_handler(cs, inline, inline_spacing, event): elif (is_button and event.button == MouseButton.LEFT # On macOS/gtk, some keys return None. or is_key and event.key is not None): - if event.inaxes == cs.axes: + if cs.axes.contains(event)[0]: cs.add_label_near(event.x, event.y, transform=False, inline=inline, inline_spacing=inline_spacing) canvas.draw() diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 2f6910f5685e..e66758d394ac 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -643,6 +643,13 @@ def test_span_selector(ax, orientation, onmove_callback, kwargs): if onmove_callback: kwargs['onmove_callback'] = onmove + # While at it, also test that span selectors work in the presence of twin axes on + # top of the axes that contain the selector. Note that we need to unforce the axes + # aspect here, otherwise the twin axes forces the original axes' limits (to respect + # aspect=1) which makes some of the values below go out of bounds. + ax.set_aspect("auto") + tax = ax.twinx() + tool = widgets.SpanSelector(ax, onselect, orientation, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) # move outside of axis @@ -925,7 +932,7 @@ def mean(vmin, vmax): # Change span selector and check that the line is drawn/updated after its # value was updated by the callback - press_data = [4, 2] + press_data = [4, 0] move_data = [5, 2] release_data = [5, 2] do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1) @@ -1033,7 +1040,7 @@ def test_TextBox(ax, toolbar): assert submit_event.call_count == 2 - do_event(tool, '_click') + do_event(tool, '_click', xdata=.5, ydata=.5) # Ensure the click is in the axes. do_event(tool, '_keypress', key='+') do_event(tool, '_keypress', key='5') @@ -1632,7 +1639,8 @@ def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box): def test_polygon_selector_box(ax): - # Create a diamond shape + # Create a diamond (adjusting axes lims s.t. the diamond lies within axes limits). + ax.set(xlim=(-10, 50), ylim=(-10, 50)) verts = [(20, 0), (0, 20), (20, 40), (40, 20)] event_sequence = [ *polygon_place_vertex(*verts[0]), diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 61e5a5d7cf76..e2d610498d40 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -149,6 +149,16 @@ def disconnect_events(self): for c in self._cids: self.canvas.mpl_disconnect(c) + def _get_data_coords(self, event): + """Return *event*'s data coordinates in this widget's Axes.""" + # This method handles the possibility that event.inaxes != self.ax (which may + # occur if multiple axes are overlaid), in which case event.xdata/.ydata will + # be wrong. Note that we still special-case the common case where + # event.inaxes == self.ax and avoid re-running the inverse data transform, + # because that can introduce floating point errors for synthetic events. + return ((event.xdata, event.ydata) if event.inaxes is self.ax + else self.ax.transData.inverted().transform((event.x, event.y))) + class Button(AxesWidget): """ @@ -215,7 +225,7 @@ def __init__(self, ax, label, image=None, self.hovercolor = hovercolor def _click(self, event): - if self.ignore(event) or event.inaxes != self.ax or not self.eventson: + if not self.eventson or self.ignore(event) or not self.ax.contains(event)[0]: return if event.canvas.mouse_grabber != self.ax: event.canvas.grab_mouse(self.ax) @@ -224,13 +234,13 @@ def _release(self, event): if self.ignore(event) or event.canvas.mouse_grabber != self.ax: return event.canvas.release_mouse(self.ax) - if self.eventson and event.inaxes == self.ax: + if self.eventson and self.ax.contains(event)[0]: self._observers.process('clicked', event) def _motion(self, event): if self.ignore(event): return - c = self.hovercolor if event.inaxes == self.ax else self.color + c = self.hovercolor if self.ax.contains(event)[0] else self.color if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: @@ -531,23 +541,22 @@ def _update(self, event): if self.ignore(event) or event.button != 1: return - if event.name == 'button_press_event' and event.inaxes == self.ax: + if event.name == 'button_press_event' and self.ax.contains(event)[0]: self.drag_active = True event.canvas.grab_mouse(self.ax) if not self.drag_active: return - elif ((event.name == 'button_release_event') or - (event.name == 'button_press_event' and - event.inaxes != self.ax)): + if (event.name == 'button_release_event' + or event.name == 'button_press_event' and not self.ax.contains(event)[0]): self.drag_active = False event.canvas.release_mouse(self.ax) return - if self.orientation == 'vertical': - val = self._value_in_bounds(event.ydata) - else: - val = self._value_in_bounds(event.xdata) + + xdata, ydata = self._get_data_coords(event) + val = self._value_in_bounds( + xdata if self.orientation == 'horizontal' else ydata) if val not in [None, self.val]: self.set_val(val) @@ -869,30 +878,26 @@ def _update(self, event): if self.ignore(event) or event.button != 1: return - if event.name == "button_press_event" and event.inaxes == self.ax: + if event.name == "button_press_event" and self.ax.contains(event)[0]: self.drag_active = True event.canvas.grab_mouse(self.ax) if not self.drag_active: return - elif (event.name == "button_release_event") or ( - event.name == "button_press_event" and event.inaxes != self.ax - ): + if (event.name == "button_release_event" + or event.name == "button_press_event" and not self.ax.contains(event)[0]): self.drag_active = False event.canvas.release_mouse(self.ax) self._active_handle = None return # determine which handle was grabbed - if self.orientation == "vertical": - handle_index = np.argmin( - np.abs([h.get_ydata()[0] - event.ydata for h in self._handles]) - ) - else: - handle_index = np.argmin( - np.abs([h.get_xdata()[0] - event.xdata for h in self._handles]) - ) + xdata, ydata = self._get_data_coords(event) + handle_index = np.argmin(np.abs( + [h.get_xdata()[0] - xdata for h in self._handles] + if self.orientation == "horizontal" else + [h.get_ydata()[0] - ydata for h in self._handles])) handle = self._handles[handle_index] # these checks ensure smooth behavior if the handles swap which one @@ -900,10 +905,7 @@ def _update(self, event): if handle is not self._active_handle: self._active_handle = handle - if self.orientation == "vertical": - self._update_val_from_pos(event.ydata) - else: - self._update_val_from_pos(event.xdata) + self._update_val_from_pos(xdata if self.orientation == "horizontal" else ydata) def _format(self, val): """Pretty-print *val*.""" @@ -1119,7 +1121,7 @@ def _clear(self, event): self.ax.draw_artist(l2) def _clicked(self, event): - if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: + if self.ignore(event) or event.button != 1 or not self.ax.contains(event)[0]: return pclicked = self.ax.transAxes.inverted().transform((event.x, event.y)) distances = {} @@ -1551,7 +1553,7 @@ def stop_typing(self): def _click(self, event): if self.ignore(event): return - if event.inaxes != self.ax: + if not self.ax.contains(event)[0]: self.stop_typing() return if not self.eventson: @@ -1569,7 +1571,7 @@ def _resize(self, event): def _motion(self, event): if self.ignore(event): return - c = self.hovercolor if event.inaxes == self.ax else self.color + c = self.hovercolor if self.ax.contains(event)[0] else self.color if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: @@ -1733,7 +1735,7 @@ def _clear(self, event): self.ax.draw_artist(circle) def _clicked(self, event): - if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: + if self.ignore(event) or event.button != 1 or not self.ax.contains(event)[0]: return pclicked = self.ax.transAxes.inverted().transform((event.x, event.y)) _, inds = self._buttons.contains(event) @@ -2006,7 +2008,7 @@ def onmove(self, event): return if not self.canvas.widgetlock.available(self): return - if event.inaxes != self.ax: + if not self.ax.contains(event)[0]: self.linev.set_visible(False) self.lineh.set_visible(False) @@ -2016,10 +2018,10 @@ def onmove(self, event): return self.needclear = True - self.linev.set_xdata((event.xdata, event.xdata)) + xdata, ydata = self._get_data_coords(event) + self.linev.set_xdata((xdata, xdata)) self.linev.set_visible(self.visible and self.vertOn) - - self.lineh.set_ydata((event.ydata, event.ydata)) + self.lineh.set_ydata((ydata, ydata)) self.lineh.set_visible(self.visible and self.horizOn) if self.visible and (self.vertOn or self.horizOn): @@ -2139,15 +2141,17 @@ def clear(self, event): info["background"] = canvas.copy_from_bbox(canvas.figure.bbox) def onmove(self, event): - if (self.ignore(event) - or event.inaxes not in self.axes - or not event.canvas.widgetlock.available(self)): + axs = [ax for ax in self.axes if ax.contains(event)[0]] + if self.ignore(event) or not axs or not event.canvas.widgetlock.available(self): return + ax = cbook._topmost_artist(axs) + xdata, ydata = ((event.xdata, event.ydata) if event.inaxes is ax + else ax.transData.inverted().transform((event.x, event.y))) for line in self.vlines: - line.set_xdata((event.xdata, event.xdata)) + line.set_xdata((xdata, xdata)) line.set_visible(self.visible and self.vertOn) for line in self.hlines: - line.set_ydata((event.ydata, event.ydata)) + line.set_ydata((ydata, ydata)) line.set_visible(self.visible and self.horizOn) if self.visible and (self.vertOn or self.horizOn): self._update() @@ -2271,15 +2275,14 @@ def ignore(self, event): if (self.validButtons is not None and event.button not in self.validButtons): return True - # If no button was pressed yet ignore the event if it was out - # of the Axes + # If no button was pressed yet ignore the event if it was out of the Axes. if self._eventpress is None: - return event.inaxes != self.ax + return not self.ax.contains(event)[0] # If a button was pressed, check if the release-button is the same. if event.button == self._eventpress.button: return False # If a button was pressed, check if the release-button is the same. - return (event.inaxes != self.ax or + return (not self.ax.contains(event)[0] or event.button != self._eventpress.button) def update(self): @@ -2307,8 +2310,9 @@ def _get_data(self, event): """Get the xdata and ydata for event, with limits.""" if event.xdata is None: return None, None - xdata = np.clip(event.xdata, *self.ax.get_xbound()) - ydata = np.clip(event.ydata, *self.ax.get_ybound()) + xdata, ydata = self._get_data_coords(event) + xdata = np.clip(xdata, *self.ax.get_xbound()) + ydata = np.clip(ydata, *self.ax.get_ybound()) return xdata, ydata def _clean_event(self, event): @@ -2316,7 +2320,8 @@ def _clean_event(self, event): Preprocess an event: - Replace *event* by the previous event if *event* has no ``xdata``. - - Clip ``xdata`` and ``ydata`` to the axes limits. + - Get ``xdata`` and ``ydata`` from this widget's axes, and clip them to the axes + limits. - Update the previous event. """ if event.xdata is None: @@ -2746,7 +2751,8 @@ def _press(self, event): # Clear previous rectangle before drawing new rectangle. self.update() - v = event.xdata if self.direction == 'horizontal' else event.ydata + xdata, ydata = self._get_data_coords(event) + v = xdata if self.direction == 'horizontal' else ydata if self._active_handle is None and not self.ignore_event_outside: # when the press event outside the span, we initially set the @@ -2832,10 +2838,12 @@ def _hover(self, event): def _onmove(self, event): """Motion notify event handler.""" - v = event.xdata if self.direction == 'horizontal' else event.ydata + xdata, ydata = self._get_data_coords(event) if self.direction == 'horizontal': + v = xdata vpress = self._eventpress.xdata else: + v = ydata vpress = self._eventpress.ydata # move existing span @@ -3317,8 +3325,7 @@ def _init_shape(self, **props): def _press(self, event): """Button press event handler.""" - # make the drawn box/line visible get the click-coordinates, - # button, ... + # make the drawn box/line visible get the click-coordinates, button, ... if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) else: @@ -3331,8 +3338,7 @@ def _press(self, event): if (self._active_handle is None and not self.ignore_event_outside and self._allow_creation): - x = event.xdata - y = event.ydata + x, y = self._get_data_coords(event) self._visible = False self.extents = x, x, y, y self._visible = True @@ -3407,21 +3413,19 @@ def _onmove(self, event): # The calculations are done for rotation at zero: we apply inverse # transformation to events except when we rotate and move state = self._state - rotate = ('rotate' in state and - self._active_handle in self._corner_order) + rotate = 'rotate' in state and self._active_handle in self._corner_order move = self._active_handle == 'C' resize = self._active_handle and not move + xdata, ydata = self._get_data_coords(event) if resize: inv_tr = self._get_rotation_transform().inverted() - event.xdata, event.ydata = inv_tr.transform( - [event.xdata, event.ydata]) + xdata, ydata = inv_tr.transform([xdata, ydata]) eventpress.xdata, eventpress.ydata = inv_tr.transform( - [eventpress.xdata, eventpress.ydata] - ) + (eventpress.xdata, eventpress.ydata)) - dx = event.xdata - eventpress.xdata - dy = event.ydata - eventpress.ydata + dx = xdata - eventpress.xdata + dy = ydata - eventpress.ydata # refmax is used when moving the corner handle with the square state # and is the maximum between refx and refy refmax = None @@ -3436,16 +3440,16 @@ def _onmove(self, event): # rotate an existing shape if rotate: # calculate angle abc - a = np.array([eventpress.xdata, eventpress.ydata]) - b = np.array(self.center) - c = np.array([event.xdata, event.ydata]) + a = (eventpress.xdata, eventpress.ydata) + b = self.center + c = (xdata, ydata) angle = (np.arctan2(c[1]-b[1], c[0]-b[0]) - np.arctan2(a[1]-b[1], a[0]-b[0])) self.rotation = np.rad2deg(self._rotation_on_press + angle) elif resize: size_on_press = [x1 - x0, y1 - y0] - center = [x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2] + center = (x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2) # Keeping the center fixed if 'center' in state: @@ -3455,19 +3459,19 @@ def _onmove(self, event): if self._active_handle in self._corner_order: refmax = max(refx, refy, key=abs) if self._active_handle in ['E', 'W'] or refmax == refx: - hw = event.xdata - center[0] + hw = xdata - center[0] hh = hw / self._aspect_ratio_correction else: - hh = event.ydata - center[1] + hh = ydata - center[1] hw = hh * self._aspect_ratio_correction else: hw = size_on_press[0] / 2 hh = size_on_press[1] / 2 # cancel changes in perpendicular direction if self._active_handle in ['E', 'W'] + self._corner_order: - hw = abs(event.xdata - center[0]) + hw = abs(xdata - center[0]) if self._active_handle in ['N', 'S'] + self._corner_order: - hh = abs(event.ydata - center[1]) + hh = abs(ydata - center[1]) x0, x1, y0, y1 = (center[0] - hw, center[0] + hw, center[1] - hh, center[1] + hh) @@ -3480,26 +3484,24 @@ def _onmove(self, event): if 'S' in self._active_handle: y0 = y1 if self._active_handle in ['E', 'W'] + self._corner_order: - x1 = event.xdata + x1 = xdata if self._active_handle in ['N', 'S'] + self._corner_order: - y1 = event.ydata + y1 = ydata if 'square' in state: # when using a corner, find which reference to use if self._active_handle in self._corner_order: refmax = max(refx, refy, key=abs) if self._active_handle in ['E', 'W'] or refmax == refx: - sign = np.sign(event.ydata - y0) - y1 = y0 + sign * abs(x1 - x0) / \ - self._aspect_ratio_correction + sign = np.sign(ydata - y0) + y1 = y0 + sign * abs(x1 - x0) / self._aspect_ratio_correction else: - sign = np.sign(event.xdata - x0) - x1 = x0 + sign * abs(y1 - y0) * \ - self._aspect_ratio_correction + sign = np.sign(xdata - x0) + x1 = x0 + sign * abs(y1 - y0) * self._aspect_ratio_correction elif move: x0, x1, y0, y1 = self._extents_on_press - dx = event.xdata - eventpress.xdata - dy = event.ydata - eventpress.ydata + dx = xdata - eventpress.xdata + dy = ydata - eventpress.ydata x0 += dx x1 += dx y0 += dy @@ -3514,8 +3516,8 @@ def _onmove(self, event): not self._allow_creation): return center = [eventpress.xdata, eventpress.ydata] - dx = (event.xdata - center[0]) / 2. - dy = (event.ydata - center[1]) / 2. + dx = (xdata - center[0]) / 2 + dy = (ydata - center[1]) / 2 # square shape if 'square' in state: @@ -4058,7 +4060,7 @@ def _release(self, event): elif (not self._selection_completed and 'move_all' not in self._state and 'move_vertex' not in self._state): - self._xys.insert(-1, (event.xdata, event.ydata)) + self._xys.insert(-1, self._get_data_coords(event)) if self._selection_completed: self.onselect(self.verts) @@ -4080,16 +4082,17 @@ def _onmove(self, event): # Move the active vertex (ToolHandle). if self._active_handle_idx >= 0: idx = self._active_handle_idx - self._xys[idx] = event.xdata, event.ydata + self._xys[idx] = self._get_data_coords(event) # Also update the end of the polygon line if the first vertex is # the active handle and the polygon is completed. if idx == 0 and self._selection_completed: - self._xys[-1] = event.xdata, event.ydata + self._xys[-1] = self._get_data_coords(event) # Move all vertices. elif 'move_all' in self._state and self._eventpress: - dx = event.xdata - self._eventpress.xdata - dy = event.ydata - self._eventpress.ydata + xdata, ydata = self._get_data_coords(event) + dx = xdata - self._eventpress.xdata + dy = ydata - self._eventpress.ydata for k in range(len(self._xys)): x_at_press, y_at_press = self._xys_at_press[k] self._xys[k] = x_at_press + dx, y_at_press + dy @@ -4109,7 +4112,7 @@ def _onmove(self, event): if len(self._xys) > 3 and v0_dist < self.grab_range: self._xys[-1] = self._xys[0] else: - self._xys[-1] = event.xdata, event.ydata + self._xys[-1] = self._get_data_coords(event) self._draw_polygon() @@ -4131,12 +4134,12 @@ def _on_key_release(self, event): and (event.key == self._state_modifier_keys.get('move_vertex') or event.key == self._state_modifier_keys.get('move_all'))): - self._xys.append((event.xdata, event.ydata)) + self._xys.append(self._get_data_coords(event)) self._draw_polygon() # Reset the polygon if the released key is the 'clear' key. elif event.key == self._state_modifier_keys.get('clear'): event = self._clean_event(event) - self._xys = [(event.xdata, event.ydata)] + self._xys = [self._get_data_coords(event)] self._selection_completed = False self._remove_box() self.set_visible(True) @@ -4232,7 +4235,7 @@ def onrelease(self, event): if self.ignore(event): return if self.verts is not None: - self.verts.append((event.xdata, event.ydata)) + self.verts.append(self._get_data_coords(event)) if len(self.verts) > 2: self.callback(self.verts) self.line.remove() @@ -4240,16 +4243,12 @@ def onrelease(self, event): self.disconnect_events() def onmove(self, event): - if self.ignore(event): - return - if self.verts is None: - return - if event.inaxes != self.ax: - return - if event.button != 1: + if (self.ignore(event) + or self.verts is None + or event.button != 1 + or not self.ax.contains(event)[0]): return - self.verts.append((event.xdata, event.ydata)) - + self.verts.append(self._get_data_coords(event)) self.line.set_data(list(zip(*self.verts))) if self.useblit: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy