Live update several plots in Jupyter Notebook - python

I want to display history of CPU and RAM usage in Jupyter Notebook in real time. Something like this (Process Explorer in Windows):
I don't interactivity so I use matplotlib in inline mode. I run a separate background thread and try to update two different plots from there. It works well with one plot but the second one blinks and has duplicates.
Here's a minimal example (also I pickle/unpickle plot so I can initialize it only once and reuse later).
Installed packages:
ipykernel 5.1.3
ipywidgets 7.5.1
jupyter 1.0.0
jupyter-core 4.6.1
matplotlib 3.1.1
notebook 6.0.0
import pickle
import threading
import time
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
def init_history_plot():
"""
Create plot template (dump)
Returns: pickled str
"""
# plt.figure(figsize=(15, 1.2))
# ax = plt.axes()
fig, ax = plt.subplots(figsize=(15, 1.2))
# Y axis min-max
ax.set_ylim(0, 100)
# ax.get_xaxis().set_visible(False)
ax.grid(axis='y')
# right tick labels https://stackoverflow.com/a/13369977
ax.yaxis.tick_right()
# hide ticks https://stackoverflow.com/a/33707647
ax.yaxis.set_ticks_position('none')
# borders https://stackoverflow.com/a/27361819
# for i in ax.spines.values(): # 'left', 'right', 'top', 'bottom'
# ax.spines[i].set_visible(False)
# https://stackoverflow.com/questions/18603959/borderless-matplotlib-plots
ax.set_frame_on(False)
dat = pickle.dumps(fig)
plt.close()
return dat
def load_figure(dump):
"""
Load Figure from dump
Returns: (Figure, Axes)
"""
# https://github.com/ipython/ipykernel/issues/231
import ipykernel.pylab.backend_inline as back_inline
import matplotlib.backends.backend_agg as back_agg
back_inline.new_figure_manager_given_figure = back_agg.new_figure_manager_given_figure
figure = pickle.loads(dump)
# https://github.com/matplotlib/matplotlib/issues/17627/
figure._cachedRenderer = None
return figure, figure.axes[0]
template_fig = init_history_plot()
btn_start = widgets.ToggleButton(description="Start thread")
plt1_parent = widgets.Output()
plt2_parent = widgets.Output()
_interface = widgets.VBox(children=[btn_start, plt1_parent, plt2_parent])
def worker():
while btn_start.value:
with plt1_parent:
plt1_parent.clear_output(wait=True)
fig, ax = load_figure(template_fig)
dat = np.random.normal(scale=20, size=50) + 50
ax.plot(dat, color='green')
plt.show()
# THE FOLLOWING BLOCK BLINKS
with plt2_parent:
plt2_parent.clear_output(wait=True)
fig, ax = load_figure(template_fig)
dat = np.random.normal(scale=20, size=50) + 50
ax.plot(dat, color='red')
plt.show()
############################
time.sleep(1)
def start_thread(_):
if btn_start.value:
thread = threading.Thread(target=worker)
thread.start()
btn_start.observe(start_thread, 'value')
_interface

