Python 如何實(shí)時繪制數(shù)據(jù)

提到 GUI 繪圖,大家可能第一反應(yīng)是 OpenGL 和 Matplotlib,但其實(shí)基于 Qt 平臺還有個功能強(qiáng)大的 pyqtgraph 繪圖庫,不僅支持豐富的圖形種類,還能實(shí)時更新繪圖數(shù)據(jù)并進(jìn)行交互式操作。
不同于網(wǎng)上其他文章或代碼講解,今天我們集中只關(guān)注實(shí)時繪制數(shù)據(jù)功能的實(shí)現(xiàn)。為了更精準(zhǔn)學(xué)習(xí)該 pyqtgraph 模塊功能,我們將參考官方給出的實(shí)例來邊學(xué)邊練。
1. pyqtgraph 簡介
1.1 pyqtgraph 特點(diǎn)
關(guān)于 pyqtgraph 與 Matplotlib 的對比,大致要點(diǎn)如下:
-
pyqtgraph 在畫圖方面不如 Matplotlib 功能完整和成熟,但運(yùn)行更快 -
Matplotlib 旨在繪制高質(zhì)量圖像,pyqtgraph 則主要面向數(shù)據(jù)抓取和數(shù)據(jù)分析的應(yīng)用 -
相比 Matplotlib,pyqtgraph 對 python 和 qt 編程更親和 -
pyqtgraph 具備更好的圖像交互、3D展示等
1.2 pyqtgraph 安裝
一般配合 PyQt5 使用,這些都要預(yù)先安裝好,我們這里只提 pyqtgraph 相關(guān):
pip install pyqtgraph
1.3 pyqtgraph 實(shí)例全集
官方專門給出了一個實(shí)例集合,包含了展示與源碼,非常方便學(xué)習(xí),通過以下代碼來運(yùn)行:
import pyqtgraph.examples
pyqtgraph.examples.run()
運(yùn)行后,會出現(xiàn)如下 GUI 界面
今天我們主要關(guān)注實(shí)時繪制數(shù)據(jù),找到左側(cè)目錄中的 "Scrolling plots",單擊右側(cè)可以看到源碼
雙擊或者點(diǎn)擊下方的 "Run Example" 便可展示運(yùn)行效果:
特定截圖:
2. 實(shí)時繪制學(xué)習(xí)
結(jié)合著實(shí)例代碼和演示效果,我們可以看到有如下不同實(shí)時展示模式:
-
模式1: 從 0 開始固定 x 軸數(shù)值范圍,數(shù)據(jù)在該范圍內(nèi)向左移動展示 -
模式2: 數(shù)據(jù)帶著 x 軸坐標(biāo)一起向左移動展示 -
模式3: 固定 x 軸數(shù)值右側(cè)范圍到 0,數(shù)據(jù)左移展示 -
模式4: 左側(cè)固定從 0 開始,數(shù)據(jù)累積展示 -
模式5: 數(shù)據(jù)范圍右側(cè)截止到 0,但仍可查看大于 0 范圍
2.1 模式1: 固定 x 范圍,左移展示數(shù)據(jù)
2.1.1 模式1效果
2.1.2 實(shí)例1代碼
我們可以在實(shí)例匯總的代碼中將該部分代碼抽離出來,大致如下:
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('Scrolling Plots Mode 1')
p1 = win.addPlot()
data1 = np.random.normal(size=300)
curve1 = p1.plot(data1)
def update1():
global data1, ptr1
data1[:-1] = data1[1:] # shift data in the array one sample left
# (see also: np.roll)
data1[-1] = np.random.normal()
curve1.setData(data1)
timer = pg.QtCore.QTimer()
timer.timeout.connect(update1)
timer.start(50)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
注意,模式 1 中實(shí)時繪制效果的實(shí)現(xiàn),是通過將數(shù)據(jù)列表中的數(shù)據(jù)整體左移實(shí)現(xiàn)的,關(guān)鍵語句就是 data1[:-1] = data1[1:],再通過計(jì)時器來綁定該左移數(shù)據(jù)的函數(shù),最終達(dá)到了展示中的數(shù)據(jù)動態(tài)展示效果。
2.1.3 寫成 PlotWidget 形式
總結(jié)下模式 1 的原理:x 坐標(biāo)數(shù)據(jù)不變化,對應(yīng)的 y 數(shù)據(jù)設(shè)置個左移變換的函數(shù),計(jì)時器信號綁定該左移數(shù)據(jù)的函數(shù),把 y 數(shù)據(jù)能實(shí)時設(shè)置到圖中即可。
實(shí)例 1 中繪制圖的寫法比較少見,通常應(yīng)用是通過 pyqtgraph.PlotWidget.plot() 來實(shí)現(xiàn)在控件中作圖再添加到 GUI 控件中,所以我們將采用 PlotWidget 的寫法來實(shí)現(xiàn)模式1的繪制,代碼如下:
__author__ = 'Ted'
from PyQt5.Qt import *
from pyqtgraph import PlotWidget
from PyQt5 import QtCore
import numpy as np
import pyqtgraph as pq
class Window(QWidget):
def __init__(self):
super().__init__()
# 設(shè)置下尺寸
self.resize(600,600)
# 添加 PlotWidget 控件
self.plotWidget_ted = PlotWidget(self)
# 設(shè)置該控件尺寸和相對位置
self.plotWidget_ted.setGeometry(QtCore.QRect(25,25,550,550))
# 仿寫 mode1 代碼中的數(shù)據(jù)
# 生成 300 個正態(tài)分布的隨機(jī)數(shù)
self.data1 = np.random.normal(size=300)
self.curve1 = self.plotWidget_ted.plot(self.data1, name="mode1")
# 設(shè)定定時器
self.timer = pq.QtCore.QTimer()
# 定時器信號綁定 update_data 函數(shù)
self.timer.timeout.connect(self.update_data)
# 定時器間隔50ms,可以理解為 50ms 刷新一次數(shù)據(jù)
self.timer.start(50)
# 數(shù)據(jù)左移
def update_data(self):
self.data1[:-1] = self.data1[1:]
self.data1[-1] = np.random.normal()
# 數(shù)據(jù)填充到繪制曲線中
self.curve1.setData(self.data1)
if __name__ == '__main__':
import sys
# PyQt5 程序固定寫法
app = QApplication(sys.argv)
# 將綁定了繪圖控件的窗口實(shí)例化并展示
window = Window()
window.show()
# PyQt5 程序固定寫法
sys.exit(app.exec())
我們在自己寫的代碼中重新設(shè)置了下窗口尺寸位置,數(shù)據(jù)還是按照實(shí)例中的寫法來完成的。
2.1.4 自寫模式1效果
2.2 數(shù)據(jù)隨 x 軸一起左移
2.2.1 模式2效果
2.2.2 實(shí)例2代碼
該模式代碼抽離出來大致如下:
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: Scrolling Plots')
p2 = win.addPlot()
data1 = np.random.normal(size=300)
curve2 = p2.plot(data1)
ptr1 = 0
def update1():
global data1, ptr1
data1[:-1] = data1[1:] # shift data in the array one sample left
data1[-1] = np.random.normal()
ptr1 += 1
curve2.setData(data1)
curve2.setPos(ptr1, 0)
timer = pg.QtCore.QTimer()
timer.timeout.connect(update1)
timer.start(50)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
對比模式1代碼,此部分多了個 curve2.setPos(ptr1, 0),通過 Qt 官網(wǎng)中搜索查閱,setPos(x,y) 是將原點(diǎn)設(shè)置到 (x,y):
?Sets the position of the item to pos, which is in parent coordinates. For items with no parent, pos is in scene coordinates. The position of the item describes its origin (local coordinate (0, 0)) in parent coordinates.
?
這樣我們可以大致理解為,通過設(shè)置坐標(biāo)系相對原點(diǎn)位置來產(chǎn)生 x 軸移動的效果。
2.2.3 寫成 PlotWidget 形式
總結(jié)下模式 2 的原理:y 數(shù)據(jù)與模式1相同,設(shè)置左移變換的函數(shù),計(jì)時器信號綁定該左移數(shù)據(jù)的函數(shù),把 y 數(shù)據(jù)能實(shí)時設(shè)置到圖中;x 數(shù)據(jù)則通過 setPos() 函數(shù)隨著 y 的變化同步進(jìn)行設(shè)置,產(chǎn)生 x 軸同步移動的效果。
我們繼續(xù)采用 PlotWidget 的寫法來實(shí)現(xiàn)模式2的繪制,在模式1基礎(chǔ)上添加幾行代碼即可,為作區(qū)分我們把曲線定義為 curve2:
__author__ = 'Ted'
from PyQt5.Qt import *
from pyqtgraph import PlotWidget
from PyQt5 import QtCore
import numpy as np
import pyqtgraph as pq
class Window(QWidget):
def __init__(self):
super().__init__()
# 設(shè)置下尺寸
self.resize(600,600)
# 添加 PlotWidget 控件
self.plotWidget_ted = PlotWidget(self)
# 設(shè)置該控件尺寸和相對位置
self.plotWidget_ted.setGeometry(QtCore.QRect(25,25,550,550))
# 仿寫 mode1 代碼中的數(shù)據(jù)
# 生成 300 個正態(tài)分布的隨機(jī)數(shù)
self.data1 = np.random.normal(size=300)
self.curve2 = self.plotWidget_ted.plot(self.data1, name="mode2")
self.ptr1 = 0
# 設(shè)定定時器
self.timer = pq.QtCore.QTimer()
# 定時器信號綁定 update_data 函數(shù)
self.timer.timeout.connect(self.update_data)
# 定時器間隔50ms,可以理解為 50ms 刷新一次數(shù)據(jù)
self.timer.start(50)
# 數(shù)據(jù)左移
def update_data(self):
self.data1[:-1] = self.data1[1:]
self.data1[-1] = np.random.normal()
# 數(shù)據(jù)填充到繪制曲線中
self.curve2.setData(self.data1)
# x 軸記錄點(diǎn)
self.ptr1 += 1
# 重新設(shè)定 x 相關(guān)的坐標(biāo)原點(diǎn)
self.curve2.setPos(self.ptr1,0)
if __name__ == '__main__':
import sys
# PyQt5 程序固定寫法
app = QApplication(sys.argv)
# 將綁定了繪圖控件的窗口實(shí)例化并展示
window = Window()
window.show()
# PyQt5 程序固定寫法
sys.exit(app.exec())
我們在自己寫的代碼中重新設(shè)置了下窗口尺寸位置,數(shù)據(jù)還是按照實(shí)例中的寫法來完成的。
2.2.4 自寫模式1效果
3. 小結(jié)
今天先只簡單整理這兩個較簡單的實(shí)時繪制模式,給定的代碼中數(shù)據(jù)是用的隨機(jī)正態(tài)分布數(shù)據(jù),我們結(jié)合著模式 1 和 2 的實(shí)例代碼來分析其原理算法來仿寫了常用版本的代碼。
掌握模式 1 和模式 2 的用法后,我們便可以對更多的數(shù)據(jù)來進(jìn)行動態(tài)展示,比如 CPU 占用率、股票實(shí)時價格等,配合著 PyQt5 的 GUI 圖形界面,那么完全可以用 Python 來寫出看著高大上的數(shù)據(jù)可視化界面了,這個后續(xù)我們再繼續(xù)研究。
◆ ◆ ◆ ◆ ◆
麟哥新書已經(jīng)在當(dāng)當(dāng)上架了,點(diǎn)擊下方小程序即可進(jìn)入當(dāng)當(dāng)購買頁面:
數(shù)據(jù)森麟公眾號的交流群已經(jīng)建立,許多小伙伴已經(jīng)加入其中,感謝大家的支持。大家可以在群里交流關(guān)于數(shù)據(jù)分析&數(shù)據(jù)挖掘的相關(guān)內(nèi)容,還沒有加入的小伙伴可以掃描下方管理員二維碼,進(jìn)群前一定要關(guān)注公眾號奧,關(guān)注后讓管理員幫忙拉進(jìn)群,期待大家的加入。
管理員二維碼:
點(diǎn)擊閱讀原文,即可參與當(dāng)當(dāng)購書活動
