diff --git a/doc/api/next_api_changes/deprecations/23348-AL.rst b/doc/api/next_api_changes/deprecations/23348-AL.rst new file mode 100644 index 000000000000..e4f0443f58e0 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/23348-AL.rst @@ -0,0 +1,7 @@ +The ``canvas`` and ``background`` attributes of ``MultiCursor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated with no replacement. + +All parameters to ``MultiCursor`` starting from *useblit* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are becoming keyword-only (passing them positionally is deprecated). diff --git a/doc/users/next_whats_new/multicursor_multifigure.rst b/doc/users/next_whats_new/multicursor_multifigure.rst new file mode 100644 index 000000000000..04b39f9d0c56 --- /dev/null +++ b/doc/users/next_whats_new/multicursor_multifigure.rst @@ -0,0 +1,8 @@ +``MultiCursor`` now supports Axes split over multiple figures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, `.MultiCursor` only worked if all target Axes belonged to the same +figure. + +As a consequence of this change, the first argument to the `.MultiCursor` +constructor has become unused (it was previously the joint canvas of all Axes, +but the canvases are now directly inferred from the list of Axes). diff --git a/examples/widgets/multicursor.py b/examples/widgets/multicursor.py index 6e0775dd85e7..618fa17c5ad6 100644 --- a/examples/widgets/multicursor.py +++ b/examples/widgets/multicursor.py @@ -5,22 +5,27 @@ Showing a cursor on multiple plots simultaneously. -This example generates two subplots and on hovering the cursor over data in one -subplot, the values of that datapoint are shown in both respectively. +This example generates three axes split over two different figures. On +hovering the cursor over data in one subplot, the values of that datapoint are +shown in all axes. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import MultiCursor t = np.arange(0.0, 2.0, 0.01) s1 = np.sin(2*np.pi*t) -s2 = np.sin(4*np.pi*t) +s2 = np.sin(3*np.pi*t) +s3 = np.sin(4*np.pi*t) fig, (ax1, ax2) = plt.subplots(2, sharex=True) ax1.plot(t, s1) ax2.plot(t, s2) +fig, ax3 = plt.subplots() +ax3.plot(t, s3) -multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1) +multi = MultiCursor(None, (ax1, ax2, ax3), color='r', lw=1) plt.show() ############################################################################# diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 1ab48a3814cd..515b60a6da4f 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1516,11 +1516,12 @@ def test_polygon_selector_box(ax): [(True, True), (True, False), (False, True)], ) def test_MultiCursor(horizOn, vertOn): - fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) + (ax1, ax3) = plt.figure().subplots(2, sharex=True) + ax2 = plt.figure().subplots() # useblit=false to avoid having to draw the figure to cache the renderer multi = widgets.MultiCursor( - fig.canvas, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn + None, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn ) # Only two of the axes should have a line drawn on them. diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index c5b6ec20093e..56267d12cf55 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1680,8 +1680,8 @@ class MultiCursor(Widget): Parameters ---------- - canvas : `matplotlib.backend_bases.FigureCanvasBase` - The FigureCanvas that contains all the Axes. + canvas : object + This parameter is entirely unused and only kept for back-compatibility. axes : list of `matplotlib.axes.Axes` The `~.axes.Axes` to attach the cursor to. @@ -1708,21 +1708,29 @@ class MultiCursor(Widget): See :doc:`/gallery/widgets/multicursor`. """ + @_api.make_keyword_only("3.6", "useblit") def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True, **lineprops): - self.canvas = canvas + # canvas is stored only to provide the deprecated .canvas attribute; + # once it goes away the unused argument won't need to be stored at all. + self._canvas = canvas + self.axes = axes self.horizOn = horizOn self.vertOn = vertOn + self._canvas_infos = { + ax.figure.canvas: {"cids": [], "background": None} for ax in axes} + xmin, xmax = axes[-1].get_xlim() ymin, ymax = axes[-1].get_ylim() xmid = 0.5 * (xmin + xmax) ymid = 0.5 * (ymin + ymax) self.visible = True - self.useblit = useblit and self.canvas.supports_blit - self.background = None + self.useblit = ( + useblit + and all(canvas.supports_blit for canvas in self._canvas_infos)) self.needclear = False if self.useblit: @@ -1742,33 +1750,39 @@ def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True, self.connect() + canvas = _api.deprecate_privatize_attribute("3.6") + background = _api.deprecated("3.6")(lambda self: ( + self._backgrounds[self.axes[0].figure.canvas] if self.axes else None)) + def connect(self): """Connect events.""" - self._cidmotion = self.canvas.mpl_connect('motion_notify_event', - self.onmove) - self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear) + for canvas, info in self._canvas_infos.items(): + info["cids"] = [ + canvas.mpl_connect('motion_notify_event', self.onmove), + canvas.mpl_connect('draw_event', self.clear), + ] def disconnect(self): """Disconnect events.""" - self.canvas.mpl_disconnect(self._cidmotion) - self.canvas.mpl_disconnect(self._ciddraw) + for canvas, info in self._canvas_infos.items(): + for cid in info["cids"]: + canvas.mpl_disconnect(cid) + info["cids"].clear() def clear(self, event): """Clear the cursor.""" if self.ignore(event): return if self.useblit: - self.background = ( - self.canvas.copy_from_bbox(self.canvas.figure.bbox)) + for canvas, info in self._canvas_infos.items(): + info["background"] = canvas.copy_from_bbox(canvas.figure.bbox) for line in self.vlines + self.hlines: line.set_visible(False) def onmove(self, event): - if self.ignore(event): - return - if event.inaxes not in self.axes: - return - if not self.canvas.widgetlock.available(self): + if (self.ignore(event) + or event.inaxes not in self.axes + or not event.canvas.widgetlock.available(self)): return self.needclear = True if not self.visible: @@ -1785,17 +1799,20 @@ def onmove(self, event): def _update(self): if self.useblit: - if self.background is not None: - self.canvas.restore_region(self.background) + for canvas, info in self._canvas_infos.items(): + if info["background"]: + canvas.restore_region(info["background"]) if self.vertOn: for ax, line in zip(self.axes, self.vlines): ax.draw_artist(line) if self.horizOn: for ax, line in zip(self.axes, self.hlines): ax.draw_artist(line) - self.canvas.blit() + for canvas in self._canvas_infos: + canvas.blit() else: - self.canvas.draw_idle() + for canvas in self._canvas_infos: + canvas.draw_idle() class _SelectorWidget(AxesWidget):