I think your code is really cool, and so I greedily stole it because I also want to have plots that live-update in a Jupyter notebook without blocking the kernel.
Anyway, the weird flickering was bothering me as well. I tried switching from threading to asyncio, simply because I am more familiar with asyncio than threading, and that actually seems to have solved the problem! Though I have no idea why.
Try:
import pickle
import asyncio
import time
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
def init_history_plot():
"""
Create plot template (dump)
Returns: pickled str
"""
# plt.figure(figsize=(15, 1.2))
# ax = plt.axes()
fig, ax = plt.subplots(figsize=(15, 1.2))
# Y axis min-max
ax.set_ylim(0, 100)
# ax.get_xaxis().set_visible(False)
ax.grid(axis='y')
# right tick labels https://stackoverflow.com/a/13369977
ax.yaxis.tick_right()
# hide ticks https://stackoverflow.com/a/33707647
ax.yaxis.set_ticks_position('none')
# borders https://stackoverflow.com/a/27361819
# for i in ax.spines.values(): # 'left', 'right', 'top', 'bottom'
# ax.spines[i].set_visible(False)
# https://stackoverflow.com/questions/18603959/borderless-matplotlib-plots
ax.set_frame_on(False)
dat = pickle.dumps(fig)
plt.close()
return dat
def load_figure(dump):
"""
Load Figure from dump
Returns: (Figure, Axes)
"""
# https://github.com/ipython/ipykernel/issues/231
import ipykernel.pylab.backend_inline as back_inline
import matplotlib.backends.backend_agg as back_agg
back_inline.new_figure_manager_given_figure = back_agg.new_figure_manager_given_figure
figure = pickle.loads(dump)
# https://github.com/matplotlib/matplotlib/issues/17627/
figure._cachedRenderer = None
return figure, figure.axes[0]
template_fig = init_history_plot()
btn_start = widgets.ToggleButton(description="Start thread")
plt1_parent = widgets.Output()
plt2_parent = widgets.Output()
_interface = widgets.VBox(children=[btn_start, plt1_parent, plt2_parent])
async def worker():
while btn_start.value:
with plt1_parent:
plt1_parent.clear_output(wait=True)
fig, ax = load_figure(template_fig)
dat = np.random.normal(scale=20, size=50) + 50
ax.plot(dat, color='green')
plt.show()
with plt2_parent:
plt2_parent.clear_output(wait=True)
fig, ax = load_figure(template_fig)
dat = np.random.normal(scale=20, size=50) + 50
ax.plot(dat, color='red')
plt.show()
await asyncio.sleep(1)
def start_thread(_):
if btn_start.value:
task = asyncio.create_task(worker())
btn_start.observe(start_thread, 'value')
_interface

Related

Python live scatterplot in WSL with background image

I have a ROS node that subscribes to a certain topic 'first' and this returns (x,y) values, which I would like to plot on a graph with a background image. I want to be able to show only the last tuple.
I am running this python script in WSL-Ubuntu-18.04 and have installed VcXsrv in Windows to visualize the graph.
This is the code I am using but I don't know how to plot only the last (x,y) output. At the moment, all the values are plotted and after a while the plotting slows terribly down because of all the points (I guess).
from Tkinter import Canvas
import Tkinter as Tk
import rospy
from std_msgs.msg import Int32MultiArray, String
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from time import sleep
from threading import Thread
import numpy as np
from PIL import ImageTk, Image
def callback(data):
received = data.data.split(',')
x = int(float(received[0]))
y = int(float(received[1]))
plt.scatter(x,y)
def listener():
rospy.Subscriber('first', String, callback)
rospy.spin()
def animate(i):
pass
def visual():
root = Tk.Tk()
label = Tk.Label(root, text="Realtime plot ")
root.geometry("750x720")
img = plt.imread("back.jpg")
fig, ax = plt.subplots()
#mng = plt.get_current_fig_manager()
#mng.full_screen_toggle()
ax.imshow(img,extent=[0, 17947, -200, 5330], aspect='equal')
fig.canvas.draw()
plt.show()
plotcanvas = FigureCanvasTkAgg(fig, root)
plotcanvas.get_tk_widget().grid(column=0, row=0)
ani = FuncAnimation(fig, animate, interval=100, blit=False)
Tk.mainloop()
t1 = Thread(target=visual)
t1.start()
listener()
t1.join()
I'm also struggling with scaling the image (hence the whole graph) to full screen. I've tried something but only the frame gets bigger

Matplotlib FuncAnimation Created twice - duplicate when embbeded in tkinter

