I want to write unittest for interactive matplotlib plots. My problem is that I couldn't find a good way of simulating key press or mouse button press events. I know about pyautogui, but then I'd have to care about the position of the matplotlib window on the screen (also, for example on TravisCI I doubt it would work properly without configuring it). I've tried looking into Matplotlib's unittests, but I couldn't find anything useful yet. The best solution would be triggering an event inside the code without involving the GUI part, but so far I couldn't solve it.
The easiest example I've managed to come up with is below. You can mark points on the plot using the i key. The function here I want to test is on_press.
import numpy as np
import matplotlib.pyplot as plt
class PointRecorder:
def __init__(self, x, y):
plt.ion()
self.figure = plt.figure()
self.cid = self.figure.canvas.mpl_connect("key_press_event", self.on_press)
self.x = x
self.y = y
self.x_points, self.y_points = [2], [0.5]
plt.plot(self.x, self.y, "r")
self.pts, = plt.plot(self.x_points, self.y_points, "ko", markersize=6, zorder=99)
plt.show(block=True)
def on_press(self, event):
ix, iy = event.xdata, event.ydata
if event.inaxes is None:
return
if event.key == 'i':
self.x_points.append(ix)
self.y_points.append(iy)
self.pts.set_data(self.x_points, self.y_points)
if self.pts.stale:
self.figure.canvas.draw_idle()
def get_data(self):
return self.pts.get_data()
if __name__ == "__main__":
x = np.linspace(0, 6, 100)
y = np.sin(x)
graph = PointRecorder(x, y)
print(*graph.get_data())
Can you suggest a way how this kind of functionality should be tested properly?
I'm no expert on unit testing, but my guess is that you need to instantiate an Event object (in the case of key_press_event, it should be a KeyEvent) and call graph.on_press(event) from your testing code
As suggested, the solution is the following: First define a blank event.
from unittest import mock
def mock_event(xdata, ydata, button, key, fig, canvas, inaxes=True):
event = mock.Mock()
event.button = button
event.key = key
event.xdata, event.ydata = xdata, ydata
event.inaxes = inaxes
event.fig = fig
event.canvas = canvas
event.guiEvent = None
event.name = 'MockEvent'
return event
Then initialize the PointRecorder class above. After that, define a mock_event which suits the PointRecorder.on_press method properly. Also patch plt.show to avoid blocking execution.
#mock.patch("matplotlib.pyplot.show")
def test_insert(mock_show):
x, y = np.arange(100), np.arange(100)
obj = PointRecorder(x, y)
mck = mock_event(xdata=50, ydata=40, button="i", key="i", fig=obj.figure, canvas=obj.figure.canvas, inaxes=True)
obj.on_clicked(event=mck)
a, b = obj.get_data()
np.testing.assert_array_equal(a, np.array([2, 50])) # 2 was originally in the __init__
np.testing.assert_array_equal(b, np.array([0.5, 40])) # 0.5 was originally in the __init__
mock_show.assert_called()
Related
I have tried to create an interactive matplotlib plot using some functions. I want to group the functions into one class (I am still new to this, took help from someone else's code)
import matplotlib.pyplot as plt
def draw_line(startx,starty):
ax = plt.gca()
xy = plt.ginput(1)
x = [startx,xy[0][0]]
y = [starty,xy[0][1]]
line = ax.plot(x,y, picker=True , pickradius = 5 , color = "blue")
ax.figure.canvas.draw()
def onclick(event):
"""
This implements click functionality. If it's a double click do something,
else ignore.
Once in the double click block, if its a left click, wait for a further
click and draw a line between the double click co-ordinates and that click
(using ginput(1) - the 1 means wait for one mouse input - a higher number
is used to get multiple clicks to define a polyline)
"""
ax = plt.gca()
if event.dblclick:
if event.button == 1:
# Draw line
draw_line(event.xdata,event.ydata) # here you click on the plot
else:
pass # Do nothing
if event.button == 1:
pass
def onpick(event):
ax = plt.gca()
"""
Handles the pick event - if an object has been picked, store a
reference to it. We do this by simply adding a reference to it
named 'stored_pick' to the axes object. Note that in python we
can dynamically add an attribute variable (stored_pick) to an
existing object - even one that is produced by a library as in this
case
"""
this_artist = event.artist # the picked object is available as event.artist
ax.picked_object = this_artist
def on_key(event):
"""
Function to be bound to the key press event
If the key pressed is delete and there is a picked object,
remove that object from the canvas
"""
if event.key == u'delete':
ax = plt.gca()
if ax.picked_object:
ax.picked_object.remove()
ax.picked_object = None
ax.figure.canvas.draw()
def applyplt():
fig = plt.gcf()
ax = plt.gca()
cidonclic = fig.canvas.mpl_connect('button_press_event', onclick)
cidonpic = fig.canvas.mpl_connect('pick_event', onpick)
cidonkey = fig.canvas.mpl_connect('key_press_event', on_key)
"""
Basic Plot to test the function.
"""
fig1 = plt.figure(figsize = (10,10))
gs = fig1.add_gridspec(10,10)
ax101 = fig1.add_subplot(gs[:,:])
ax101.set_ylim(0,10)
ax101.set_xlim(0,10)
applyplt()
plt.show()
I want to group these event functions in one class name(object) (e.g.: class Drawer(object))
If any other optimization can be done, please suggest that too. Thanks!
Here are the functions grouped into a class:
import matplotlib.pyplot as plt
class Interactivity:
def __init__(self, fig = None):
self.fig = plt.gcf() if fig is None else fig
self.ax = self.fig.gca()
self.connections = ()
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
def connect(self):
""" Install the event handlers for the plot. """
self.connections = (
self.fig.canvas.mpl_connect('button_press_event', self.onclick),
self.fig.canvas.mpl_connect('pick_event', self.onpick),
self.fig.canvas.mpl_connect('key_press_event', self.on_key),
)
def disconnect(self):
""" Uninstall the event handlers for the plot. """
for connection in self.connections:
self.fig.canvas.mpl_disconnect(connection)
def draw_line(self, startx, starty):
xy = plt.ginput(1)
x = [startx, xy[0][0]]
y = [starty, xy[0][1]]
self.ax.plot(x, y, picker=True, pickradius=5, color='blue')
self.ax.figure.canvas.draw()
def onclick(self, event):
"""
This implements click functionality. If it's a double click do
something, else ignore.
Once in the double click block, if its a left click, wait for a further
click and draw a line between the double click co-ordinates and that
click (using ginput(1) - the 1 means wait for one mouse input - a
higher number is used to get multiple clicks to define a polyline)
"""
print('onclick')
if event.dblclick:
if event.button == 1:
self.draw_line(event.xdata, event.ydata)
def onpick(self, event):
"""
Handles the pick event - if an object has been picked, store a
reference to it. We do this by simply adding a reference to it
named 'picked_object' to the axes object.
"""
print('onpick')
this_artist = event.artist
# the picked object is available as event.artist
self.ax.picked_object = this_artist
def on_key(self, event):
"""
Function to be bound to the key press event
If the key pressed is delete and there is a picked object,
remove that object from the canvas
"""
print('onkey: ', event.key)
if event.key == 'delete' and self.ax.picked_object:
self.ax.picked_object.remove()
self.ax.picked_object = None
self.ax.figure.canvas.draw()
Usage:
# Basic plot to test the functionality
fig = plt.figure(figsize = (10,10))
gs = fig.add_gridspec(10,10)
ax101 = fig.add_subplot(gs[:,:])
ax101.set_ylim(0,10)
ax101.set_xlim(0,10)
with Interactivity():
plt.show()
As you can see, it can be used as a context handler to install and uninstall the handlers automatically, or you can create an instance of it, and then call its connect method to install the handlers manually (and then later optionally call the disconnect method to uninstall the handlers).
I am using matplotlib with NavigationToolbar2QT. The toolbar is showing the position of the cursor. But I would like that the cursor snaps to the nearest data point (when close enough) or simply show the coordinate of nearest data point. Can that be somehow arranged?
If you are working with large sets of points, I advice you to use CKDtrees:
import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial
points = np.column_stack([np.random.rand(50), np.random.rand(50)])
fig, ax = plt.subplots()
coll = ax.scatter(points[:,0], points[:,1])
ckdtree = scipy.spatial.cKDTree(points)
I refactored kpie's answer here little bit. Once ckdtree is created, you can identify closest points instantly and various kind of information about them with a little effort:
def closest_point_distance(ckdtree, x, y):
#returns distance to closest point
return ckdtree.query([x, y])[0]
def closest_point_id(ckdtree, x, y):
#returns index of closest point
return ckdtree.query([x, y])[1]
def closest_point_coords(ckdtree, x, y):
# returns coordinates of closest point
return ckdtree.data[closest_point_id(ckdtree, x, y)]
# ckdtree.data is the same as points
Interactive display of cursor position.
If you want coordinates of the closest point to be displayed on Navigation Toolbar:
def val_shower(ckdtree):
#formatter of coordinates displayed on Navigation Bar
return lambda x, y: '[x = {}, y = {}]'.format(*closest_point_coords(ckdtree, x, y))
plt.gca().format_coord = val_shower(ckdtree)
plt.show()
Using events.
If you want another kind of interactivity, you can use events:
def onclick(event):
if event.inaxes is not None:
print(closest_point_coords(ckdtree, event.xdata, event.ydata))
fig.canvas.mpl_connect('motion_notify_event', onclick)
plt.show()
You could subclass NavigationToolbar2QT and override the mouse_move handler. The xdata and ydata attributes contain the current mouse position in plot coordinates. You can snap that to the closest data point before passing the event to the base class mouse_move handler.
Full example, with highlighting of the closest point in the plot as a bonus:
import sys
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas, NavigationToolbar2QT
from matplotlib.figure import Figure
class Snapper:
"""Snaps to data points"""
def __init__(self, data, callback):
self.data = data
self.callback = callback
def snap(self, x, y):
pos = np.array([x, y])
distances = np.linalg.norm(self.data - pos, axis=1)
dataidx = np.argmin(distances)
datapos = self.data[dataidx,:]
self.callback(datapos[0], datapos[1])
return datapos
class SnappingNavigationToolbar(NavigationToolbar2QT):
"""Navigation toolbar with data snapping"""
def __init__(self, canvas, parent, coordinates=True):
super().__init__(canvas, parent, coordinates)
self.snapper = None
def set_snapper(self, snapper):
self.snapper = snapper
def mouse_move(self, event):
if self.snapper and event.xdata and event.ydata:
event.xdata, event.ydata = self.snapper.snap(event.xdata, event.ydata)
super().mouse_move(event)
class Highlighter:
def __init__(self, ax):
self.ax = ax
self.marker = None
self.markerpos = None
def draw(self, x, y):
"""draws a marker at plot position (x,y)"""
if (x, y) != self.markerpos:
if self.marker:
self.marker.remove()
del self.marker
self.marker = self.ax.scatter(x, y, color='yellow')
self.markerpos = (x, y)
self.ax.figure.canvas.draw()
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
canvas = FigureCanvas(Figure(figsize=(5,3)))
layout.addWidget(canvas)
toolbar = SnappingNavigationToolbar(canvas, self)
self.addToolBar(toolbar)
data = np.random.randn(100, 2)
ax = canvas.figure.subplots()
ax.scatter(data[:,0], data[:,1])
self.highlighter = Highlighter(ax)
snapper = Snapper(data, self.highlighter.draw)
toolbar.set_snapper(snapper)
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()
The following code will print the coordinates of the dot closest to the mouse when you click.
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(19680801)
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
fig,ax = plt.subplots()
plt.scatter(x, y)
points = list(zip(x,y))
def distance(a,b):
return(sum([(k[0]-k[1])**2 for k in zip(a,b)])**0.5)
def onclick(event):
dists = [distance([event.xdata, event.ydata],k) for k in points]
print(points[dists.index(min(dists))])
fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
Another possibility is to use the picking support axes already have. See this section in the event handling docs.
Jim
I have the following class that draws a vertical line through the y-axis, so that when I click on it a horizontal line gets drawn at the location.
My goal is to get the y-coordinate to actually print right at the y-axis where the horizontal line gets drawn. For testing, I am trying to print a title with the y-ccordinate, but it is not working as expected.
What I am trying to really accomplish is to make the y-axis on a bar plot clickable so that the user can select a point on the y-axis, and then a bunch of "stuff" happens (including the drawing of a horizontal line). I really see no other way to make this happen, other than drawing a plottable vertical line through the y-axis to make it pickable. This seems rather kludgey, but I have not had any success with any other methods.
import matplotlib.pyplot as plt
class PointPicker(object):
def __init__(self):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot((0, 0), (-6000, 10000), 'k-', linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
fig.canvas.mpl_connect('button_press_event', self.onclick)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axhline(y=y)
print(y)
ax.axvspan(0, 0, facecolor='y', alpha=0.5, picker=10)
self.fig.canvas.draw()
def onclick(event):
self.fig.canvas.set_title('Selected item came from {}'.format(event.ydata))
print(event.xdata, event.ydata)
if __name__ == '__main__':
plt.ion()
p = PointPicker()
plt.show()
Assuming there is no ther way to achieve my end result, all is well with this method, except I cannot for the life of me get the title to print (using the self.fig.canvas.set_title('Selected item came from {}'.format(event.ydata)).
You can use the 'button_press_event' to connect to a method, which verifies that the click has happened close enough to the yaxis spine and then draws a horizontal line using the clicked coordinate.
import matplotlib.pyplot as plt
class PointPicker(object):
def __init__(self, ax, clicklim=0.05):
self.fig=ax.figure
self.ax = ax
self.clicklim = clicklim
self.horizontal_line = ax.axhline(y=.5, color='y', alpha=0.5)
self.text = ax.text(0,0.5, "")
print self.horizontal_line
self.fig.canvas.mpl_connect('button_press_event', self.onclick)
def onclick(self, event):
if event.inaxes == self.ax:
x = event.xdata
y = event.ydata
xlim0, xlim1 = ax.get_xlim()
if x <= xlim0+(xlim1-xlim0)*self.clicklim:
self.horizontal_line.set_ydata(y)
self.text.set_text(str(y))
self.text.set_position((xlim0, y))
self.fig.canvas.draw()
if __name__ == '__main__':
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar([0,2,3,5],[4,5,1,3], color="#dddddd")
p = PointPicker(ax)
plt.show()
I'm trying to add vertical lines to a matplotlib plot dynmically when a user clicks on a particular point.
import matplotlib.pyplot as plt
import matplotlib.dates as mdate
class PointPicker(object):
def __init__(self,dates,values):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot_date(dates, values, linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
print self.ax.axvline(x=x, visible=True)
x = mdate.num2date(x)
print x,y,type(x)
if __name__ == '__main__':
import numpy as np
import datetime
dates=[datetime.datetime.now()+i*datetime.timedelta(days=1) for i in range(100)]
values = np.random.random(100)
plt.ion()
p = PointPicker(dates,values)
plt.show()
Here's an (almost) working example. When I click a point, the onpick method is indeed called and the data seems to be correct, but no vertical line shows up. What do I need to do to get the vertical line to show up?
Thanks
You need to update the canvas drawing (self.fig.canvas.draw()):
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axvline(x=x)
self.fig.canvas.draw()
I am trying to plot a polygon of user clicks and render them over a matplotlib canvas:
def helperClick(self, clickEvent):
self.lastXClick = clickEvent.x
self.lastYClick = clickEvent.y
self.lastButtonClick = clickEvent.button
def measurePoly(self):
self.lastButtonClick = None
cid = self.ui.canvas2.mpl_connect('button_press_event', self.helperClick)
#Exit render loop on right click
while self.lastButtonClick != 3:
print('waiting')
if self.lastButtonClick == 1:
#Eventually render polygon on click of 1
print('clicked')
self.ui.canvas2.mpl_disconnect(cid)
#do more stuff with polygon data
I am just trying to "wait" for user clicks, do something on a user click, then continue down the function on a left-click. However, my infinite loop freezes up python and crashes. I know this is a bad way to do this (clearly as it doesn't work :P) but I am not sure how to properly do this.
Thanks,
tylerthemiler
It sounds like you're trying to manually run the "mainloop" in your own code?
The whole point of using callback functions is that you let the gui toolkit run its own mainloop (in this case, it's entered when you call show).
Here's a very simple example of something along the lines of what you're trying to do.
It adds verticies when you left-click on the (initially blank) plot, and then draws the corresponding polygon when you right click. Nothing is drawn until you right-click (It's not too hard to draw the polygon while you're adding points by left-clicking, but doing it efficiently in matplotlib is a bit verbose).
import matplotlib.pyplot as plt
class Plot(object):
def __init__(self):
self.poly = []
self.fig, self.ax = plt.subplots()
self.ax.axis([0, 10, 0, 10])
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
plt.show()
def on_click(self, event):
if event.button == 1:
self.poly.append((event.xdata, event.ydata))
elif event.button == 3:
self.draw_poly()
def draw_poly(self):
self.ax.fill(*zip(*self.poly))
self.poly = []
self.fig.canvas.draw()
Plot()