Using FigureCanvasTkAgg in two TKinter "pages" with python

Alexandre de Castro Maciel picture Alexandre de Castro Maciel · Nov 16, 2015 · Viewed 15.5k times · Source

I am trying to get two TKinter 'pages' each one with a different plot updated using the matplotlib animation function.

The problem is only one of the pages show correctly the plot.

Here is the code:

import matplotlib
matplotlib.use("TkAgg")

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation

import Tkinter as tk
import ttk

a = Figure(figsize=(4,4))
plot_a = a.add_subplot(111)

b = Figure(figsize=(4,4))
plot_b = b.add_subplot(111)

x = [1,2,3,4,5]
y_a = [1,4,9,16,25]
y_b = [25,16,9,4,1]


def updateGraphs(i):

    plot_a.clear()
    plot_a.plot(x,y_a)

    plot_b.clear()
    plot_b.plot(x,y_b)

class TransientAnalysis(tk.Tk):

    def __init__(self,*args,**kwargs):

        tk.Tk.__init__(self,*args,**kwargs)
        tk.Tk.wm_title(self, "Transient Analysis GUI: v1.0")

        container = tk.Frame(self)      
        container.pack(side="top", fill="both", expand=True)      
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}

        for F in ( GraphPageA, GraphPageB):

            frame = F(container, self)

            self.frames[F] = frame

            frame.grid(row=0, column=0, sticky="nsew")

        self.show_frame(GraphPageA)

    def show_frame(self, cont):

        frame = self.frames[cont]
        frame.tkraise()    

class GraphPageA(tk.Frame):

    def __init__(self, parent, controller):

        tk.Frame.__init__(self, parent)

        button1 = ttk.Button(self, text="Show Graph B",
                            command = lambda: controller.show_frame(GraphPageB))
        button1.grid(row=1, column=0,pady=20,padx=10, sticky='w')

        canvasA = FigureCanvasTkAgg(a, self)
        canvasA.show()
        canvasA.get_tk_widget().grid(row=1, column=1, pady=20,padx=10, sticky='nsew')

class GraphPageB(tk.Frame):

    def __init__(self, parent, controller):

        tk.Frame.__init__(self, parent)

        button1 = ttk.Button(self, text="Show Graph A",
                            command = lambda: controller.show_frame(GraphPageA))
        button1.grid(row=1, column=0,pady=20,padx=10, sticky='w')

        canvasB = FigureCanvasTkAgg(b, self)
        canvasB.show()
        canvasB.get_tk_widget().grid(row=1, column=1, pady=20,padx=10, sticky='nsew')

app = TransientAnalysis()
app.geometry("800x600")
aniA    = animation.FuncAnimation(a, updateGraphs, interval=1000)
aniB    = animation.FuncAnimation(b, updateGraphs, interval=1000)
app.mainloop()

To be more specific, the page that show correctly the plot is the last on called in the for loop for F in ( GraphPageA, GraphPageB):

I also tried using different updateGraphs function, one to each plot, but the result is the same.

How can I have two TKinter pages plotting these two different graphs? I am using Python 2.7.

Answer

tacaswell picture tacaswell · Nov 16, 2015
from __future__ import print_function
import matplotlib
import numpy as np

matplotlib.use("TkAgg")

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation

import Tkinter as tk
import ttk


a = Figure(figsize=(4, 4))
plot_a = a.add_subplot(111)
plot_a.set_xlim([0, 2*np.pi])
plot_a.set_ylim([-1, 1])
lna, = plot_a.plot([], [], color='orange', lw=5)

b = Figure(figsize=(4, 4))
plot_b = b.add_subplot(111)
plot_b.set_xlim([0, 2*np.pi])
plot_b.set_ylim([-1, 1])

lnb, = plot_b.plot([], [], color='olive', lw=5)

x = np.linspace(0, 2*np.pi, 1024)


def updateGraphsA(i):
    lna.set_xdata(x)
    lna.set_ydata(np.sin(x + i * np.pi / 10))
    print('in A')


def updateGraphsB(i):
    lnb.set_xdata(x)
    lnb.set_ydata(np.sin(x - i * np.pi / 10))

    print('in B')


class TransientAnalysis(tk.Tk):

    def __init__(self, *args, **kwargs):
        self._running_anim = None
        tk.Tk.__init__(self, *args, **kwargs)
        tk.Tk.wm_title(self, "Transient Analysis GUI: v1.0")

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}

        for F in (GraphPageA, GraphPageB):

            frame = F(container, self)

            self.frames[F] = frame

            frame.grid(row=0, column=0, sticky="nsew")

        self.show_frame(GraphPageA)

    def show_frame(self, cont):

        frame = self.frames[cont]
        frame.tkraise()
        frame.canvas.draw_idle()