I have a troubleing bug that i just could not understands it's origin. Several days of attempts and still no luck.
I'm trying to create a line cursor that correspond to played audio with FuncAnimation and for some reason, the animation is created twice ONLY when the callback (line_select_callback) that activates the function is triggered from RectangleSelector widget after drawing wiith the mouse. when I use a standard TK button to activate the SAME function (line_select_callback), it operates well.
some debugging code with reevant prints is present.
I've created minimal working example.
My guess is it has something to do with the figure that is not attached to the tk window, and is silently activated in addition to the embedded figure, I'm not really sure.
Any help will be very much appreciated, Thanks! :)
import os
import threading
import tkinter as tk
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg)
from matplotlib.widgets import RectangleSelector
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from matplotlib import animation
class LineAnimation:
def __init__(self, fig, ax):
print(' enter LineAnimation ctor')
# Parameters
self.ax = ax
self.fig = fig
self.xdata, self.ydata = [], []
self.ln, = plt.plot([], [], 'ro')
# Print figures list
figures = [manager.canvas.figure
for manager in matplotlib._pylab_helpers.Gcf.get_all_fig_managers()]
print('figures BEFORE animation: ', figures)
self.animation = animation.FuncAnimation(fig=self.fig,
func=self.update,
init_func=self.init,
frames=np.linspace(0, 2 * np.pi, 128),
interval=25,
blit=True, repeat=False,
cache_frame_data=False)
self.fig.canvas.draw()
# Print figures list
figures = [manager.canvas.figure
for manager in matplotlib._pylab_helpers.Gcf.get_all_fig_managers()]
print('figures AFTER animation: ', figures, '\n')
def init(self):
# Prints for debugging
print('\nenter init animate')
print('Thread id: ', threading.get_ident())
print('Process id: ', os.getpid(), '\n')
# Init
self.ax.set_xlim(0, 2*np.pi)
self.ax.set_ylim(-1, 1)
return self.ln,
def update(self, frame):
self.xdata.append(frame)
self.ydata.append(np.sin(frame))
self.ln.set_data(self.xdata, self.ydata)
return self.ln,
class Example:
def __init__(self):
# init window
self.root = tk.Tk(className=' Species segmentation')
self.fig, self.ax = plt.subplots()
# init sine audio file
self.fs = 44100
self.dur = 2
self.freq = 440
self.x = np.sin(2*np.pi*np.arange(self.fs*self.dur)*self.freq/self.fs)
# plt.ion()
# Embedd in tk
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().grid()
# Plot something
self.N = 100000
self.xp = np.linspace(0, 10, self.N)
self.ax.plot(self.xp, np.sin(2*np.pi*self.xp))
self.ax.set_title(
"Plot for demonstration purpuse")
# init Rectangle Selector
self.RS = RectangleSelector(self.ax, self.line_select_callback,
drawtype='box', useblit=True,
button=[1, 3], # avoid using middle button
minspanx=5, minspany=5,
spancoords='pixels', interactive=True,
rectprops={'facecolor': 'yellow', 'edgecolor': 'black', 'alpha': 0.15, 'fill': True})
self.canvas.draw()
# plt.show()
tk.mainloop()
def line_select_callback(self, eclick, erelease):
print('enter line_select_callback')
self.anim = LineAnimation(
self.fig,
self.ax)
self.fig.canvas.draw()
# plt.show()
Example()
I managed to isolate the cause for this issue: The presence of the
rectangle selector (which uses blitting) and the use of animation (which also uses blitting) on the same axes.
I've managed to create the animation properly, but only when I disabled the rectangle selector
self.RS.set_active(False)
self.RS.update()
self.canvas.flush_events()
and removed his artists (i needed to do that manually in my code) using:
for a in self.RS.artists:
a.set_visible(False)
after that, The animation worked properly.

Add arrow to Matplotlib animation when button clicked

