Hello friends! I hope you are doing great with the PyQt5 learning series. Thank you for your valuable comments and appreciation. Alright, let’s discuss today’s GUI, which is a continuation of our previous Matplotlib based GUI series. Today we will use the GUI of part 15 and integrate the exciting yet straightforward feature of the data cursor. It means that whenever the user clicks on the plot, a small label window will popup on the canvas showing the x and y data. Alright, so all the changes only need to be made in the main.py. The rest of the code is the same as before. We will use mpldatacursor
library, which provides interactive data cursors (that are clickable annotation boxes) for Matplotlib. However, currently, it is only supported for the Matplotlib version 3.1.2. It would not work for the latest version of Matplotlib. However, you are free to give it a try.
Installation of matplolib and mpldatacursor
pip3 install matplotlib==3.1.2
pip3 install mpldatacursor
The next step is to check if the mpldatacursor is working fine or not.
Testing mpldatacursor
The following code will plot seven lines and you can click on each line to see the cursor.
import matplotlib.pyplot as plt
import numpy as np
from mpldatacursor import datacursor
data = np.outer(range(5), range(1, 8))
fig, ax = plt.subplots()
lines = ax.plot(data,label='Data')
ax.set_title('Click on line')
datacursor(lines)
plt.show()
The most important three steps in the above code are: 1) importing the matplotlib class from mpldatacursor import datacursor
2) getting the lines object lines = ax.plot(data,label='Data')
3) assigning the lines to the datacursor datacursor(lines)
So lets use the above three steps in our main.py code as folows:
main.py
# -*- coding: utf-8 -*-
# Subscribe to PyShine Youtube channel for more detail!
#
# Form implementation generated from reading ui file 'main.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
#
# WEBSITE: www.pyshine.com
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
from PyQt5 import QtCore, QtGui, QtWidgets
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('Qt5Agg')
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QFileDialog
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as Navi
from matplotlib.figure import Figure
import seaborn as sns
import pandas as pd
import sip # can be installed : pip install sip
from datetime import datetime
# We require a canvas class
from mpldatacursor import datacursor
class MatplotlibCanvas(FigureCanvasQTAgg):
def __init__(self,parent=None, dpi = 120):
fig = Figure(dpi = dpi)
self.axes = fig.add_subplot(111)
super(MatplotlibCanvas,self).__init__(fig)
fig.tight_layout()
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1440, 1000)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.label_1 = QtWidgets.QLabel(self.centralwidget)
self.label_1.setObjectName("label_1")
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setObjectName("label_2")
self.label_3 = QtWidgets.QLabel(self.centralwidget)
self.label_3.setObjectName("label_3")
self.comboBox = QtWidgets.QComboBox(self.centralwidget)
self.comboBox.setObjectName("comboBox")
self.horizontalLayout.addWidget(self.comboBox)
self.comboBox_1 = QtWidgets.QComboBox(self.centralwidget)
self.comboBox_1.setObjectName("comboBox_1")
self.comboBox_2 = QtWidgets.QComboBox(self.centralwidget)
self.comboBox_2.setObjectName("comboBox_2")
self.radioButton = QtWidgets.QRadioButton(self.centralwidget)
self.radioButton.setObjectName("radioButton")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setObjectName("pushButton")
self.horizontalLayout.addWidget(self.pushButton)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.horizontalLayout.addWidget(self.label_1)
self.horizontalLayout.addWidget(self.comboBox_1)
self.horizontalLayout.addWidget(self.label_2)
self.horizontalLayout.addWidget(self.comboBox_2)
self.horizontalLayout.addWidget(self.label_3)
self.horizontalLayout.addWidget(self.radioButton)
self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(self.spacerItem1)
self.gridLayout.addLayout(self.verticalLayout, 1, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionOpen_csv_file = QtWidgets.QAction(MainWindow)
self.actionOpen_csv_file.setObjectName("actionOpen_csv_file")
self.actionExit = QtWidgets.QAction(MainWindow)
self.actionExit.setObjectName("actionExit")
self.menuFile.addAction(self.actionOpen_csv_file)
self.menuFile.addAction(self.actionExit)
self.menubar.addAction(self.menuFile.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
self.filename = ''
self.canv = MatplotlibCanvas(self)
self.df = []
self.toolbar = Navi(self.canv,self.centralwidget)
self.horizontalLayout.addWidget(self.toolbar)
self.themes = ['bmh', 'classic', 'dark_background', 'fast',
'fivethirtyeight', 'ggplot', 'grayscale', 'seaborn-bright',
'seaborn-colorblind', 'seaborn-dark-palette', 'seaborn-dark',
'seaborn-darkgrid', 'seaborn-deep', 'seaborn-muted', 'seaborn-notebook',
'seaborn-paper', 'seaborn-pastel', 'seaborn-poster', 'seaborn-talk',
'seaborn-ticks', 'seaborn-white', 'seaborn-whitegrid', 'seaborn',
'Solarize_Light2', 'tableau-colorblind10']
self.comboBox.addItems(self.themes)
self.comboBox_1.addItems(['Select horizontal axis here'])
self.comboBox_2.addItems(['Select vertical axis here'])
self.pushButton.clicked.connect(self.getFile)
self.comboBox.currentIndexChanged['QString'].connect(self.Update)
self.comboBox_1.currentIndexChanged['QString'].connect(self.selectXaxis)
self.comboBox_2.currentIndexChanged['QString'].connect(self.selectYaxis)
self.actionExit.triggered.connect(MainWindow.close)
self.actionOpen_csv_file.triggered.connect(self.getFile)
self.radioButton.clicked.connect(self.vsAll)
self.dataset={}
self.x_axis_slt=None
self.y_axis_slt=None
self.vsall = False
def vsAll(self):
"""
This function will be called upon triggering the radio check button. If set to True, all the columsn in the csv
will be plotted against the x-axis column. Please note that vs all means versus all, so that whatever value is
selected as the x-axis, it wont be plotted against itself in this mode. Moreover, the time series data will be
dedicated for the datetime x-axis and it wont be displayed in the vs all contents.
"""
if self.vsall==False:
self.vsall=True
else:
self.vsall=False
self.Update(self.themes[0])
def selectXaxis(self,value):
"""
This function will update the plot according to the data of x axis selected from combo box
"""
self.x_axis_slt=value
self.Update(self.themes[0])
def selectYaxis(self,value):
"""
This function will update the plot according to the data of y axis selected from combo box
"""
self.y_axis_slt=value
self.Update(self.themes[0])
def Update(self,value):
"""
This function will input the value of theme and accordingly plot the data, if the data is relative, i.e., x verus y-axis
then the user can assign x and y axis from the combo box. If all data should be plotted in paraller then leave,
the combo boxes of axis selections to their default starting location.
"""
plt.clf()
plt.style.use(value)
try:
self.horizontalLayout.removeWidget(self.toolbar)
self.verticalLayout.removeWidget(self.canv)
sip.delete(self.toolbar)
sip.delete(self.canv)
self.toolbar = None
self.canv = None
self.verticalLayout.removeItem(self.spacerItem1)
except Exception as e:
print(e)
pass
self.canv = MatplotlibCanvas(self)
self.toolbar = Navi(self.canv,self.centralwidget)
self.horizontalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canv)
self.canv.axes.cla()
ax = self.canv.axes
try:
if self.vsall:
for k,v in self.dataset.items():
if k!=self.x_axis_slt and type(v[0])!=datetime:
lines = ax.plot(self.dataset[self.x_axis_slt],v,label=k)
datacursor(lines)
legend = ax.legend()
legend.set_draggable(True)
ax.set_xlabel(self.x_axis_slt)
ax.set_ylabel('ALL OTHERS')
ax.set_title(self.Title)
#plt.setp(ax.xaxis.get_majorticklabels(), rotation=25) # uncomment if you want the x-axis to tilt 25 degree
else:
lines = ax.plot(self.dataset[self.x_axis_slt],self.dataset[self.y_axis_slt],label=self.y_axis_slt)
datacursor(lines)
legend = ax.legend()
legend.set_draggable(True)
ax.set_xlabel(self.x_axis_slt)
ax.set_ylabel(self.y_axis_slt)
ax.set_title(self.Title)
#plt.setp(ax.xaxis.get_majorticklabels(), rotation=25) # uncomment if you want the x-axis to tilt 25 degree
except Exception as e:
print(e)
try:
lines = self.df.plot(ax = self.canv.axes)
datacursor(lines)
legend = ax.legend()
legend.set_draggable(True)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_title(self.Title)
except Exception as e:
print(e)
pass
pass
self.canv.draw()
def getFile(self):
""" This function will get the address of the csv file location
also calls a readData function
"""
try:
self.filename = QFileDialog.getOpenFileName(filter = "csv (*.csv)")[0]
self.readData()
except Exception as e:
print(e)
pass
def getDataset(self,csvfilename):
"""
This function will convert csv file to a dictionary of dataset, with keys as the columns' names and
values as values. The datatime format should be one of the standard datatime formats. Before plottting
we need to convert the string of data time in the csv file values to datatime format.
"""
df = pd.read_csv(csvfilename,encoding='utf-8').fillna(0)
LIST_OF_COLUMNS = df.columns.tolist()
dataset={}
# time_format = "%d/%m/%Y %H:%M:%S"
# time_format = "%m/%d/%Y"
# time_format = "%d/%m/%Y"
# time_format = "%m-%d-%Y"
# time_format = "%d-%m-%Y"
# time_format = "%H:%M:%S"
# time_format = "%M:%S"
# time_format = '%d/%m/%Y %H:%M%f'
time_format = '%Y-%m-%d %H:%M:%S.%f'
for col in LIST_OF_COLUMNS:
dataset[col] = df[col].iloc[0:].values
try:
dataset[col] = [datetime.strptime(i, time_format) for i in df[col].iloc[0:].values]
except Exception as e:
pass
print(e)
return dataset,LIST_OF_COLUMNS
def readData(self):
""" This function will read the data using pandas and call the update
function to plot
"""
import os
self.dataset={}
self.x_axis_slt=None
self.y_axis_slt=None
base_name = os.path.basename(self.filename)
self.Title = os.path.splitext(base_name)[0]
self.dataset, LIST_OF_COLUMNS = self.getDataset(self.filename)
self.df = pd.read_csv(self.filename,encoding = 'utf-8').fillna(0)
self.Update(self.themes[0]) # lets 0th theme be the default : bmh
self.comboBox_1.clear()
self.comboBox_2.clear()
self.comboBox_1.addItems(['Select horizontal axis here'])
self.comboBox_2.addItems(['Select vertical axis here'])
self.comboBox_1.addItems(LIST_OF_COLUMNS)
self.comboBox_2.addItems(LIST_OF_COLUMNS)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.label.setText(_translate("MainWindow", "Select Theme"))
self.label_1.setText(_translate("MainWindow", "X-axis"))
self.label_2.setText(_translate("MainWindow", "Y-axis"))
self.label_3.setText(_translate("MainWindow", "vs all"))
self.pushButton.setText(_translate("MainWindow", "Open"))
self.menuFile.setTitle(_translate("MainWindow", "File"))
self.actionOpen_csv_file.setText(_translate("MainWindow", "Open csv file"))
self.actionExit.setText(_translate("MainWindow", "Exit"))
# Subscribe to PyShine Youtube channel for more detail!
# WEBSITE: www.pyshine.com
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
We used the mpldatacursors in the above code and only in the def Update(self,value)
function. Try your csv file and enjoy interactive data visualization. Please do give your feedback which is very important for us. Thank you