Search code examples
pythonmatplotlibtkinterplotlive

Multiple matplotlib instances in tkinter GUI


I have build simple tkinter GUI. Now, I am trying to visualise 3 different graphs (by calling the same function with different variables) and place them in 3 different rows of the GUI.

When I do that I get 2 problems:

  1. Every time I run the script (interface.py) I get 2 windows - both GUI and external graph's window. How to get rid of the second one?
  2. I am not able to visualize all the 3 graphs. The script stops after showing the first one. I believe this is because of that the first graph works in a loop (iterates through plenty of data points). Is there any work around it?

Interface:

# -*- coding: utf-8 -*-
"""
Created on Tue Oct  6 10:24:35 2020

@author: Dar0
"""

from tkinter import * #import tkinter module
from visualizer import main #import module 'visualizer' that shows the graph in real time

class Application(Frame):
    ''' Interface for visualizing graphs, indicators and text-box. '''
    def __init__(self, master):
        super(Application, self).__init__(master)
        self.grid()
        self.create_widgets()
    
    def create_widgets(self):
        # Label of the 1st graph
        Label(self,
              text='Hook Load / Elevator Height / Depth vs Time'
              ).grid(row = 0, column = 0, sticky = W)
        
        # Graph 1 - Hook Load / Elevator Height / Depth vs Time
        # button that displays the plot 
        #plot_button = Button(self,2
        #                     command = main,
        #                     height = 2, 
        #                     width = 10, 
        #                     text = "Plot"
        #                     ).grid(row = 1, column = 0, sticky = W)
        
        self.graph_1 = main(root, 1, 0)
        # place the button 
        # in main window 
        
        # Label of the 2nd graph
        Label(self,
              text = 'Hook Load / Elevator Height vs Time'
              ).grid(row = 3, column = 0, sticky = W)
        
        # Graph 2 - Hook Load / Elevator Height vs Time
        self.graph_2 = main(root, 4, 0)
        
        #Label of the 3rd graph
        Label(self,
              text = 'Hook Load vs Time'
              ).grid(row = 6, column = 0, sticky = W)
        
        #Graph 3 - Hook Load vs Time
        
        #Label of the 1st indicator
        Label(self,
              text = '1st performance indicator'
              ).grid(row = 0, column = 1, sticky = W)
        
        #1st performance indicator
        
        #Label of 2nd performance indicator
        Label(self,
              text = '2nd performance indicator'
              ).grid(row = 3, column = 1, sticky = W)
        
        #2nd performance indicator
        
        #Label of 3rd performance indicator
        Label(self,
              text = '3rd performance indicator'
              ).grid(row = 6, column = 1, sticky = W)
        
        #Text-box showing comments based on received data
        self.text_box = Text(self, width = 50, height = 10, wrap = WORD)
        self.text_box.grid(row = 9, column = 0, columnspan = 1)
        self.text_box.delete(0.0, END)
        self.text_box.insert(0.0, 'My message will be here.')
        
#Main part
root = Tk()
root.title('WiTSML Visualizer by Dar0')
app = Application(root)
root.mainloop()

Visualizer:

#WiTSML visualizer
#Created by Dariusz Krol
#import matplotlib
#matplotlib.use('TkAgg')
#from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
#from matplotlib.figure import Figure

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random

