汪群超 Chun-Chao Wang

Dept. of Statistics, National Taipei University, Taiwan

從認識 PyQtGraph 開始

本單元必須安裝 pyqtgraph 套件:
在 Python 環境:>pip install pyqtgraph
註:How to use pyqtgraph:> https://pyqtgraph.readthedocs.io/en/latest/how_to_use.html

在 Anaconda 環境:>conda install -c anaconda pyqtgraph

Objective:

  1. 學習適用於 GUI 應用程式的繪圖套件 PyQtGraph
  2. 了解、探索並學習 PyQtGraph 的優點(有別於 MatplotLib)
PyQtGraph is a graphics and user interface library for Python that provides functionality commonly required in designing and science applications. Its primary goals are to provide fast, interactive graphics for displaying data (plots, video, etc.) and second is to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer).

reference: https://www.geeksforgeeks.org/pyqtgraph-getting-plot-item-from-plot-window/

另,參考 pyqtgraph 官網關於繪圖的基本方法PyQtGraph vs. matplotlib

Prerequisite:

  1. 基本的 Python 程式設計能力
  2. 懂得使用 python 的 IDE 環境(編輯器),如 VS Code, Spider

範例 1:繪製函數 f(x) = sin(x)+cos(x)


注意事項:

  1. pyqtgraph 的繪圖方式與最常見的 matplotlib 套件非常不同。首先,pyqtgraph 必須附著於一個應用視窗(Application)上。在下列的示範程式中,最後 app = QtGui.QApplication.instance() 開啟一個視窗程序,並透過 app.exec() 開啟並將視窗留在螢幕上。一般使用 sys.exit(app.exec()) 除了開啟視窗外,也讓系統負責視窗結束後的善後工作。事實上,動用了 QtGui 來開啟視窗,已經牽涉到 PyQt 內的類別了(Class)。也就是要安裝過 PyQt6(或較早版本的 PyQt5 或 PySide6, PySide4 等),不單是 pyqtgraph 的事情了。

  2. pyqtgraph 與 numpy 合作無間,效率高。

  3. 示範程式故意用 plt = pg.plot() 建立一個繪圖視窗的物件並以 plt 為名,延續 matplotlib 的使用習慣。利用變數 plt 可以位視窗加上線條與其他座標圖上的標示,包括 legend。

  4. plot 是繪製線條標準的指令,而為線條物件加入修飾的方式與 matplotlib 的方式大不同,此處示範 pen 屬性的用法,包括直接給于顏色,或透過 mkPen 來指定各項屬性,譬如顏色(color)、粗細(width)、虛線(linestyle)…等。

  5. 在下列程式碼製作畫筆的 mkPen() 指令,style=QtCore.Qt.DashLine 指定了線條為虛線。如果這個屬性沒有如期呈現或出現執行錯誤,可以暫時將其去除,先不影響整個程式的運作。至於為何會出錯,原因還不確定,很可能是使用到 PyQt6 的 QtCore.Qt.DashLine,而 PyQt6 並沒有這項屬性。這個用法在 PyQt5 的時代可行,即 PyQt5.QtCore.Qt.DashLine 存在。

  6. pyqtgraph 擅長互動的特性。讀者可以用滑鼠拖曳圖形往不同方式移動、可以放大或縮小圖形、按滑鼠右鍵還用各種功能。這個特點是 matplotlib 沒有的。

import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore # 這個宣告會引用 PyQt 的 QtGui, QtCore

title = "Basic pyqtgraph plot"
x = np.linspace(-3*np.pi, 3*np.pi, 1000)
y1 = np.sin(x)
y2 = np.cos(x)
y = y1 + y2

# create plot window object
plt = pg.plot()

# some regular settings
plt.showGrid(x = True, y = True)
plt.addLegend()
plt.setLabel('left', 'y')
plt.setLabel('bottom', 'x')
# plt.setXRange(0, 10)
plt.setYRange(-2.5, 2.5)
plt.setWindowTitle(title)

