Event handling

We now have a good background for some practical work with matplotlib events. We are now ready to explore the nitty-gritty. Let's begin with the list of events that matplotlib supports:

Event name

Class

Description

button_press_event

MouseEvent

The mouse button is pressed

button_release_event

MouseEvent

The mouse button is released

draw_event

DrawEvent

The canvas draw occurs

key_press_event

KeyEvent

A key is pressed

key_release_event

KeyEvent

A key is released

motion_notify_event

MouseEvent

Motion of the mouse

pick_event

PickEvent

An object in the canvas is selected

resize_event

ResizeEvent

The figure canvas is resized

scroll_event

MouseEvent

The scroll wheel of the mouse is rolled

figure_enter_event

LocationEvent

The mouse enters a figure

figure_leave_event

LocationEvent

The mouse leaves a figure

axes_enter_event

LocationEvent

The mouse enters an axes object

axes_leave_event

LocationEvent

The mouse leaves an axes object

Note that the classes listed in the preceding events table are defined in matplotlib.backend_bases.

The perceptual field of matplotlib is the canvas object—this is where the events take place, and this is the object that provides the interface to connect the code to the events. The canvas object has an mpl_connect method, which must be called if you want to provide custom user interaction features along with your plots. This method just takes the following two arguments:

  • A string value for the event, which can be any of the values listed in the Event Name column of the preceding table
  • A callback function or method

We will demonstrate some ways to use mpl_connect in the subsequent examples.

Mouse events

In matplotlib, the mouse events may be any of the following:

  • button_press_event: This event involves a mouse button press
  • button_release_event: This event involves a mouse button release
  • scroll_event: This event involves scrolling of the mouse
  • motion_notify_event: This event involves a notification pertaining to the mouse movement

The notebook for this chapter has a section on mouse events, which shows several examples. We will take a look at one of these in particular; it demonstrates the press and release events, and it does so with an almost tactile feedback mechanism. These are the tasks that it does:

  • It connects a callback for mouse press and mouse release
  • It records the time of the click
  • It records the time of the release
  • It draws a circle at the coordinates of the click
  • It draws another partially transparent circle whose size is determined by the amount of time that passed between the click and the release

Let's take a look at the code so that you can clearly see how to implement each of the aforementioned things:

In [8]: class Callbacks:
            def __init__(self):
                (figure, axes) = plt.subplots()
                axes.set_aspect(1)
                figure.canvas.mpl_connect(
                    'button_press_event', self.press)
                figure.canvas.mpl_connect(
                    'button_release_event', self.release)

            def start(self):
                plt.show()

            def press(self, event):
                self.start_time = time.time()

            def release(self, event):
                self.end_time = time.time()
                self.draw_click(event)

            def draw_click(self, event):
                size = 4 * (self.end_time - self.start_time) ** 2
                c1 = plt.Circle(
                    [event.xdata, event.ydata], 0.002,)
                c2 = plt.Circle(
                    [event.xdata, event.ydata], 0.02 * size,
                    alpha=0.2)
                event.canvas.figure.gca().add_artist(c1)
                event.canvas.figure.gca().add_artist(c2)
                event.canvas.figure.show()

        cbs = Callbacks()
        cbs.start()

Here, we saw the aforementioned mpl_connect method in action. The constructor in our class sets up the figure's canvas instance with two callbacks. One will be fired when the canvas detects button_press_event, and the other will be fired when the canvas detects button_release_event.

When you run the preceding code in the notebook and click on the resulting graph, you will see something like this (depending on where you click and how long you hold down the mouse button):

Mouse events

There are a few things worth highlighting here:

  • We don't need to store any references to the canvas or the figure in this object; we utilize the constructor merely to set up the callbacks for the two events
  • We utilized the canvas' mpl_connect method to register callbacks, as previously discussed
  • The event object not only provides access to event-specific attributes such as the coordinates of the mouse click, but also has references to useful objects, such as the canvas instance and therefore, the figure instance as well

