tkinter GUI 中的多个 matplotlib 实例

Multiple matplotlib instances in tkinter GUI

我已经构建了简单的 tkinter GUI。 现在,我正在尝试可视化 3 个不同的图形(通过使用不同的变量调用相同的函数)并将它们放在 GUI 的 3 个不同行中。

当我这样做时,我遇到了 2 个问题:

  1. 每次我 运行 脚本 (interface.py) 我得到 2 windows - GUI 和外部图形的 window。如何去掉第二个?
  2. 我无法将所有 3 个图表都可视化。脚本在显示第一个后停止。我相信这是因为第一个图循环工作(遍历大量数据点)。有什么解决办法吗?

接口:

# -*- 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()

可视化工具:

#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()

创建图形的函数:

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

关键是当您想在 tkinter 内绘制时不要使用 pyplot,如 official example. Use matplotlib.figure.Figure instead (see this 所示以获取更多信息)。

下面是一个最小示例,它沿着我在您的代码中看到的 Text 小部件绘制了 3 个独立的图形:

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()

结果: