Reputation: 11
I tried to modify some code I found on the stackoverflow forum (How can I plot the same figure standalone and in a subplot in Matplotlib? the first answer).
What it basically does is zooming in on a subplot if that subplot is clicked (only show the clicked subplot on the canvas), and zoom out when it is clicked again (show all subplots on canvas). I tried to modify the code to adjust it to my program on several ways, however I keep running into the same problem. The figure canvas with subplots is created correctly and the Zooming subplot class is entered, however it doesn't seem to connect with my click event (it doesn't enter 'on_click').
I tried finding out what is wrong and tried several modifications, but keep on running into the same problem. I can't use the code as is show in the topic I retrieved it from as it doesn't fit into the rest of my program.
import numpy as np
from matplotlib import pyplot as plt
class ZoomingSubplots(object):
''' zoom to subplot if subplot is clicked, unzoom when clicked again'''
def __init__(self, fig):
print 'class entered'
self.fig = fig
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def zoom(self, selected_ax):
for ax in self.axes.flat:
ax.set_visible(False)
self._original_size = selected_ax.get_position()
selected_ax.set_position([0.125, 0.1, 0.775, 0.8])
selected_ax.set_visible(True)
self._zoomed = True
def unzoom(self, selected_ax):
selected_ax.set_position(self._original_size)
for ax in self.axes.flat:
ax.set_visible(True)
self._zoomed = False
def on_click(self, event):
print 'click event'
if event.inaxes is None:
return
if self._zoomed:
self.unzoom(event.inaxes)
else:
self.zoom(event.inaxes)
self.fig.canvas.draw()
#make a figure with 9 random imshows
plots = 9 #number of plots
plotrows = 3 #subplot rows
plotcols = 3 #subplot columns
fig = plt.figure()
for i in range(plots):
arr = np.random.rand(10,10)
ax = fig.add_subplot(plotrows, plotcols, i+1)
ax.imshow(arr, interpolation = 'nearest')
ax.set_title('%s %i' % ('plot', i+1), fontsize = 10)
# connect with zoom class
ZoomingSubplots(fig)
plt.show()
A tweaked this to a simpler code in which you can notice the same problem:
import numpy as np
from matplotlib import pyplot as plt
class ZoomingSubplots(object):
def __init__(self, fig):
print 'class entered'
self.fig = fig
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def on_click(self, event):
print 'click event'
#make a figure with 9 random imshows
fig = plt.figure()
for i in range(9):
arr = np.random.rand(10,10)
ax = fig.add_subplot(3, 3, i+1)
ax.imshow(arr, interpolation = 'nearest')
# connect with zoom class
ZoomingSubplots(fig)
plt.show()
Upvotes: 1
Views: 659
Reputation: 284890
What's happening is a bit of a classic gotcha with matplotlib's "weak" references to callbacks, etc. Your class instance is being garbage collected before the plot is displayed.
Because pylab state machine has to be "long-lived", and it needs to keep references to lots of different things, matplotlib uses "weak" references for most user-supplied objects. This allows things to be garbage collected even though a figure may still have a reference to them.
It's used for a good reason in most cases, but you can make an argument that matplotlib should not use weak references for callbacks. At any rate, what you're experiencing is a result of this long-standing feature/bug/design-choice.
The simplest solution is to just do:
x = ZoomingSubplots(fig)
The reference to x
will keep the class instance from being garbage collected until x
goes out of scope (and because x
is a global variable in this particular case, that won't happen until the program ends).
The solution above works perfectly, but I don't like having unused variables hanging around (and things like pylint, etc, will complain as well).
Therefore, I'll often add a show
method to things like this that calls plt.show()
and enters the gui mainloop. For example:
import numpy as np
from matplotlib import pyplot as plt
class ZoomingSubplots(object):
def __init__(self, fig):
print 'class entered'
self.fig = fig
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def on_click(self, event):
print 'click event'
def show(self):
plt.show()
#make a figure with 9 random imshows
fig = plt.figure()
for i in range(9):
arr = np.random.rand(10,10)
ax = fig.add_subplot(3, 3, i+1)
ax.imshow(arr, interpolation = 'nearest')
# connect with zoom class
ZoomingSubplots(fig).show()
In this case, the gui mainloop (i.e. plt.show()
) is called as a method of the class instance, and therefore, the class instance can't be garbage collected before show()
ends.
Which is "better" is purely a matter of personal style. However, I feel like the first example is less future-proof, as someone else might come along and go "x
is unused, let's delete it", and inadvertently re-introduce the problem.
Upvotes: 2