g_odim_3
g_odim_3

Reputation: 117

How to prevent real time graph from squeezing, Python3-Matplotlib

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

Answers (1)

Zephyr
Zephyr

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.

Complete Code

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()

enter image description here

enter image description here

Upvotes: 1

Related Questions