Adding Interactivity: Widgets

Widgets are objects built-in to Matplotlib which build upon the event callbacks we previously discussed, encapsulating more complicated behavior. There are many possibilities available, and we'll go through a few of them here.

In [ ]:
%pylab

Buttons

A button in matplotlib is exactly what you think it is: a clickable region, in which clicking returns a callback that can be linked to any action.

A simple button can be created like this:

In [ ]:
from matplotlib.widgets import Button

fig = plt.figure()

def callback(event):
    print "clicked:", event
    sys.stdout.flush()

ax1 = plt.axes([0.2, 0.5, 0.1, 0.075])
ax2 = plt.axes([0.7, 0.5, 0.1, 0.075])

b1 = Button(ax1, 'Button 1')
b1.on_clicked(callback)

b2 = Button(ax2, 'Button 2')
b2.on_clicked(callback)
In [ ]:
from matplotlib.widgets import Button

fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.2)

t = np.linspace(0, 10, 1000)
line, = plt.plot(t, np.sin(t), lw=2)

class Index:
    dt = 0
    def next(self, event):
        self.dt -= 1
        line.set_ydata(np.sin(t + self.dt))
        fig.canvas.draw()

    def prev(self, event):
        self.dt += 1
        line.set_ydata(np.sin(t + self.dt))
        fig.canvas.draw()

callback = Index()
axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
axnext = plt.axes([0.81, 0.05, 0.1, 0.075])

bnext = Button(axnext, '>')
bnext.on_clicked(callback.next)

bprev = Button(axprev, '<')
bprev.on_clicked(callback.prev)

Sliders

A Slider is another type of widget which can be used to select a continuous value. Let's see an example similar to the previous one:

In [ ]:
from matplotlib.widgets import Slider

fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.2, left=0.1)

t = np.linspace(0, 10, 1000)
line, = plt.plot(t, np.sin(t), lw=2)

def on_change(val):
    line.set_ydata(np.sin(t - val))

slider_ax = plt.axes([0.1, 0.1, 0.8, 0.02])
slider = Slider(slider_ax, "Offset", -5, 5, valinit=0, color='#AAAAAA')
slider.on_changed(on_change)

Selectors

Selectors can be used to select regions within the plot. Here is an example where points can be selected and changed to a different color:

In [ ]:
from matplotlib.widgets import RectangleSelector

fig, ax = plt.subplots()
x = np.random.normal(size=1000)
y = np.random.normal(size=1000)
c = np.zeros((1000, 3))
c[:, 2] = 1  # set to blue
points = ax.scatter(x, y, s=20, c=c)

def selector_callback(eclick, erelease):
    x1, y1 = eclick.xdata, eclick.ydata
    x2, y2 = erelease.xdata, erelease.ydata
    global c
    c[(x >= min(x1, x2)) & (x <= max(x1, x2))
      & (y >= min(y1, y2)) & (y <= max(y1, y2))] = [1, 0, 0]
    points.set_facecolors(c)
    fig.canvas.draw()
    
    
selector = RectangleSelector(ax, selector_callback,
                             drawtype='box', useblit=True,
                             button=[1,3], # don't use middle button
                             minspanx=5, minspany=5,
                             spancoords='pixels')

You might imagine using this to dynamically interact with your data. For example, you could select a region of your plot, and compute the mean or standard deviation of a third parameter. I've found this useful in interactively exploring multi-dimensional astronomical data.

A similar process can be performed with widgets.Lasso.

Check Buttons

Check buttons are a set of buttons within which multiple options can be selected at once. The callback can then be used to update the plot. Here we'll use the check buttons to display a combination of three curves:

In [ ]:
from matplotlib.widgets import CheckButtons

fig, ax = plt.subplots()
fig.subplots_adjust(left=0.2)

# plot some hidden curves
freqs = np.arange(1, 4)
t = np.linspace(0, 2, 1000)
s = np.sin(2 * np.pi * freqs[:, None] * t)
lines = plt.plot(t, s.T, lw=2, visible=False)
ax.set_ylim(-1.5, 1.5)

# Build check button axes
rax = plt.axes([0.02, 0.4, 0.13, 0.2], aspect='equal')
labels = ('2 Hz', '4 Hz', '6 Hz')
check = CheckButtons(rax, labels, (False, False, False))

def func(label):
    i = labels.index(label)
    lines[i].set_visible(not lines[i].get_visible())
    fig.canvas.draw()

check.on_clicked(func)

Radio Buttons

Radio Buttons, as opposed to check buttons, give a set of buttons of which only one can be selected at a time. Here we'll use radio buttons do select a line style for a curve:

In [ ]:
from matplotlib.widgets import RadioButtons

fig, ax = plt.subplots()
fig.subplots_adjust(left=0.3)

t = np.linspace(0, 10, 1000)
lines = ax.plot(t, np.sin(t))

rax = plt.axes([0.05, 0.4, 0.15, 0.15], axisbg=axcolor)
radio = RadioButtons(rax, ('-', '--', '-.', 'steps', ':'))

def stylefunc(label):
    lines[0].set_linestyle(label)
    plt.draw()
    
radio.on_clicked(stylefunc)

Cursor Tracking

Sometimes it's useful to track the cursor so as to better see where in the plot you're pointing. This can be a simple 1-axes cursor or a multi-axes cursor.

First we'll look at a simple single cursor:

In [ ]:
from matplotlib.widgets import Cursor

fig, ax = plt.subplots()

ax.scatter(np.random.normal(size=1000), np.random.normal(size=1000))

# useblit = True can lead to better performance on some backends
cursor = Cursor(ax, useblit=True, color='gray', linewidth=1)

To track movements across multiple axes, we can use a multi-cursor:

In [ ]:
from matplotlib.widgets import MultiCursor

fig, ax = plt.subplots(2)

x, y, z = np.random.normal(0, 1, (3, 1000))

ax[0].scatter(x, y)
ax[1].scatter(x, z)

multi = MultiCursor(fig.canvas, ax, useblit=True, color='gray', lw=1)

All of these widgets can be used together to quickly build some very interesting interactive data analysis tools. Because they are so quick and easy to set up, they can be deployed on-the-fly to help visualize specific data sets in ways that are the most convenient.

Exercise: Zooming to the Cube

Let's return to the cube we were using before. Here we'll create a slider which controls the zoom on the cube.

In [ ]:
from tutorial_lib.simple_cube import Cube
c = Cube()

# Create the figure and axes, and add the cube to the axes

# create a slider titled "perspective" with values from 1 to 20

# set the slider so that the following function is called when it is changed:
def zoom(val):
    c.set_view((0, 0, val))
    ax.set_xlim(-val, val)
    ax.set_ylim(-val, val)
    fig.canvas.draw()

Bonus: combine the slider with the rotation scripts we used previously, so that you can zoom into the cube with the slider, and rotate the cube with the mouse.

In [ ]: