Interactive figures and asynchronous programming#
Matplotlib supports rich interactive figures by embedding figures intoa GUI window. The basic interactions of panning and zooming in anAxes to inspect your data is available out-of-the-box. This issupported by a full mouse and keyboard event handling system thatyou can use to build sophisticated interactive graphs.
This guide is meant to be an introduction to the low-level details ofhow Matplotlib integration with a GUI event loop works. For a morepractical introduction to the Matplotlib event API seeeventhandling system,Interactive Tutorial, andInteractive Applications using Matplotlib.
GUI events#
All GUI frameworks (Qt, Wx, Gtk, Tk, macOS, or web) have some method ofcapturing user interactions and passing them back to the application, butthe exact details depend on the toolkit (for example callbacks in Tk ortheSignal /Slot framework in Qt). The Matplotlibbackends encapsulate the details of the GUI frameworks andprovide a framework-independent interface to GUI events through Matplotlib'sevent handling system. By connecting functionsto the event handling system (seeFigureCanvasBase.mpl_connect), you caninteractively respond to user actions in a GUI toolkit agnostic way.
Event loops#
Fundamentally, all user interaction (and networking) is implemented asan infinite loop waiting for events from the user (via the OS) andthen doing something about it. For example, a minimal Read EvaluatePrint Loop (REPL) is
exec_count=0whileTrue:inp=input(f"[{exec_count}] > ")# Readret=eval(inp)# Evaluateprint(ret)# Printexec_count+=1# Loop
This is missing many niceties (for example, it exits on the firstexception!), but is representative of the event loops that underlieall terminals, GUIs, and servers[1]. In general theRead stepis waiting on some sort of I/O -- be it user input or the network --while theEvaluate andPrint are responsible for interpreting theinput and thendoing something about it.
In practice we interact with a framework that provides a mechanism toregister callbacks to be run in response to specific events ratherthan directly implement the I/O loop[2]. For example "when theuser clicks on this button, please run this function" or "when theuser hits the 'z' key, please run this other function". This allowsusers to write reactive, event-driven, programs without having todelve into the nitty-gritty[3] details of I/O. The core event loopis sometimes referred to as "the main loop" and is typically started,depending on the library, by methods with names likeexec,run, orstart.
Command prompt integration#
So far, so good. We have the REPL (like the IPython terminal) thatlets us interactively send code to the interpreter and get resultsback. We also have the GUI toolkit that runs an event loop waitingfor user input and lets us register functions to be run when thathappens. However, if we want to do both we have a problem: the promptand the GUI event loop are both infinite loops and cannot run inparallel. In order for both the prompt and the GUI windows to beresponsive we need a method to allow the loops to "timeshare" :
Blocking the prompt: let the GUI main loop block the pythonprocess when you want interactive windows
Input hook integration: let the CLI main loop block the pythonprocess and intermittently run the GUI loop
Full embedding: fully embed python in the GUI(but this is basically writing a full application)
Blocking the prompt#
Display all open figures. | |
Run the GUI event loop forinterval seconds. | |
Start a blocking event loop. | |
Stop the current blocking event loop. |
The simplest solution is to start the GUI event loop and let it runexclusively, which results in responsive figure windows. However, theCLI event loop will not run, so that you cannot enter new commands.We call this "blocking" mode. (Your terminal may echo the typed characters,but they will not yet be processed by the CLI event loop because the Pythoninterpreter is busy running the GUI event loop).
It is possible to stop the GUI event loop and return control to the CLIevent loop. You can then use the prompt again, but any still open figurewindows are non-responsive. Re-starting the GUI event loop will make thesefigure responsive again (and will process any queued up user interaction).
The typical command to show all figures and run the GUI event loopexclusively until all figures are closed is
plt.show()
Alternatively, you can start the GUI event loop for a fixed amount of timeusingpyplot.pause.
If you are not usingpyplot you can start and stop the event loopsviaFigureCanvasBase.start_event_loop andFigureCanvasBase.stop_event_loop. However, in most contexts whereyou would not be usingpyplot you are embedding Matplotlib in alarge GUI application and the GUI event loop should already be runningfor the application.
Away from the prompt, this technique can be very useful if you want towrite a script that pauses for user interaction, or displays a figurebetween polling for additional data. SeeScripts and functionsfor more details.
Input hook integration#
While running the GUI event loop in a blocking mode or explicitlyhandling UI events is useful, we can do better! We really want to beable to have a usable promptand interactive figure windows.
We can do this using the "input hook" feature of the interactiveprompt. This hook is called by the prompt as it waits for the userto type (even for a fast typist the prompt is mostly waiting for thehuman to think and move their fingers). Although the details varybetween prompts the logic is roughly
start to wait for keyboard input
start the GUI event loop
as soon as the user hits a key, exit the GUI event loop and handle the key
repeat
This gives us the illusion of simultaneously having interactive GUIwindows and an interactive prompt. Most of the time the GUI eventloop is running, but as soon as the user starts typing the prompttakes over again.
This time-share technique only allows the event loop to run whilepython is otherwise idle and waiting for user input. If you want theGUI to be responsive during long running code it is necessary toperiodically flush the GUI event queue as described inExplicitly spinning the event loop.In this case it is your code, not the REPL, whichis blocking the process so you need to handle the "time-share" manually.Conversely, a very slow figure draw will block the prompt until itfinishes drawing.
Full embedding#
It is also possible to go the other direction and fully embed figures(and aPython interpreter) in a richnative application. Matplotlib provides classes for each toolkitwhich can be directly embedded in GUI applications (this is how thebuilt-in windows are implemented!). SeeEmbedding Matplotlib in graphical user interfaces formore details.
Scripts and functions#
Flush the GUI events for the figure. | |
Request a widget redraw once control returns to the GUI event loop. | |
Blocking call to interact with a figure. | |
Blocking call to interact with a figure. | |
Display all open figures. | |
Run the GUI event loop forinterval seconds. |
There are several use-cases for using interactive figures in scripts:
capture user input to steer the script
progress updates as a long running script progresses
streaming updates from a data source
Blocking functions#
If you only need to collect points in an Axes you can useFigure.ginput. However if you have written some custom eventhandling or are usingwidgets you will need to manually run the GUIevent loop using the methods describedabove.
You can also use the methods described inBlocking the promptto suspend run the GUI event loop. Once the loop exits your code willresume. In general, any place you would usetime.sleep you can usepyplot.pause instead with the added benefit of interactive figures.
For example, if you want to poll for data you could use something like
fig,ax=plt.subplots()ln,=ax.plot([],[])whileTrue:x,y=get_new_data()ln.set_data(x,y)plt.pause(1)
which would poll for new data and update the figure at 1Hz.
Explicitly spinning the event loop#
Flush the GUI events for the figure. | |
Request a widget redraw once control returns to the GUI event loop. |
If you have open windows that have pending UIevents (mouse clicks, button presses, or draws) you can explicitlyprocess those events by callingFigureCanvasBase.flush_events.This will run the GUI event loop until all UI events currently waitinghave been processed. The exact behavior is backend-dependent buttypically events on all figure are processed and only events waitingto be processed (not those added during processing) will be handled.
For example
importtimeimportmatplotlib.pyplotaspltimportnumpyasnpplt.ion()fig,ax=plt.subplots()th=np.linspace(0,2*np.pi,512)ax.set_ylim(-1.5,1.5)ln,=ax.plot(th,np.sin(th))defslow_loop(N,ln):forjinrange(N):time.sleep(.1)# to simulate some workln.figure.canvas.flush_events()slow_loop(100,ln)
While this will feel a bit laggy (as we are only processing user inputevery 100ms whereas 20-30ms is what feels "responsive") it willrespond.
If you make changes to the plot and want it re-rendered you will needto calldraw_idle to request that the canvas bere-drawn. This method can be thought ofdraw_soon in analogy toasyncio.loop.call_soon.
We can add this to our example above as
defslow_loop(N,ln):forjinrange(N):time.sleep(.1)# to simulate some workifj%10:ln.set_ydata(np.sin(((j//10)%5*th)))ln.figure.canvas.draw_idle()ln.figure.canvas.flush_events()slow_loop(100,ln)
The more frequently you callFigureCanvasBase.flush_events the moreresponsive your figure will feel but at the cost of spending moreresources on the visualization and less on your computation.
Stale artists#
Artists (as of Matplotlib 1.5) have astale attribute which isTrue if the internal state of the artist has changed since the lasttime it was rendered. By default the stale state is propagated up tothe Artists parents in the draw tree, e.g., if the color of aLine2Dinstance is changed, theAxes andFigure thatcontain it will also be marked as "stale". Thus,fig.stale willreport if any artist in the figure has been modified and is out of syncwith what is displayed on the screen. This is intended to be used todetermine ifdraw_idle should be called to schedule a re-renderingof the figure.
Each artist has aArtist.stale_callback attribute which holds a callbackwith the signature
defcallback(self:Artist,val:bool)->None:...
which by default is set to a function that forwards the stale state tothe artist's parent. If you wish to suppress a given artist from propagatingset this attribute to None.
Figure instances do not have a containing artist and theirdefault callback isNone. If you callpyplot.ion and are not inIPython we will install a callback to invokedraw_idle whenever theFigure becomes stale. InIPython we use the'post_execute' hook to invokedraw_idle on any stale figuresafter having executed the user's input, but before returning the promptto the user. If you are not usingpyplot you can use the callbackFigure.stale_callback attribute to be notified when a figure hasbecome stale.
Idle draw#
Render the | |
Request a widget redraw once control returns to the GUI event loop. | |
Flush the GUI events for the figure. |
In almost all cases, we recommend usingbackend_bases.FigureCanvasBase.draw_idle overbackend_bases.FigureCanvasBase.draw.draw forces a rendering ofthe figure whereasdraw_idle schedules a rendering the next timethe GUI window is going to re-paint the screen. This improvesperformance by only rendering pixels that will be shown on the screen. Ifyou want to be sure that the screen is updated as soon as possible do
fig.canvas.draw_idle()fig.canvas.flush_events()
Threading#
Most GUI frameworks require that all updates to the screen, and hencetheir main event loop, run on the main thread. This makes pushingperiodic updates of a plot to a background thread impossible.Although it seems backwards, it is typically easier to push yourcomputations to a background thread and periodically updatethe figure on the main thread.
In general Matplotlib is not thread safe. If you are going to updateArtist objects in one thread and draw from another you should makesure that you are locking in the critical sections.
Event loop integration mechanism#
CPython / readline#
The Python C API provides a hook,PyOS_InputHook, to register afunction to be run ("The function will be called when Python'sinterpreter prompt is about to become idle and wait for user inputfrom the terminal."). This hook can be used to integrate a secondevent loop (the GUI event loop) with the python input prompt loop.The hook functions typically exhaust all pending events on the GUIevent queue, run the main loop for a short fixed amount of time, orrun the event loop until a key is pressed on stdin.
Matplotlib does not currently do any management ofPyOS_InputHook dueto the wide range of ways that Matplotlib is used. This management is left todownstream libraries -- either user code or the shell. Interactive figures,even with Matplotlib in "interactive mode", may not work in the vanilla pythonrepl if an appropriatePyOS_InputHook is not registered.
Input hooks, and helpers to install them, are usually included withthe python bindings for GUI toolkits and may be registered on import.IPython also ships input hook functions for all of the GUI frameworksMatplotlib supports which can be installed via%matplotlib. Thisis the recommended method of integrating Matplotlib and a prompt.
IPython / prompt_toolkit#
With IPython >= 5.0 IPython has changed from using CPython's readlinebased prompt to aprompt_toolkit based prompt.prompt_toolkithas the same conceptual input hook, which is fed intoprompt_toolkit via theIPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook()method. The source for theprompt_toolkit input hooks lives atIPython.terminal.pt_inputhooks.
Footnotes
[1]A limitation of this design is that you can only wait for oneinput, if there is a need to multiplex between multiple sourcesthen the loop would look something like
fds=[...]whileTrue:# Loopinp=select(fds).read()# Readeval(inp)# Evaluate / Print
Or you canwrite your own if you must.
[3]These examples are aggressively dropping many of thecomplexities that must be dealt with in the real world such askeyboard interrupts, timeouts, bad input, resourceallocation and cleanup, etc.