簡體   English   中英

我如何使用 matplotlib 為多個圖形制作動畫,每個圖形都包含實時數據的推進 window 並利用 blitting?

[英]How can I use matplotlib to animate several graphs each with an advancing window of real-time data and utilizing blitting?

我目前有一個工作 python 程序,它同時為一個或多個圖表制作動畫,每個圖表都帶有實時數據的推進 window。 該程序利用 FuncAnimation 並使用軸 plot 例程重新繪制每個圖形。 希望在 animation 中每秒顯示更新,並且程序能夠在動畫一些圖形時按預期執行。 但是,matplotlib 在嘗試為多個 (>5) 個圖形設置動畫時無法在 1 秒的時間范圍內完成更新。

了解更新整個圖表需要時間,我正在嘗試對 animation 進程使用 blitting。

我試圖簡化和注釋代碼以便於理解。 我使用的數據是來自文件的二進制 stream。 stream 中的數據幀在運行下面的代碼之前被識別和標記。 在每一幀數據中都包含要繪制的電子信號值。 每個電子信號在單個二進制數據幀中都有一個或多個數據點。 需要能夠同時查看多達十幾個繪制的信號。 代碼如下並進行了注釋。

我使用 python 雙端隊列來模擬 10 秒的數據 window。 每次調用 FuncAnimation 例程時,都會將 1 秒的數據放入雙端隊列,然后處理雙端隊列以生成數據點的 xValues 和 yValues 數組。 代碼底部是每 1 秒調用一次的 FuncAnimation 例程 (DisplayAnimatedData)。 在該例程中有 2 個打印語句,我用它們來確定雙端隊列中的 xValues 和 yValues arrays 中的數據是正確的,並且每個圖形的 plot set_xlim 以推進動畫數據 window 的方式正確更改。

策划是一種工作。 但是,在使用調用 set_xlim 正確應用初始刻度值集后,x 軸刻度值不會更新。 而且,我希望 yaxis ylim 能夠自動縮放到數據。 但事實並非如此。 如何讓 xaxis 刻度值隨着數據 window 的前進而前進? 如何讓 yaxis 刻度值正確顯示? 最后,您會注意到代碼隱藏了除最后一個圖形之外的所有圖形的 x 軸。 我設計這個想法是,雖然每次通過 FuncAnimation 都會調用每個圖形的 set_xlim,但沒有時間花在重繪上,只有一個 xaxis。 我希望這會提高性能。 您的見解將不勝感激。

from matplotlib import animation
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from collections import deque
from PyQt5 import QtCore, QtGui, QtWidgets

#PlotsUI is code created via Qt Designer
class PlotsUI(object):
    def setupUi(self, PlotsUI):
        PlotsUI.setObjectName("PlotsUI")
        PlotsUI.setWindowModality(QtCore.Qt.NonModal)
        PlotsUI.resize(1041, 799)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, 
                     QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(PlotsUI.sizePolicy().hasHeightForWidth())
        PlotsUI.setSizePolicy(sizePolicy)
        self.gridLayout_2 = QtWidgets.QGridLayout(PlotsUI)
        self.gridLayout_2.setObjectName("gridLayout_2")
        self.plotLayout = QtWidgets.QVBoxLayout()
        self.plotLayout.setObjectName("plotLayout")
        self.gridLayout_2.addLayout(self.plotLayout, 0, 0, 1, 1)

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

    def retranslateUi(self, PlotsUI):
        _translate = QtCore.QCoreApplication.translate
        PlotsUI.setWindowTitle(_translate("PlotsUI", "Plots"))

