0

I've tried running this example path_editor.ipynb in Jupyter lab and Jupyter notebook and the script path_editor.py from terminal, and in all cases it was static - not interactive.

When I run the script from terminal, I get the message:

QWidget::repaint: Recursive repaint detected
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::end: Painter not active, aborted

I have matplotlib version 3.8.4, Jupyter Lab version 4.1.8, and Jupyter Notebook version 7.1.3.

Here's the code for that example (from the website linked above):

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backend_bases import MouseButton
from matplotlib.patches import PathPatch
from matplotlib.path import Path

fig, ax = plt.subplots()

pathdata = [
    (Path.MOVETO, (1.58, -2.57)),
    (Path.CURVE4, (0.35, -1.1)),
    (Path.CURVE4, (-1.75, 2.0)),
    (Path.CURVE4, (0.375, 2.0)),
    (Path.LINETO, (0.85, 1.15)),
    (Path.CURVE4, (2.2, 3.2)),
    (Path.CURVE4, (3, 0.05)),
    (Path.CURVE4, (2.0, -0.5)),
    (Path.CLOSEPOLY, (1.58, -2.57)),
]

codes, verts = zip(*pathdata)
path = Path(verts, codes)
patch = PathPatch(
    path, facecolor='green', edgecolor='yellow', alpha=0.5)
ax.add_patch(patch)


class PathInteractor:
    """
    A path editor.

    Press 't' to toggle vertex markers on and off.  When vertex markers are on,
    they can be dragged with the mouse.
    """

    showverts = True
    epsilon = 5  # max pixel distance to count as a vertex hit

    def __init__(self, pathpatch):

        self.ax = pathpatch.axes
        canvas = self.ax.figure.canvas
        self.pathpatch = pathpatch
        self.pathpatch.set_animated(True)

        x, y = zip(*self.pathpatch.get_path().vertices)

        self.line, = ax.plot(
            x, y, marker='o', markerfacecolor='r', animated=True)

        self._ind = None  # the active vertex

        canvas.mpl_connect('draw_event', self.on_draw)
        canvas.mpl_connect('button_press_event', self.on_button_press)
        canvas.mpl_connect('key_press_event', self.on_key_press)
        canvas.mpl_connect('button_release_event', self.on_button_release)
        canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
        self.canvas = canvas

    def get_ind_under_point(self, event):
        """
        Return the index of the point closest to the event position or *None*
        if no point is within ``self.epsilon`` to the event position.
        """
        xy = self.pathpatch.get_path().vertices
        xyt = self.pathpatch.get_transform().transform(xy)  # to display coords
        xt, yt = xyt[:, 0], xyt[:, 1]
        d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
        ind = d.argmin()
        return ind if d[ind] < self.epsilon else None

    def on_draw(self, event):
        """Callback for draws."""
        self.background = self.canvas.copy_from_bbox(self.ax.bbox)
        self.ax.draw_artist(self.pathpatch)
        self.ax.draw_artist(self.line)
        self.canvas.blit(self.ax.bbox)

    def on_button_press(self, event):
        """Callback for mouse button presses."""
        if (event.inaxes is None
                or event.button != MouseButton.LEFT
                or not self.showverts):
            return
        self._ind = self.get_ind_under_point(event)

    def on_button_release(self, event):
        """Callback for mouse button releases."""
        if (event.button != MouseButton.LEFT
                or not self.showverts):
            return
        self._ind = None

    def on_key_press(self, event):
        """Callback for key presses."""
        if not event.inaxes:
            return
        if event.key == 't':
            self.showverts = not self.showverts
            self.line.set_visible(self.showverts)
            if not self.showverts:
                self._ind = None
        self.canvas.draw()

    def on_mouse_move(self, event):
        """Callback for mouse movements."""
        if (self._ind is None
                or event.inaxes is None
                or event.button != MouseButton.LEFT
                or not self.showverts):
            return

        vertices = self.pathpatch.get_path().vertices

        vertices[self._ind] = event.xdata, event.ydata
        self.line.set_data(zip(*vertices))

        self.canvas.restore_region(self.background)
        self.ax.draw_artist(self.pathpatch)
        self.ax.draw_artist(self.line)
        self.canvas.blit(self.ax.bbox)


