Add arrow to Matplotlib animation when button clicked - python

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

Related

Matplotlib save animaiton as video but get empty content

I am trying to make an animation with continue rotating an image, but the output video file has empty content(Only axis left), how to fix it?
import math
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import scipy.misc
from scipy import ndimage
my_image="img.png"
out_file="myvideo.mp4"
class UpdateDist:
def __init__(self, ax):
self.ax = ax
self.img = mpimg.imread(my_image)
self.ax.imshow(self.img)
self.degree = 1
def __call__(self, i):
rotated_img = ndimage.rotate(img, self.degree*10)
self.ax.imshow(rotated_img)
self.degree += 1
return self.ax,
plt.axis(False)
plt.grid(False)
fig, ax = plt.subplots()
ud = UpdateDist(ax)
anim = FuncAnimation(fig, ud, frames=100, interval=10, blit=True)
plt.show()
ani.save(out_file, fps=30, extra_args=['-vcodec', 'libx264'])
I applied some edits to your code:
replaced self.degree with i: i increases by 1 in each iteration, no need for another counter
moved ax.grid(False) and ax.axis(False) (and added ax.clear()) within __call__ method, in order to use them in each frame
removed blit parameter from FuncAnimation
replaced .mp4 output file format with .gif
used imagemagik as writer
Let me know if this code achieves your goal or if you need any further modifications.
Complete Code
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from scipy import ndimage
import numpy as np
my_image='img.png'
out_file='myvideo.gif'
class UpdateDist:
def __init__(self, ax, rotational_speed):
self.ax = ax
self.img = plt.imread(my_image)
self.rotational_speed = rotational_speed
def __call__(self, i):
rotated_img = ndimage.rotate(self.img, self.rotational_speed*i, reshape=False)
self.ax.clear()
self.ax.grid(False)
self.ax.axis(False)
self.ax.imshow((rotated_img*255).astype(np.uint8))
return self.ax,
fig, ax = plt.subplots()
ud = UpdateDist(ax = ax, rotational_speed = 1)
anim = FuncAnimation(fig, ud, frames = 91, interval = 1)
anim.save(filename = out_file, writer = 'pillow', fps = 30)
Animation

Live update several plots in Jupyter Notebook

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

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.

How to position matplotlib's Button without moving the title and labels as well

I'm a begginner at matplotlib/Python and I am building an animated scatter plot. I want to place a button below the plot so the user can start/pause/resume the animation. This is the plot (I know it's a mess, still working on it):
As a start I tried to add a button named Play to replay the animation after its first automatic execution, but I'm having trouble understanding how positioning it below the plot works, for it is "dragging" everything with it (the title, xlabel, ylabel and so on):
Here's the code:
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import mplcursors
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider, Button, RadioButtons
df = pd.read_excel('nations2.xls')
df.head()
uniqueYears = df['Ano'].unique()
font = {'family': 'sans-serif',
'color': 'black',
'weight': 'normal',
'size': 16,
}
fig, ax = plt.subplots()
def animate(frames):
ax.clear()
data = df[df['Ano'] == uniqueYears[frames]]
ax.scatter(y = data['ExpecVida'],
x = data['PIBperCapita'],
s = data['PopX1000']/40000,
c = data['Regiao'].astype('category').cat.codes,
cmap = cm.viridis,
edgecolors = 'none',
alpha = 0.5)
ax.set_xlim([0,50000], auto=True)
ax.set_ylim([0,100], auto=True)
plt.title('Wealth and Health of Nations', fontsize=18)
plt.xlabel('GDP per Capita ($)', fontsize=14)
plt.ylabel('Life Expectancy (years)', fontsize=14)
plt.grid(color = '#A9A9A9')
plt.text(2, 0.65, uniqueYears[frames], fontdict=font)
for i, txt in enumerate(data['Pais']):
ax.annotate(txt, (data['PIBperCapita'].iat[i], data['ExpecVida'].iat[i]), fontsize = 6)
anim = FuncAnimation(fig, animate, frames=len(uniqueYears),interval = 200, repeat=False)
class Index(object):
ind = 0
def play(self, event):
FuncAnimation(fig, animate, frames=len(uniqueYears),interval = 200, repeat=False)
callback = Index()
axplay = plt.axes([0.81, 0.05, 0.1, 0.075])
bplay = Button(axplay, 'Play')
bplay.on_clicked(callback.play)

Categories

Resources