Monday, July 20, 2015

Displaying Plots Inside Loops

In the early stages of some programming projects, I often like to explore the problem visually: to run a series of simulations with different values of the input parameters and create a series of graphs in order to gain some intuition about the problem. A convenient approach is to embed some plotting commands in a loop.

For example, suppose I want to see what the first ten Bessel functions look like. It seems that the following script should do what I want:

import numpy as np
import matplotlib.pyplot as plt
from scipy.special import jn                # Import Bessel function.

r = np.linspace(0,20,101)

for n in range(10):
    plt.plot(r, jn(n,r))                    # Draw nth Bessel function.
    plt.title("Bessel function J[%d](r)." % n)
    input("Press <Enter> to continue.")     # Wait for user input to continue.
    plt.cla()                               # Clear axes for next plot.

However, when I run the script, all I see is an empty plot window, even though I have to hit <Enter> ten times at the command prompt!

This behavior is due to the way Python handles events in the graphical user interface (GUI) used to display plots. Basically, Python creates the figure, but moves on to the next command before giving me a chance to look at it. What I need is some way to force Python to display the plot until I am ready to move on. (Asking for input at the command line does not accomplish this.)

plt.waitforbuttonpress()

PyPlot provides the perfect function: plt.waitforbuttonpress(). Referring to its documentation, we see that this function is a “blocking call to interact with the figure.” In other words, Python is not allowed to execute any other commands until the user interacts with the figure. The documentation also explains that there is an optional timeout argument, and the function returns different values depending on what happens:

  • True — The user pressed a key on the keyboard.
  • False — The user clicked the mouse.
  • None — The (optional) timer elapsed.

Clicking the mouse on buttons like “Zoom” or “Save Figure” do not return a value. Only a mouse click within the actual plot causes the function to return False.

The following loop will force Python to display each plot until I press a button on the keyboard or click with the mouse:

for n in range(10):
    plt.plot(r, jn(n,r))                # Draw nth Bessel function.
    plt.title("Bessel function J[%d](r)." % n)
    plt.waitforbuttonpress()            # Display plot and wait for user input.
    plt.cla()                           # Clear axes for next plot.

When I run the script, I can see each Bessel function, but if I try to zoom in on the figure, Python advances to the next plot. I can get around this by placing the command inside an “infinite” loop and taking advantage of the different return values of the function:

for n in range(10):
    plt.plot(r, jn(n,r))                    # Draw nth Bessel function.
    plt.title("Bessel function J[%d](r)." % n)
    while True:
        if plt.waitforbuttonpress(): break  # Exit loop if user presses a key.
    plt.cla()                               # Clear axes for next plot.

At each iteration of the while loop, Python waits for the result of plt.waitforbuttonpress(). If I click the mouse to zoom or move the axes, the return value is False, so the loop executes again. However, if I press a key on the keyboard, the return value is True and Python breaks out of the loop. I can use the mouse to interact with each plot as much as I like, then press a key to move on to the next one when I am ready.

You can accomplish the same task in fewer lines with a different while loop:

while not plt.waitforbuttonpress(): pass

This while loop does nothing at each iteration (pass) and continues to cycle so long as the return value of plt.waitforbuttonpress() evaluates to False.

Exercises

Rewrite the script so that Python will display each plot for 5 seconds unless the user presses a key or clicks the mouse (inside the active portion of the plot window), at which point the countdown timer resets.

Rewrite the script so that Python will display each plot until the user presses a key on the keyboard or there are 10 seconds of inactivity — whichever comes first.

Thursday, July 16, 2015

Raising a Figure Window to the Foreground

This post describes a utility function that will raise a plot window to the foreground of your screen. The function will only work with the Qt graphics backend, so I will start with a brief overview of graphics backends. If you just want to use the function as a black box, you can do the following:

  1. Set the Graphics Backend to “Qt” in the Spyder preferences menu.
  2. Copy this function into your working directory.

Graphics Backends

You may have have written a script to produce precisely the data you need, but a lot of processing is required to transform these numbers into a figure. You need to create a plot window, draw a figure inside of it, and manage all of the attributes that control a figure’s appearance: title, axis labels, line widths, colors, tick marks, etc. All of this happens in the background when you type a command like plt.plot(x,y). A graphics backend is the software that Python uses to physically draw a figure on your computer screen.

If you have already imported PyPlot, you can see which backend you are currently using:

plt.get_backend()

There are many backends available. You can get a list of those available for your Python environment as follows:

import matplotlib
matplotlib.rcsetup.all_backends

My installation lists 23 available backends. Many of these have cryptic names like “TkAgg” or “Qt5Agg”. These names describe the software packages used to create Python libraries for displaying windows. (See Qt, Tk, or Agg, for instance.) If you are working from the command line (outside of Spyder), you can select an available backend by typing