interactor = PathInteractor(patch)
ax.set_title('drag vertices to update path')
ax.set_xlim(-3, 4)
ax.set_ylim(-3, 4)

plt.show()

Is there a way that I can make this example work?

EDIT

After installing ipympl, splitting up the notebook into two cells (one for the imports and the second for everything else), and putting %matplotlib ipympl at the top of the second cell, I now get the following error message:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\IPython\core\formatters.py:974, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    971     method = get_real_method(obj, self.print_method)
    973     if method is not None:
--> 974         return method(include=include, exclude=exclude)
    975     return None
    976 else:

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\ipympl\backend_nbagg.py:336, in Canvas._repr_mimebundle_(self, **kwargs)
    333     plaintext = plaintext[:110] + '…'
    335 buf = io.BytesIO()
--> 336 self.figure.savefig(buf, format='png', dpi='figure')
    338 base64_image = b64encode(buf.getvalue()).decode('utf-8')
    339 self._data_url = f'data:image/png;base64,{base64_image}'

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\figure.py:3390, in Figure.savefig(self, fname, transparent, **kwargs)
   3388     for ax in self.axes:
   3389         _recursively_make_axes_transparent(stack, ax)
-> 3390 self.canvas.print_figure(fname, **kwargs)

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backend_bases.py:2193, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2189 try:
   2190     # _get_renderer may change the figure dpi (as vector formats
   2191     # force the figure dpi to 72), so we need to set it again here.
   2192     with cbook._setattr_cm(self.figure, dpi=dpi):
-> 2193         result = print_method(
   2194             filename,
   2195             facecolor=facecolor,
   2196             edgecolor=edgecolor,
   2197             orientation=orientation,
   2198             bbox_inches_restore=_bbox_inches_restore,
   2199             **kwargs)
   2200 finally:
   2201     if bbox_inches and restore_bbox:

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backend_bases.py:2043, in FigureCanvasBase._switch_canvas_and_return_print_method.<locals>.<lambda>(*args, **kwargs)
   2039     optional_kws = {  # Passed by print_figure for other renderers.
   2040         "dpi", "facecolor", "edgecolor", "orientation",
   2041         "bbox_inches_restore"}
   2042     skip = optional_kws - {*inspect.signature(meth).parameters}
-> 2043     print_method = functools.wraps(meth)(lambda *args, **kwargs: meth(
   2044         *args, **{k: v for k, v in kwargs.items() if k not in skip}))
   2045 else:  # Let third-parties do as they see fit.
   2046     print_method = meth

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backends\backend_agg.py:497, in FigureCanvasAgg.print_png(self, filename_or_obj, metadata, pil_kwargs)
    450 def print_png(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
    451     """
    452     Write the figure to a PNG file.
    453 
   (...)
    495         *metadata*, including the default 'Software' key.
    496     """
--> 497     self._print_pil(filename_or_obj, "png", pil_kwargs, metadata)

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backends\backend_agg.py:445, in FigureCanvasAgg._print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata)
    440 def _print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata=None):
    441     """
    442     Draw the canvas, then save it using `.image.imsave` (to which
    443     *pil_kwargs* and *metadata* are forwarded).
    444     """
--> 445     FigureCanvasAgg.draw(self)
    446     mpl.image.imsave(
    447         filename_or_obj, self.buffer_rgba(), format=fmt, origin="upper",
    448         dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs)

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backends\backend_agg.py:388, in FigureCanvasAgg.draw(self)
    385 # Acquire a lock on the shared font cache.
    386 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    387       else nullcontext()):
--> 388     self.figure.draw(self.renderer)
    389     # A GUI class may be need to update a window using this draw, so
    390     # don't forget to call the superclass.
    391     super().draw()

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     93 @wraps(draw)
     94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95     result = draw(artist, renderer, *args, **kwargs)
     96     if renderer._rasterizing:
     97         renderer.stop_rasterizing()

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\figure.py:3164, in Figure.draw(self, renderer)
   3161 finally:
   3162     self.stale = False
-> 3164 DrawEvent("draw_event", self.canvas, renderer)._process()

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backend_bases.py:1271, in Event._process(self)
   1269 def _process(self):
   1270     """Process this event on ``self.canvas``, then unset ``guiEvent``."""