Keyboard events

Keyboard events are similar to the mouse events, though there are only two of them:

  • key_press_event
  • key_release_event

These are used in the same way as the corresponding mouse button events, though they provide an interesting opportunity for the ergonomically sensitive user.

As of matplotlib version 1.4 and IPython version 2.3, keyboard events for plots are not supported in the nbagg backend. Therefore, we will use IPython from the terminal for this section.

In the following section, we're going to take a look at an example that may give you some ideas with regard to the customization of your own plots. We're going to make a simple data viewer. Before we look at the code, let's describe what we want to do:

  • We want to easily navigate through large datasets with simple keyboard commands
  • Given a large dataset, we would like to have each subset displayed in its own plots
  • In the GUI, we would like to navigate forwards and backwards through the datasets, visualizing the plot of each set on the fly
  • We would like to return to the beginning of the datasets with a key press

This is pretty straightforward, and it an excellent use case for keyboard events. We'll have to build some pieces for this, though. Here's what we will need:

  • A function that returns each member of our large dataset one after the other
  • A class that lets us move forwards and backwards through our dataset (by making use of the preceding function)
  • A class that performs the basic configuration and setup
  • Additional support functions to create and update the plots

In a terminal IPython session, we can perform the initial imports and run the color map setup in the following way:

In [1]: import numpy as np
In [2]: import matplotlib as mpl
In [3]: from matplotlib import pyplot as plt
In [4]: import seaborn as sns

In [5]: pallete_name = "husl"
In [6]: colors = sns.color_palette(pallete_name, 8)
In [7]: colors.reverse()
In [8]: cmap = mpl.colors.LinearSegmentedColormap.from_list(
            pallete_name, colors)

Next, we need a function that can act as a given dataset as well as another function that will act as the very large dataset (infinite, in this case). The second function is a good fit if you wish to use a generator:

In [9]: def make_data(n, c):
            r = 4 * c * np.random.rand(n) ** 2
            theta = 2 * np.pi * np.random.rand(n)
            area = 200 * r**2 * np.random.rand(n)
            return (r, area, theta)

In [10]: def generate_data(n, c):
             while True:
                 yield make_data(n, c)

We've now received an endless number of datasets that can be used in our viewer. Now, let's create the data Carousel class. It's needs to perform the following tasks:

  • Maintain a reference to the data
  • Move to the next data in the set, putting the viewed data into a queue
  • Move back to the previous data items, pushing the ones it passes onto a separate queue while it takes them off the other queue
  • Provide a callback for key_press_event and then dispatch to the other functions, depending on the pressed key
  • Provide an initial plot as well as the event-handling functions for specific keys

Here's what the data Carousel class looks like:

In [13]: class Carousel:
             def __init__(self, data):
                 (self.left, self.right) = ([], [])
                 self.gen = data
                 self.last_key = None
             def start(self, axes):
                 make_plot(*self.next(), axes=axes)
             def prev(self):
                 if not self.left:
                     return []
                 data = self.left.pop()
                 self.right.insert(0, data)
                 return data
             def next(self):
                 if self.right:
                     data = self.right.pop(0)
                 else:
                     data = next(self.gen)
                 self.left.append(data)
                 return data
             def reset(self):
                 self.right = self.left + self.right
                 self.left = []
             def dispatch(self, event):
                 if event.key == "right":
                     self.handle_right(event)
                 elif event.key == "left":
                     self.handle_left(event)
                 elif event.key == "r":
                     self.handle_reset(event)
             def handle_right(self, event):
                 print("Got right key ...")
                 if self.last_key == "left":
                     self.next()
                 update_plot(*self.next(), event=event)
                 self.last_key = event.key
             def handle_left(self, event):
                 print("Got left key ...")
                 if self.last_key == "right":
                     self.prev()
                 data = self.prev()
                 if data:
                     update_plot(*data, event=event)
                 self.last_key = event.key
             def handle_reset(self, event):
                 print("Got reset key ...")
                 self.reset()
                 update_plot(*self.next(), event=event)
                 self.last_key = event.key