class GraphPageA(tk.Frame):

    def __init__(self, parent, controller):

        tk.Frame.__init__(self, parent)

        button1 = ttk.Button(self, text="Show Graph B",
                             command=(
                                 lambda: controller.show_frame(GraphPageB)))
        button1.grid(row=1, column=0, pady=20, padx=10, sticky='w')

        canvasA = FigureCanvasTkAgg(a, self)
        canvasA.show()
        canvasA.get_tk_widget().grid(
            row=1, column=1, pady=20, padx=10, sticky='nsew')
        self.canvas = canvasA


class GraphPageB(tk.Frame):

    def __init__(self, parent, controller):

        tk.Frame.__init__(self, parent)

        button1 = ttk.Button(self, text="Show Graph A",
                             command=(
                                 lambda: controller.show_frame(GraphPageA)))

        button1.grid(row=1, column=0, pady=20, padx=10, sticky='w')

        canvasB = FigureCanvasTkAgg(b, self)
        canvasB.show()
        canvasB.get_tk_widget().grid(
            row=1, column=1, pady=20, padx=10, sticky='nsew')
        self.canvas = canvasB


app = TransientAnalysis()
app.geometry("800x600")
aniA = animation.FuncAnimation(a, updateGraphsA, interval=1000, blit=False)
aniB = animation.FuncAnimation(b, updateGraphsB, interval=1000, blit=False)

app.mainloop()

The problem was that the animation is kicked off the first time that the figure is drawn. For what ever reason (which is going to be some nasty internal detail of either Tk or mpl) the draw event event for the non-initially visible axes never gets fired (or is fired before the animation is created) so the second animation never even gets started. By adding a canvas attribute to your classes you can explicitly trigger the draw_idle every time you switch the graph being shown.

Note that I also:

  • split the animation functions into a function per figure
  • update existing artists instead of creating new ones
  • left in debugging prints

On a more general level, you should probably refactor your GraphPage* classes to be one parameterized class which is responsible for :

  • creating and holding it's artists
  • creating and holding it's animation object
  • switch the shown graph based on a string key, not the class type.

For example:

from __future__ import print_function
import matplotlib
import numpy as np

matplotlib.use("TkAgg")

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation

import Tkinter as tk
import ttk

x = np.linspace(0, 2*np.pi, 1024)


class TransientAnalysis(tk.Tk):

    pages = ((1, 'Switch to "-"', '-', '+', 'orange'),
             (-1, 'Switch to "+"', '+', '-', 'olive'))

    def __init__(self, *args, **kwargs):
        self._running_anim = None
        tk.Tk.__init__(self, *args, **kwargs)
        tk.Tk.wm_title(self, "Transient Analysis GUI: v1.0")

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}

        for (direction, text, other_key, my_key, color) in self.pages:

            frame = MovingSinGraphPage(direction, text, other_key,
                                       my_key, color,
                                       container, self)

            self.frames[my_key] = frame

            frame.grid(row=0, column=0, sticky="nsew")

    def show_frame(self, cont):

        frame = self.frames[cont]
        frame.tkraise()
        frame.canvas.draw_idle()


class MovingSinGraphPage(tk.Frame):

    def __init__(self, move_dir, text, other_key, my_key,
                 color, parent, controller):
        self._sgn = np.sign(move_dir)

        tk.Frame.__init__(self, parent)

        button1 = ttk.Button(self, text=text,
                             command=(
                                 lambda: controller.show_frame(other_key)))
        button1.grid(row=1, column=0, pady=20, padx=10, sticky='w')

        # make mpl objects
        a = Figure(figsize=(4, 4))
        plot_a = a.add_subplot(111)
        # set up the axes limits and title
        plot_a.set_title(my_key)
        plot_a.set_xlim([0, 2*np.pi])
        plot_a.set_ylim([-1, 1])
        # make and stash the plot artist
        lna, = plot_a.plot([], [], color=color, lw=5)
        self._line = lna

        # make the canvas to integrate with Tk
        canvasA = FigureCanvasTkAgg(a, self)
        canvasA.show()
        canvasA.get_tk_widget().grid(
            row=1, column=1, pady=20, padx=10, sticky='nsew')

        # stash the canvas so that we can use it above to ensure a re-draw
        # when we switch to this page
        self.canvas = canvasA
        # create and save the animation
        self.anim = animation.FuncAnimation(a, self.update,
                                            interval=100)

    def update(self, i):
        self._line.set_xdata(x)
        self._line.set_ydata(np.sin(x + self._sgn * i * np.pi / 10))


app = TransientAnalysis()
app.geometry("800x600")

app.mainloop()

This apparently captured my attention today..