-> 1271     self.canvas.callbacks.process(self.name, self)
   1272     self._guiEvent_deleted = True

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\cbook.py:303, in CallbackRegistry.process(self, s, *args, **kwargs)
    301 except Exception as exc:
    302     if self.exception_handler is not None:
--> 303         self.exception_handler(exc)
    304     else:
    305         raise

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\cbook.py:87, in _exception_printer(exc)
     85 def _exception_printer(exc):
     86     if _get_running_interactive_framework() in ["headless", None]:
---> 87         raise exc
     88     else:
     89         traceback.print_exc()

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\cbook.py:298, in CallbackRegistry.process(self, s, *args, **kwargs)
    296 if func is not None:
    297     try:
--> 298         func(*args, **kwargs)
    299     # this does not capture KeyboardInterrupt, SystemExit,
    300     # and GeneratorExit
    301     except Exception as exc:

Cell In[2], line 73, in PathInteractor.on_draw(self, event)
     71 self.ax.draw_artist(self.pathpatch)
     72 self.ax.draw_artist(self.line)
---> 73 self.canvas.blit(self.ax.bbox)

File ~\AppData\Local\anaconda3\envs\joe_env\lib\site-packages\matplotlib\backends\backend_webagg_core.py:195, in FigureCanvasWebAggCore.blit(self, bbox)
    193 def blit(self, bbox=None):
    194     self._png_is_old = True
--> 195     self.manager.refresh_all()

AttributeError: 'NoneType' object has no attribute 'refresh_all'
7
  • 1
    There is an ongoing discussion about this example here, though it should work if you did not try to resize the window. What backend and operating system are you using? Commented May 4, 2024 at 17:32
  • I'm on Windows 11. I don't know what you mean by "backend", but my conda environment has pyqt 5.15.7. Commented May 4, 2024 at 17:58
  • By default you are using the QtAgg backend in your script then. Within a notebook, for interactivity you need the ipympl backend matplotlib.org/stable/users/explain/figure/backends.html Commented May 4, 2024 at 18:05
  • The documentation about ipympl backend and triggering it in current JupyterLab and Jupyter Notebook 7+, after install with %matplotlib ipympl is here. However, for this example, I'm not seeing it work to allow this code to run in modern Jupyter. I'm still puzzling a bit over this one. Commented May 4, 2024 at 18:23
  • 1
    No, you didn't do that incorrectly; I see the same thing in my environment where matplotlib animations and other 'interactive' features/abilities of matplotlib work, namely here and here. This example seems to need another level of something to work. I haven't found the right incantation yet to allow this one to work in Jupyter. I'm still investigating. I'm wondering though if this is one that only works in Qt or TKinter as written. Commented May 4, 2024 at 18:27

1 Answer 1

2

Putting the two things together worked out in comments.

  • Install ipympl and use with %matplotlib ipympl in modern JupyterLab and Jupyter Notebook 7+, see here for more details. If you are looking at this well after May 2024, then that step is probably all that is needed because as I note in an UPDATE comment, the Matplotlib interactive path editor example code was altered to address the code issues that in part prompted this post initially.

  • Comment out a line self.canvas.blit(self.ax.bbox) in the on_draw() function block to avoid the error AttributeError: 'NoneType' object has no attribute 'refresh_all' about blit(). Note this step is probably not needed by most, see the bullet point above.

Gives updated version of OP code as below (Note: though you probably should be using the current the Matplotlib interactive path editor example code, if it is now much after May 2024):

%matplotlib ipympl
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backend_bases import MouseButton
from matplotlib.patches import PathPatch
from matplotlib.path import Path

fig, ax = plt.subplots()

pathdata = [
    (Path.MOVETO, (1.58, -2.57)),
    (Path.CURVE4, (0.35, -1.1)),
    (Path.CURVE4, (-1.75, 2.0)),
    (Path.CURVE4, (0.375, 2.0)),
    (Path.LINETO, (0.85, 1.15)),
    (Path.CURVE4, (2.2, 3.2)),
    (Path.CURVE4, (3, 0.05)),
    (Path.CURVE4, (2.0, -0.5)),
    (Path.CLOSEPOLY, (1.58, -2.57)),
]

codes, verts = zip(*pathdata)
path = Path(verts, codes)
patch = PathPatch(
    path, facecolor='green', edgecolor='yellow', alpha=0.5)
ax.add_patch(patch)


class PathInteractor:
    """
    A path editor.

    Press 't' to toggle vertex markers on and off.  When vertex markers are on,
    they can be dragged with the mouse.
    """

    showverts = True
    epsilon = 5  # max pixel distance to count as a vertex hit

    def __init__(self, pathpatch):

        self.ax = pathpatch.axes
        canvas = self.ax.figure.canvas
        self.pathpatch = pathpatch
        self.pathpatch.set_animated(True)

        x, y = zip(*self.pathpatch.get_path().vertices)

        self.line, = ax.plot(
            x, y, marker='o', markerfacecolor='r', animated=True)

        self._ind = None  # the active vertex

        canvas.mpl_connect('draw_event', self.on_draw)
        canvas.mpl_connect('button_press_event', self.on_button_press)
        canvas.mpl_connect('key_press_event', self.on_key_press)
        canvas.mpl_connect('button_release_event', self.on_button_release)
        canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
        self.canvas = canvas

    def get_ind_under_point(self, event):
        """
        Return the index of the point closest to the event position or *None*
        if no point is within ``self.epsilon`` to the event position.
        """
        xy = self.pathpatch.get_path().vertices
        xyt = self.pathpatch.get_transform().transform(xy)  # to display coords
        xt, yt = xyt[:, 0], xyt[:, 1]
        d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
        ind = d.argmin()
        return ind if d[ind] < self.epsilon else None

    def on_draw(self, event):
        """Callback for draws."""
        self.background = self.canvas.copy_from_bbox(self.ax.bbox)
        self.ax.draw_artist(self.pathpatch)
        self.ax.draw_artist(self.line)
        #self.canvas.blit(self.ax.bbox)

    def on_button_press(self, event):
        """Callback for mouse button presses."""
        if (event.inaxes is None
                or event.button != MouseButton.LEFT
                or not self.showverts):
            return
        self._ind = self.get_ind_under_point(event)

    def on_button_release(self, event):
        """Callback for mouse button releases."""
        if (event.button != MouseButton.LEFT
                or not self.showverts):
            return
        self._ind = None

    def on_key_press(self, event):
        """Callback for key presses."""
        if not event.inaxes:
            return
        if event.key == 't':
            self.showverts = not self.showverts
            self.line.set_visible(self.showverts)
            if not self.showverts:
                self._ind = None
        self.canvas.draw()

    def on_mouse_move(self, event):
        """Callback for mouse movements."""
        if (self._ind is None
                or event.inaxes is None
                or event.button != MouseButton.LEFT
                or not self.showverts):
            return

        vertices = self.pathpatch.get_path().vertices

        vertices[self._ind] = event.xdata, event.ydata
        self.line.set_data(zip(*vertices))

        self.canvas.restore_region(self.background)
        self.ax.draw_artist(self.pathpatch)
        self.ax.draw_artist(self.line)
        self.canvas.blit(self.ax.bbox)


interactor = PathInteractor(patch)
ax.set_title('drag vertices to update path')
ax.set_xlim(-3, 4)
ax.set_ylim(-3, 4)

plt.show()
Sign up to request clarification or add additional context in comments.

3 Comments

That worked for me too! FYI - there are two lines self.canvas.blit(self.ax.bbox), but you only comment out the one in the on_draw method.
Good point. In the error you posted above it says, Cell In[2], line 73, in PathInteractor.on_draw(self, event) and so it was the the blit() in the on_draw() that was causing the issue. The other one is in the on_mouse_move() function. I've tested moving the mouse seen it not error and so presumably moving the mouse ends up not causing self.ax.bbox to be NoneType and it works. I updated the details in my answer to specify which one now.
UPDATE: the change to the documentation has been made and the issue closed, and so once that rolls out, there shouldn't need to be edits done to the Matplotlib interactive path editor example code. ipympl is still needed for this and more of the the interactive abilities of matplotlib.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.