Reputation: 117
My program generates data continuously (like real time) and then graph it with plots. But after a while, it squeezes the graph from the left to the right of the screen.
How can I prevent it but being able to scroll too see past results?
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()
EDIT: I've found this solution (not mine) which is what I want,but without an option to scroll left or right.
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_())
Upvotes: 1
Views: 254
Reputation: 12496
You could set up a matplotlib.widgets.RangeSlider
in order to control the lower and upper limit of the dataframe to show. An example could be:
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))
This slider has a minimum and maximum values which you can use to define the start and stop index of the dataframe to be plotted:
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')
If slider values are (0, 1)
then the whole dataframe will be plotted; if slider values are (0.2, 0.6)
then only the slice of the dataframe going from 20% to 60% will be plotted and so on.
Since your dataframe is going to grow up in each iteration, also the slice (start, stop)
is going to change in each iteration.
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()
Upvotes: 1