In [14]: %paste

The class that will perform the setup and configuration duties, including the connecting of key_press_event to the callback and the instantiating of the Carousel object, is as follows:

In [15]: class CarouselManager:
             def __init__(self, density=300, multiplier=1):
                 (figure, self.axes) = plt.subplots(
                     figsize=(12,12), subplot_kw={"polar": "True"})
                 self.axes.hold(False)
                 data = generate_data(density, multiplier)
                 self.carousel = Carousel(data)
                 _ = figure.canvas.mpl_connect(
                     'key_press_event', self.carousel.dispatch)
             def start(self):
                 self.carousel.start(self.axes)
                 plt.show()
In [16]: %paste

Note

Note that after pasting the code for each class, IPython will need you to use the %paste command so that it can accurately parse what was pasted.

And with this, we're ready to navigate through our data visually. We'll make the displayed circles larger than the default size by setting the multiplier to a higher number in the following way:

In [17]: cm = CarouselManager(multiplier=2)
In [18]: cm.start()

This will make a GUI window pop up on the screen with an image that looks like this:

Keyboard events

Now try pressing the right and left arrows of your keyboard to view the different (randomly generated) datasets. As coded in the Carousel class, pressing R will return us to the beginning of the data.

Note

Depending on the web browser that you are using, you may see different results, experience poor performance, or find out that some actions don't work at all. We have found out that the best user experience with regard to a browser for matplotlib is with the Firefox and Chrome browsers.

The most practical understanding that you can take away, which you can use in your own projects, is that you need to write a dispatch method or function for every key (or key combination) press and release event. The simplicity of the matplotlib interface of providing a single key press event for all the keys ensures that the matplotlib code stays compact while keeping the options open for the developers. The flip side is that you need to implement the individual key handling yourself for whichever keys your particular use case has a need of.

Axes and figure events

We can execute callbacks when the mouse enters and leaves figures or axes in a way that is similar to the connection of the keyboard and mouse events. This can be helpful if you have complex plots with many subplots. It will allow you to provide a visual feedback to the user regarding the subplot that is our focus or even expose a larger view of the focused plot. The IPython Notebook for this chapter covers an example of this.

Object picking

The next event that we will mention is a special one—the event of an object being picked. Object picking is one of the greatest, although unsung, features of matplotlib as it allows one to essentially create a custom data browser that is capable of revealing the details in the deeply nested or rich data across large scales.

Every Artist instance (naturally including subclasses of Artist) has an attribute called picker. The setting of this attribute is what enables object picking in matplotlib.

The definition of picked can vary, depending on the context. For instance, setting Artist.picker can have the following results:

  • If the result is True, picking is enabled for the artist object and pick_event will be fired every time a mouse event occurs over the artist object in the figure.
  • If the result is a number (for instance, float or int), the value is interpreted as a tolerance. If the event's data (such as the x and y values) is within the value of this tolerance, pick_event will be fired.
  • If the result is a callable, then the provided function or method returns a boolean value, which determines whether pick_event is fired.
  • If the result is None, picking is disabled.

The object picking feature provides the means by which a programmer can create dynamic views on data. This feature is rivaled only by expensive proprietary software. Thanks to this capability, in conjunction with matplotlib's custom styles, one can easily create beautiful, compelling data visualization applications that are tailored for the needs of the user.

The IPython Notebook in this chapter covers an example of this, and it should serve as a source of inspiration for a great number of use cases.

Compound event handling

This section discusses the combining of multiple events or other sources of data in order to provide a more highly customized user experience for visual plot updates, the preparation of data, the setting of object properties, or the updating of widgets. The multiple events or decisions that are made based on multiple or cascading events is what we will refer to as compound events.

The navigation toolbar