plt.plot(x, y1, pen = 'g', name = 'sin(x)')
plt.plot(x, y2, pen = 'r', name = 'cos(x)')

pen = pg.mkPen(color='y', width=3, style=QtCore.Qt.DashLine) # DotLine
plt.plot(x, y, pen = pen, name = 'sin(x)+cos(x)')

# main method
if __name__ == '__main__':
     
    # Create the main application instance
    # QtGui.QApplication.instance().exec()
    import sys
    app = QtGui.QApplication.instance()
    sys.exit(app.exec())

範例 2:繪製兩條折線圖

其中 y1 = [2, 8, 6, 8, 6, 11, 14, 13, 18, 19], y2 = [3, 1, 5, 8, 9, 11, 16, 17, 14, 16] 代表兩條折線圖的位置資料, X軸以 1:10 表示。利用範例 1 的程式,變更部分如下左,呈現之圖形如下右。

注意事項:

  1. 折線圖著重在符號(symbol) 的表現。讀者可以試著更動所有 symbol 相關的參數。

  2. pyqtgraph 的圖形中的文字大小偏小,讀者可以試著找到更動 fontsize 的指令。

  3. 本範例取材自 GeeksforGeeks 網站:https://www.geeksforgeeks.org/pyqtgraph-getting-plot-item-from-plot-window/

...
...
x = range(0, 10)
y1 = [2, 8, 6, 8, 6, 11, 14, 13, 18, 19]
y2 = [3, 1, 5, 8, 9, 11, 16, 17, 14, 16]
...
...
line1 = plt.plot(x, y1, pen ='g', symbol ='x', \
    symbolPen ='g', symbolBrush = 0.2, name ='green')
 
line2 = plt.plot(x, y2, pen ='y', symbol ='o', \
    symbolPen ='y', symbolBrush = 0.2, name ='blue')
...
...


範例 3:繪製散佈圖(scatter plot)

如下右圖。資料來自兩個常態獨立變數的隨機樣本,各 1000 個。

注意事項:

  1. 本範例程式刻意改採另一種開啟應用視窗的方式:mkQApp(),意即 make Qt Application。這也是最常見的方式。請留意視窗變數 app 的來源不同。

  2. 程式前兩行的設定,將背景顏色改為白色,前景顏色改為黑色。原來 pyqtgraph 預設的圖形背景顏色是黑色,前景是白色。

  3. pyqtgraph 繪製散佈圖的方式與之前的範例不同,充分展現了物件導向的特質:先建立一個繪圖工具區(PlotWidget),再以新增繪圖項目(addItem)的方式放進繪圖工具區。

  4. 繪圖工具區(PlotWidget)設定後,必須 show() 出來才能呈現圖形,這點與 matplotlib 套件相同。

  5. 散佈圖 scatter 並非以一個指令完成繪製,而是先製作繪圖區的 Item,譬如,ScatterPlotItem(),再以 addItem() 的方式加入繪圖區。最後才以 setData() 加入資料,呈現散佈圖。

  6. 以 setData() 的方式呈現最後的圖形,常用來做更動圖形之用途。換句話說,只要再 setData() 一次,圖形便更換了,不需要再呼叫繪圖指令,因此 pyqtgraph 被認為有效率的繪圖套件。

  7. setData() 對於資料的擺放也有幾種作法,以便呈現出不同的效果。下列的程式以 setData(x,y) 的方式呈現,另兩行被註解的作法,則是以位置字典(position dictionary) 的方式。

  8. 程式來源:https://gist.github.com/Lauszus/16d37c476f24596f8bf43a74847a2fc0

PlotWidget 是 pyqtgraph 下的一個類別(class),它的角色常讓初學者混淆。以下是網路上找到的描述:

In PyQtGraph all plots are created using the PlotWidget widget. This widget provides a contained canvas on which plots of any type can be added and configured. Under the hood, this plot widget uses Qt native QGraphicsScene meaning it fast and efficient yet simple to integrate with the rest of your app. You can create a PlotWidget as for any other widget.

