Как предотвратить сжатие графика в реальном времени, Python3-Matplotlib

#python #pandas #dataframe #matplotlib #animation

Вопрос:

Моя программа непрерывно генерирует данные (например, в реальном времени), а затем отображает их в виде графиков. Но через некоторое время он сжимает график слева направо от экрана.

Как я могу предотвратить это, но будучи в состоянии прокручивать слишком, чтобы видеть прошлые результаты?

 
import random
import pandas as pd
import numpy as np

from datetime import datetime,timedelta
from dateutil.relativedelta import relativedelta
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation as funca


#plt.style.use('fivethirtyeight')
plt.rcParams.update({'font.size':9})

limit = 10000
datetime_format = '%Y-%m-%d %I:%M-%p'
date_string = '2021-01-01 12:10-am'

#here I'm creating a dataframe, to simulate a company income over time

df = pd.DataFrame({"DATE":pd.to_datetime([],format=datetime_format),"INCOME":np.array([],dtype=np.int64)})
start_date = datetime.strptime(date_string,datetime_format)

#This will add  1 month for each frame of the graph animation.
incrementing = relativedelta(years= 0,months= 1,days= 0,hours= 0,minutes= 0)

#This function is responsible for the animation
def animating_graph(i):

    #dataframe related code
    global df
    global limit
    global start_date
    global incrementing
    datetime_quantity = len(df['DATE'])
    INDEX = np.arange(datetime_quantity)

    #Graph related code

    plt.cla()
    plt.plot(INDEX,df['INCOME'],linestyle='-',linewidth=2,marker='o',label='Income')
#   plt.xticks(ticks=INDEX,labels=df['DATE'].dt.date)

   #it will mark an area<=limit, which represents lost of money
    plt.fill_between(INDEX,df['INCOME'],limit,where=(df['INCOME']<=limit),interpolate=True,alpha=0.5,label='Loss of Income')
    plt.title('INCOME OVER TIME')
    plt.xlabel('Income over each month')
    plt.ylabel('Income(USD)')

    #here I am appending to df each month that passed, and the income(random number)
    df = df.append({'DATE':start_date,'INCOME':random.randint(5000,30000)},ignore_index=True,sort=False)
    start_date = start_date   incrementing


animating = funca(plt.gcf(),animating_graph,interval=1000)
plt.tight_layout()
plt.grid(False)
plt.show()
 

РЕДАКТИРОВАТЬ: Я нашел это решение (не мое), которое мне нужно, но без возможности прокрутки влево или вправо.

 import sys
import os
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import functools
import numpy as np
import random as rd
import matplotlib
matplotlib.use("Qt5Agg")
from matplotlib.figure import Figure
from matplotlib.animation import TimedAnimation
from matplotlib.lines import Line2D
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import time
import threading

class CustomMainWindow(QMainWindow):
    def __init__(self):
        super(CustomMainWindow, self).__init__()
        # Define the geometry of the main window
        self.setGeometry(300, 300, 800, 400)
        self.setWindowTitle("my first window")
        # Create FRAME_A
        self.FRAME_A = QFrame(self)
        self.FRAME_A.setStyleSheet("QWidget { background-color: %s }" % QColor(210,210,235,255).name())
        self.LAYOUT_A = QGridLayout()
        self.FRAME_A.setLayout(self.LAYOUT_A)
        self.setCentralWidget(self.FRAME_A)
        # Place the zoom button
        self.zoomBtn = QPushButton(text = 'zoom')
        self.zoomBtn.setFixedSize(100, 50)
        self.zoomBtn.clicked.connect(self.zoomBtnAction)
        self.LAYOUT_A.addWidget(self.zoomBtn, *(0,0))
        # Place the matplotlib figure
        self.myFig = CustomFigCanvas()
        self.LAYOUT_A.addWidget(self.myFig, *(0,1))
        # Add the callbackfunc to ..
        myDataLoop = threading.Thread(name = 'myDataLoop', target = dataSendLoop, daemon = True, args = (self.addData_callbackFunc,))
        myDataLoop.start()
        self.show()
        return

    def zoomBtnAction(self):
        print("zoom in")
        self.myFig.zoomIn(5)
        return

    def addData_callbackFunc(self, value):
        # print("Add data: "   str(value))
        self.myFig.addData(value)
        return