I am using matplotlib.animation right now I got some good result.
I was looking to add some stuff to the chart but couldn't find how.
adding "buy"/ "sell" arrow when button clicked let say '1' - for buy , '2' for sell.
simple label/legend that will show current live values (open,high,low,close , volume)
This is my code below:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import pandas as pd
import mplfinance as mpf
import matplotlib.animation as animation
idf = pd.read_csv('aapl.csv',index_col=0,parse_dates=True)
#df = idf.loc['2011-07-01':'2012-06-30',:]
pkwargs=dict(type='candle',mav=(20,200))
fig, axes = mpf.plot(idf.iloc[0:20],returnfig=True,volume=True,
figsize=(11,8),panel_ratios=(3,1),
title='\n\nS&P 500 ETF',**pkwargs,style='starsandstripes')
ax1 = axes[0]
ax2 = axes[2]
ax1.spines['top'].set_visible(True)
ax1.grid(which='major', alpha=0.1)
ax2.grid(which='major', alpha=0.1)
#fig = plt.figure()
def run_animation():
ani_running = True
def onClick(event):
nonlocal ani_running
if ani_running:
ani.event_source.stop()
ani_running = False
else:
ani.event_source.start()
ani_running = True
def animate(ival):
if (20+ival) > len(idf):
print('no more data to plot')
ani.event_source.interval *= 3
if ani.event_source.interval > 12000:
exit()
return
#print("here")
#mpf.plot(idf,addplot=apd)
data = idf.iloc[100+ival:(250+ival)]
print(idf.iloc[ival+250])
ax1.clear()
ax2.clear()
mpf.plot(data,ax=ax1,volume=ax2,**pkwargs,style='yahoo')
fig.canvas.mpl_connect('button_press_event', onClick)
ani = animation.FuncAnimation(fig, animate, interval=240)
run_animation()
mpf.show()
I am not clear on what exactly you want when you say you want to add "buy"/ "sell" arrow, but I can answer your question on creating a legend.
I think the easiest thing is to build a custom legend by specifying the colors and labels. The labels will change as your animation proceeds, so you will need to update the legend in your animate function after you clear both axes and plot the latest data - I assume that the row of the DataFrame that you are printing is the one you want to display in the legend:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import pandas as pd
import mplfinance as mpf
import matplotlib.patches as mpatches
idf = pd.read_csv('https://raw.githubusercontent.com/matplotlib/sample_data/master/aapl.csv',index_col=0,parse_dates=True)
#df = idf.loc['2011-07-01':'2012-06-30',:]
pkwargs=dict(type='candle',mav=(20,200))
fig, axes = mpf.plot(idf.iloc[0:20],returnfig=True,volume=True,
figsize=(11,8),panel_ratios=(3,1),
title='\n\nS&P 500 ETF',**pkwargs,style='starsandstripes')
ax1 = axes[0]
ax2 = axes[2]
## define the colors and labels
colors = ["red","green","red","green"]
labels = ["Open", "High", "Low", "Close"]
#fig = plt.figure()
def run_animation():
ani_running = True
def onClick(event):
nonlocal ani_running
if ani_running:
ani.event_source.stop()
ani_running = False
else:
ani.event_source.start()
ani_running = True
def animate(ival):
if (20+ival) > len(idf):
print('no more data to plot')
ani.event_source.interval *= 3
if ani.event_source.interval > 12000:
exit()
return
#print("here")
#mpf.plot(idf,addplot=apd)
data = idf.iloc[100+ival:(250+ival)]
print(idf.iloc[ival+250])
## what to display in legend
values = idf.iloc[ival+250][labels].to_list()
legend_labels = [f"{l}: {str(v)}" for l,v in zip(labels,values)]
handles = [mpatches.Patch(color=c, label=ll) for c,ll in zip(colors, legend_labels)]
ax1.clear()
ax2.clear()
mpf.plot(data,ax=ax1,volume=ax2,**pkwargs,style='yahoo')
## add legend after plotting
ax1.legend(handles=handles, loc=2)
fig.canvas.mpl_connect('button_press_event', onClick)
ani = animation.FuncAnimation(fig, animate, interval=240)
run_animation()
mpf.show()

How to align matplotlib chart in its window?