建議初學者先學會用,再慢慢理解 pyqtgraph 各種繪圖的手段。
import sys
import numpy as np
import pyqtgraph as pg

# Set white background and black foreground
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')

# Generate random points
n = 1000
data = np.random.normal(size=(2, n))

# Create the main application instance
app = pg.mkQApp()

# Create the view
view = pg.PlotWidget()
view.resize(640, 480)
view.setWindowTitle('Scatter plot using pyqtgraph')
view.setAspectLocked(True)
view.show()

# Create the scatter plot and add it to the view
pen = pg.mkPen(width=5, color='r')
scatter = pg.ScatterPlotItem(pen=pen, symbol='o', size=1)
view.addItem(scatter)

# Convert data array into a list of dictionaries with the x,y-coordinates
# pos = [{'pos': data[:, i]} for i in range(n)]
# scatter.setData(pos) # 2D dictionary
scatter.setData(data[0,:], data[1,:])

# execute the application and Gracefully exit the application
sys.exit(app.exec())

範例 4:繪製曲線間的面積圖(patch area)

繪製標準常態 PDF 函數在 95% 信心水準下的面積,如下右圖。

注意事項:

  1. 下列程式示範另一種使用 pyqtgraph 應用視窗的設定。直接採 pg.exec()。

  2. 先建立 plot 繪圖視窗,再利用 FillBetweenItem 製作兩曲線下之面積,最後再以 addItem() 的方式加入。

  3. 若要變更面積範圍,譬如,改為 99% 的信賴區間,只需更動兩條曲線的資料即可(setData(x,y))。如程式碼下方註解的四行程式,便能變更面積。不須再執行 pg.FillBetweenItem()。

import sys
import numpy as np
from scipy.stats import norm
import pyqtgraph as pg

plt = pg.plot()
plt.setWindowTitle('FillBetweenItem')
plt.setYRange(0, 0.5)

x = np.linspace(-5, 5, 1000)
y = norm.pdf(x)

pen = pg.mkPen(color=(255, 0, 0), width = 10) # linestyle=Qt.DotLine, Qt.DashDotLine and Qt.DashDotDotLine    
cur = plt.plot(x, y, pen = pen)

x = np.linspace(-1.96, 1.96, 200)
y = norm.pdf(x)
cur1 = plt.plot(x, y, pen = 'r', name = 'Demo')
cur2 = plt.plot(x, np.zeros(len(y)))
        # add color patch under curve
patchcur = pg.FillBetweenItem(curve1 = cur1, curve2 = cur2, \
    brush = 'green')
plt.addItem(patchcur)

# x = np.linspace(-2.576, 2.576, 200)
# y = norm.pdf(x)
# cur1.setData(x, y)
# cur2.setData(x, np.zeros(len(y)))

if __name__ == '__main__':
    sys.exit(pg.exec())

欣賞來自 pyqtgraph 官方的範例

  1. 執行下列兩行程式碼,可帶出 pyqtgraph 官方製作的豐富範例與程式碼。

  2. 為執行其中與 3D 有關的範例,應先安裝 pyopengl 套件。即 pip install pyopengl。

  3. 下圖為執行後的範例表列與第一個範例的程式與執行結果。

import pyqtgraph.examples
pyqtgraph.examples.run()

練習題:練習繪製以下兩張在前面單元使用 matplotlib 畫的圖

注意事項:

  1. 左上圖之函數為: f(x) = 2x-x^2, g(x) = x^2

  2. 左圖座標區以 latex 與法將積分式標示出來。在 pyqtgraph 似乎不能使用 latex(?),找找看有甚麼替代方案?

  3. pyqtgraph 與 matplotlib 繪圖風格不同,因此不需要追求產生一模一樣的外觀,而是盡量朝各自的優點發揮,只要呈現該有的內涵即可。

範例 5:多重視窗(分割視窗、子視窗)

模仿 pyqtgraph 所提供的範例,製作如下右圖的四個分割子視窗。

