How switch between two graphs and maintain radio button and slider updates working?

Viewed 197

I made a graph viewer GUI program in Python using Tkinter and matplotlib, where I switch between two graphs.

I have three problems I don't know how to fix:

  1. Can't change the radiobutton after I move the slider it stops updating.
  2. Can't change radiobutton after I switch the graph.
  3. I would like to switch between graphs with 1 subplot and 2 subplots, but when I switch to graph with 2 subplots with slider and radiobar I can't move back to first.

I think the problem might be in the way I update the slider and radiobutton

Here is the code:

import matplotlib
import tkinter as Tk
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
import numpy as np
from tkinter import *
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.backend_bases import key_press_handler
from matplotlib.widgets import Slider, RadioButtons

# Seperated out config of plot to just do it once
def config_plot():
    fig, ax = plt.subplots()
    ax.set(xlabel='time (s)', ylabel='voltage (mV)',
           title='Graph One')
    return (fig, ax)


class matplotlibSwitchGraphs:
    def __init__(self, master):
        self.master = master
        self.frame = Frame(self.master)
        self.fig, self.ax = config_plot()
        self.graphIndex = 0
        self.canvas = FigureCanvasTkAgg(self.fig, self.master)
        self.config_window()
        self.draw_graph_one()
        self.frame.pack(expand=YES, fill=BOTH)

    def config_window(self):
        self.canvas.mpl_connect("key_press_event", self.on_key_press)
        toolbar = NavigationToolbar2Tk(self.canvas, self.master)
        toolbar.update()
        self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
        self.button = Button(self.master, text="Quit", command=self._quit)
        self.button.pack(side=BOTTOM)
        self.button_switch = Button(self.master, text="Switch Graphs", command=self.switch_graphs)
        self.button_switch.pack(side=BOTTOM)

    def plot_data(self):
        def func3(x, y):
            return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2 + y ** 2))
        dx, dy = 0.05, 0.05
        x = np.arange(-3.0, 3.0, dx)
        y = np.arange(-3.0, 3.0, dy)
        X, Y = np.meshgrid(x, y)
        self.extent = np.min(x), np.max(x), np.min(y), np.max(y)
        self.Z1 = np.add.outer(range(8), range(8)) % 2  # chessboard
        self.Z2 = func3(X, Y)

    def draw_graph_one(self):
        self.plot_data()
        self.ax.remove()
        self.ax = plt.imshow(self.Z2, cmap=plt.cm.viridis, alpha=.9, interpolation='bilinear',
                             extent=self.extent)
        self.canvas.draw()

    def draw_graph_two(self):
        self.plot_data()
        self.ax.remove()
        self.ax = plt.subplot(1, 2, 1)
        self.ax = plt.imshow(self.Z1, cmap=plt.cm.gray, interpolation='nearest',
                         extent=self.extent)
        self.ax = plt.subplot(1, 2, 2)
        self.a = plt.imshow(self.Z2, cmap=plt.cm.viridis, alpha=.9, interpolation='bilinear',
                         extent=self.extent)
        self.b = plt.imshow(self.Z1, cmap=plt.cm.gray, interpolation='nearest',
                         extent=self.extent)
        self.canvas.draw()

        plt.subplots_adjust(left=0.15, bottom=0.15)
        slider_ax = plt.axes([0.06, 0.25, 0.0225, 0.5])

        alfa_slider = Slider(slider_ax,
                             label="Transparency",
                             valmin=0,
                             valmax=1,
                             valinit=0,
                             orientation="vertical"
                             )

        alfa_slider.on_changed(self.update_slider)
        rax = plt.axes([0.06, 0.05, 0.15, 0.15])
        radio = RadioButtons(rax, ('Reds', 'Greens', 'Blues', 'Oranges', 'Wistia', 'plasma', 'inferno'), active=0)
        radio.on_clicked(self.update_radio)
        self.canvas.draw()

    def update_slider(self,alpha_):
        self.b.set_alpha(alpha_)
        self.canvas.draw()

    def update_radio(self,label_):
        self.b.set_cmap(label_)
        self.canvas.draw()

    def on_key_press(self,event,toolbar):
        print("you pressed {}".format(event.key))
        key_press_handler(event, self.canvas, toolbar)

    def _quit(self):
        self.master.quit()  # stops mainloop

    def switch_graphs(self):
        # Need to call the correct draw, whether we're on graph one or two
        self.graphIndex = (self.graphIndex + 1) % 2
        if self.graphIndex == 0:
            self.draw_graph_one()
        else:
            self.draw_graph_two()


def main():
    root = Tk()
    matplotlibSwitchGraphs(root)
    root.mainloop()


if __name__ == '__main__':
    main()

I tried to input update_slider and update_radio function inside of function, but than I cant use self.canvas.draw(). Also .remove() and .cla() doesn't clear the window.

Here is the view of the graphs I would like to switch between: enter image description here enter image description here

1 Answers

Answering your last question first, your big issue is that you are trying to use one figure to display two seperate graphs, without understanding how pyplot actually works. It is possible to do so, as I will outline below, but you lose any modifications you make to the graphs when you switch between them. I would recommend reading the working with multiple axes and figures section of the pyplot tutorial. The key takeaway as that in MATLAB and pyplot, all plotting functions are applied to the last figure and axes.

