'Matplotlib mpl_connect not working when placed in subroutine called by Jupyter notebook

I'm working with the Draggable Rectangles example provided in the interactive matplotlib example online: https://matplotlib.org/stable/users/explain/event_handling.html#draggable-rectangle-exercise. I've modified it a bit for my usage: in addition to being draggable, each rectangle drawn should also flip color when on_release is triggered.

When I put this all in the same Jupyter cell, it runs as expected:

%matplotlib widget

class DraggableRectangle:
    def __init__(self, coords, width, height, color, id_val, ax):
        rect = Rectangle(coords, width, height, color=color)
        ax.add_patch(rect)
        self.rect = rect
        self.id_val = id_val
        self.press = None

    def connect(self):
        """Connect to all the events we need."""
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        """Check whether mouse is over us; if so, store some data."""
        if event.inaxes != self.rect.axes:
            return
        contains, attrd = self.rect.contains(event)
        if not contains:
            return
        print('event contains', self.rect.xy)
        self.press = self.rect.xy, (event.xdata, event.ydata), self.id_val

    def on_motion(self, event):
        """Move the rectangle if the mouse is over us."""
        if self.press is None or event.inaxes != self.rect.axes:
            return

        (x0, y0), (xpress, ypress), _ = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        print(f'x0={x0}, xpress={xpress}, event.xdata={event.xdata}, '
              f'dx={dx}, x0+dx={x0+dx}')
        self.rect.set_x(x0+dx)
        self.rect.set_y(y0+dy)

        self.rect.figure.canvas.draw()

    def on_release(self, event):
        """Clear button press information."""
        print("Releasing the rectangle")
        if self.id_val == self.press[2]:
            if self.rect.get_facecolor() == (0.0, 0.0, 1.0, 1.0):
                self.rect.set_color((1.0, 0.0, 0.0, 1.0))
            else:
                self.rect.set_color((0.0, 0.0, 1.0, 1.0))

        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        """Disconnect all callbacks."""
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)

fig = plt.figure()
ax = fig.add_subplot(111)
        
drs = []
id_val = 1
for i in np.arange(0, 250, 50):
    for j in np.arange(0, 150, 50):
        dr = DraggableRectangle((100 + i, 100 + j), 25, 25, (0.0, 0.0, 1.0, 1.0), id_val, ax)
        dr.connect()
        drs.append(dr)
        id_val += 1

plt.xlim([0, 1000])
plt.ylim([0, 1000])

However, let's say I refactor the code as such:

rectangle.py

# assume all imports have been made
class DraggableRectangle:
    def __init__(self, coords, width, height, color, id_val, ax):
        rect = Rectangle(coords, width, height, color=color)
        ax.add_patch(rect)
        self.rect = rect
        self.id_val = id_val
        self.press = None

    def connect(self):
        """Connect to all the events we need."""
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        """Check whether mouse is over us; if so, store some data."""
        if event.inaxes != self.rect.axes:
            return
        contains, attrd = self.rect.contains(event)
        if not contains:
            return
        print('event contains', self.rect.xy)
        self.press = self.rect.xy, (event.xdata, event.ydata), self.id_val

    def on_motion(self, event):
        """Move the rectangle if the mouse is over us."""
        if self.press is None or event.inaxes != self.rect.axes:
            return

        (x0, y0), (xpress, ypress), _ = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        print(f'x0={x0}, xpress={xpress}, event.xdata={event.xdata}, '
              f'dx={dx}, x0+dx={x0+dx}')
        self.rect.set_x(x0+dx)
        self.rect.set_y(y0+dy)

        self.rect.figure.canvas.draw()

    def on_release(self, event):
        """Clear button press information."""
        print("Releasing the rectangle")
        if self.id_val == self.press[2]:
            if self.rect.get_facecolor() == (0.0, 0.0, 1.0, 1.0):
                self.rect.set_color((1.0, 0.0, 0.0, 1.0))
            else:
                self.rect.set_color((0.0, 0.0, 1.0, 1.0))

        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        """Disconnect all callbacks."""
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)

def plot():
    fig = plt.figure()
    ax = fig.add_subplot(111)
        
    drs = []
    id_val = 1
    for i in np.arange(0, 250, 50):
        for j in np.arange(0, 150, 50):
            dr = DraggableRectangle((100 + i, 100 + j), 25, 25, (0.0, 0.0, 1.0, 1.0), id_val, ax)
            dr.connect()
            drs.append(dr)
            id_val += 1

    plt.xlim([0, 1000])
    plt.ylim([0, 1000])

Jupyter cell:

%matplotlib widget
import rectangle
rectangle.plot()

The refactored version displays the rectangles but doesn't add any of the desired interactive features.

Would appreciate any pointers in this regard. Would I have to implement mpl_connect directly inside Jupyter or is there a way I can refactor my code so it can add the interactivity?



Solution 1:[1]

The callbacks are stored as weak references and stop functioning when the containing object is garbage collected. All DraggableRectangle objects are lost when you exit the plot function. Return drs and hold it in a variable.

https://matplotlib.org/stable/users/explain/event_handling.html#:~:text=The%20canvas%20retains%20only,functions%20used%20as%20callbacks.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1