#DataSeriesMgr is given a collection of values for a user selected electronic signal 
#found in the stream of binary data frames. One instance of this class is dedicated to
#manage the values of one electronic signal.
class DataSeriesMgr:
    def __init__(self, frameMultiple, timeRange, dataSeries):
        self._dataSeries = dataSeries 
        #frame multiple will typically be number of binary data frames required
        #for 1 second of data (default 100 frames)
        self._frameMultiple = frameMultiple

        #create a data deque to support the windowing of animated data
        #timeRange is the number of framesMultiples(seconds) of data stored in deque
        self._dataDeque = deque(maxlen=timeRange)
        self._timeRange = timeRange

        #index into dataSeries
        #keep track of what data has been processed
        self._xValueIndex = 0 #byte number in buffer from binary file
        self._dataSeriesSz = len(dataSeries)

        #get the first available xvalue and yvalue arrays to help facilitate
        #the calculation of x axis limits (by default 100 frames of data at a time)
        self._nextXValues, self._nextYValues = self.XYDataSetsForAnimation()
        if self._nextXValues is not None:
            self._nextXLimits = (self._nextXValues[0], self._nextXValues[0] + 
                                 self._timeRange)
        else:
            self._nextXLimits = (None, None)

    @property
    def DataDeque(self):
        return self._dataDeque

    @property
    def TimeRange(self):
        return self._timeRange

    @property
    def NextXValues(self):
        return self._nextXValues

    def GetXYValueArrays(self):
        allXValues = []
        allYValues = []
        #xyDataDeque is a collection of x values, y values tuples each 1 sec in duration
        #convert what's in the deque to arrays of x and y values
        xyDataArray = list(self._dataDeque)
        for dataSet in xyDataArray:
            for xval in dataSet[0]:
                allXValues.append(xval)
            for yval in dataSet[1]:
                allYValues.append(yval)
        #and set the data for the plot line
        #print(f'{key}-NumOfX:  {len(allXValues)}\n\r')
        return allXValues,allYValues

    def GatherFrameData(self, dataSubSet):
        consolidatedXData = []
        consolidatedYData = []
        for frameData in dataSubSet:  # each frame of data subset will have one or more data points
            for dataPointTuple in frameData:  # (unimportantValue, x, y) values
                if dataPointTuple[0] is None: #no data in this frame
                    continue
                consolidatedXData.append(dataPointTuple[1])
                consolidatedYData.append(dataPointTuple[2])
        return consolidatedXData,consolidatedYData

    def XYDataSetsForAnimation(self):
        index = self._xValueIndex #the current location in the data array for animation
        nextIndex = index + self._frameMultiple
        if nextIndex > self._dataSeriesSz: #we are beyond the number of frames
            #there are no more data points to plot for this specific signal
            return None, None
        dataSubset = self._dataSeries[index:nextIndex]
        self._xValueIndex = nextIndex #prepare index for next subset of data to be animated
        #gather data points from data subset
        xyDataSet = self.GatherFrameData(dataSubset)
        #add it to the deque
        # the deque holds a window of a number of seconds of data
        self._dataDeque.append(xyDataSet)
        #convert the deque to arrays of x and y values
        xValues, yValues = self.GetXYValueArrays()
        return xValues, yValues

    def NextXYDataSets(self):
        xValues = self._nextXValues
        yValues = self._nextYValues
        xlimits = self._nextXLimits
        self._nextXValues, self._nextYValues = self.XYDataSetsForAnimation()
        if self._nextXValues is not None:
            self._nextXLimits = (self._nextXValues[0], self._nextXValues[0] + 
                                 self._timeRange)
        else:
            self._nextXLimits = (None, None)
        return xValues, yValues, xlimits