The first example of compound events that we will touch upon are those managed by a backend widget for interactive navigation. This widget is available for all backends (including the nbagg backend for IPython when it is not in the inline mode). The navigation toolbar widget has multiple buttons, each with a specific function. In brief, the functionality associated with each button is as follows:

  • Home: This button returns the figure to its originally rendered state.
  • Previous: This returns to the previous view in the plot's history.
  • Next: This moves to the next view in the plot's history.
  • Pan/Zoom: You can pan across the plot by clicking and holding the left mouse button. You can zoom by clicking and holding the right mouse button (behavior differs between the Cartesian and Polar plots).
  • Zoom-to-Rectangle: You can zoom in on a selected portion of the plot by using this button.
  • Subplot Configuration: With this button, you can configure the display of subplots via a pop-up widget with various parameters.
  • Save: This button will save the plot in its currently displayed state to a file.

Furthermore, when a toolbar action is engaged, the toolbar instance sets the toolbar's current mode. For instance, when the Zoom-to-Rectangle button is clicked, the mode will be set to zoom rect. When in Pan/Zoom, the mode will be set to pan/zoom. These can be used in conjunction with the supported events to fire callbacks in response to the toolbar activity.

As a matter of fact, the matplotlib.backend_bases.NavigationToolbar2 toolbar class is an excellent place to look for examples of compound events. Let's examine the Pan/Zoom button. The class tracks the following via the attributes that can be set:

  • The connection ID for a press event
  • The connection ID for a release event
  • The connection ID for a mouse move event (this is correlated to a mouse drag later in the code)
  • Whether the toolbar is active
  • The toolbar mode
  • The zoom mode

During the toolbar setup, the toolbar button events are connected to callbacks. When these buttons are pressed and the callbacks are fired, old events are disconnected and the new ones are connected. In this way, a chain of events may be set up with a particular sequence of events firing only a particular set of callbacks in a particular order.

Specialized events

The code in NavigationToolbar2 is a great starting place if you want some ideas on how you can combine events in your own projects. You can have a workflow that requires responses to plot updates only if a series of other events have taken place first. You can accomplish this by connecting the events to and disconnecting them from various callbacks.

Interactive panning and zooming

Let's utilize a combination of the toolbar mode and a mouse button release for a practical example that demonstrates the creation of a compound event.

The problem that we want to address is this—when a user pans or zooms out of the range of the previously computed data in a plotted area, the user is presented with parts of an empty grid with no visualization in the newly exposed area. It would be nice if we could put our newfound event callback skills to use in order to solve this issue.

