Search code examples
pythonmatplotlibtkintertkinter-canvasmatplotlib-widget

How to use slider.on_changed() within a class in Tkinter in Python


I'm trying to use a horizontal slider to change the xlim of my plot. But first, I can't figure out how to get the slider to update using the on_changed() method. I don't have a strong understanding of how classes, and objects interact with each other.

I'm using this slider example as a template:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons

fig, ax = plt.subplots()
plt.subplots_adjust(left=0.25, bottom=0.25)
t = np.arange(0.0, 1.0, 0.001)
a0 = 5
f0 = 3
delta_f = 5.0
s = a0 * np.sin(2 * np.pi * f0 * t)
l, = plt.plot(t, s, lw=2)
ax.margins(x=0)

axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor)
axamp = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=axcolor)

sfreq = Slider(axfreq, 'Freq', 0.1, 30.0, valinit=f0, valstep=delta_f)
samp = Slider(axamp, 'Amp', 0.1, 10.0, valinit=a0)


def update(val):
    amp = samp.val
    freq = sfreq.val
    l.set_ydata(amp*np.sin(2*np.pi*freq*t))
    fig.canvas.draw_idle()


sfreq.on_changed(update)
samp.on_changed(update)

resetax = plt.axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, 'Reset', color=axcolor, hovercolor='0.975')


def reset(event):
    sfreq.reset()
    samp.reset()
button.on_clicked(reset)

rax = plt.axes([0.025, 0.5, 0.15, 0.15], facecolor=axcolor)
radio = RadioButtons(rax, ('red', 'blue', 'green'), active=0)


def colorfunc(label):
    l.set_color(label)
    fig.canvas.draw_idle()
radio.on_clicked(colorfunc)

plt.show()

The part that I'm having trouble implementing in my app is:

sfreq.on_changed(update)
samp.on_changed(update)

It works fine if you're opening a plot using plt.show(), but if you're packing it into a canvas like I'm doing below, it stops working. Any ideas why?

Here is the code for my app: UPDATE: I simplified the code to focus on the problem.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import(FigureCanvasTkAgg, NavigationToolbar2Tk)

from tkinter import *
import tkinter as tk

global running
running = True

global graph_exists
graph_exists = False

class MyApp:
    def __init__(self, parent):
        self.myParent = parent ###remember my parent, the root
        self.sfreq = 0
        self.samp = 0

        
        
    def run(self):
        self.fig, self.ax = plt.subplots()
        plt.subplots_adjust(left=0.25, bottom=0.25)
        self.t = np.arange(0.0, 1.0, 0.001)
        a0 = 5
        f0 = 3
        delta_f = 5.0
        s = a0 * np.sin(2 * np.pi * f0 * self.t)
        self.l, = plt.plot(self.t, s, lw=2)
        self.ax.margins(x=0)

        axcolor = 'lightgoldenrodyellow'
        axfreq = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor)
        axamp = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=axcolor)

        self.sfreq = Slider(axfreq, 'Freq', 0.1, 30.0, valinit=f0, valstep=delta_f)
        self.samp = Slider(axamp, 'Amp', 0.1, 10.0, valinit=a0)

        canvas = FigureCanvasTkAgg(self.fig, root)
        canvas.draw()
        canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
        graph_exists = True

        

    def update(self, val):
        amp = self.samp.val
        freq = self.sfreq.val
        self.l.set_ydata(amp*np.sin(2*np.pi*freq*self.t))
        self.fig.canvas.draw_idle()

    if graph_exists:
        self.sfreq.on_changed(update)
        self.samp.on_changed(update)
        

#Run the event loop
root = tk.Tk()
myapp = MyApp(root)
myapp.run()
root.mainloop()

Solution

  • The problem with your code is the method update is never called... You need to put the part if graph_exists into the run method. Besides, as suggested in the post @acw1668 shared, you also need to add the figure into canvas before plotting.

    Try the code below

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.widgets import Slider, Button, RadioButtons
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import(FigureCanvasTkAgg, NavigationToolbar2Tk)
    
    from tkinter import *
    import tkinter as tk
    
    global running
    running = True
    
    global graph_exists
    graph_exists = False
    
    class MyApp:
    
        def __init__(self, parent):
            self.myParent = parent ###remember my parent, the root
            self.sfreq = 0
            self.samp = 0
    
        def run(self):
            self.fig, self.ax = plt.subplots()
            canvas = FigureCanvasTkAgg(self.fig, root)
            canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
            
            plt.subplots_adjust(left=0.25, bottom=0.25)
            self.t = np.arange(0.0, 1.0, 0.001)
            a0 = 5
            f0 = 3
            delta_f = 5.0
            s = a0 * np.sin(2 * np.pi * f0 * self.t)
            self.l, = plt.plot(self.t, s, lw=2)
            self.ax.margins(x=0)
    
            axcolor = 'lightgoldenrodyellow'
            axfreq = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor)
            axamp = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=axcolor)
    
            self.sfreq = Slider(axfreq, 'Freq', 0.1, 30.0, valinit=f0, valstep=delta_f)
            self.samp = Slider(axamp, 'Amp', 0.1, 10.0, valinit=a0)
            graph_exists = True
            
            if graph_exists:
                self.sfreq.on_changed(self.update)
                self.samp.on_changed(self.update)
    
        def update(self, val):
            print('inside update')
            amp = self.samp.val
            freq = self.sfreq.val
            self.l.set_ydata(amp*np.sin(2*np.pi*freq*self.t))
            
            self.fig.canvas.draw_idle()
            
    
    #Run the event loop
    root = tk.Tk()
    myapp = MyApp(root)
    myapp.run()
    root.mainloop()