Bài 37: Trực quan hóa dữ liệu – PyQtGraph-Excel Data-PyQt6 – Part 7

Bài học này Tui sẽ nâng cấp bài 36, đó là dữ liệu thay vì hardcode thì Tui đọc tự động từ Excel:

  • Tự lấy tick trong cột file Excel (Hà Nội, Huế, TP.HCM, Cần Thơ, An Giang)
  • Tự lấy Legend cho Bar Item (Quarter 1, Quarter 2, Quarter 3, Quarter 4)
  • Màu các Bar Item được tạo ngẫu nhiên theo 3 thông số R, G, B
  • Tự động vẽ Chart mà không lệ thuộc vào tiêu đề Cột hay tiều đề dòng của dữ liệu trong File Excel
  • Các chức năng còn lại được kế thừa từ bài 36

Ta tiến hành từng bước như sau:

Bước 0: Cài thư viện “xlrd

pip install xlrd

Bước 1: Tạo dự án tên “LearnPyQtBarGraphPart7” có cấu trúc như dưới đây:

  • dataset-revenue.xlsx” là file Excel mẫu của dự án
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp.

Bạn lưu ý trong bài này Tui bổ sung nhóm QHBoxLayout ở bên trên để chọn Dataset là file Excel.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(647, 518)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setObjectName("label")
        self.horizontalLayout_2.addWidget(self.label)
        self.lineEditDataset = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditDataset.setObjectName("lineEditDataset")
        self.horizontalLayout_2.addWidget(self.lineEditDataset)
        self.pushButtonPickDataset = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonPickDataset.setObjectName("pushButtonPickDataset")
        self.horizontalLayout_2.addWidget(self.pushButtonPickDataset)
        self.verticalLayout_2.addLayout(self.horizontalLayout_2)
        self.layoutGraph = QtWidgets.QVBoxLayout()
        self.layoutGraph.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
        self.layoutGraph.setObjectName("layoutGraph")
        self.verticalLayout_2.addLayout(self.layoutGraph)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.chkBackgroundGrid = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkBackgroundGrid.setChecked(True)
        self.chkBackgroundGrid.setObjectName("chkBackgroundGrid")
        self.horizontalLayout.addWidget(self.chkBackgroundGrid)
        self.chkLegend = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkLegend.setChecked(True)
        self.chkLegend.setObjectName("chkLegend")
        self.horizontalLayout.addWidget(self.chkLegend)
        self.pushButtonChangBackground = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangBackground.setObjectName("pushButtonChangBackground")
        self.horizontalLayout.addWidget(self.pushButtonChangBackground)
        self.cboBarItem = QtWidgets.QComboBox(parent=self.centralwidget)
        self.cboBarItem.setObjectName("cboBarItem")
        self.horizontalLayout.addWidget(self.cboBarItem)
        self.pushButtonChangeBarColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeBarColor.setObjectName("pushButtonChangeBarColor")
        self.horizontalLayout.addWidget(self.pushButtonChangeBarColor)
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setObjectName("pushButtonClose")
        self.horizontalLayout.addWidget(self.pushButtonClose)
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 647, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - BarGraph"))
        self.label.setText(_translate("MainWindow", "Choose Dataset:"))
        self.pushButtonPickDataset.setText(_translate("MainWindow", "Pick Dataset"))
        self.chkBackgroundGrid.setText(_translate("MainWindow", "Background Grid"))
        self.chkLegend.setText(_translate("MainWindow", "Legend"))
        self.pushButtonChangBackground.setText(_translate("MainWindow", "Chart Background"))
        self.pushButtonChangeBarColor.setText(_translate("MainWindow", "Bar Color"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Viết mã lệnh cho “MainWindowEx.py”

Khai báo lớp MainWindowEx kế thừa từ Generate Python code ở bước 3, dưới đây là hàm override setupUi, Tui chỉnh sửa chút xíu so với bài học trước, đó là trong hàm này nó sẽ gọi 2 hạm setupBarGraph() để cấu hình Graph trống ban đầu và setupSignal() để xủ lý sự kiến:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog, QFileDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg
import pandas as pd
import random as nd

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.setupBarGraph()
        self.setupSignal()

Hàm setupBarGraph() được viết theo mã lệnh như dưới đây:

def setupBarGraph(self):
    self.graphWidget = pg.PlotWidget()
    self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
    self.graphWidget.setBackground('w')

    labelStyle = {"color": "green", "font-size": "18px"}
    labelBrandStyle = {"color": "purple", "font-size": "18px"}
    self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
    self.graphWidget.setLabel("bottom","Cities",**labelStyle)
    self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)

    self.graphWidget.showGrid(x=True, y=True)

    self.width = 0.2
    self.bargraphItems = []

    self.legend = self.graphWidget.addLegend()

    self.layoutGraph.addWidget(self.graphWidget)

Hàm setupBarGraph() ở trên thiết kế Graph trống như hình dưới đây (các mã lệnh Tui đã giải thích chi tiết ở các bài học trước, nếu chưa hiểu thì các bạn xem lại):

Hàm setupSignal() để gán các Signal để triệu gọi các hàm, slot:

def setupSignal(self):
    self.pushButtonPickDataset.clicked.connect(self.processPickDataset)
    self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
    self.chkLegend.stateChanged.connect(self.processChangeLegend)
    self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
    self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

Hàm setupSignal() Tui có bổ sung slot “processPickDataset”, slot này sẽ cung cấp chức năng lựa chọn file Excel cho người dùng và vẽ lên Graph.

def processPickDataset(self):
    filters = "Dataset (*.xlsx);;All files(*)"
    filename, selected_filter = QFileDialog.getOpenFileName(
        self.MainWindow,
        filter=filters,
    )
    self.lineEditDataset.setText(filename)

    self.drawChart(filename)

Hàm trên, Tui dùng QFileDialog cho người dùng chọn File được filter .xlsx mặc định.

Chương trình sẽ hiển thị tên file và đường dẫn lên trên QLineEdit

Và gọi hàm drawChart() để vẽ các BarGraph Item.

def drawChart(self,datasetPath):
    #read Excel file into DataFrame by Pandas
    dataframe = pd.read_excel(datasetPath)
    #Read columns, we omit first column (corner)
    #Hà Nội,Huế,TP.HCM,Cần Thơ,An Giang:
    self.xlab = dataframe.columns.values[1:]
    #create an array from 1 to number of column
    self.positions = np.arange(1, len(self.xlab) + 1)
    #remove existing BarGraph Item
    #(user can choose file many times)
    for barItem in self.bargraphItems:
        self.graphWidget.removeItem(barItem)
    self.bargraphItems = []
    self.cboBarItem.clear()
    i = 0
    #loop to read row data
    for row in range(len(dataframe.values)):
        #Get value in the first column for each row(quarter X)
        name = dataframe.values[row][0]
        #Get an Array data for each row
        revenues = dataframe.values[row][1:].tolist()
        #setup the position for each graph bar item
        x = self.positions + i * self.width
        #random color for each bar item
        brush = (nd.randint(0, 255), nd.randint(0, 255), nd.randint(0, 255))
        #create BarGraphItem:
        bargraphItem = pg.BarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name)
        i = i + 1
        #store bargraphItem into bargraphItems
        self.bargraphItems.append(bargraphItem)
        #add the Bar Graph Item into the Chart
        self.graphWidget.addItem(bargraphItem)
        #Draw the name of BarItem into QComBoBox
        self.cboBarItem.addItem(bargraphItem.name(), bargraphItem)
    #setup tick
    self.autoTick(self.graphWidget, self.xlab)
    #setup label
    self.autolabel(self.bargraphItems)

