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()
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()
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