When you call self.draw_graph_two(), the last axes you create are the RadioButtons. Therefore, pyplot has these axes as the current axes. When you then call self.draw_graph_one() for the second time (the first time was when you intialised), it draws the plot on the current figure and current axes, where the RadioButtons are.

To solve this, call plt.clf() as the first line of draw_graph_one and draw_graph_two, before attempting to draw the new graph. This clears the figure, allowing for a new plot to be drawn from scratch (which is what your two draw functions do). This allows you to continuously switch between your two plots, although each time you switch the plot is cleared and any modifications to the plot are lost. You can also remove the self.ax.remove() lines, as they cause errors and are not needed.

For your first two questions, these can be answered by reading the documentation for the RadioButtons and Slider here. You must keep a reference to the widgets for them to remain responsive. So modifying your draw_graph_two to return alfa_slider and radio keeps these widgets responsive. Alternatively, declaring them as instance variables also keeps a reference (self.alfa_slider and self.radio).

So your working graph viewer is:

import matplotlib
import tkinter as tk
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.backend_bases import key_press_handler
from matplotlib.widgets import Slider, RadioButtons

# Seperated out config of plot to just do it once
def config_plot():
    fig, ax = plt.subplots()
    ax.set(xlabel='time (s)', ylabel='voltage (mV)',
           title='Graph One')
    return (fig, ax)


class matplotlibSwitchGraphs:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.fig, self.ax = config_plot()
        self.graphIndex = 0
        self.canvas = FigureCanvasTkAgg(self.fig, self.master)
        self.config_window()
        self.plot_data()
        self.draw_graph_one()
        self.frame.pack(expand=True, fill=tk.BOTH)

    def config_window(self):
        self.canvas.mpl_connect("key_press_event", self.on_key_press)
        toolbar = NavigationToolbar2Tk(self.canvas, self.master)
        toolbar.update()
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
        self.button = tk.Button(self.master, text="Quit", command=self._quit)
        self.button.pack(side=tk.BOTTOM)
        self.button_switch = tk.Button(self.master, text="Switch Graphs", command=self.switch_graphs)
        self.button_switch.pack(side=tk.BOTTOM)

    def plot_data(self):
        def func3(x, y):
            return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2 + y ** 2))
        dx, dy = 0.05, 0.05
        x = np.arange(-3.0, 3.0, dx)
        y = np.arange(-3.0, 3.0, dy)
        X, Y = np.meshgrid(x, y)
        self.extent = np.min(x), np.max(x), np.min(y), np.max(y)
        self.Z1 = np.add.outer(range(8), range(8)) % 2  # chessboard
        self.Z2 = func3(X, Y)

    def draw_graph_one(self):
        plt.clf()
        self.ax = plt.imshow(self.Z2, cmap=plt.cm.viridis, alpha=.9, interpolation='bilinear',
                             extent=self.extent)
        self.canvas.draw()

    def draw_graph_two(self):
        plt.clf()
        self.ax = plt.subplot(1, 2, 1)
        self.ax = plt.imshow(self.Z1, cmap=plt.cm.gray, interpolation='nearest',
                         extent=self.extent)
        self.ax = plt.subplot(1, 2, 2)
        self.a = plt.imshow(self.Z2, cmap=plt.cm.viridis, alpha=.9, interpolation='bilinear',
                         extent=self.extent)
        self.b = plt.imshow(self.Z1, cmap=plt.cm.gray, interpolation='nearest',
                         extent=self.extent)
        self.canvas.draw()

        plt.subplots_adjust(left=0.15, bottom=0.15)
        slider_ax = plt.axes([0.06, 0.25, 0.0225, 0.5])

        self.alfa_slider = Slider(slider_ax,
                             label="Transparency",
                             valmin=0,
                             valmax=1,
                             valinit=0,
                             orientation="vertical"
                             )

        self.alfa_slider.on_changed(self.update_slider)
        rax = plt.axes([0.06, 0.05, 0.15, 0.15])
        self.radio = RadioButtons(rax, ('Reds', 'Greens', 'Blues', 'Oranges', 'Wistia', 'plasma', 'inferno'), active=0)
        self.radio.on_clicked(self.update_radio)
        self.canvas.draw()

    def update_slider(self,alpha_):
        self.b.set_alpha(1-alpha_)
        self.canvas.draw()

    def update_radio(self,label_):
        self.b.set_cmap(label_)
        self.canvas.draw()

    def on_key_press(self,event,toolbar):
        print("you pressed {}".format(event.key))
        key_press_handler(event, self.canvas, toolbar)

    def _quit(self):
        self.master.quit()  # stops mainloop

    def switch_graphs(self):
        # Need to call the correct draw, whether we're on graph one or two
        self.graphIndex = (self.graphIndex + 1) % 2
        if self.graphIndex == 0:
            self.draw_graph_one()
        else:
            self.draw_graph_two()


def main():
    root = tk.Tk()
    matplotlibSwitchGraphs(root)
    root.mainloop()


if __name__ == '__main__':
    main()

Note I also made some other slight modifications. Don't use from tkinter import *, you had already imported it above and using import * is a bad habit. I reversed the slider so that the transparency makes sense. self.plot_data() is only called once in __init__ as it doesn't change.

Related