注意事項:

  1. 本範例主要是展現 pyqtgraph 分割繪圖視窗的方式。關鍵指令為 pg.GraphicsLayoutWidget(…)。這是在 Qt 視窗中建立一個可排列多個圖形的元件(widget)。之後,使用 addPlot() 方式產生一個個圖形物件。需要換新的一列時,則用 nextRow()。

  2. 這裡也刻意安排直方圖的畫法,有別於 matplotlib 的直方圖指令,pyqtgraph 的方式比較「婉轉」。其他如最後一張橢圓,以 QtGui.QGraphicsEllipseItem 的方式被加進去 addItem()。

import sys
import numpy as np
import pyqtgraph as pg

# Create the main application instance
app = pg.mkQApp()

# Create the view
win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
plt1 = win.addPlot()
plt2 = win.addPlot()

## make interesting distribution of values
vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)])
## compute standard histogram
y, x = np.histogram(vals, bins=np.linspace(-3, 8, 40))
## Using stepMode="center" causes the plot to draw two lines for each sample.
## notice that len(x) == len(y)+1
plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0,0,255,150))
## Now draw all points as a nicely-spaced scatter plot
y = pg.pseudoScatter(vals, spacing=0.15)
plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150))

win.nextRow()
plt3 = win.addPlot(title="Parametric, grid enabled")
plt4 = win.addPlot(title="Ellipse")

x = np.cos(np.linspace(0, 2*np.pi, 1000))
y = np.sin(np.linspace(0, 4*np.pi, 1000))
plt3.plot(x, y)
plt3.showGrid(x=True, y=True)

p_ellipse = pg.QtGui.QGraphicsEllipseItem(0, 0, 10, 20)  # x, y, width, height
p_ellipse.setPen(pg.mkPen((0, 0, 0, 100)))
p_ellipse.setBrush(pg.mkBrush((50, 50, 200)))
plt4.addItem(p_ellipse)

# execute the application and Gracefully exit the application
sys.exit(app.exec())

練習題:練習繪製下列在前面單元使用 matplotlib 畫的圖


範例 6:長條圖與座標軸刻度文字及大小


注意事項:

  1. 在 pyqtgraph 裡,長條圖(Bar Graph)被歸類於一種特殊的圖形項目,稱為 BarGraphItem,同樣地,必須經過定義後,以 addItem 的方式加入圖形。細節見下列程式。

  2. 長條圖通常代表某個組別的數量,因此在長條圖下的 X 軸刻度上,最好能秀出組別文字並調整文字大小。如下圖的 A, B, C 與較大的字體,另外也調整了顏色。

    import numpy as np
    import pyqtgraph as pg
    from pyqtgraph.Qt import QtGui, QtCore # 這個宣告會引用 PyQt 的 QtGui, QtCore
    
    # define the data
    title = "Basic pyqtgraph plot: Bar Graph & xTicks"
    plt = pg.plot()
    x = [1, 2, 3]
    y = [10, 30, 20]
    Ticks = ["A","B","C"]
    
    barItem = pg.BarGraphItem(x = x, height = y, width = 0.3, brush=(107,200,224))
    plt.addItem(barItem)
    
    plt.getAxis('bottom').setTicks([[(i, Ticks[i-1]) for i in x]])
    font = QtGui.QFont()
    font.setPixelSize(20)
    plt.getAxis("bottom").setStyle(tickFont = font)
    plt.getAxis("left").setStyle(tickFont = font)
    plt.getAxis("bottom").setTextPen(color='g')
    plt.getAxis("bottom").setPen(color='y')
    plt.setWindowTitle(title)
    
    # main method
    if __name__ == '__main__':
        import sys
        app = QtGui.QApplication.instance()
        sys.exit(app.exec())
    
商學院  7F16
ccw@gm.ntpu.edu.tw
(02)8674-1111 
ext 66777

部落格統計

  • 99,323 點擊次數
%d bloggers like this: