Search code examples
pythonmatplotlibipythonspyder

Matplotlib Freezes When input() used in Spyder


Windows 7. If I open a plain ipython terminal at the command line I can type:

import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4, 5])
plt.show(block=False)
input("Hello ")

But if I do the same thing in Spyder, as soon as I ask for user input, the Matplotlib window freezes, so I can't interact with it. I need to interact with the plot while the prompt is showing.

In both Spyder and the plain console, matplotlib.get_backend() return 'Qt4Agg'

Edit: To clarify, I have matplotlib set up where it shows up in its own window, not embedded as a PNG. (I had to set Backend: Automatic originally to get this behavior)

As an aside, in Spyder, the plot opens instantly after plt.plot(). In the regular console, it only opens after plt.show(). Also, if I press Ctrl-C after typing input() in Spyder, the entire console hangs unexpectedly. Vs. in IPython, it just raises KeyboardInterrupt and returns control to the console.

Edit: Even more complete example: Works in IPython console, not in Spyder (freezes). Want to move the plot around, according to user input.

import matplotlib.pyplot as pl

def anomaly_selection(indexes, fig, ax):
    selected = []

    for i in range(0, len(indexes)):
        index = indexes[i]
        ax.set_xlim(index-100, index+100)
        ax.autoscale_view()
        fig.canvas.draw()
        print("[%d/%d] Index %d " % (i, len(indexes), index), end="")
        while True:   
            response = input("Particle? ")
            if response == "y":
                selected.append(index)
                break
            elif response == "x":
                return selected
            elif response == "n":
                break

fig, ax = pl.subplots(2, sharex=True)
ax[0].plot([1, 2, 3, 4, 5]) # just pretend data
pl.show(block=False)

sel = anomaly_selection([100, 1000, 53000, 4300], fig, ax[0])

Lots of Edit: I believe this is an issue with input() blocking Qt. My workaround if this question doesn't get traction, is to build a Qt window with the Matplotlib plot embedded in it, and get keyboard input through the window instead.


Solution

  • After a lot more digging I came to the conclusion that you simply should be making a GUI. I would suggest you use PySide or PyQt. In order for matplotlib to have a graphical window it runs an event loop. Any click or mouse movement fires an event which triggers the graphical part to do something. The problem with scripting is that every bit of code is top level; it suggests the code is running sequentially.

    When you manually input the code into the ipython console it works! This is because ipython has already started a GUI event loop. Every command that you call is handled within the event loop allowing other events to happen as well.

    You should be creating a GUI and declare that GUI backend as the same matplotlib backend. If you have a button click trigger the anomaly_selection function then that function is running in a separate thread and should allow you to still interact within the GUI.

    With lots of fiddling and moving around the way you call fucntions you could get the thread_input function to work.

    Fortunately, PySide and PyQt allow you to manually make a call to process GUI events. I added a method that asks for input in a separate thread and loops through waiting for a result. While it is waiting it tells the GUI to process events. The return_input method will hopefully work if you have PySide (or PyQt) installed and are using it as matplotlib's backend.

    import threading
    
    def _get_input(msg, func):
        """Get input and run the function."""
        value = input(msg)
        if func is not None:
            func(value)
        return value
    # end _get_input
    
    def thread_input(msg="", func=None):
        """Collect input from the user without blocking. Call the given function when the input has been received.
    
        Args:
            msg (str): Message to tell the user.
            func (function): Callback function that will be called when the user gives input.
        """
        th = threading.Thread(target=_get_input, args=(msg, func))
        th.daemon = True
        th.start()
    # end thread_input
    
    def return_input(msg=""):
        """Run the input method in a separate thread, and return the input."""
        results = []
        th = threading.Thread(target=_store_input, args=(msg, results))
        th.daemon = True
        th.start()
        while len(results) == 0:
            QtGui.qApp.processEvents()
            time.sleep(0.1)
    
        return results[0]
    # end return_input
    
    if __name__ == "__main__":
    
        stop = [False]
        def stop_print(value):
            print(repr(value))
            if value == "q":
                stop[0] = True
                return
            thread_input("Enter value:", stop_print)
    
        thread_input("Enter value:", stop_print)
        add = 0
        while True:
            add += 1
            if stop[0]:
                break
    
        print("Total value:", add)
    

    This code seems to work for me. Although it did give me some issues with ipython kernel.

    from matplotlib import pyplot as pl
    
    import threading
    
    
    def anomaly_selection(selected, indexes, fig, ax):
        for i in range(0, len(indexes)):
            index = indexes[i]
            ax.set_xlim(index-100, index+100)
            ax.autoscale_view()
            #fig.canvas.draw_idle() # Do not need because of pause
            print("[%d/%d] Index %d " % (i, len(indexes), index), end="")
            while True:
                response = input("Particle? ")
                if response == "y":
                    selected.append(index)
                    break
                elif response == "x":
                    selected[0] = True
                    return selected
                elif response == "n":
                    break
    
        selected[0] = True
        return selected
    
    
    fig, ax = pl.subplots(2, sharex=True)
    ax[0].plot([1, 2, 3, 4, 5]) # just pretend data
    pl.show(block=False)
    
    sel = [False]
    th = threading.Thread(target=anomaly_selection, args=(sel, [100, 1000, 53000, 4300], fig, ax[0]))
    th.start()
    #sel = anomaly_selection([100, 1000, 53000, 4300], fig, ax[0])
    
    
    while not sel[0]:
        pl.pause(1)
    th.join()