Hàm ẩn hiển thị lưới processChangeGrid():

def processChangeGrid(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.graphWidget.showGrid(x=True, y=True)
    else:
        self.graphWidget.showGrid(x=False, y=False)

Hàm ẩn hiển thị legend processChangeLegend():

def processChangeLegend(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.legend.show()
    else:
        self.legend.hide()

Hàm thay đổi màu nền của Chart processChangeChartBackground():

def processChangeChartBackground(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        self.graphWidget.setBackground(color.name())
        del dialog

Hàm thay đổi màu của từng Baritem, lưu ý là người sử dụng sẽ chọn Bar item trong QComboBox trước, sau đó nhấn vào button “bar color” để đổi màu:

def processChangeBarColor(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
        bargraphItem.opts["brush"]=color.name()
        bargraphItem._updateColors(bargraphItem.opts)
        del dialog

Hàm autoTick() sẽ thiết lập label cho các thành phố ở bottom Cities:

def autoTick(self,graphWidget, xlab):
    ticks = []
    for i, item in enumerate(xlab):
        ticks.append((i +1+ 2*self.width, item))
    ticks = [ticks]
    ax = graphWidget.getAxis('bottom')
    ax.setTicks(ticks)

Hàm autolabel() sẽ thiết lập label cho các Bar Item:

def autolabel(self,barItems):
    # attach some text labels
    for barItem in barItems:
        xs, heights = barItem.getData()
        for i in range(len(heights)):
            height = heights[i]
            x = xs[i]
            clr = barItem.opts["brush"]
            text = pg.TextItem(str(height), color=clr)
            text.setParentItem(barItem)
            text.setX(x)
            text.setY(height)
            text.setAnchor((QPointF(0.5, 0.75)))

Label cho từng Bar item giúp người sử dụng dễ dàng quan sát và so sánh cũng như đánh giá dữ liệu.

Dưới đây là coding đầy đủ của MainWindowEx.py:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog, QFileDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg
import pandas as pd
import random as nd

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.setupBarGraph()
        self.setupSignal()
    def setupBarGraph(self):
        self.graphWidget = pg.PlotWidget()
        self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
        self.graphWidget.setBackground('w')

        labelStyle = {"color": "green", "font-size": "18px"}
        labelBrandStyle = {"color": "purple", "font-size": "18px"}
        self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
        self.graphWidget.setLabel("bottom","Cities",**labelStyle)
        self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)

        self.graphWidget.showGrid(x=True, y=True)

        self.width = 0.2
        self.bargraphItems = []

        self.legend = self.graphWidget.addLegend()

        self.layoutGraph.addWidget(self.graphWidget)

    def setupSignal(self):
        self.pushButtonPickDataset.clicked.connect(self.processPickDataset)
        self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
        self.chkLegend.stateChanged.connect(self.processChangeLegend)
        self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
        self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

    def processPickDataset(self):
        filters = "Dataset (*.xlsx);;All files(*)"
        filename, selected_filter = QFileDialog.getOpenFileName(
            self.MainWindow,
            filter=filters,
        )
        self.lineEditDataset.setText(filename)

        self.drawChart(filename)

    def drawChart(self,datasetPath):
        #read Excel file into DataFrame by Pandas
        dataframe = pd.read_excel(datasetPath)
        #Read columns, we omit first column (corner)
        #Hà Nội,Huế,TP.HCM,Cần Thơ,An Giang:
        self.xlab = dataframe.columns.values[1:]
        #create an array from 1 to number of column
        self.positions = np.arange(1, len(self.xlab) + 1)
        #remove existing BarGraph Item
        #(user can choose file many times)
        for barItem in self.bargraphItems:
            self.graphWidget.removeItem(barItem)
        self.bargraphItems = []
        self.cboBarItem.clear()
        i = 0
        #loop to read row data
        for row in range(len(dataframe.values)):
            #Get value in the first column for each row(quarter X)
            name = dataframe.values[row][0]
            #Get an Array data for each row
            revenues = dataframe.values[row][1:].tolist()
            #setup the position for each graph bar item
            x = self.positions + i * self.width
            #random color for each bar item
            brush = (nd.randint(0, 255), nd.randint(0, 255), nd.randint(0, 255))
            #create BarGraphItem:
            bargraphItem = pg.BarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name)
            i = i + 1
            #store bargraphItem into bargraphItems
            self.bargraphItems.append(bargraphItem)
            #add the Bar Graph Item into the Chart
            self.graphWidget.addItem(bargraphItem)
            #Draw the name of BarItem into QComBoBox
            self.cboBarItem.addItem(bargraphItem.name(), bargraphItem)
        #setup tick
        self.autoTick(self.graphWidget, self.xlab)
        #setup label
        self.autolabel(self.bargraphItems)
    def processChangeGrid(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.graphWidget.showGrid(x=True, y=True)
        else:
            self.graphWidget.showGrid(x=False, y=False)
    def processChangeLegend(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.legend.show()
        else:
            self.legend.hide()
    def processChangeChartBackground(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.graphWidget.setBackground(color.name())
            del dialog
    def processChangeBarColor(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
            bargraphItem.opts["brush"]=color.name()
            bargraphItem._updateColors(bargraphItem.opts)
            del dialog
    def autoTick(self,graphWidget, xlab):
        ticks = []
        for i, item in enumerate(xlab):
            ticks.append((i +1+ 2*self.width, item))
        ticks = [ticks]
        ax = graphWidget.getAxis('bottom')
        ax.setTicks(ticks)

    def autolabel(self,barItems):
        # attach some text labels
        for barItem in barItems:
            xs, heights = barItem.getData()
            for i in range(len(heights)):
                height = heights[i]
                x = xs[i]
                clr = barItem.opts["brush"]
                text = pg.TextItem(str(height), color=clr)
                text.setParentItem(barItem)
                text.setX(x)
                text.setY(height)
                text.setAnchor((QPointF(0.5, 0.75)))

    def show(self):
        self.MainWindow.show()

Bước 5: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Ta nhấn vào nút “Pick Dataset” rồi trỏ tới file Excel data mẫu:

Chọn File Excel rồi nhấn Open, Sau khi nhấn Open ta có kết quả:

Chương trình sẽ tự động nạp tên vào QComBoBox như bài trước để ta có thể chọn và đổi màu cho từng bar item:

Ví dụ như ta chọn “Quarter 3” Bar Item rồi chọn Bar Color:

Ta thử chọn màu đỏ rồi bấm OK:

Màu Bar Item cũng đổi như ta mong muốn, tương tự như bài trước.

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với Multiple BarGraph đọc dữ liệu từ EXCEL. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget.

Ôn tập cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Ôn tập cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/89e33vfnoiob6dl/LearnPyQtBarGraphPart7.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách kế thừa BarGraphItem để lắng nghe và xử lý sự kiện click chuột ngay trên từng Baritem

Chúc các bạn thành công

One thought on “Bài 37: Trực quan hóa dữ liệu – PyQtGraph-Excel Data-PyQt6 – Part 7”

Leave a Reply