Search code examples
pythonpython-2.7eventsmatplotlib

2D intensity map with cursors in matplotlib


I am currently trying to create a simple GUI based on matplotlib(Python 2.7). My aim is to plot a 2d intensity map and look at the x and y slices with a user controlled curser.

It already worked out the way I want it.(See the example below). But it seems that there is one mayor limitation in the program given by the array size I can use. If I exceed a few million entries in the array the process starts to lag. I think the reason for that is that I redraw all the figures during the movement of the curser.

Is there an option how can I only redraw the curser and the slices but not the intensity map? I didn't find anything on that. Or is there another option, except for writing the curser with a real GUI like Tkinter?

In the example below the initial position of the Cursor is on 00. And will will follow the mouse movement if you press the right mouse button near the curser position and keep it pressed until you shifted the cursor to the desired position and release the button.

    # -*- noplot -*-
    #from __future__ import print_function
    import matplotlib.pyplot as plt
    import numpy as np
    
    
    class Cursor(object):
        """
        creates a GUI object that plots given 2d data with imshow and creates a curser
        which can be moved by drag and drop. The horizontal and vertical line of the curser
        are giving the position of a sliced trough the 2d-data and are plotted separately.

        """
        def __init__(self,data,scale_x,scale_y):
            
            self.motion=False
            self.data=data
            self.scale_x=scale_x
            self.scale_y=scale_y
            self.create_fig()
            # text location in axes coords
            self.txt = self.ax1.text(0.7, 0.9, '', transform=self.ax1.transAxes)
            self.create_events()
            
    #        print self.range_x,self.range_y
    #        print 
    #        print 
            
        def create_events(self):
            """
            Handles user events
            """
            self.cid1=plt.connect('motion_notify_event', self.mouse_move)
            self.cid2=plt.connect('button_press_event', self.mouse_press)
            self.cid3=plt.connect('button_release_event', self.mouse_release)
            
        def create_fig(self):
            """
            Creates the GUI, initializes the cursers at minimum of the axes and plots the 2d-data
            """
            
            #Create figure and axes
            f=plt.figure(dpi=150)
            self.ax1=f.add_subplot(221)
            self.ax2=f.add_subplot(223,sharex=self.ax1)
            self.ax3=f.add_subplot(222,sharey=self.ax1)
            # plot in ax1
            self.ax1.imshow(self.data,interpolation='none',aspect='auto',extent=[np.min(self.scale_x),np.max(self.scale_x),np.min(self.scale_y),np.max(self.scale_y)])
            
            #Creates the limits
            self.ax1.axis([np.min(self.scale_x),np.max(self.scale_x),np.min(self.scale_y),np.max(self.scale_y)])        
            self.ax3.set_xlim(np.min(self.data),np.max(self.data)) 
            self.ax2.set_ylim(np.min(self.data),np.max(self.data))
            
    
                    
            #Create Curser @ minimum-minimum position of the axes
            self.lx = self.ax1.axhline(color='k')  # the horiz line
            self.ly = self.ax1.axvline(color='k')  # the vert line        
            self.lx.set_ydata(np.min(self.scale_y))
            self.ly.set_xdata(np.min(self.scale_x))
    
            #Creates sliced plots @ initial values of the curser
            # the change of scale needs to be considered therefore
            # the program checks for the minimum difference between curser pos and self.scale_... entries 
            # and uses the position of the entry to slice the data array
            self.slice_y,=self.ax3.plot(np.flipud(self.data[:,np.argmin(np.abs(self.scale_x-self.ly.get_xdata()))]),self.scale_y)
            self.slice_x,=self.ax2.plot(self.scale_x,self.data[np.shape(self.scale_y)-np.argmin(np.abs(self.scale_y-self.lx.get_ydata()))-1,:][0])
            # garanties fixed distances beetween the plots
            plt.tight_layout()
    
    
        def sliced_vertical(self,ax):
            #gets the sliced vertical sliced data
            self.slice_y.set_xdata(np.flipud(self.data[:,np.argmin(np.abs(self.scale_x-self.ly.get_xdata()))]))
        
        def sliced_horizontal(self,ax):
            #gets the horizontal sliced data
            self.slice_x.set_ydata(self.data[np.shape(self.scale_y)-np.argmin(np.abs(self.scale_y-self.lx.get_ydata()))-1,:])  
    
            
        def cursermovement(self,event):
            """
            tracks the curser movement and if a left click appeard near the curser
            the curser will folow the motion
            """
            if not event.inaxes:
                return
            if self.motion:   
                x, y = event.xdata, event.ydata
                # update the line positions
                self.lx.set_ydata(y)
                self.ly.set_xdata(x)
                #update the text
                self.txt.set_text('x=%1.2f, y=%1.2f' % (x, y))
                #update the sliced data            
                self.sliced_vertical(self.ax2)
                self.sliced_horizontal(self.ax3)
                #replot everything
                plt.draw()        
            
            
        def mouse_move(self, event):
            self.cursermovement(event)
                
        def mouse_press(self,event):
            #check range for moving the cursers here in case of zoom in or out
            self.range_x=np.abs(self.ax1.get_xlim()[0]-self.ax1.get_xlim()[1])/20
            self.range_y=np.abs(self.ax1.get_ylim()[0]-self.ax1.get_ylim()[1])/20           
            # check if click occurred near cursor
            if (self.ly.get_xdata()+self.range_x>event.xdata>self.ly.get_xdata()-self.range_x) or (self.lx.get_ydata()+self.range_y>event.ydata>self.lx.get_ydata()-self.range_y):
                self.motion=True
            #curser jumps without motion to the mouse    
            self.cursermovement(event)
            
        def mouse_release(self,event):
            #checks if rigth mouse button was released
            self.motion=False
            
            
            
    """
    program starts here
    """        
    # define the plot range in x and y axes and change array size       
    t = np.arange(0.0, 40.0, 0.01)
    t2 = np.arange(0.0, 20.0, 0.01)
    #create a 2d grid to create the intensity map
    t_x,t_y=np.meshgrid(t,t2)
    #create the intensity map
    s = 10*np.sin(0.1*np.pi*(t_x))*np.sin(0.5*np.pi*t_y)+t_x+t_y
    #create the Gui class
    cursor = Cursor(s,t,t2)
    plt.show()

