1

I'm looking for a a method to generate interactive figures that works in Windows and Linux, commandline or Spyder. For example, the following script:

import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 1, tight_layout=True)
#plt.ion() # doesn't seem to do much
ax.plot([0,1,2],[0,3,2])
fig.show()
#plt.show(block=False) # similar behavior as fig.show()
#plt.pause(1)
input('Press enter to quit.')

Behavior for different environments:

  • Linux command line: plot window shows up and is responsive while the script waits for user input. The window stays even if the program continues running (not in this short example), although the zoom buttons don't work anymore. This is desired behavior.

  • Windows command line: an empty nonresponsive plot window shows up, which disappears when the program ends. Adding plt.pause() results in an interactive plot, but it is only responsive for the specified number of seconds.

  • Linux/Windows Spyder with iPython, configured for automatic plots: figures show up and are responsive, but only after the script finishes.

  • Linux/Windows Spyder, configured for inline plots: plots show up after the script finishes, but with warnings due to the tight_layout=True parameter: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. and matplotlib is currently using a non-GUI backend. (Note that I need tight_layout because otherwise often axis labels are clipped or figures with multiple subplots have bad margins.)

  • Linux/Windows Spyder, inline plots: with plt.plot(...); plt.show() (rather than the fig.plot(...); fig.show() object-oriented way), the inline plots show up during program execution.

How can I write code that generates interactive plots during program execution (possibly while waiting for a keypress) that will run correctly from the Windows command line, Linux command line, and Spyder?

Edit: plt.show() instead of fig.show() will result in a plot being shown correctly, but outside IPython, it blocks execution of the script until I click the close button of the figure window. This is rather cumbersome when there are multiple figures or when the calculation is not yet finished. And plt.show(block=False) has the same behavior as fig.show().

I'm using an noncustomized Anaconda 5.1 environment with Python 3.6.4, matplotlib 2.1.2, spyder 3.2.6. In Spyder: Tools > Preferences > IPython > Backend, set to 'inline' or 'automatic' (and restarting kernel after changing this setting.)

