This chapter marks the beginning of our transition into specialized topics. While we will continue making references to the preceding overview, the subsequent chapters will focus on taking you deeper into the domain of advanced matplotlib usage.
In this chapter, we will delve into matplotlib events and interactive plots and cover the following topics:
The material presented here will be pulled from matplotlib's artist and backend layers, and it should provide a nice starting point for the vast world of user interaction in data visualization.
To follow along with this chapter's code, clone the notebook's repository and start up IPython by using the following code:
$ git clone https://github.com/masteringmatplotlib/interaction.git $ cd interaction $ make
Since we will be using the IPython Notebook backend—which is also known as nbagg
—for interactive plots, we will not enter the usual %matplotlib inline
command:
In [1]: import matplotlib matplotlib.use('nbagg')
We'll use the regular imports as well as a few other imports in the following examples:
In [2]: import random import sys import time import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt import seaborn as sns from IPython.display import Image from typecheck import typecheck sys.path.append("../lib") import topo
In the chapter on matplotlib architecture, we mentioned the event loops that the GUI toolkits use and which are integrated with matplotlib. As you dig through the matplotlib libraries, you will eventually come across several mentions with regard to the event loops that are not from the GUI toolkits. So that we can better understand the difference between these and, more importantly, the event handling and user interaction with plots, we're going to spend some time learning more about these event loops in matplotlib.
In general, event loops are part of a recurring set of patterns that are used together in various computing frameworks. Often, they comprise of the following:
Event-based systems are typically asynchronous in nature, allowing events to be sent to the system and then responded to in any order without the slower operations preventing the faster ones from being executed sooner. It is this characteristic that makes the event-based systems such a compelling option for software architects and developers. When used properly, asynchronous programming can lead to significant improvements in the perceived performance of an application.
Event-based systems take many forms. Some common examples for the same include the following:
It is a combination of the first two examples with which we will occupy ourselves in this chapter. Before we tackle this though, let's take a look at the basic workings of event-based systems, and in particular, the event loop.
So, what did we mean previously when we said a looping construct? Well, the simplest loop that you can create in Python is the following:
while True: pass
This will just run forever until you press the Ctrl + C keys or the power goes out. However, this particular loop does nothing useful. Here's another loop that will run until its event fires a change of a value from True
to False
:
In [4]: x = True while x: time.sleep(1) if random.random() < 0.1: x = False
So, what relation do these simple loops have with the loops that power toolkits, such as GTK and Qt, or frameworks, such as Twisted and Tornado, possess? Usually, the event systems have the following:
During each run, a loop will usually check a data structure to see whether there are any new events that occurred since the last time it looped. In a network event system, each loop will check to see whether any file descriptors are ready to read or write. In a GUI toolkit, each loop will check to see whether any clicks or button presses occurred.
Given the preceding simple criteria, let's explore another slightly more sophisticated and minimally demonstrative event loop. To keep this small, we are not going to integrate with socket or GUI events. The event that our loop will respond to will be quite minimal indeed—a keyboard interrupt:
In [5]: class EventLoop: def __init__(self): self.command = None self.status = None self.handlers = {"interrupt": self.handle_interrupt} self.resolution = 0.1 def loop(self): self.command = "loop" while self.command != "stop": self.status = "running" time.sleep(self.resolution) def start(self): self.command = "run" try: self.loop() except KeyboardInterrupt: self.handle_event("interrupt") def stop(self): self.command = "stop" @typecheck def add_handler(self, fn: callable, event: str): self.handlers[event] = fn @typecheck def handle_event(self, event: str): self.handlers[event]() def handle_interrupt(self): print("Stopping event loop ...") self.stop()
Here's what we did:
interrupt
eventloop
methodNow, you can run this loop in the notebook by using the following code:
In [*]: el = EventLoop() el.start()
When you evaluate this cell, IPython will display the usual indicator that a cell is running, in other words, the In [*]
cell. When you're satisfied that the loop is merrily looping, go to the IPython Notebook menu and navigate to Kernel | Interrupt
. The cell with a loop in it will finish, and the asterisk will be replaced by the input number. The interrupt handler will be printed out as a status message as well:
In [6]: el = EventLoop() el.start() Stopping event loop ...
Though this event loop is fairly different from the power networking libraries or GUI toolkits, it's very close (both in nature as well as the code) to the default event loops matplotlib provides for its canvas objects. As such, this is a perfect starting place if you want to have a deeper understanding of matplotlib. To continue in this vein, reading the matplotlib backend source code will serve you well.
The preceding practical background information should be enough for you to more fully appreciate the following few sections.
How are the main loops in the GUI toolkits different from the preceding ones? Well, for one, the GTK and Qt event systems aren't written in Python. The GTK main loop is written in C, and it is a part of one of the libraries underlying GTK—GLib
. More importantly, the GUI toolkits are designed to respond to the user input from many types of devices, and sometimes, at a very fine-grained level. The loop demonstrated previously is a simple, generic loop that allows one to define any sort of event, which can also be connected to a callback.
However, even with all the differences, they can still be mapped to each other. The GLib
event loop documentation provides the following diagram:
In the simple EventLoop
class, setting the initial value of the handlers attribute is similar to what GLib
can do in a prepare()
or query()
call. Our version of the check()
call is really just a part of the Python control flow; we took the easy route with a try/except
method wrapped around the loop call. The only way the class can monitor the outside world for events is through a KeyboardInterrupt
exception. Our dispatch()
is the call to the registered handler for the interrupt event. Hence, despite the simplicity of our example, one can see how it relates to the fully featured, real-world event systems.
An in-depth exploration of the GUI main loops is well beyond the scope of this book, but before we leave the topic entirely behind, let's look at what's running under IPython's bonnet.
IPython originally started as an advanced read-eval-print loop (REPL) for Python. After about a decade of becoming a powerful tool for Python scientific computing, using NumPy and matplotlib, and providing parallel computing features, IPython added support for HTML notebooks. Since then, the popularity of IPython Notebook and the user-created content that runs on it has exploded. The matplotlib development community has responded with a matplotlib backend that enables interaction in the browser, which is similar to what users might expect when running a GTK or Qt backend.
The IPython Notebook requires two main components—an HTTP server and a web client, such as your browser. The functionality of a websocket-enabled HTTP server is provided by Tornado, a Python web framework, and an asynchronous networking library. Your browser provides the infrastructure necessary to support the websocket communications with the web server. Through ZeroMQ, IPython provides the means to send the data it receives over websockets to the rest of the components in the IPython architecture.
Some of these components have their own event loops. They include the following:
However, what this picture is missing is how this relates to matplotlib. In order for a web-based matplotlib backend to work, it needs the following:
ioloop
in IPython Notebook appThe last bullet item may cause confusion. So, it is important to make this clear—the event loops that matplotlib uses for its canvases are completely different and quite separate from the GUI event loops. In fact, the default figure canvas event loop is very similar to the loop method in the EventLoop
class from the previous section.
The GUI and matplotlib event loops have different code. Yet, they are still connected. In the case of an IPython Notebook app, the web browser's JavaScript events and websockets ultimately communicate with the IPython kernel, which in turn communicates with the instances of matplotlib figure managers, figures, and canvases.
Understanding how these pieces work together and communicate is the key to mastering the matplotlib events, both for the creation of more advanced plots, as well as the inevitable debugging that they will need. The best resource for a deeper understanding of these events is the matplotlib source code. Spend time getting to know how figures and canvases respond to events, messages get sent from one class to another, how matplotlib generalizes the event experience across the GUI toolkits, and how this affects the toolkit of your choice.