I'm trying to align the chart to the top and left side of the window, eleminating all the whitespace there. Also the labels on the x axis are cut-off for some reason, even though I'm using tight_layout(). On top of this I have trouble removing the scientific notation from the y axis. (I want to display the original str or float held in the variable, If I use plt.yaxis.set_major_formatter(ScalarFormatter(useOffset=False)) I get an attribute arror: 'matplotlib.pyplot' has no attribute 'yaxis'.
This is what it looks like:
from binance.client import Client
import time, os, csv, datetime
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib as mpl
from matplotlib.ticker import ScalarFormatter
client = Client(apikey, apisecret)
mpl.rcParams['toolbar'] = 'None'
fig = plt.figure(figsize=(4,3))
plt.style.use('ggplot')
plt.rcParams['ytick.right'] = plt.rcParams['ytick.labelright'] = True
plt.rcParams['ytick.left'] = plt.rcParams['ytick.labelleft'] = False
x_vars = []
y_vars = []
def animate(i):
global x_vars
global y_vars
if len(x_vars) > 30:
x_vars = x_vars[-30:]
y_vars = y_vars[-30:]
else:
pass
current_time = client.get_server_time()
current_price = client.get_symbol_ticker(symbol="XRPBTC")
trstime = current_time["serverTime"] / 1000
time = datetime.datetime.fromtimestamp(int(trstime)).strftime('%M:%S')
x_vars.append(str(time))
y_vars.append(float(current_price["price"]))
plt.cla()
plt.plot(x_vars, y_vars)
plt.xticks(rotation = 45)
ani = animation.FuncAnimation(fig, animate, interval=500)
plt.tight_layout()
plt.show()
Set the y-axis to the right.:
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
Don't display offset:
plt.setp(ax.get_yaxis().get_offset_text(), visible=False)
Dealing with missing ticks (disabling tight display):
# plt.tight_layout()
To confirm this, I created an animation with the binance API and checked it. Since the essence of this question is not the animation, I think you will get faster answers from more people if you focus your question only on the graph part.
full code:
import datetime
import json
import requests
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.animation import FuncAnimation
url = "https://api.binance.com/api/v3/ticker/price?symbol=XRPBTC"
mpl.rcParams['toolbar'] = 'None'
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111)
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
plt.setp(ax.get_yaxis().get_offset_text(), visible=False)
plt.style.use('ggplot')
x_vars = []
y_vars = []
def animate(i):
global x_vars
global y_vars
if len(x_vars) > 30:
x_vars = x_vars[-30:]
y_vars = y_vars[-30:]
else:
pass
current_time = 0
current_price = 0
r = requests.get(url)
response_dict = r.json()
current_price = float(response_dict['price'])
dt_now = datetime.datetime.now()
ttime = '{:02}:{:02}'.format(dt_now.minute, dt_now.second)
x_vars.append(ttime)
y_vars.append(current_price)
ax.cla()
ax.plot(x_vars, y_vars)
ax.set_xticklabels(x_vars, rotation=45)
ani = FuncAnimation(fig, animate, frames=30, interval=500, repeat=False)
# plt.tight_layout()
plt.show()

FuncAnimation update function not called

I'm not quite getting how to create a class for animating data. Here is the gist:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
x = np.arange(100).reshape((100, 1))
y = np.random.randn(100, 1)
xy = np.hstack((x, y))
class PlotData:
def __init__(self):
fig, ax = plt.subplots()
fig.set_size_inches((11, 9))
self.fig = fig
self.ax = ax
self.ln0, = ax.plot([], [])
def init(self):
self.ln0.set_data([], [])
return(self.ln0, )
def update(self, frame_no):
data = xy[0:frame_no + 1]
self.ln0.set_data(data[:, 0], data[:, 1])
return(self.ln0, )
if __name__ == '__main__':
my_plot = PlotData()
anim = animation.FuncAnimation(my_plot.fig, my_plot.update,
init_func=my_plot.init, blit=True,
frames=99, interval=50)
plt.show()
This only produces the init method output but not the update, so ends up a blank plot with no animation. What is going on?
For me your code works perfectly fine. The only problem is that most of the data are outside of the plotting limits. If you adjust your plot limits like this:
class PlotData:
def __init__(self):
fig, ax = plt.subplots(figsize = (11,9))
self.fig = fig
self.ax = ax
self.ax.set_xlim([0,100])
self.ax.set_ylim([-3,3])
self.ln0, = ax.plot([], [])
The line is animated just fine. If you want that the x- and y-limits are adjusted automatically, see this question on how to do it. However, if I recall correctly, this will only work properly with blit=False.

Categories

Resources