matplotlib.use(backend name)

before you import PyPlot. If you have already imported PyPlot, this command will have no effect.

In Spyder, you must select a backend from the Preferences menu: Preferences > IPython > Graphics. There is a button with a drop-down menu that allows you to select a graphics backend. You must reset the kernel or restart Spyder for the change to take effect.

In my installation of Spyder, the Preferences menu allows me to select among 5 options: Inline, Automatic, Mac OSX, Tkinter, and Qt.

Each of these has its own strengths and weaknesses. “Inline” is fast, but it does not allow you to interact with the plots you create. “Mac OSX” allows me to use OS X shortcut keys like <Cmd-~> to cycle through open plot windows and <Cmd-W> to close the current plot window. “Tkinter” is an extremely clean and simple graphical user interface (GUI). (You can use Tkinter to develop your own widgets — GUI’s for scripts you write — but that is a topic for another post …) However, the “Qt” backend offers two useful features not present in the other backends:

  1. Interactive plot menu: When you create a plot, you can access an interactive menu to adjust line properties, axis properties, range, and labels.
  2. A window manager that allows you to bring the plot window to the foreground.

The interactive plot menu is nice when the figure you have created is almost perfect. You can do a few minor modifications without having to run a plotting script again. (It can be frustrating to wait for Python to redraw a complex figure when you only want to change the size of the title font!)

The window manager is useful if your installation of Spyder — like mine — tends to create new figure windows in the background where you cannot see them. Instead of minimizing Spyder and whatever other applications you have open, you can type a command at the IPython prompt to bring the figure to the foreground.

Sending a Window to the Foreground

To see how this works, start a new IPython session using the Qt backend. (Remember: If you are using Spyder, you must set this in the Preferences menu. If you are working from the command line, you must set the backend before you import PyPlot.)

import matplotlib
matplotlib.use('Qt5Agg')

import numpy as np
import matplotlib.pyplot as plt

Now, generate some data, create a figure with a name, and plot the data.

x = np.linspace(-1,1,101)
y = x**3 - x

plt.figure('cubic')
plt.plot(x, y, 'r', lw=3)

When you create a figure, the Qt backend creates a figure manager, an object that can draw and print the figure to the screen and manipulate the figure window. To gain control of the figure manager in our example, type

cfm = plt.get_current_fig_manager('cubic')

One of the objects associated with a figure manager created by the Qt backend is called window. It is essentially the object that controls the window used by your operating system to display the figure you have drawn. Using two of the window object’s methods, we can raise the plot window to the foreground from the command line:

cfm.window.activateWindow()
cfm.window.raise_()

Your figure window should have come to the foreground of your screen when you executed these commands.

A Utility Function

If you find this useful, you may wish to save typing in the future and combine all of these commands into a single function:

def raise_window(figname=None):
    """
    Raise the plot window for Figure figname to the foreground.  If no argument
    is given, raise the current figure.

    This function will only work with a Qt graphics backend.  It assumes you
    have already executed the command 'import matplotlib.pyplot as plt'.
    """

    if figname: plt.figure(figname)
    cfm = plt.get_current_fig_manager()
    cfm.window.activateWindow()
    cfm.window.raise_()

Save this function in a script or module in your working directory and import it whenever you wish to use it. After creating a plot, you can raise the figure to the foreground with a single command:

plt.figure('quartic')
plt.plot(x, x**4 - x**2, 'b', lw=3)
raise_window('quartic')

If you don’t give your figures names as you create them, you can refer to them by number as well.

Tk Backend

You can accomplish the same task of raising a window using the “Tk” backend. Use the following commands:

plt.plot(x,y)

cfm = plt.get_current_fig_manager()
cfm.window.attributes('-topmost', True)

This will fix the plot window in the foreground until you close it. To allow other application windows to rise to the top of the screen as you access them, type

cfm.window.attributes('-topmost', False)

This will leave the plot window in the foreground until you click on another application.

You can bundle these commands into a utility function as well:

def raise_window(figname=None):
    """
    Raise the plot window for Figure figname to the foreground.  If no argument
    is given, raise the current figure.

    This function will only work with a Tk graphics backend.  It assumes you
    have already executed the command 'import matplotlib.pyplot as plt'.
    """

    if figname: plt.figure(figname)
    cfm = plt.get_current_fig_manager()
    cfm.window.attributes('-topmost', True)
    cfm.window.attributes('-topmost', False)
    return cfm

After creating a plot, you can raise the figure to the foreground (if you are using a Tk backend, such as “TkAgg”) with a single command:

plt.figure('quartic')
plt.plot(x, x**4 - x**2, 'b', lw=3)
raise_window('quartic')

Again, if you don’t give your figures names as you create them, you can refer to them by number as well.