I have a figure consisting in multiple axes, and on several of them I want to draw N rectangles to highligtht x intervals. There would be N identical rectangles on each panel. On a click, a new rectangle is created on all panels, while the mouse button is pressed and on mouse motion, the rectangles are resized on all panels following the cursor position, and on release I stop resizing. A new click creates a new rectangle on all panels etc.
I have a working version which involves fig.canvas.draw() each time I update the figure. This is OK for the press and release events, since there are few of them, but for the motion_notify_event, this is too slow in a real case (14 plots to redraw).
Here is a small working example for that version :
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
class Zones(object):
"""
this class allows the user to draw rectangular zones
across several axes and get the corresponding intervals
"""
#==========================================================
#==========================================================
def __init__(self,axes, nbzones=4):
"""create an area made of 'nbzone' zone
@param nbzones : max number of rectangular zones to be drawn
@param axes : one or several axes in a list [ax1,]
Exemple : Zone(axes, nbzones=5)
Creation : 2013-11-13 09:08:45.239114
"""
self.axes = axes # the list of axes
self.fig = axes[0].figure # the (only) associated figure
# events : press, released, motion
self.pressid = self.fig.canvas.mpl_connect('button_press_event',
self._on_click)
self.releaid = self.fig.canvas.mpl_connect('button_release_event',
self._on_release)
self.motioid = self.fig.canvas.mpl_connect('motion_notify_event',
self._on_motion)
# allows to move the rectangle only when button is pressed
self._is_pressed = False
#will count the number of defined zones
self.count = 0
self.nbzones = nbzones # max number of defined zones
# will store the defined intervals
self.intervals = np.ndarray(shape=(nbzones,2))
#==========================================================
#==========================================================
#==========================================================
def _on_click(self,event):
"""on_click event
Creation : 2013-11-13 09:13:37.922407
"""
self._is_pressed = True # the button is now defined as pressed
# store the rectangles for all axes
# this will be useful because other events will loop on them
self.rects = []
# for now loop on all axes and create one set of identical rectangles
# on each one of them
# the width of the rect. is something tiny like 1e-5 to give the
# impression that it has 0 width before the user moves the mouse
for ax in self.axes:
self.rects.append(Rectangle((event.xdata,0),
1e-5, 10,
alpha=0.3,
color='g'))
# add the rectangle to the current ax
ax.add_patch(self.rects[-1])
# the set of rectangles is created for all axes
# now draw them
self.fig.canvas.draw()
#==========================================================
#==========================================================
#==========================================================
def _on_release(self,event):
"""on_release event
Creation : 2013-11-13 09:18:04.246367
"""
self._is_pressed = False # the button is now released
# now we loop on all the rects defined last click
# and we change their width according to the current
# position of the cursor and x0
for rect in self.rects:
rect.set_width(event.xdata - rect.get_x())
# all rects. have been updated so draw them
self.fig.canvas.draw()
# now store the interval and increment the
# total number of defined zones
self.intervals[self.count,0] = rect.get_x()
self.intervals[self.count,1] = event.xdata
self.count+=1
# if we have reached the max number of zones
# then we stop listening for events
if self.count == self.nbzones:
self.fig.canvas.mpl_disconnect(self.pressid)
self.fig.canvas.mpl_disconnect(self.releaid)
self.fig.canvas.mpl_disconnect(self.motioid)
print 'all intervals defined!'
#==========================================================
#==========================================================
#==========================================================
def _on_motion(self,event):
""" on_motion event
Creation : 2013-11-13 09:21:59.747476
"""
# this event must apply only when the mouse button is pressed
if self._is_pressed == True:
print 'motion while pressed'
# we loop over rectangles of this click
# and update their width according to the current
# position of the cursor
for rect in self.rects:
rect.set_width(event.xdata - rect.get_x())
# all rects. have been updated so draw them
self.fig.canvas.draw()
#==========================================================
#==========================================================
#==========================================================
def get_intervals(self):
""" returns an array of all the intervals (zones)
@return: ndarray shape = (nbzones,2)
Exemple : intervals = myzones.get_intervals()
Creation : 2013-11-13 09:23:44.147194
"""
self.intervals.sort() # we want x0 < x1
return self.intervals
#==========================================================
def main():
fig = plt.figure()
ax1 = fig.add_subplot(411)
ax2 = fig.add_subplot(412)
ax3 = fig.add_subplot(413)
ax4 = fig.add_subplot(414)
axes = [ax1,ax2,ax3,ax4]
for ax in axes:
ax.set_xlim((0,10))
ax.set_ylim((0,10))
z = Zones(axes, nbzones=6)
return z
if __name__ == '__main__':
main()
I want to write a version that would, on this motion event, redraw, on each panel, only the rectangle that is currently being resized.
I'm not sure I fully understand how to use canvas.blit(), axes.draw_artist(). I think I need to focus on the '_on_motion()' method.
here is my attempt, where I've changed the '_on_motion()' method:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
class Zones(object):
"""
this class allows the user to draw rectangular zones
across several axes and get the corresponding intervals
"""
#==========================================================
#==========================================================
def __init__(self,axes, nbzones=4):
"""create an area made of 'nbzone' zone
@param nbzones : max number of rectangular zones to be drawn
@param axes : one or several axes in a list [ax1,]
Exemple : Zone(axes, nbzones=5)
Creation : 2013-11-13 09:08:45.239114
"""
self.axes = axes # the list of axes
self.fig = axes[0].figure # the (only) associated figure
# events : press, released, motion
self.pressid = self.fig.canvas.mpl_connect('button_press_event',
self._on_click)
self.releaid = self.fig.canvas.mpl_connect('button_release_event',
self._on_release)
self.motioid = self.fig.canvas.mpl_connect('motion_notify_event',
self._on_motion)
# allows to move the rectangle only when button is pressed
self._is_pressed = False
#will count the number of defined zones
self.count = 0
self.nbzones = nbzones # max number of defined zones
# will store the defined intervals
self.intervals = np.ndarray(shape=(nbzones,2))
#==========================================================
#==========================================================
#==========================================================
def _on_click(self,event):
"""on_click event
Creation : 2013-11-13 09:13:37.922407
"""
self._is_pressed = True # the button is now defined as pressed
# store the rectangles for all axes
# this will be useful because other events will loop on them
self.rects = []
# for now loop on all axes and create one set of identical rectangles
# on each one of them
# the width of the rect. is something tiny like 1e-5 to give the
# impression that it has 0 width before the user moves the mouse
for ax in self.axes:
self.rects.append(Rectangle((event.xdata,0),
1e-5, 10,
alpha=0.3,
color='g'))
# add the rectangle to the current ax
ax.add_patch(self.rects[-1])
# the set of rectangles is created for all axes
# now draw them
self.fig.canvas.draw()
#==========================================================
#==========================================================
#==========================================================
def _on_release(self,event):
"""on_release event
Creation : 2013-11-13 09:18:04.246367
"""
self._is_pressed = False # the button is now released
# now we loop on all the rects defined last click
# and we change their width according to the current
# position of the cursor and x0
for rect in self.rects:
rect.set_width(event.xdata - rect.get_x())
# all rects. have been updated so draw them
self.fig.canvas.draw()
# now store the interval and increment the
# total number of defined zones
self.intervals[self.count,0] = rect.get_x()
self.intervals[self.count,1] = event.xdata
self.count+=1
# if we have reached the max number of zones
# then we stop listening to events
if self.count == self.nbzones:
self.fig.canvas.mpl_disconnect(self.pressid)
self.fig.canvas.mpl_disconnect(self.releaid)
self.fig.canvas.mpl_disconnect(self.motioid)
print 'all intervals defined!'
#==========================================================
#==========================================================
#==========================================================
def _on_motion(self,event):
""" on_motion event
Creation : 2013-11-13 09:21:59.747476
"""
# this event must apply only when the mouse button is pressed
if self._is_pressed == True:
print 'motion while pressed'
# we loop over rectangles of this click
# and update their width according to the current
# position of the cursor
for ax,rect in zip(self.axes,self.rects):
rect.set_width(event.xdata - rect.get_x())
ax.draw_artist(rect)
self.fig.canvas.blit(rect.clipbox)
#==========================================================
#==========================================================
#==========================================================
def get_intervals(self):
""" returns an array of all the intervals (zones)
@return: ndarray shape = (nbzones,2)
Exemple : intervals = myzones.get_intervals()
Creation : 2013-11-13 09:23:44.147194
"""
self.intervals.sort() # we want x0 < x1
return self.intervals
#==========================================================
def main():
fig = plt.figure()
ax1 = fig.add_subplot(411)
ax2 = fig.add_subplot(412)
ax3 = fig.add_subplot(413)
ax4 = fig.add_subplot(414)
axes = [ax1,ax2,ax3,ax4]
for ax in axes:
ax.set_xlim((0,10))
ax.set_ylim((0,10))
z = Zones(axes, nbzones=6)
return z
if __name__ == '__main__':
main()
On my machine, when I move my cursor while button is pressed, I see rectangles drawn on th bottom panel only, it looks weird like there are several rectangles being drawn, I don't know... but nothing on the first three axes. Once I release the mouse button, I see all rectangles on all panels drawn with the appropriate size.
I have made an even more simple example code, to see what's going on with draw_artist() and blit(), and that apparently does what I'd think it should do :
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
def rect():
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
axes = [ax1,ax2]
rects = []
for ax in axes:
rects.append(mpatches.Rectangle([0,0],0.2,1,color='b',alpha=0.4))
ax.add_artist(rects[-1])
fig.canvas.draw()
# now resize
for ax,r in zip(axes,rects):
r.set_width(0.1)
r.set_color('r')
r.set_alpha(0.2)
ax.draw_artist(r)
fig.canvas.blit(r.clipbox)
# fig.canvas.blit(ax.bbox) # this seems to do the same job as r.clipbox??
i.e. create and draw a rectangle with a given width on two axes, then change the properties of the rectangles for all axes (this simulates a mouse motion) and re-draw the rectangles. That seems to work as I don't see anymore the blue rectangle but only the red one.
What I don't get is this small code is apparently doing the same thing as my _on_motion() method, so what's wrong?