I have an algorithm which consists of two distinct parts which I want to visualize one after another (while possibly keeping the final state of animation 1 on the screen when animation 2 starts).
I can visualize both parts individually by calling animation.FuncAnimation and plt.show(). Since both parts have set number of frames and their very own behaviour, I would like to keep their implementations apart in two different classes and then do a wrapper around them which plays them in sequence.
However, is it possible to have two (or more) animation objects to be displayed one after another in the same figure?
Many thanks,
Matt
Thanks to the hint of ImportanceOfBeingErnest, I came up with a solution which updates only certain elements of my animator state depending on the current time step. Here is a small example illustrating this approach:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from math import sin, radians
class AnimationHandler:
def __init__(self, ax):
self.ax = ax
self.lines = [self.ax.plot([], []), self.ax.plot([], [])]
self.colors = ['cyan', 'red']
self.n_steps = [360, 360]
self.step = 0
def init_animation(self):
for anim_idx in [0, 1]:
self.lines[anim_idx], = self.ax.plot([0, 10], [0, 0], c=self.colors[anim_idx], linewidth=2)
self.ax.set_ylim([-2, 2])
self.ax.axis('off')
return tuple(self.lines)
def update_slope(self, step, anim_idx):
self.lines[anim_idx].set_data([0, 10], [0, sin(radians(step))])
def animate(self, step):
# animation 1
if 0 < step < self.n_steps[0]:
self.update_slope(step, anim_idx=0)
# animation 2
if self.n_steps[0] < step < sum(self.n_steps):
self.update_slope(step - self.n_steps[0], anim_idx=1)
return tuple(self.lines)
if __name__ == '__main__':
fig, axes = plt.subplots()
animator = AnimationHandler(ax=axes)
my_animation = animation.FuncAnimation(fig,
animator.animate,
frames=sum(animator.n_steps),
interval=10,
blit=True,
init_func=animator.init_animation,
repeat=False)
Writer = animation.writers['ffmpeg']
writer = Writer(fps=24, metadata=dict(artist='Me'), bitrate=1800)
my_animation.save('./anim_test.mp4', writer=writer)
plt.show()
I used this approach to visualize/debug an algorithm which has different elements with varying runtimes. Approach is the same: You know the number of steps of each subsequence and adjust the state accordingly.
Related
I would like to define a function taking in input ax and plotting data on it.
My quick dummy example, working on spyder 4 (backend: PyQT5):
from matplotlib import pyplot as plt
def func(L, ax):
L_modified = [x+k for k, x in enumerate(L)]
ax.plot(L_modified)
return L_modified
f, ax = plt.subplots(1, 1)
L = [1, 2, 3, 4, 5]
data = func(L, ax)
And that works fine, data is a list containing the modified input; and a plot appears correctly. On different post/explanation, I see that the function drawing the plot usually returns ax, which is not my case. Is it necessary to return ax to have this type of function work for every IDE? Is it working only because I'm using spyder?
As my question syntax seems a little too broad for some, here is the big all-in-one: What is the best way to create a custom plotting function?
EDIT: Example where I create a figure if needed
def butter_lowpass_filter(self, cutoff, order=5, plot=False, ax=None, **plt_kwargs):
nyq = 0.5 * self.fs
normal_cutoff = cutoff / nyq
b, a = butter(order, normal_cutoff, btype='low', analog=False)
data_filtered = lfilter(b, a, self.data)
if plot is True:
if ax is None:
f, ax = plt.subplots(2, 1, sharex=True, sharey=True, figsize=(15, 10))
f.subplots_adjust(wspace=0, hspace=0.15)
ax[0].plot(self.data, color='blue')
ax[0].plot(data_filtered, color='teal')
ax[0].set_title("Signal + Fitlered signal")
ax[1].plot(data_filtered, color='teal')
ax[1].set_title("Filtered signal with a Butterworth filter")
else:
ax.plot(data_filtered, **plt_kwargs)
ax.set_title("Filtered signal with a Butterworth filter")
return data_filtered
I'm not sure that I understand what you are looking for. I suppose you want a function that modifies a figure. There are two versions of the code, the second being slightly commented and an improvement of the first.
I don't use spyder (only the terminal) and I have no graphical result for your code.
In general, I think it's a good practice, in case you use matplotlib, to completely define the graphical window (the figure) and to not leave python doing the job behind the scene. You'll need the definition if the program gets complicated.
name of the figure is important
fig.add_subplot in case you use more than one coordinate systems
try to fix the axes' boundaries according to your graphical objects
I wrote down a minimal example starting from yours. It is based on your function which is called more than one time. So the image evolves. Now, what you get is some rudimentary animation. But, even so, there are many flaws as you can easily see:
the axes are not stable
the graphs are piling up.
The function should be improved; since I don't know if I go in the right direction, I stop here for the moment.
Why does the function return that list anyway?
First version
import matplotlib as mpl
from matplotlib import pyplot as plt
import time
def func(L, fig):
L_modified = [x+k for k, x in enumerate(L)]
ax = fig.axes[0]
ax.plot(L_modified)
return L_modified
plt.ion()
delta_t = .5
f = plt.figure(figsize=(8, 6))
ax = f.add_subplot(1,1,1, aspect='equal')
L = [1, 2, 3, 4, 5]
data = func(L, f)
plt.pause(delta_t)
for j in range(len(L)):
L[j] = L[j]-3*(j+1)
data = func(L, f)
plt.pause(delta_t)
f.show()
plt.show(block=True)
Second, improved, and commented version of the code.
I'm using also the output of the function, let's say.
import matplotlib as mpl
from matplotlib import pyplot as plt
import time
# I modified the function such that the graphical changes that it induces
# are all realised when the function is called.
def func(L, fig):
L_modified = [x+k for k, x in enumerate(L)]
ax = fig.axes[0]
curves = ax.get_lines() # list of curves in axes
if len(curves)==0:
ax.plot(L_modified)
else:
curves[-1].set_ydata(L_modified) # modify the y data of the curve
plt.show()
plt.pause(delta_t) # pause for delta_t seconds
return L_modified
plt.ion() # turn the interactive mode on.
f = plt.figure(figsize=(8, 6))
ax = f.add_subplot(1,1,1, aspect='equal')
ax.set(xlim=(-1, 5), ylim=(-5.25, 9.25)) # fix the drawing region
delta_t = .5
L = [1, 2, 3, 4, 5]
data = func(L, f)
plt.pause(delta_t) # pause for delta_t seconds
for j in range(len(L)):
L[j] = -data[j]
data = func(L, f)
plt.show(block=True)
'''
Display all open figures and change block to True; it was on False due
to plt.ion().
'''
I'm trying to make an animated 3-D scatter plot with the ability to plot a dynamic number of classes as different colors. This is one of the attempts. I've included the whole code in case it is helpful, and marked the trouble spot with a row of stars:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.animation as animation
from random import uniform
x_arr,y_arr,depth_arr,time_arr,ml_arr,cluster_arr = np.loadtxt(data, unpack=5, usecols=(0, 1, 2, 5, 6))
class Point:
def __init__(self,x,y,depth,time,cluster):
self.x=x
self.y=y
self.depth=depth
self.time=time
self.cluster=cluster
points = []
for i in range(0,len(x_arr)):
points.append(Point(x_arr[i],y_arr[i],depth_arr[i],time_arr[i],cluster_arr[i]))
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.set_xlim(min(x_arr), max(x_arr))
ax.set_ylim(min(y_arr), max(y_arr))
ax.set_zlim(min(depth_arr), max(depth_arr))
colors_1 = plt.cm.jet(np.linspace(0,max(cluster_arr),max(cluster_arr)+1))
colors = colors_1.reshape(-1,4)
def plot_points(time):
x = []
y = []
z = []
clust = []
points_cp = list(np.copy(points))
for i in range(0,(int(max(cluster_arr))+1)):
for event in points_cp:
if event.cluster == i:
if event.time < time:
points_cp.remove(event)
elif event.time <= time + 86400:
x.append(event.x)
y.append(event.y)
z.append(event.depth)
clust.append(event.cluster)
points_cp.remove(event)
# **************************************************************
color_ind = 0
first_ind = 0
last_ind = 0
for i in range(0,len(x)):
if clust[i] != color_ind:
last_ind = i
for i in range(0,len(x)):
ax.scatter(x[first_ind:last_ind],y[first_ind:last_ind],z[first_ind:last_ind],c=colors[int(color_ind)])
color_ind = clust[i]
first_ind = i
time = np.linspace(min(time_arr),max(time_arr),100)
ani = animation.FuncAnimation(fig,plot_points,time)
plt.show()
This gives me a plot with the correct colors, but once a point is plotted, it remains throughout the entire animation.
I have also tried set_x, set_color, etc., but this doesn't work with a loop (it is updated with each iteration, so that only the last class is actually plotted), and I need to use a for loop to accommodate a variable number of classes. I've tried using a colormap with a fixed extent, but have been unsuccessful, as colormapping doesn't work with the plot function, and I haven't been able to get the rest of the code to work with a scatter function.
Thanks in advance for your help, and my apologies if the code is a little wonky. I'm pretty new to this.
I am acquiring data from an external device and I am plotting it in real-time using matplotlib.animation.FuncAnimation. For this measurement, it is important to keep the bottom y-axis limit at 0, but keeping the upper limit free. The device itself returns two sets of data for this measurement so I am animating both sets at the same time, hence the subplots.
Searching online suggests using axes.set_ylim(bottom=0), both in this question and this one. However their solutions do not work for me.
The autoscalling that's already part of the code is using axes.relim() and axes.autoscale_view(True, True, True), from the answer to another question which I have since forgotten. Messing with these lines of code seems to fix the viewing window but it no longer scales with the data. The data could then animate itself 'off-screen'.
I've recreated below the essence of what the acquisition (ideally) looks like since it's easier then using multiple files.
I am assuming that the problem lies in animate_corr(i) with the scaling. The rest is of the code is simply getting and plotting the data.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
#used to generate test data
import random
pi = np.pi
factor = 1
random.seed(123)
#procedural data generation for left plot
def get_data(data):
global mu
global b
global index
global factor
b=b*0.99
factor=factor*1.01
new_data = [factor*(((1-((1/(2*b))*np.exp(-1*(abs(i-mu))/b)))*random.random())+10) for i in index]
return new_data
#procedural data generation for right plot
def get_data_norm(data):
global mu
global b
global index
new_data = [((1-((1/(2*b))*np.exp(-1*(abs(i-mu))/b)))+10) for i in index]
return new_data
#animation function, assuming problem is here
def animate_corr(i):
global dat
global dat_norm
dat = get_data(dat)
dat_norm = get_data_norm(dat_norm)
#these two not working as expected
axs_corr[0].set_ylim((0, None), auto=True)
axs_corr[1].set_ylim(bottom=0, top=None, auto=True)
line_corr.set_ydata(dat)
line_corr_norm.set_ydata(dat_norm)
#rescales axes automatically
axs_corr[0].relim()
axs_corr[0].autoscale_view(True,True,True)
axs_corr[1].relim()
axs_corr[1].autoscale_view(True,True,True)
return line_corr, line_corr_norm,
#plots definitions
fig_corr, axs_corr = plt.subplots(1,2, sharex=True, figsize=(10,5))
fig_corr.suptitle('Animated Correlation')
#x is fixed
length = 1001
index = np.linspace(-10,10,length)
#laplacian distribution parameters
mu = 0
b = 2
#data
dat = [(1-((1/(2*b))*np.exp(-1*(abs(i-mu))/b)))+10 for i in index]
dat_norm = [(1-(1/(2*b))*np.exp(-(abs(i-mu))/b))+10 for i in index]
#initial plots
line_corr, = axs_corr[0].plot(index, dat)
line_corr_norm, = axs_corr[1].plot(index, dat_norm)
#titles
axs_corr[0].set_title('Random')
axs_corr[1].set_title('No Random')
#axes labels
fig_corr.text(0.51, 0.04, 'Time (ns)', ha='center')
fig_corr.text(0.04, 0.5, 'Coincidinces', va='center', rotation='vertical')
#animation call
ani_corr = animation.FuncAnimation(fig_corr, animate_corr, interval=10, blit=False, save_count=50)
plt.show()
I would like to have both plots have the y-axis limit fixed at 0. So the left one would keep increasing its max value and seeing this reflected in its scale. The right plot would have its dip get sharper and sharper but once its smaller than 0, the plot wouldn't change its scale anymore (since this plot doesn't have its values get larger).
#ivallesp almost had it. Removing axs_corr[0].set_ylim((0, None), auto=True) and axs_corr[1].set_ylim((0, None), auto=True) from before the set_ydata method and placing them after the autoscale_view call, for both plots, made it work as I wanted it too.
The following code should work :D.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
#used to generate test data
import random
pi = np.pi
factor = 1
random.seed(123)
#procedural data generation for left plot
def get_data(data):
global mu
global b
global index
global factor
b=b*0.99
factor=factor*1.01
new_data = [factor*(((1-((1/(2*b))*np.exp(-1*(abs(i-mu))/b)))*random.random())+10) for i in index]
return new_data
#procedural data generation for right plot
def get_data_norm(data):
global mu
global b
global index
new_data = [((1-((1/(2*b))*np.exp(-1*(abs(i-mu))/b)))+10) for i in index]
return new_data
#animation function, assuming problem is here
def animate_corr(i):
global dat
global dat_norm
dat = get_data(dat)
dat_norm = get_data_norm(dat_norm)
#these two not working as expected
axs_corr[0].set_ylim((0, None), auto=True)
axs_corr[1].set_ylim(bottom=0, top=None, auto=True)
line_corr.set_ydata(dat)
line_corr_norm.set_ydata(dat_norm)
#rescales axes automatically
axs_corr[0].relim()
axs_corr[0].autoscale_view(True,True,True)
axs_corr[0].set_ylim(0, None)
axs_corr[1].relim()
axs_corr[1].autoscale_view(True,True,True)
axs_corr[1].set_ylim(0, None)
return line_corr, line_corr_norm,
#plots definitions
fig_corr, axs_corr = plt.subplots(1,2, sharex=True, figsize=(10,5))
fig_corr.suptitle('Animated Correlation')
#x is fixed
length = 1001
index = np.linspace(-10,10,length)
#laplacian distribution parameters
mu = 0
b = 2
#data
dat = [(1-((1/(2*b))*np.exp(-1*(abs(i-mu))/b)))+10 for i in index]
dat_norm = [(1-(1/(2*b))*np.exp(-(abs(i-mu))/b))+10 for i in index]
#initial plots
line_corr, = axs_corr[0].plot(index, dat)
line_corr_norm, = axs_corr[1].plot(index, dat_norm)
#titles
axs_corr[0].set_title('Random')
axs_corr[1].set_title('No Random')
#axes labels
fig_corr.text(0.51, 0.04, 'Time (ns)', ha='center')
fig_corr.text(0.04, 0.5, 'Coincidinces', va='center', rotation='vertical')
#animation call
ani_corr = animation.FuncAnimation(fig_corr, animate_corr, interval=10, blit=False, save_count=50)
plt.show()
I have a large dataset (~30GB) that I want to visualize by looking at it scrolling past. A great example is the top graph in this video.
My data is coming from CSV files.
What I have tried so far is importing the massive CSV files into a numpy array and using np.roll() to shift in a new column from the right side (like in the video) repeatedly until I hit the last column of the array (by calling np.roll() in the mpl.animation.FuncAnimation iterations.
This takes a large amount of CPU, and a much larger amount of memory.
Any suggestion on how to approach this? I couldn't find very many examples online that could help me with this.
here is some code from the mat plot lib tutorials.
import numpy as np
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import math
class Scope(object):
def __init__(self, ax, maxt=2, dt=0.02):
self.ax = ax
self.dt = dt
self.maxt = maxt
self.tdata = [0]
self.ydata = [0]
self.line = Line2D(self.tdata, self.ydata)
self.ax.add_line(self.line)
self.ax.set_ylim(-.1, 1.1)
self.ax.set_xlim(0, self.maxt)
def update(self, y):
lastt = self.tdata[-1]
if lastt > self.tdata[0] + self.maxt: # reset the arrays
self.tdata = [self.tdata[-1]]
self.ydata = [self.ydata[-1]]
self.ax.set_xlim(self.tdata[0], self.tdata[0] + self.maxt)
self.ax.figure.canvas.draw()
t = self.tdata[-1] + self.dt
self.tdata.append(t)
self.ydata.append(y)
self.line.set_data(self.tdata, self.ydata)
return self.line,
def emitter(x=0):
'return a random value with probability p, else 0'
while True:
if x<361:
x = x + 1
yield math.sin(math.radians(x))
else:
x=0
x =x + 1
yield math.sin(math.radians(x))
# Fixing random state for reproducibility
np.random.seed(19680801)
fig, ax = plt.subplots()
scope = Scope(ax)
# pass a generator in "emitter" to produce data for the update func
ani = animation.FuncAnimation(fig, scope.update, emitter, interval=10,
blit=True)
plt.show()
my suggestion is to build a generator that yields the next data set you want to display every time it is called. this way you wont need to load the whole file into memory. more on that here. replace emitter function with generator that will pull from you file. disadvantage of this is I don't believe the full array will be available in the plot.
I've got an animation with lines and now I want to label the points.
I tried plt.annotate() and I tried plt.text() but the labes don't move.
This is my example code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
def update_line(num, data, line):
newData = np.array([[1+num,2+num/2,3,4-num/4,5+num],[7,4,9+num/3,2,3]])
line.set_data(newData)
plt.annotate('A0', xy=(newData[0][0],newData[1][0]))
return line,
fig1 = plt.figure()
data = np.array([[1,2,3,4,5],[7,4,9,2,3]])
l, = plt.plot([], [], 'r-')
plt.xlim(0, 20)
plt.ylim(0, 20)
plt.annotate('A0', xy=(data[0][0], data[1][0]))
# plt.text( data[0][0], data[1][0], 'A0')
line_ani = animation.FuncAnimation(fig1, update_line, 25, fargs=(data, l),
interval=200, blit=True)
plt.show()
Can you help me please?
My next step is:
I have vectors with origin in these Points. These vectors change their length and their direction in each animation step.
How can I animate these?
Without animation this works:
soa =np.array( [ [data[0][0],data[1][0],F_A0[i][0][0],F_A0[i][1][0]],
[data[0][1],data[1][1],F_B0[i][0][0],F_B0[i][1][0]],
[data[0][2],data[1][2],F_D[i][0][0],F_D[i][1][0]] ])
X,Y,U,V = zip(*soa)
ax = plt.gca()
ax.quiver(X,Y,U,V,angles='xy',scale_units='xy',scale=1)
First thanks a lot for your fast and very helpful answer!
My Vector animation problem I have solved with this:
annotation = ax.annotate("C0", xy=(data[0][2], data[1][2]), xycoords='data',
xytext=(data[0][2]+1, data[1][2]+1), textcoords='data',
arrowprops=dict(arrowstyle="->"))
and in the 'update-function' I write:
annotation.xytext = (newData[0][2], newData[1][2])
annotation.xy = (data[0][2]+num, data[1][2]+num)
to change the start and end position of the vectors (arrows).
But what is, wehn I have 100 vectors or more? It is not practicable to write:
annotation1 = ...
annotation2 = ...
.
:
annotation100 = ...
I tried with a list:
...
annotation = [annotation1, annotation2, ... , annotation100]
...
def update(num):
...
return line, annotation
and got this error:
AttributeError: 'list' object has no attribute 'axes'
What can I do? Have you any idea?
I'm coming here from this question, where an annotation should be updated that uses both xy and xytext. It appears that, in order to update the annotation correctly, one needs to set the attribute .xy of the annotation to set the position of the annotated point and to use the .set_position() method of the annotation to set the position of the annotation. Setting the .xytext attribute has no effect -- somewhat confusing in my opinion. Below a complete example:
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
fig, ax = plt.subplots()
ax.set_xlim([-1,1])
ax.set_ylim([-1,1])
L = 50
theta = np.linspace(0,2*np.pi,L)
r = np.ones_like(theta)
x = r*np.cos(theta)
y = r*np.sin(theta)
line, = ax.plot(1,0, 'ro')
annotation = ax.annotate(
'annotation', xy=(1,0), xytext=(-1,0),
arrowprops = {'arrowstyle': "->"}
)
def update(i):
new_x = x[i%L]
new_y = y[i%L]
line.set_data(new_x,new_y)
##annotation.xytext = (-new_x,-new_y) <-- does not work
annotation.set_position((-new_x,-new_y))
annotation.xy = (new_x,new_y)
return line, annotation
ani = animation.FuncAnimation(
fig, update, interval = 500, blit = False
)
plt.show()
The result looks something like this:
In case that versions matter, this code has been tested on Python 2.7 and 3.6 with matplotlib version 2.1.1, and in both cases setting .xytext had no effect, while .set_position() and .xy worked as expected.
You have the return all objects that changed from your update function. So since your annotation changed it's position you should return it also:
line.set_data(newData)
annotation = plt.annotate('A0', xy=(newData[0][0],newData[1][0]))
return line, annotation
You can read more about matplotlib animations in this tutorial
You should also specify the init function so that the FuncAnimation knows which elements to remove from the plot when redrawing on the first update. So the full example would be:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
# Create initial data
data = np.array([[1,2,3,4,5], [7,4,9,2,3]])
# Create figure and axes
fig = plt.figure()
ax = plt.axes(xlim=(0, 20), ylim=(0, 20))
# Create initial objects
line, = ax.plot([], [], 'r-')
annotation = ax.annotate('A0', xy=(data[0][0], data[1][0]))
annotation.set_animated(True)
# Create the init function that returns the objects
# that will change during the animation process
def init():
return line, annotation
# Create the update function that returns all the
# objects that have changed
def update(num):
newData = np.array([[1 + num, 2 + num / 2, 3, 4 - num / 4, 5 + num],
[7, 4, 9 + num / 3, 2, 3]])
line.set_data(newData)
# This is not working i 1.2.1
# annotation.set_position((newData[0][0], newData[1][0]))
annotation.xytext = (newData[0][0], newData[1][0])
return line, annotation
anim = animation.FuncAnimation(fig, update, frames=25, init_func=init,
interval=200, blit=True)
plt.show()
I think I figured out how to animate multiple annotations through a list. First you just create your annotations list:
for i in range(0,len(someMatrix)):
annotations.append(ax.annotate(str(i), xy=(someMatrix.item(0,i), someMatrix.item(1,i))))
Then in your "animate" function you do as you have already written:
for num, annot in enumerate(annotations):
annot.set_position((someMatrix.item((time,num)), someMatrix.item((time,num))))
(You can write it as a traditional for loop as well if you don't like the enumerate way). Don't forget to return the whole annotations list in your return statement.
Then the important thing is to set "blit=False" in your FuncAnimation:
animation.FuncAnimation(fig, animate, frames="yourframecount",
interval="yourpreferredinterval", blit=False, init_func=init)
It is good to point out that blit=False might slow things down. But its unfortunately the only way I could get animation of annotations in lists to work...