Solution

  • Matplotlib provides a widget called Cursor. You can use this already for the lines in the heatmap plot. This can use blitting in order not to redraw the canvas all the time,

    matplotlib.widgets.Cursor(ax1,useblit=True)
    

    To update the plots next to it, you may use the same blitting technique, but need to implement it manually. This way only the lines that change while moving the mouse will be updated and hence the whole interactive experience is much smoother.

    import matplotlib.pyplot as plt
    import matplotlib.widgets
    import numpy as np
    
    # define the plot range in x and y axes and change array size       
    t = np.arange(0.0, 40.0, 0.01)
    t2 = np.arange(0.0, 20.0, 0.01)
    #create a 2d grid to create the intensity map
    t_x,t_y=np.meshgrid(t,t2)
    #create the intensity map
    s = 10*np.sin(0.1*np.pi*(t_x))*np.sin(0.5*np.pi*t_y)+t_x+t_y
    #create the Gui class
    
    
    fig=plt.figure(dpi=150)
    ax1=fig.add_subplot(221)
    ax2=fig.add_subplot(223,sharex=ax1)
    ax3=fig.add_subplot(222,sharey=ax1)
    ax1.margins(0)
    ax2.margins(0)
    ax3.margins(0)
    ax2.set_ylim(s.min(), s.max())
    ax3.set_xlim(s.min(), s.max())
    
    ax1.imshow(s,aspect='auto')
    l2, = ax2.plot(np.arange(0,s.shape[1]),np.ones(s.shape[1])*np.nan)
    l3, = ax3.plot(np.ones(s.shape[0])*np.nan, np.arange(0,s.shape[0]))
    
    
    class Cursor():
        def __init__(self, **kwargs):
            self.cursor = matplotlib.widgets.Cursor(ax1,useblit=True,**kwargs)
            self.cid = fig.canvas.mpl_connect("motion_notify_event", self.cursor_move)
            self.cid2 = fig.canvas.mpl_connect("draw_event", self.clear)
            self.bg1 = None
            self.bg2 = None
            self.needclear = False
    
        def cursor_move(self,event):
            if event.inaxes == ax1:
                self.needclear = True
                x,y = int(event.xdata),int(event.ydata)
                slice_y = s[:,x]
                slice_x = s[y,:]
                l2.set_ydata(slice_x)
                l3.set_xdata(slice_y)
                fig.canvas.restore_region(self.bg1)
                fig.canvas.restore_region(self.bg2)
                l2.set_visible(True); l3.set_visible(True)
                ax2.draw_artist(l2)
                ax3.draw_artist(l3)
                fig.canvas.blit(ax2.bbox)
                fig.canvas.blit(ax3.bbox)
            else:
                if self.needclear:
                    self.clear()
                    self.needclear = False
    
        def clear(self, event=None):
            l2.set_visible(False); l3.set_visible(False)
            self.bg1 = fig.canvas.copy_from_bbox(ax2.bbox)
            self.bg2 = fig.canvas.copy_from_bbox(ax3.bbox)
    
    c = Cursor(color="crimson")
    
    plt.show()
    

    enter image description here

    If you only want to move the cursor on clicking, instead of moving the mouse, you can disconnect its events and connect a new button_press_event. The relevant part of the code would then be

    # code as above
    
    class Cursor():
        def __init__(self, **kwargs):
            self.cursor = matplotlib.widgets.Cursor(ax1,useblit=True,**kwargs)
            self.cursor.disconnect_events()
            self.cursor.connect_event('draw_event', self.cursor.clear)
            self.cursor.connect_event('button_press_event', self.cursor.onmove)
    
            self.cid = fig.canvas.mpl_connect("button_press_event", self.cursor_move)
            self.cid2 = fig.canvas.mpl_connect("draw_event", self.clear)
            self.bg1 = None
            self.bg2 = None
            self.needclear = False
    
        # rest of code as above