pythongarbage-collectionmatplotlib-widget

How to prevent garbage collection in Python when an interactive plot runs in the background (Python, matplotlib)?


I have a code that uses matplotlib and the Button widget. It all works well, but when this code is written as a function, the buttons stop working. This is because after the function runs, the button objects are being removed by the garbage collector.

Here is an example of the code that does not work well:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button

def fun():
    def Prev(x):
        print("Prev")
    def Next(x):
        print("Next")

    freqs = np.arange(2, 20, 3)
    fig, ax = plt.subplots()
    plt.subplots_adjust(bottom=0.2)
    t = np.arange(0.0, 1.0, 0.001)
    s = np.sin(2*np.pi*freqs[0]*t)
    l, = plt.plot(t, s, lw=2)
    axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
    axnext = plt.axes([0.81, 0.05, 0.1, 0.075])
    bnext = Button(axnext, 'Next')
    bnext.on_clicked(Next)
    bprev = Button(axprev, 'Previous')
    bprev.on_clicked(Prev)

fun()

Here is my non-so-elegant solution:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button

def fun():
    def Prev(x):
        print("Prev")
    def Next(x):
        print("Next")

    freqs = np.arange(2, 20, 3)
    fig, ax = plt.subplots()
    plt.subplots_adjust(bottom=0.2)
    t = np.arange(0.0, 1.0, 0.001)
    s = np.sin(2*np.pi*freqs[0]*t)
    l, = plt.plot(t, s, lw=2)
    axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
    axnext = plt.axes([0.81, 0.05, 0.1, 0.075])
    bnext = Button(axnext, 'Next')
    bnext.on_clicked(Next)
    bprev = Button(axprev, 'Previous')
    bprev.on_clicked(Prev)
    return bnext,bprev

b1,b2=fun()

Is there a better, best-practice solution for this kind of problem?


Solution

  • As you correctly assumed the key is to keep alive a reference to the button in the outer scope. If you don't want to return a collection of buttons what you could do is, to add the buttons as attribute of the figure. I wouldn't say there is a best practice for such a thing of just plotting one figure, and for a more complex UI there might be better solutions. But this code solves this specific issue without having to explicitly returning the button references.

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.widgets import Button
    import  matplotlib
    matplotlib.use("TkAgg")
    
    
    def fun():
        def Prev(x):
            print("Prev")
        def Next(x):
            print("Next")
    
        freqs = np.arange(2, 20, 3)
        fig, ax = plt.subplots()
        plt.subplots_adjust(bottom=0.2)
        t = np.arange(0.0, 1.0, 0.001)
        s = np.sin(2*np.pi*freqs[0]*t)
        l, = plt.plot(t, s, lw=2)
        axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
        axnext = plt.axes([0.81, 0.05, 0.1, 0.075])
        bnext = Button(axnext, 'Next')
        bnext.on_clicked(Next)
        bprev = Button(axprev, 'Previous')
        bprev.on_clicked(Prev)
        fig.bnext = bnext    # add button references to figure object
        fig.bprev = bprev
    
    
    if __name__ == "__main__":
        fun()
        plt.show()