''' End Class '''


class CustomFigCanvas(FigureCanvas, TimedAnimation):
    def __init__(self):
        self.addedData = []
        print(matplotlib.__version__)
        # The data
        self.xlim = 200
        self.n = np.linspace(0, self.xlim - 1, self.xlim)
        a = []
        b = []
        a.append(2.0)
        a.append(4.0)
        a.append(2.0)
        b.append(4.0)
        b.append(3.0)
        b.append(4.0)
        self.y = (self.n * 0.0)   50
        # The window
        self.fig = Figure(figsize=(5,5), dpi=100)
        self.ax1 = self.fig.add_subplot(111)
        # self.ax1 settings
        self.ax1.set_xlabel('time')
        self.ax1.set_ylabel('raw data')
        self.line1 = Line2D([], [], color='blue')
        self.line1_tail = Line2D([], [], color='red', linewidth=2)
        self.line1_head = Line2D([], [], color='red', marker='o', markeredgecolor='r')
        self.ax1.add_line(self.line1)
        self.ax1.add_line(self.line1_tail)
        self.ax1.add_line(self.line1_head)
        self.ax1.set_xlim(0, self.xlim - 1)
        self.ax1.set_ylim(0, 100)
        FigureCanvas.__init__(self, self.fig)
        TimedAnimation.__init__(self, self.fig, interval = 50, blit = True)
        return

    def new_frame_seq(self):
        return iter(range(self.n.size))

    def _init_draw(self):
        lines = [self.line1, self.line1_tail, self.line1_head]
        for l in lines:
            l.set_data([], [])
        return

    def addData(self, value):
        self.addedData.append(value)
        return

    def zoomIn(self, value):
        bottom = self.ax1.get_ylim()[0]
        top = self.ax1.get_ylim()[1]
        bottom  = value
        top -= value
        self.ax1.set_ylim(bottom,top)
        self.draw()
        return

    def _step(self, *args):
        # Extends the _step() method for the TimedAnimation class.
        try:
            TimedAnimation._step(self, *args)
        except Exception as e:
            self.abc  = 1
            print(str(self.abc))
            TimedAnimation._stop(self)
            pass
        return

    def _draw_frame(self, framedata):
        margin = 2
        while(len(self.addedData) > 0):
            self.y = np.roll(self.y, -1)
            self.y[-1] = self.addedData[0]
            del(self.addedData[0])

        self.line1.set_data(self.n[ 0 : self.n.size - margin ], self.y[ 0 : self.n.size - margin ])
        self.line1_tail.set_data(np.append(self.n[-10:-1 - margin], self.n[-1 - margin]), np.append(self.y[-10:-1 - margin], self.y[-1 - margin]))
        self.line1_head.set_data(self.n[-1 - margin], self.y[-1 - margin])
        self._drawn_artists = [self.line1, self.line1_tail, self.line1_head]
        return

''' End Class '''


# You need to setup a signal slot mechanism, to
# send data to your GUI in a thread-safe way.
# Believe me, if you don't do this right, things
# go very very wrong..
class Communicate(QObject):
    data_signal = pyqtSignal(float)

''' End Class '''



def dataSendLoop(addData_callbackFunc):
    # Setup the signal-slot mechanism.
    mySrc = Communicate()
    mySrc.data_signal.connect(addData_callbackFunc)

    # Simulate some data
    n = np.linspace(0, 499, 500)
    y = 50   25*(np.sin(n / 8.3))   10*(np.sin(n / 7.5)) - 5*(np.sin(n / 1.5))
    i = 0

    while(True):
        if(i > 499):
            i = 0
        time.sleep(0.1)
        mySrc.data_signal.emit(y[i]) # <- Here you emit a signal!
        i  = 1
    ###
###

if __name__== '__main__':
    app = QApplication(sys.argv)
    QApplication.setStyle(QStyleFactory.create('Plastique'))
    myGUI = CustomMainWindow()
    sys.exit(app.exec_())
 

Ответ №1:

Вы можете настроить a matplotlib.widgets.RangeSlider для управления нижним и верхним пределом отображаемого кадра данных. Примером может быть:

 fig, ax = plt.subplots()