class Visualizer(object):
    """ Includes all the methods needed to show streamed data. """
    def __init__(self):
        self.file_path = 'C:/Anaconda/_my_files/witsml_reader/modified_witsml.csv' #Defines which file is streamed

        self.datetime_mod = []
        self.bpos_mod = []
        self.woh_mod = []
        self.torq_mod = []
        self.spp_mod = []
        self.depth_mod = []
        self.flow_in_mod = []
        self.rpm_mod = []

    def open_file(self):
        self.df = pd.read_csv(self.file_path, low_memory = False, nrows = 300000) #Opens the STREAMED file (already modified so that data convert is not required)
        self.df = self.df.drop(0)
        self.df = pd.DataFrame(self.df)

        return self.df

    def convert_dataframe(self):
        self.df = self.df.values.T.tolist() #Do transposition of the dataframe and convert to list
        #Columns are as following:
        # - DATETIME
        # - BPOS
        # - WOH
        # - TORQ
        # - SPP
        # - DEPTH
        # - FLOW_IN
        # - RPM
        self.datetime_value = self.df[0]
        self.bpos_value = self.df[1]
        self.woh_value = self.df[2]
        self.torq_value = self.df[3]
        self.spp_value = self.df[4]
        self.depth_value = self.df[5]
        self.flow_in_value = self.df[5]
        self.rpm_value = self.df[7]

        return self.datetime_value, self.bpos_value, self.woh_value, self.torq_value, self.spp_value, self.depth_value, self.flow_in_value, self.rpm_value
        #print(self.bpos_value)

    def deliver_values(self, no_dp, columns):
        ''' Method gets no_dp amount of data points from the original file. '''
        self.no_dp = no_dp #defines how many data points will be presented in the graph

        val_dict = {
            'datetime': [self.datetime_value, self.datetime_mod],
            'bpos': [self.bpos_value, self.bpos_mod],
            'woh': [self.woh_value, self.woh_mod],
            'torq': [self.torq_value, self.torq_mod],
            'spp': [self.spp_value, self.spp_mod],
            'depth': [self.depth_value, self.depth_mod],
            'flow_in': [self.flow_in_value, self.flow_in_mod],
            'rpm': [self.rpm_value, self.rpm_mod]
            }
        
        for item in columns:
            if self.no_dp > len(val_dict[item][0]):
                dp_range = len(val_dict[item][0])
            else:
                dp_range = self.no_dp
                
            for i in range(dp_range):
                val_dict[item][1].append(val_dict[item][0][i])

        return self.datetime_mod, self.bpos_mod, self.woh_mod, self.torq_mod, self.spp_mod, self.depth_mod, self.flow_in_mod, self.rpm_mod    
        
    def show_graph2(self, tr_val, row, column):
        from pylive_mod import live_plotter, live_plotter2

        self.open_file()
        self.convert_dataframe()
        self.deliver_values(no_dp = 100000, columns = ['datetime', 'depth', 'bpos', 'woh'])

        fst_p = 0
        size = 300 # density of points in the graph (100 by default)
        
        x_vec = self.datetime_mod[fst_p:size]
        y_vec = self.depth_mod[fst_p:size]
        y2_vec = self.bpos_mod[fst_p:size]
        y3_vec = self.woh_mod[fst_p:size]
        line1 = []
        line2 = []
        line3 = []
        
        for i in range(self.no_dp):
            #print(self.datetime_mod[i:6+i])
            #print('Ostatni element y_vec: ', y_vec[-1])
            #print(x_vec)
            x_vec[-1] = self.datetime_mod[size+i]
            y_vec[-1] = self.depth_mod[size+i]
            y2_vec[-1] = self.bpos_mod[size+i]
            y3_vec[-1] = self.woh_mod[size+i]
            
            line1, line2, line3 = live_plotter2(tr_val, row, column, x_vec, y_vec, y2_vec, y3_vec, line1, line2, line3)

            x_vec = np.append(x_vec[1:], 0.0)
            y_vec = np.append(y_vec[1:], 0.0)
            y2_vec = np.append(y2_vec[1:], 0.0)
            y3_vec = np.append(y3_vec[1:], 0.0)

def main(tr_val, row, column):
    Graph = Visualizer()
    Graph.open_file() #Opens the streamed file
    Graph.convert_dataframe() #Converts dataframe to readable format
    Graph.show_graph2(tr_val, row, column)

#Show us the graph
#main()

Function that creates the graph:

def live_plotter2(tr_val, row, column, x_data, y1_data, y2_data, y3_data, line1, line2, line3, identifier='',pause_time=1):
    if line1 == [] and line2 == [] and line3 == []:
        # this is the call to matplotlib that allows dynamic plotting
        plt.ion()
        fig = plt.figure(figsize = (5, 4), dpi = 100)
        fig.subplots_adjust(0.15)
        
# -------------------- FIRST GRAPH --------------------
        host = fig.add_subplot()

        ln1 = host
        ln2 = host.twinx()
        ln3 = host.twinx()

        ln2.spines['right'].set_position(('axes', 1.))
        ln3.spines['right'].set_position(('axes', 1.12))
        make_patch_spines_invisible(ln2)
        make_patch_spines_invisible(ln3)
        ln2.spines['right'].set_visible(True)
        ln3.spines['right'].set_visible(True)              
        
        ln1.set_xlabel('Date & Time') #main x axis
        ln1.set_ylabel('Depth') #left y axis
        ln2.set_ylabel('Elevator Height')
        ln3.set_ylabel('Weight on Hook')

        #
        x_formatter = FixedFormatter([x_data])
        x_locator = FixedLocator([x_data[5]])

        #ln1.xaxis.set_major_formatter(x_formatter)
        ln1.xaxis.set_major_locator(x_locator)
        #
        
        ln1.locator_params(nbins = 5, axis = 'y')
        ln1.tick_params(axis='x', rotation=90) #rotates x ticks 90 degrees down

        ln2.axes.set_ylim(0, 30)
        ln3.axes.set_ylim(200, 250)
        
        line1, = ln1.plot(x_data, y1_data, color = 'black', linestyle = 'solid', alpha=0.8, label = 'Depth')
        line2, = ln2.plot(x_data, y2_data, color = 'blue', linestyle = 'dashed', alpha=0.8, label = 'Elevator Height')
        line3, = ln3.plot(x_data, y3_data, color = 'red', linestyle = 'solid', alpha=0.8, label = 'Weight on Hook')
        
        fig.tight_layout() #the graphs is not clipped on sides
        plt.title('WiTSML Visualizer')
        plt.grid(True)
        
        #Shows legend
        lines = [line1, line2, line3]
        host.legend(lines, [l.get_label() for l in lines], loc = 'lower left')        

        #Shows the whole graph
        #plt.show()     
        
        #-------------------- Embedding --------------------
        canvas = FigureCanvasTkAgg(fig, master=tr_val)
        canvas.draw()
        canvas.get_tk_widget().grid(row=row, column=column, ipadx=40, ipady=20)

        # navigation toolbar
        toolbarFrame = tk.Frame(master=tr_val)
        toolbarFrame.grid(row=row,column=column)
        toolbar = NavigationToolbar2Tk(canvas, toolbarFrame)
        

    # after the figure, axis, and line are created, we only need to update the y-data
    mod_x_data = convert_x_data(x_data, 20)
    line1.axes.set_xticklabels(mod_x_data)
    line1.set_ydata(y1_data)
    line2.set_ydata(y2_data)
    line3.set_ydata(y3_data)

    
    #Debugging
    #rint('plt.lim: ', ln2.axes.get_ylim())
    
    # adjust limits if new data goes beyond bounds
    # limit for line 1
    if np.min(y1_data)<=line1.axes.get_ylim()[0] or np.max(y1_data)>=line1.axes.get_ylim()[1]:
        plt.ylim(0, 10)
        line1.axes.set_ylim([np.min(y1_data)-np.std(y1_data),np.max(y1_data)+np.std(y1_data)])

    # limit for line 2
    if np.min(y2_data)<=line2.axes.get_ylim()[0] or np.max(y2_data)>=line2.axes.get_ylim()[1]:
        plt.ylim([np.min(y2_data)-np.std(y2_data),np.max(y2_data)+np.std(y2_data)])
        #plt.ylim(0, 25)

    # limit for line 3
    if np.min(y3_data)<=line3.axes.get_ylim()[0] or np.max(y3_data)>=line3.axes.get_ylim()[1]:
        plt.ylim([np.min(y3_data)-np.std(y3_data),np.max(y3_data)+np.std(y3_data)])
        #plt.ylim(0, 25)

    # Adds lines to the legend
    #host.legend(lines, [l.get_label() for l in lines])
    # this pauses the data so the figure/axis can catch up - the amount of pause can be altered above
    plt.pause(pause_time)
    
    # return line so we can update it again in the next iteration
    return line1, line2, line3

Solution

  • The key is to not use pyplot when you want to plot within tkinter as shown in the official example. Use matplotlib.figure.Figure instead (see this for added info).

    Below is a minimum sample that plots 3 independent graphs along a Text widget which I see in your code:

    import pandas as pd
    import numpy as np
    import tkinter as tk
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
    from matplotlib.figure import Figure
        
    class Graph(tk.Frame):
        def __init__(self, master=None, title="", *args, **kwargs):
            super().__init__(master, *args, **kwargs)
            self.fig = Figure(figsize=(4, 3))
            ax = self.fig.add_subplot(111)
            df = pd.DataFrame({"values": np.random.randint(0, 50, 10)}) #dummy data
            df.plot(ax=ax)
            self.canvas = FigureCanvasTkAgg(self.fig, master=self)
            self.canvas.draw()
            tk.Label(self, text=f"Graph {title}").grid(row=0)
            self.canvas.get_tk_widget().grid(row=1, sticky="nesw")
            toolbar_frame = tk.Frame(self)
            toolbar_frame.grid(row=2, sticky="ew")
            NavigationToolbar2Tk(self.canvas, toolbar_frame)
        
    root = tk.Tk()
    
    for num, i in enumerate(list("ABC")):
        Graph(root, title=i, width=200).grid(row=num//2, column=num%2)
    
    text_box = tk.Text(root, width=50, height=10, wrap=tk.WORD)
    text_box.grid(row=1, column=1, sticky="nesw")
    text_box.delete(0.0, "end")
    text_box.insert(0.0, 'My message will be here.')
    
    root.mainloop()
    

    Result:

    enter image description here