2

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?

tm8cc
  • 1,111
  • 2
  • 12
  • 26
  • This is somewhat better if I add self.bckgs = [] for ax in self.axes: self.bckg.append(self.fig.canvas.copy_from_bbox(ax.bbox)) in the _on_click method after the call to draw(), and self.fig.canvas.restore_region(bckg) in the on_motion method just after the for. But I still don't get why I only see the rectangle being resized in the bottom axe and not on all of them ! – tm8cc Nov 18 '13 at 13:10
  • Ok I have managed to see all rectangles being resized by changing fig.canvas.blit(ax.bbox) into fig.canvas.blit(fig.bbox) and putting that out of the axes loop. My question is still not answered though because I don't understand why this change makes it work. based on this post http://stackoverflow.com/questions/8955869/why-is-plotting-with-matplotlib-so-slow it should work with ax.bbox too ? – tm8cc Nov 18 '13 at 13:20
  • 1
    Good question! At first glance, it looks like part of the problem with your second example is that you're not restoring the background before blitting over it. Therefore, the artist gets drawn on top of what it drew last time. I don't have time to give a full example of fixing it right now, but I'll try to tonight if no one beats me to it. On the other hand, I'm not having the same issue you are with only the bottom subplot being drawn during "dragging" the mouse. – Joe Kington Nov 18 '13 at 14:48
  • 1
    please find here http://bpaste.net/show/Fmkw75xZH3JFJbRi2qob/ a more recent version of the code. This version works well. If I comment out line 185 (self.fig.canvas.blit(self.fig.bbox)) and uncomment line 183 (#self.fig.canvas.blit(ax.bbox)) and then I see the rectangle being dragged only on the bottom axe (but al is fine on_release). – tm8cc Nov 18 '13 at 15:23
  • Glad you have it working! On a side note, all mouse events in matplotlib have an `event.inaxes` attribute. It's `None` if the event occurred outside an axes, and the axes object it occurred inside if not. Therefore, you can drop your `_on_axenter` and `_on_axleave` methods and just use `event.inaxes is not None` in place of `self._isonaxe`. (Or, alternately, `if event.inaxes in self.axes`, if you want to limit the effect to specific axes.) – Joe Kington Nov 20 '13 at 15:06
  • ok, that's nice to know! – tm8cc Nov 20 '13 at 15:31

0 Answers0