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