Han-Kwang Nienhuys
  • 3,084
  • 2
  • 12
  • 31
  • There are explanation of (almost) all observed behaviour. However, what is the problem you want to solve? Why not simply use `plt.show()` to generate the figure window? – ImportanceOfBeingErnest Nov 16 '18 at 14:39
  • @ImportanceOfBeingErnest I'd like to keep figure windows in view while program execution continues. `plt.show()` is blocking the program until the window is closed and `plt.show(block=False)` suffers from the same problems as `fig.show()`. – Han-Kwang Nienhuys Nov 19 '18 at 10:04
  • How about checking in your code [which platform you're running on](https://stackoverflow.com/questions/1854/python-what-os-am-i-running-on#answer-1857) and given this condition running a specific part of code? For Spyder, you can check the environmental variables and check if the Spyder's ones exist. – arudzinska Nov 19 '18 at 10:10
  • @arudzinska I experimented a bit with solutions that at least work for one environment, but I've not found something that allows interactive figures to persist in Windows after interaction. With `plt.pause(1)` in a loop that runs until a keypress, the figure window steals the focus every second (in Linux, not in Windows 7). I guess that I'll have to live with plots becoming interactive in Spyder after program termination and no solution for Windows command line. – Han-Kwang Nienhuys Nov 19 '18 at 13:09
  • Update: I wrote my own solution with environment detection. Does not work well in all cases, though. – Han-Kwang Nienhuys Nov 19 '18 at 14:22

2 Answers2

0

It seems the following is what you're after:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, 1, tight_layout=True)

plt.ion()
ax.plot([0,1,2],[0,3,2])

plt.show()
plt.pause(0.1)

input('Press enter to quit.')
plt.close()

In general, a python program is evaluated linearly. This contradicts the desire to have a blocking command line (input) but a non-blocking GUI.
What happens in the above is that a GUI is created, but without any propper event-loop started. Therefore the code can continue to be executed even after the GUI being shown. The drawback of this is clearly that without event-loop, you cannot interact with the GUI.

In case a plotting window needs to be shown and some user input is required, or further code is to be executed after showing the fully interactive figure, one can run such code within the GUI event loop.

import matplotlib.pyplot as plt

def run_after(callback, pause=10, figure=None):
    figure = figure or plt.gcf()
    timer = figure.canvas.new_timer(interval=pause)
    timer.single_shot = True
    timer.add_callback(callback)
    timer.start()
    return timer


fig, ax = plt.subplots(1, 1, tight_layout=True)
ax.plot([0,1,2],[0,3,2])

# This function contains the code to be executed within the GUI event loop
def mycallback():
    import tkinter
    from tkinter import simpledialog
    root = tkinter.Tk()
    root.withdraw()
    w = simpledialog.askinteger("Title", "How many numbers do you want to print")

    for i in range(w):
        print(i)


cb = run_after(mycallback)

plt.show()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • If I run this code from Spyder in Linux, I get an empty plot window and a dialog; after dismissing the dialog, the plot gets updated. If the program continues after `plt.show()`, e.g. `import time; time.sleep(5)`, then the dialog shows up after the program finishes. – Han-Kwang Nienhuys Nov 19 '18 at 14:15
  • Maybe 10 milliseconds is too fast. Try `cb = run_after(mycallback, 500)` instead. – ImportanceOfBeingErnest Nov 19 '18 at 14:46
  • The extra pause helps in the original script - although it would require me to estimate the time needed to plot (depending on computer speed and data size). But if the program continues after `plt.show()`, the dialog and plot do not show up until the program finishes, which I can also achieve with `plt.show(block=False)` without a callback. – Han-Kwang Nienhuys Nov 19 '18 at 15:38
  • I'm not sure I understand what you mean by "if the program continues after `plt.show()`". The `mycallback` **is** the continuation of the program after `plt.show()`; you cannot combine this with `plt.ion()`. – ImportanceOfBeingErnest Nov 19 '18 at 15:46
  • I want to look at preliminary data before a calculation is fully converged. For example, add `import time; time.sleep(5)` after the last line of the script. Then I don't see a plot until the script is finished. – Han-Kwang Nienhuys Nov 19 '18 at 16:33
  • So your calculation has two parts? One preliminary, and one final? Then using the first approach, you'd do `plt.ion()` -> preliminary -> `plt.show()` -> final -> `plt.ioff(); plt.show()`. Using the second approach, you'd put that into the `mycallback` function: preliminary -> `fig.canvas.draw_idle()` -> final -> `fig.canvas.draw_idle()`. – ImportanceOfBeingErnest Nov 19 '18 at 16:57
0

Here is an approach that sort of works, but not very well for all environments:

import matplotlib.pyplot as plt
import sys
import matplotlib

def plt_show_interactive():
    """Show plot windows, with interaction if possible; prompt to continue.""" 
    in_ipython = ('get_ipython' in globals())
    inline_backend = ('inline' in matplotlib.get_backend())
    in_linux = (sys.platform == 'linux')

    if inline_backend:
        plt.show()
    elif not in_linux and in_ipython:
        print("Press Ctrl-C to continue.")
        try:    
            while True:
                plt.pause(0.5)
        except KeyboardInterrupt:
            print("Continuing.")
    elif in_linux and not in_ipython:
        # Command line: plots are interactive during wait for input.
        plt.show(block=False)
        input("Press ENTER to continue.")
    elif in_linux and in_ipython:
        # Loop with plt.pause(1) causes repeated focus stealing.
        plt.pause(1)
        print("Sorry, plots are not interactive until program has finished.")
    elif not in_linux and not in_ipython:
        # Ctrl-C is handled differently here.
        plt.pause(1)
        input("Sorry, not interactive. Press ENTER to continue.")


def prompt_if_not_ipython(verb="end"):
    """Ask user to press ENTER if not we're not inside IPython."""
    if ('get_ipython' not in globals()):
        input("Press ENTER to %s." % verb)


fig, ax = plt.subplots(1, 1, tight_layout=True)
ax.plot([0,1,2],[0,3,2])

plt_show_interactive()
prompt_if_not_ipython()
Han-Kwang Nienhuys
  • 3,084
  • 2
  • 12
  • 31