class Graph:
    def __init__(self, title, dataSeriesMgr):
        self._title = title
        self._ax = None
        self._line2d = None
        self._xlimits = None
        self._dataSeriesMgr = dataSeriesMgr

    @property 
    def DataSeriesMgr(self):
        return self._dataSeriesMgr

    @DataSeriesMgr.setter
    def DataSeriesMgr(self, val):
        self._dataSeriesMgr = val

    @property
    def AX(self):
        return self._ax
    
    @AX.setter
    def AX(self, ax):
        self._ax = ax
        line2d, = self._ax.plot([], [], animated=True)
        self._line2d = line2d
        self._ax.set_title(self._title, fontweight='bold', size=10)

    @property
    def Line2D(self):
        return self._line2d

    @Line2D.setter
    def Line2D(self,val):
        self._line2d = val

    @property
    def Title(self):
        return self._title

    @property
    def ShowXAxis(self):
        return self._showXAxis

    @ShowXAxis.setter
    def ShowXAxis(self, val):
        self._showXAxis = val
        self._ax.xaxis.set_visible(val)

    @property
    def XLimits(self):
        return self._xlimits

    @XLimits.setter
    def XLimits(self, tup):
        self._xlimits = tup
        self._ax.set_xlim(tup[0], tup[1])

class Plotter(QtWidgets.QDialog):
    def __init__(self, parentWindow):
        super(Plotter, self).__init__()
        self._parentWindow = parentWindow

        #Matplotlib Figure
        self._figure = Figure()

        self._frameMultiple = 100 #there are 100 frames of data per second
        self._xaxisRange = 10 #make the graphs have a 10 second xaxis range
        self._animationInterval = 1000 #one second

        #PyQt5 UI
        #add the canvas to the UI
        self.ui = PlotsUI()
        self.ui.setupUi(self)
        self._canvas = FigureCanvas(self._figure)
        self.ui.plotLayout.addWidget(self._canvas)

        self.show()

    def PlaceGraph(self,aGraph,rows,cols,pos):
        ax = self._figure.add_subplot(rows,cols,pos)
        aGraph.AX = ax

    def Plot(self, dataSeriesDict):
        self._dataSeriesDict = {}
        self._graphs = {}
        #for this example, simplify the structure of the data to be plotted
        for binaryFileAlias, dataType, dataCode, dataSpec, dataTupleArray in dataSeriesDict.YieldAliasTypeCodeAndData():
            self._dataSeriesDict[dataCode] = DataSeriesMgr(self._frameMultiple, self._xaxisRange, dataTupleArray)
        self._numberOfGraphs = len(self._dataSeriesDict.keys())

        #prepare for blitting
        pos = 1
        self._lines = []
        lastKey = None
        for k,v in self._dataSeriesDict.items():
            #create a graph for each series of data
            aGraph = Graph(k,v)
            self._graphs[k] = aGraph
            #the last graph will show animated x axis
            lastKey = k
            #and place it in the layout
            self.PlaceGraph(aGraph, self._numberOfGraphs, 1, pos)
            aGraph.ShowXAxis = False
            #collect lines from graphs
            self._lines.append(aGraph.Line2D)
            pos += 1

        #show the x axis of the last graph
        lastGraph = self._graphs[lastKey]
        lastGraph.ShowXAxis = True

        #Animate
        self._animation = animation.FuncAnimation(self._figure, self.DisplayAnimatedData, 
                            None, interval=self._animationInterval, blit=True)

    def DisplayAnimatedData(self,i):
        indx = 0
        haveData = False
        for key, graph in self._graphs.items():
            allXValues, allYValues, xlimits = graph.DataSeriesMgr.NextXYDataSets() 
            if allXValues is None: #no more data
                continue
            # print(f'{key}-NumOfX:{len(allXValues)}')
            # print(f'{key}-XLimits: {xlimits[0]}, {xlimits[1]}')
            self._lines[indx].set_data(allXValues, allYValues)
            #call set_xlim on the graph.
            graph.XLimits = xlimits
            haveData = True
            indx += 1

        if not haveData: #no data ??
            self._animation.event_source.stop()
        return self._lines

通過在更新 FuncAnimation 方法 DisplayAnimatedData 中的行之前更新 xaxis 限制,window 數據的推進似乎按預期工作。 然而,當嘗試以一秒的增量對 10 個單獨的圖形進行動畫處理時,x 軸的前進和繪圖的更新花費了將近兩倍的時間(大約 2 秒),即使在只重繪一個 x 軸和 y 軸的情況下實現 blitting只繪制一次。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM