We saw before that plot elements can be adjusted dynamically. The creation of an interactive plots is a short step from this: we simply need to add callbacks to the plot so that mouse and keyboard actions in the figure window lead to the plot elements being updated.
This is a little known aspect of matplotlib: the presence of key bindings and callback
functions which can be bound to various actions.
The main interface to these is through the plt.connect
interface. You can see the various events by looking at the documentation string
of the plt.connect
function:
# start in non-inline mode, so we can use interactive plots
%pylab
print help(plt.connect)
Using these sorts of callbacks is extremely straightforward. Let's start with looking at binding key-presses to actions.
import sys
fig, ax = plt.subplots()
def on_key_press(event):
print event.key
sys.stdout.flush()
fig.canvas.mpl_connect('key_press_event', on_key_press)
You might notice that if you hit the s
key here, you get some somewhat
unexpected behavior. For example, if you press the g
key, you'll see
that the grid lines toggle on or off. If you press the s
key, you'll
see an interactive figure save window appear.
These key-bound shortcuts are activated by default, but can be turned off with the following command:
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)
Here's the last code snippet repeated all at once:
import sys
fig, ax = plt.subplots()
def on_key_press(event):
print event.key
sys.stdout.flush()
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)
fig.canvas.mpl_connect('key_press_event', on_key_press)
Let's create a slightly more interesting callback function. We can use the same line updating code to dynamically adjust the color of a line:
fig, ax = plt.subplots()
x = np.linspace(0, 10, 1000)
line, = ax.plot(x, np.sin(x))
def on_key_press(event):
# If the key is one of the shorthand color notations, set the line color
if event.key in 'rgbcmyk':
line.set_color(event.key)
fig.canvas.draw()
fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)
fig.canvas.mpl_connect('key_press_event', on_key_press)
Exercise: modify the above function so that if the user presses a number, the linewidth of the line is changed to that number.
Just as we bound keyboard callbacks to actions, we can bind mouse buttons and movements to actions. The mouse events store the button as an integer ID (Note that the button press event stores both the pixel value of the event location, as well as the data value of the event location.
import sys
fig, ax = plt.subplots()
def on_button_press(event):
print dir(event)
print "Button:", event.button
print "Figure coordinates:", event.x, event.y
print "Data coordinates:", event.xdata, event.ydata
sys.stdout.flush()
fig.canvas.mpl_connect('button_press_event', on_button_press)
Another interesting mouse event is the motion_notify_event
. Here, rather than the
callback being activated whenever the mouse button is clicked, it is activated whenever
the mouse is moved.
import sys
fig, ax = plt.subplots()
def on_mouse_move(event):
print "Data coordinates:", event.xdata, event.ydata
sys.stdout.flush()
fig.canvas.mpl_connect('motion_notify_event', on_mouse_move)
Mouse callbacks allow us to do some interesting things. For example, here we'll manipulate the color of a polygon by clicking on it:
fig, ax = plt.subplots()
ax.set_xlim(-1, 2)
ax.set_ylim(-1, 2)
rect = plt.Rectangle((0, 0), 1, 1, fc=np.random.random(3))
ax.add_patch(rect)
def on_click(event):
x, y = event.xdata, event.ydata
if (x > 0) and (x < 1) and (y > 0) and (y < 1):
rect.set_fc(np.random.random(3))
fig.canvas.draw()
fig.canvas.mpl_connect('button_press_event', on_click)
Because clicking on polygons and other plot elements is such a common piece of many APIs, there is a cleaner and simpler way to do this: a pick event. A pick event is an event associated with any artist instance on the plot. We can re-create our random polygon example using pick events in this way:
fig, ax = plt.subplots()
ax.set_xlim(-1, 2)
ax.set_ylim(-1, 2)
rect = plt.Rectangle((0, 0), 1, 1, fc=np.random.random(3), picker=True)
ax.add_patch(rect)
def on_pick(event):
artist = event.artist
artist.set_fc(np.random.random(3))
fig.canvas.draw()
fig.canvas.mpl_connect('pick_event', on_pick)
We can do a similar thing for a line rather than a polygon:
fig, ax = plt.subplots()
x = np.linspace(0, 10, 1000)
ax.plot(x, np.sin(x), c=np.random.random(3), lw=3, picker=5) # 5 points tolerance
ax.plot(x, np.cos(x), c=np.random.random(3), lw=3, picker=5) # 5 points tolerance
def on_pick(event):
artist = event.artist
artist.set_color(np.random.random(3))
fig.canvas.draw()
fig.canvas.mpl_connect('pick_event', on_pick)
Use the above framework to draw N
circles, all of which can have
their color changed by clicking on them:
Now we'll do a bit of an exercise based on a blog post that I did a while ago.
I've written some code which will show a 3D cube on the matplotlib canvas, and your task is to add event hooks that allow it to be manipulated by clicking and dragging the mouse.
There's a script which creates a cube object, which can be rotated, projected, and shown on a 2D axis. It can be used like this:
from tutorial_lib.simple_cube import Cube
c = Cube()
fig, ax = plt.subplots(figsize=(8, 8))
c.add_to_ax(ax)
c.rotate(c.x - c.y, -np.pi / 6)
The cube can be rotated by calling the rotate
method with the following arguments:
axis
: a 3D vector specifying the direction of the axis of rotationangle
: a rotation angle (in radians)We can call rotate()
, followed by the draw method to show the cube rotating:
for i in range(50):
c.rotate([1, 0, 0], np.pi / 20)
fig.canvas.draw()
With this machinery in place, we can set up some key bindings to make this object interactive.
Let's first bind the left and right arrows to y-axis rotation, and the up and down arrows to x-axis rotation:
def key_press(event):
if event.key == 'left':
c.rotate(c.y, np.pi / 12)
elif event.key == 'right':
c.rotate(c.y, -np.pi / 12)
elif event.key == 'up':
c.rotate(c.x, np.pi / 12)
elif event.key == 'down':
c.rotate(c.x, -np.pi / 12)
fig.canvas.draw()
fig.canvas.mpl_connect('key_press_event', key_press)
Now we'll stop a bit for an exercise. This is your task:
Create a cube which can be manipulated by clicking and dragging
You'll likely need to set up three different callback functions:
button_press_event
: when the mouse button is pressed, set an indicator variable to True
, and store the x and y location.button_release_event
: when the mouse button is released, set an indicator variable to False
motion_notify_event
: when the mouse moves, check if the button is pressed. If so, rotate the cube about the x or y axis (or both!), depending on the new x/y location (be sure to call draw()
to re-draw the figure!)We'll take 10 minutes or so for you to try this.