One possible example where it would be useful to refresh the plot figure when it has been panned is a plot for a topographic map. We're going to do a few things with this example:

  • Add a custom cmap (matplotlib's color map) method to give the altitudes the look of a physical map
  • Provide the altitude in meters and the distance in kilometers
  • Create a class that can update the map via a method call

The custom color map and the equations to generate a topographical map have been saved to ./lib/topo.py in this chapter's IPython Notebook repository. We imported this module at the beginning of the notebook. So it's ready to use. The first class that we will define is TopoFlowMap, a wrapper class that will be used to update the plot when we pan:

In [21]: class TopoFlowMap:
             def __init__(self, xrange=None, yrange=None, seed=1):
                 self.xrange = xrange or (0,1)
                 self.yrange = yrange or (0,1)
                 self.seed = seed
                 (self.figure, self.axes) = plt.subplots(
                    figsize=(12,8))
                 self.axes.set_aspect(1)
                 self.colorbar = None
                 self.update()

             def get_ranges(self, xrange, yrange):
                 if xrange:
                     self.xrange = xrange
                 if yrange:
                     self.yrange = yrange
                 return (xrange, yrange)

             def get_colorbar_axes(self):
                 colorbar_axes = None
                 if self.colorbar:
                     colorbar_axes = self.colorbar.ax
                     colorbar_axes.clear()
                 return colorbar_axes

             def get_filled_contours(self, coords):
                 return self.axes.contourf(
                    cmap=topo.land_cmap, *coords.values())

             def update_contour_lines(self, filled_contours):
                 contours = self.axes.contour(
                    filled_contours, colors="black", linewidths=2)
                 self.axes.clabel(
                    contours, fmt="%d", colors="#330000")

             def update_water_flow(self, coords, gradient):
                 self.axes.streamplot(
                     coords.get("x")[:,0],
                     coords.get("y")[0,:],
                     gradient.get("dx"),
                     gradient.get("dy"),
                     color="0.6",
                     density=1,
                     arrowsize=2)

             def update_labels(self):
                 self.colorbar.set_label("Altitude (m)")
                 self.axes.set_title(
                    "Water Flow across Land Gradients", fontsize=20)
                 self.axes.set_xlabel("$x$ (km)")
                 self.axes.set_ylabel("$y$ (km)")

             def update(self, xrange=None, yrange=None):
                 (xrange, yrange) = self.get_ranges(xrange, yrange)
                 (coords, grad) = topo.make_land_map(
                    self.xrange, self.yrange, self.seed)
                 self.axes.clear()
                 colorbar_axes = self.get_colorbar_axes()
                 filled_contours = self.get_filled_contours(coords)
                 self.update_contour_lines(filled_contours)
                 self.update_water_flow(coords, grad)
                 self.colorbar = self.figure.colorbar(
                    filled_contours, cax=colorbar_axes)
                 self.update_labels()

The notebook returns to the IPython backend to display the graph (previously using a different backend for other examples):

In [22]: plt.switch_backend('nbAgg')
In [23]: tfm = TopoFlowMap(
           xrange=(0,1.5), yrange=(0,1.5), seed=1732)
         plt.show()

The preceding code gives us the following plot:

Interactive panning and zooming

If you click on the Pan/Zoom button in the navigation toolbar and then drag the plotted data about, you will see that the empty grid contains the data that hasn't been plotted (the area that was outside the axes prior to the panning action).

Since we do want to redraw and there is no pan event to connect to, what are our options? Well, two come to mind:

  • Piggyback on draw_event, which fires each time the canvas is moved
  • Use button_release_event, which will fire when the panning is complete

If our figure was easy to draw with simple equations, the first option would probably be fine. However, we're performing some multivariate calculus on our simulated topography. As you might have noticed, our plot does not render immediately. So, let's go with the second option.

To make our lives easier, we will take advantage of the mode attribute of NavigationTool2, which will let us know when one of the events that we care about, pan/zoom, has taken place.

Here's the manager class for the plot-refresh feature:

In [24]: class TopoFlowMapManager:
         def __init__(self, xrange=None, yrange=None, seed=1):
             self.map = TopoFlowMap(xrange, yrange, seed)
             _ = self.map.figure.canvas.mpl_connect(
                 'button_release_event',
                 self.handle_pan_zoom_release)

         def start(self):
             plt.show()

         def handle_pan_zoom_release(self, event):
             if event.canvas.toolbar.mode != "pan/zoom":
                 return
             self.map.update(event.inaxes.get_xlim(),
                             event.inaxes.get_ylim())
             event.canvas.draw()

As with the other examples, we used the constructor to set up the event callback for a mouse button release. The callback will be fired for every button click. However, the code will not execute past the conditional if we are not in the pan/zoom mode. In our case, the callback does two crucial things for our feature:

  • It recalculates the ranges of the x and y axes
  • It calls the update method on the TopoFlowMap instance, which changes the range for the NumPy mgrid function and recalculates the gradients for this new range

You can test this out with the following code:

In [25]: tfmm = TopoFlowMapManager(
             xrange=(0,1.5), yrange=(0,1.5), seed=1732)
         tfmm.start()

This particular plot is fairly involved, and hence, it is potentially similar to some real-world examples that you may come across. However, keep in mind that if you have simple data to display with little or no calculation, firing your callback on draw_event instead of button_release_event will render the update as you move the mouse.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset