Search code examples
python-2.7matplotlibpyqt4qthread

Why is pyplot having issues with PyQt4's QThread?


I am designing a gui that creates multiple QThreads to be activated. Each thread creates an excel workbook with Pandas Excelwriter and creates a heatmap using seaborn and saves that heatmap (for later use by the user for whatever) and then places it into the excel workbook.

I believe the error is that pyplot is not made into its own instance for the thread that is created..rather a resource that all threads are pointing to..if I run just one thread, there is no issue...two or more threads..in this example 4, there are internal pyplot errors pointing to dictionary size change occurring.

The issue I'm having is when pyplot is put into play. Do I have to do something specific to pyplot like I had to for getting the right backend for matplotlib? I thought the changes I made for matplotlib is inherent to pyplot?

---main.py---

import sys
from MAIN_GUI import *
from PyQt4 import QtGui, QtCore
from excel_dummy import *


df1 = pd.DataFrame(np.array([[1,22222,33333],[2,44444,55555],[3,44444,22222],[4,55555,33333]]),columns=['hour','input','out'])
df2 = pd.DataFrame(np.array([[1,22233,33344],[2,44455,55566],[3,44455,22233],[4,55566,33344]]),columns=['hour','input','out'])
df3 = pd.DataFrame(np.array([[1,23456,34567],[2,98765,45674],[3,44444,22222],[4,44455,34443]]),columns=['hour','input','out'])
df4 = pd.DataFrame(np.array([[1,24442,33443],[2,44444,54455],[3,45544,24442],[4,54455,33443]]),columns=['hour','input','out'])

df_list = [df1,df2,df3,df4]

if __name__=="__main__":
    app = QtGui.QApplication(sys.argv)


class MAIN_GUI(QtGui.QMainWindow):
    def __init__(self):
        super(MAIN_GUI, self).__init__()
        self.uiM = Ui_MainWindow()
        self.uiM.setupUi(self)
        self.connect(self.uiM.updateALL_Button,QtCore.SIGNAL('clicked()'),self.newThread)

    def newThread(self):

        count = 0
        for df in df_list:
            count += 1
            Excelify = excelify(df,count)
            self.connect(Excelify,QtCore.SIGNAL('donethread(QString)'),(self.done))
            Excelify.start()


    def done(self):
        print('done')


main_gui = MAIN_GUI()
main_gui.show()
main_gui.raise_()
sys.exit(app.exec_())

---excel_dummy.py---

import pandas as pd

import numpy as np
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import QThread
import time
import matplotlib as mpl
mpl.use('Agg')
from matplotlib.backends.backend_agg import FigureCanvas
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import seaborn.matrix as sm

class excelify(QThread):
    def __init__(self,df,count):
        QThread.__init__(self)
        self.df = df
        self.count = count

    def run(self):

        heatit = self.heatmap()

        self.emit(QtCore.SIGNAL('donethread(QString)'),'')

    def heatmap(self):

        dfu = pd.DataFrame(self.df.groupby([self.df.input,self.df.hour]).size())
        dfu.reset_index(inplace=True)
        dfu.rename(columns={'0':'Count'})
        dfu.columns=['input','hour','Count']
        dfu_2 = dfu.copy()

        mask=0
        fig = Figure()
        ax = fig.add_subplot(1,1,1)
        fig.set_canvas(FigureCanvas(fig))
        df_heatmap = dfu_2.pivot('input','hour','Count').fillna(0)

        sm.heatmap(df_heatmap,ax=ax,square=True,annot=False,mask=mask)

        plt.ylabel('ID')
        plt.xlabel('Hour')
        plt.title('heatmap for df' + str(self.count))
        plt.savefig(path + '/' + 'heat' + str(self.count) + '.png')
        plt.close()

---MAIN_GUI.py---

from PyQt4 import QtCore,QtGui
try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.unicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName(_fromUtf8("MainWindow"))
        MainWindow.resize(320,201)
        self.centralwidget = QtGui.QWidget(MainWindow)
        self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
        self.updateALL_Button = QtGui.QPushButton(self.centralwidget)
        self.updateALL_Button.setGeometry(QtCore.QRect(40,110,161,27))
        self.updateALL_Button.setFocusPolicy(QtCore.Qt.NoFocus)
        self.updateALL_Button.setObjectName(_fromUtf8("Options_updateALL_Button"))
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtGui.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 320, 24))
        self.menubar.setObjectName(_fromUtf8("menubar"))
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtGui.QStatusBar(MainWindow)
        self.statusbar.setObjectName(_fromUtf8("statusbar"))
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self,MainWindow):
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None))
        self.updateALL_Button.setText(_translate("MainWindow", "updateALL", None))

Solution

  • While the code in the question is still not really a minimal example (some undefined variable) it is much clearer, where the problem lies.

    First, one problem might be that the MAIN_GUI class looses a reference to the thread, such that it will be garbage collected before it can finish. One can prevent this by just putting all threads in a list. [See MAIN_GUI code below]

    Second, you cannot use pyplot directly to operate on different figures at once. Or in other words, how should pyplot know in which figure to place the ylabel set by plt.ylabel('ID') if there exist several at the same time?
    The way to solve this, is to create different figures and only work within those figures using the object oriented approach. [See excelify code below]

    Here is the relevant part of the code, where I also changed the signal to return the plot number for easier debugging.

    MAIN_GUI:

    class MAIN_GUI(QtGui.QMainWindow):
        def __init__(self):
            super(MAIN_GUI, self).__init__()
            self.uiM = Ui_MainWindow()
            self.uiM.setupUi(self)
            self.connect(self.uiM.updateALL_Button,QtCore.SIGNAL('clicked()'),self.newThread)
            self.threats=[]
    
        def newThread(self):
            count = 0
            for df in df_list:
                count += 1
                Excelify = excelify(df,count)
                self.connect(Excelify,QtCore.SIGNAL('donethread(int)'),(self.done))
                # appending all threats to a class attribute, 
                # such that they will persist and not garbage collected
                self.threats.append(Excelify)
                Excelify.start()
    
        def done(self, val=None):
            print('done with {nr}'.format(nr=val))
    

    excelify:

    class excelify(QThread):
        def __init__(self,df,count):
            QThread.__init__(self)
            self.df = df
            self.count = count
    
        def run(self):
            heatit = self.heatmap()
            self.emit(QtCore.SIGNAL('donethread(int)'),self.count)
    
        def heatmap(self):
            print ("{nr} started".format(nr=self.count) )
            dfu = pd.DataFrame(self.df.groupby([self.df.input,self.df.hour]).size())
            dfu.reset_index(inplace=True)
            dfu.rename(columns={'0':'Count'})
            dfu.columns=['input','hour','Count']
            dfu_2 = dfu.copy()
            mask=0
    
            # create a figure and only work within this figure
            # no plt.something inside the threat
            fig = Figure()
            ax = fig.add_subplot(1,1,1)
            fig.set_canvas(FigureCanvas(fig))
            df_heatmap = dfu_2.pivot('input','hour','Count').fillna(0)
    
            sm.heatmap(df_heatmap,ax=ax,square=True,annot=False,mask=mask)
    
            ax.set_ylabel('ID')
            ax.set_xlabel('Hour')
            ax.set_title('heatmap for df' + str(self.count))
            fig.savefig( 'heat' + str(self.count) + '.png')
            fig.clear()
            del fig