plt.subplots_adjust(left = 0.1, top = 0.75, bottom = 0.1, right = 0.9)
ax_slider = plt.axes([0.1, 0.85, 0.8, 0.1])
slider = RangeSlider(ax = ax_slider, label = 'Period', valmin = 0, valmax = 1, valinit = (0, 1))
 

Этот ползунок имеет минимальное и максимальное значения, которые вы можете использовать для определения начального и конечного индекса строящегося кадра данных:

 start, stop = slider.val
INDEX = np.arange(int(start*datetime_quantity), int(stop*datetime_quantity))
ax.plot(INDEX,df.loc[INDEX, 'INCOME'],linestyle='',linewidth=2,marker='o',label='Income')
ax.fill_between(INDEX,df.loc[INDEX, 'INCOME'],limit,where=(df.loc[INDEX, 'INCOME']<=limit),interpolate=True,alpha=0.5,label='Loss of Income')
 

Если значения ползунка (0, 1) равны, то будет отображен весь фрейм данных; если значения ползунка (0.2, 0.6) равны, то будет отображен только фрагмент фрейма данных, составляющий от 20% до 60%, и так далее.
Поскольку ваш фрейм данных будет расти на каждой итерации, срез также (start, stop) будет меняться на каждой итерации.

Полный Код

 import random
import pandas as pd
import numpy as np

from datetime import datetime,timedelta
from dateutil.relativedelta import relativedelta
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation as funca
from matplotlib.widgets import RangeSlider


#plt.style.use('fivethirtyeight')
plt.rcParams.update({'font.size':9})

limit = 10000
datetime_format = '%Y-%m-%d %I:%M-%p'
date_string = '2021-01-01 12:10-am'

#here I'm creating a dataframe, to simulate a company income over time
df = pd.DataFrame({"DATE":pd.to_datetime([],format=datetime_format),"INCOME":np.array([],dtype=np.int64)})
start_date = datetime.strptime(date_string,datetime_format)

#This will add  1 month for each frame of the graph animation.
incrementing = relativedelta(years= 0,months= 1,days= 0,hours= 0,minutes= 0)

#This function is responsible for the animation
def animating_graph(i):

    #dataframe related code
    global df
    global limit
    global start_date
    global incrementing
    datetime_quantity = len(df['DATE'])

    start, stop = slider.val
    INDEX = np.arange(int(start*datetime_quantity), int(stop*datetime_quantity))

    #Graph related code
    ax.cla()
    ax.plot(INDEX,df.loc[INDEX, 'INCOME'],linestyle='-',linewidth=2,marker='o',label='Income')

   #it will mark an area<=limit, which represents lost of money
    ax.fill_between(INDEX,df.loc[INDEX, 'INCOME'],limit,where=(df.loc[INDEX, 'INCOME']<=limit),interpolate=True,alpha=0.5,label='Loss of Income')
    ax.set_title('INCOME OVER TIME')
    ax.set_xlabel('Income over each month')
    ax.set_ylabel('Income(USD)')

    #here I am appending to df each month that passed, and the income(random number)
    df = df.append({'DATE':start_date,'INCOME':random.randint(5000,30000)},ignore_index=True,sort=False)
    start_date = start_date   incrementing

fig, ax = plt.subplots()
plt.subplots_adjust(left = 0.1, top = 0.75, bottom = 0.1, right = 0.9)
ax_slider = plt.axes([0.1, 0.85, 0.8, 0.1])
slider = RangeSlider(ax = ax_slider, label = 'Period', valmin = 0, valmax = 1, valinit = (0, 1))

animating = funca(fig,animating_graph,interval=1000)
ax.grid(False)

plt.show()
 

введите описание изображения здесь

введите описание изображения здесь

Комментарии:

1. Здравствуйте, извините, что все это время я тестировал ваше решение и тоже искал что-то новое, но в конце концов я бы воздал вам должное. Поэтому я нашел своего рода «решение», показывающее эффект, который я хотел создать, потому что эффект вышеупомянутого решения был не совсем тем, что я хотел. В этом новом испытании это дает эффект, который я искал, но я не могу прокрутить страницу, чтобы увидеть прошлые результаты. Я буду работать над этим, но я бы хотел, чтобы вы тоже посмотрели.