'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.
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 |