Using Matplotlib to Animate Data From a Velocity Recording
One of my friends approached me asking if I had any experience animating graphs, since I had experience with video editing. Though I had not had any formal experience, I decided that it would be a interesting challenge to embark.
Table of Contents
- 1 Using Matplotlib to Animate Data From a Velocity Recording
- 2 Introduction
- 3 Demonstration
- 4 Importing Libraries + Loading Data
- 5 Plotting Function
- 5.1 Explanation of Inputs
- 5.2 Explanation of Variables
- 5.2.1 Setting Up Data Section
- 5.2.2 Graph Type Settings Section
- 5.2.3 Animation Settings
- 5.2.4 Setting Up The Figure
- 5.2.5 Setting Up Positions of X and Y Outputs For Value
- 5.2.6 Setting Up The Line Plot
- 5.2.7 Initialization Function: Plot The Background of Each Frame
- 5.2.8 Animation Function. This Is Called Sequentially
- 5.2.9 Save The Animation as an mp4.
- 6 Working Through Matplotlib Animation Tutorial
- 7 Messing with a overlapping graph
Introduction
One of my friends approached me asking if I had any experience animating graphs, since I had experience with video editing. Though I had not had any formal experience, I decided that it would be a interesting challenge to embark.
I did some research and found a tutorial as seen in the section "Working Through Matplotlib Animation Tutorial"
Generating The Gifs
To generate these gifs, I used ffmpeg to convert the mp4s to gifs.
To convert the mp4s to gifs, I opened my directory in the terminal and ran the following ffmpeg command, after converting the video to gif, I added and pushed the files to github. Since github does not show local embeds, I embedded the url of the gif from the github repository.
ffmpeg -i line_tracking_animated.mp4 line_tracking_animated.gif
Alternatively, if you have ffmpeg installed, you can run the following cell to run it in a jupyter cell
Breakdown of the command
ffmpeg
tells the terminal to use ffmpeg-i
tells ffmpeg the inputline_tracking_animated.mp4
tells ffmpeg the source fileline_tracking_animated.gif
tells ffmpeg the output file title and format
!ffmpeg -i line_tracking_animated.mp4 line_tracking_animated.gif
!ffmpeg -i dot_tracking_animated.mp4 dot_tracking_animated.gif
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:100% !important; }</style>"))
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.offsetbox import AnchoredText
df = pd.read_csv("velocity.csv")
df
print("max time")
display(df["Time (ms)"].max())
print("min time")
display(df["Time (ms)"].min())
print("Velocity")
display(df["Velocity (m/s)"][1])
print("Max Velocity")
display(df["Velocity (m/s)"].max())
print("Final time")
display(df["Time (ms)"].iloc[-1]*60/1000)
print("Final time rounded")
display(int(round(df["Time (ms)"].iloc[-1]*60/1000, 1)))
print("column titles")
column_titles = list(df.columns.values)
display(column_titles)
Plotting Function
I wrote the animateGraph
after modifying Matplotlib Animation Tutorial to work for my velocity data from the Having Line Plotted Through Time
section and later Having a Dot Tracking Along the Velocity Curve
.
The function takes the general structure of the respective and makes it into a single function
Explanation of Inputs
df
inputDataFrame
x_column
column title for x axisy_column
column title for y axisdot_track
takesyes
orno
, if it is notyes
, it will default to the line graphframerate
integer that specifies the video's framerate
Explanation of Variables
Setting Up Data Section
x_df
DataFrame
of just the x values, I seperated the x and yDataFrames
to make it easier to follow in the animation functiony_df
DataFrame
of just the x values, I seperated the x and yDataFrames
to make it easier to follow in the animation functionx_max
maximum of the x values, used to set the bounds of x axisy_max
maximum of the y values, used to set the bounds of y axis, added2
to make space for the time and velocity printoutslast_index
used to preventKeyerror
, seeFiguring out Keyerror
section for a more in depth explanation
Graph Type Settings Section
plot_dot
boolean used to decide which animation to use, ifTrue
will have animation output the dot tracker on graphgraph_type
string used to keep track of the animation, used for filename- can be either
dot_tracking
orline_tracking
- can be either
Animation Settings
time_seconds
takes last entry of x value to get the duration of the data converts from miliseconds to seconds, used later fortotalFrames
animation duration calculationtotalFrames
product offramerate
andtime_seconds
to get the number of frames for matplotlib to animate
Setting Up The Figure
fig
figure variable, stores figure object- the size was set to
(20,12)
- I followed the suggestions to change the size and output a sample file from Stack Overflow How to Change Figure Size
- I followed the suggestions to change the font size from Stack Overflow How to Change Font Size
- the size was set to
Setting Up Positions of X and Y Outputs For Value
- I followed the example set by the matplotlib wiki from Text properties and layout
y_value_print
text object, will set the y value, for the demo, we use velocity set forright
andtop
is updated frame by frame in theanimate(i)
functionx_value_print
text object, will set the x value, for the demo, we use time set forleft
andtop
is updated frame by frame in theanimate(i)
function
Setting Up The Line Plot
- I used the
plot_dot
to determine which graph should be plotted- if
plot_dot
isTrue
, we will plot the existing graph and have the dot - if
plot_dot
isFalse
, we will plot the graph sections frame by frame
- if
- To get the red dot, I read the instructions of the different passable arguments from the matplotlib wiki from matplotlib.pyplot.plot
Initialization Function: Plot The Background of Each Frame
- I kept the example set from the guide see
Working Through Matplotlib Animation Tutorial
for more details
Animation Function. This Is Called Sequentially
- This section is broken up into two parts,
- Checking if the function reaches the last value of the
DataFrame
- updating the x and y column print outs
- Checking if the function reaches the last value of the
- To check if the function has reached the last value of the
DataFrame
, we have aif
conditional that checks if theanimate
function has reached the end of theDataFrame
, if it does, then it will use the value atlast_index
, we want it to keep the last value to avoid theKeyerror
as explained in the later experimental section - Next, the function will set the
x_value
andy_value
to be the final index valuex_value
andy_value
is used for animating the labels for the values of x and y
- Afterwards the function follow a second
if
statement, which will check ifplot_dot
is true or false- if
plot_dot
isFalse
, the function will output everything up until the currenti
index value.- This is used for the animation that plots graph sections frame by frame
- if
plot_dot
isTrue
, the function will output the currenti
index value.- This is used for the animation that plots the point frame by frame
- if
- The next section will update the x and y column print out values for each frame I took inspiration from
Matplotlib animations the easy way specifically the
"Changing labels and text"
sectionvalues_x
is a string variable that stores the x column title, adds colons, and the x valuevalues_y
is a string variable that stores the y column title, adds colons, and the y value- After setting
values_x
andvalues_y
we use.set_text()
to update they_value_print
andx_value_print
each time
- Once all the variables have been updated,
animate(i)
will return theline
to theanimation.FuncAnimation()
, which will continue until it reaches the last frame
Save The Animation as an mp4.
- I did not make significant changes to the original, I only changed the filename to match the graph type.
- I wanted the filename to be the '
x_column
vsy_column
graph_type
_animated.mp4' - The ideal file name would be 'Velocity_(m/s)_vs_Time (ms)_line_tracking_animated.mp4
- When I tried parsing the
x_column
as a string, python had serious issues with the slash (/) part of (m/s), enough so that it would prevent the file from being saved - An potential alternative solution would to change m/s to ms^-1, but it require changing the data or engineering a solution that read the units and replaced slashes with unit^-1. After evaluating the alternatives, I realized it would be easier to rename the file. If this function were used to generate hundreds of graphs from hundreds of source files, I would need to find a better solution. Since the purpose of this function is to make it easier to change between the dot and line tracking, I did not invest any further time into developing an alternative solution
- When I tried parsing the
- I wanted the filename to be the '
def animateGraph(df,x_column, y_column, dot_track, framerate):
# Setting up data
x_df = df[x_column]
y_df = df[y_column]
x_max = int(np.ceil(df[x_column].max()))
y_max = int(np.ceil(df[y_column].max())) + 2
last_index = len(df)-1
# Check if we are plotting the graph or a graph + dot
if(dot_track.lower() == "yes"):
plot_dot = True
graph_type = "dot_tracking"
else:
plot_dot = False
graph_type = "line_tracking"
# Animation settings
time_seconds = x_df.iloc[-1]/1000
totalFrames = int(round(framerate * time_seconds))
# Setting up the figure
fig = plt.figure()
fig.set_size_inches(20, 12)
fig.savefig('test2png.png', dpi=100)
plt.rcParams.update({'font.size': 24})
# Setting up the axes
ax = plt.axes(xlim=(0, x_max), ylim=(0, y_max))
ax.set(title= x_column + ' vs ' + y_column,
ylabel= y_column,
xlabel= x_column,
)
# Setting up the positions of velocity and time outputs
left, width = .1, .75
bottom, height = .25, .73
right = left + width
top = bottom + height
y_value_print = ax.text(right, top, "y value",
horizontalalignment='right',
verticalalignment='top',
transform=ax.transAxes)
x_value_print = ax.text(left, top, "x value",
horizontalalignment='left',
verticalalignment='top',
transform=ax.transAxes)
# Setting up the line plot
if(plot_dot == True):
plt.plot(x_df, y_df)
line, = plt.plot([], [], linestyle='none', marker = 'o', ms = 10, color='r')
else:
line, = plt.plot(x_df, y_df)
# initialization function: plot the background of each frame
def init():
line.set_data([], [])
return line,
# animation function. This is called sequentially
def animate(i):
if(i > last_index):
x_value = x_df[last_index]
y_value = y_df[last_index]
if(plot_dot == False):
x = x_df[0:last_index]
y = y_df[0:last_index]
else:
x = x_df[last_index]
y = y_df[last_index]
else:
x_value = x_df[i]
y_value = y_df[i]
if(plot_dot == False):
x = x_df[0:i]
y = y_df[0:i]
else:
x = x_df[i]
y = y_df[i]
line.set_data(x, y)
# Update the figure with x and y values
values_x = x_column + ": " + str(x_value)
values_y = y_column + ": " + str(y_value)
y_value_print.set_text(values_y)
x_value_print.set_text(values_x)
return line,
# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init,frames = totalFrames, interval=0, blit=False)
# save the animation as an mp4. This requires ffmpeg or mencoder to be
# installed. The extra_args ensure that the x264 codec is used, so that
# the video can be embedded in html5. You may need to adjust this for
# your system: for more information, see
# http://matplotlib.sourceforge.net/api/animation_api.html
anim.save(graph_type + '_animated.mp4', fps=framerate, extra_args=['-vcodec', 'libx264'])
plt.show()
animateGraph(df,"Time (ms)", "Velocity (m/s)", "yes", 60)
animateGraph(df,"Time (ms)", "Velocity (m/s)", "no", 60)
Working Through Matplotlib Animation Tutorial
I used Jake Vanderplas's sine wave example posted below as a base to understand how matplotlib animates. I modified it to make it work with the velocity and time data I was given from my friend.
"""
Matplotlib Animation Example
author: Jake Vanderplas
email: vanderplas@astro.washington.edu
website: http://jakevdp.github.com
license: BSD
Please feel free to use and modify this, but keep the above information. Thanks!
"""
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
line, = ax.plot([], [], lw=2)
# initialization function: plot the background of each frame
def init():
line.set_data([], [])
return line,
# animation function. This is called sequentially
def animate(i):
x = np.linspace(0, 2, 1000)
y = np.sin(2 * np.pi * (x - 0.01 * i))
line.set_data(x, y)
return line,
# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=200, interval=20, blit=True)
# save the animation as an mp4. This requires ffmpeg or mencoder to be
# installed. The extra_args ensure that the x264 codec is used, so that
# the video can be embedded in html5. You may need to adjust this for
# your system: for more information, see
# http://matplotlib.sourceforge.net/api/animation_api.html
anim.save('basic_animation.mp4', fps=30, extra_args=['-vcodec', 'libx264'])
plt.show()
Having Line Plotted Through Time
From the base I added my own modifications titled Tracking a Line Through time
, which included
- setting variables for velocity (
y_df
) and time (x_df
) dataframes - setting maximums for x and y
- setting a
time_seconds
variable that converts the time in milliseconds to seconds - setting a
framerate
variable that sets the framerate of the graph - setting a
totalFrames
variable that sets the duration of the animation - increasing the figure size from 64x64 to a bigger 18.5x10.4 inch higher resolution figure with
fig.set_size_inches()
- increasing the framerate to 60 frames a second (setting
framerate
to60
), which makes the plot smoother- only downside is that it will take extra time to encode the final mp4
- modifying the limits.
- adding titles for the figure
- having the the animation plot the figure as time passes, rather needing a known function to plot it
x_df = df["Time (ms)"]
y_df = df["Velocity"]
x_max = int(np.ceil(df["Time (ms)"].max()))
y_max = int(np.ceil(df["Velocity"].max()))
time_seconds = x_df.iloc[-1]/1000
framerate = 60
totalFrames = int(round(framerate * time_seconds))
# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
fig.set_size_inches(18.5, 10.5)
fig.savefig('test2png.png', dpi=100)
# transparency settings for the figure
#fig.patch.set_alpha(0.)
ax = plt.axes(xlim=(0, x_max), ylim=(0, y_max))
ax.set(title='Velocity vs Time',
ylabel="Velocity",
xlabel="Time (ms)")
line, = plt.plot(x_df, y_df)
# initialization function: plot the background of each frame
def init():
line.set_data([], [])
return line,
# animation function. This is called sequentially
def animate(i):
x = x_df[0:i]
print("i", i)
y = y_df[0:i]
line.set_data(x, y)
return line,
# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init,frames = totalFrames, interval=0, blit=False)
# save the animation as an mp4. This requires ffmpeg or mencoder to be
# installed. The extra_args ensure that the x264 codec is used, so that
# the video can be embedded in html5. You may need to adjust this for
# your system: for more information, see
# http://matplotlib.sourceforge.net/api/animation_api.html
anim.save('basic_animation.mp4', fps=framerate, extra_args=['-vcodec', 'libx264'])
plt.show()
Learning How Matplotlib plots
In the following cells, I experimented with the ranges for which data to be plotted. Once I figured out putting start:end
into a dataframe, I used this for the first function
With an understanding of this property I was able to have the graph from 0
to the value at the specific frame.
plt.plot(x_df[0:100], y_df[0:100])
plt.show()
Messing with labels
When I intially made the velocity and time printouts, I thought about using AnchoredText
boxes and having each frame update the boxes. Using this method was not the best as it made the processing of the animation take longer and the text boxes would not overlap. Later I learned how to animate text.
Although not shown here, I later found <a href = https://brushingupscience.com/2016/06/21/matplotlib-animations-the-easy-way/> a very helpful guide that demonstrated animating text and labels </a>. I did not know that I could incoorperate text changes in the animate
fucntion with on matplotlib. I implemented most of the changes of set_text()
and ax.text()
in the animateGraph()
section. It was easier to modify animateGraph()
as I could run it to get a output and would not have the same code in 3 other places of the notebook.
Another goal of this section was to experiment with matplotlib to get the outputs to my liking. I learned <a href =https://stackoverflow.com/questions/332289/how-do-you-change-the-size-of-figures-drawn-with-matplotlib> I could change the graph size with</a> fig.set_size_inches()
fig = plt.figure()
fig.set_size_inches(18.5, 10.5)
fig.savefig('test2png.png', dpi=100)
ax = plt.axes(xlim=(0, x_max), ylim=(0, y_max+2))
ax.set(title='Velocity vs Time',
ylabel="Velocity",
xlabel="Time (ms)")
at1 = AnchoredText(x_df[100],
prop=dict(size=15), frameon=True,
loc='upper left',
)
at.patch.set_boxstyle("round,pad=0.3,rounding_size=0.5")
ax.add_artist(at1)
at2 = AnchoredText(y_df[100],
prop=dict(size=15), frameon=True,
loc='upper right',
)
at2.patch.set_boxstyle("round,pad=0.3,rounding_size=0.2")
ax.add_artist(at2)
at3 = AnchoredText(1.3,
prop=dict(size=15), frameon=True,
loc='upper right',
)
at3.patch.set_boxstyle("round,pad=0.3,rounding_size=0.2")
ax.add_artist(at3)
plt.plot(x_df[0:100], y_df[0:100])
plt.show()
print statements
I used these print statements to figure out why my maxes were not being rounded to the nearest whole number.
Initially, I tried using round( #number, #decimal places)
, from Stackoverflow but the native function concatenated for values such as 20.5
to 20
instead of 21
Eventually I found a detailed explanation behind the native python round()
function from RealPython, in addition to suggested alternative functions and libraries I could use that would solve the issue I was running into. The suggestion was to use ceil
from the math
libraries. I ended up using ceil
from the numpy
libraries since I didn't want to import another library
print("y_max:", df["Time (ms)"].max())
print()
print("y_max ceil rounding:", np.ceil(df["Time (ms)"].max()))
print()
print("x_max:", df["Velocity"].max())
print()
print("x_max ceil rounding:", np.ceil(df["Velocity"].max()))
print()
print("issue rounded y_max (Time):", int(round(df["Time (ms)"].max(),1)))
print()
print("issue rounded x_max (Velocity):", int(round(df["Velocity"].max(),1)))
print()
Having a Dot Tracking Along the Velocity Curve
After making my modifications, I copied my modified version and further adjusted it further titled Having a Dot Tracking Along the Velocity Curve
to print the plot first and have a single point track along the plotted graph
Changes from the original Having Line Plotted Through time
- added red dot for tracking instead of line
- used following additional arguments to set the dot
linestyle='none', marker = 'o', ms = 10, color='r'
, I learned of these parameters from adrian prince-whelan's demonstration of Making a Matplotlib animation with a transparent background linestyle='none'
prevents lines from being drawnmarker = 'o'
sets the dotms = 10
sets the dot sizecolor='r'
sets the dot color
- used following additional arguments to set the dot
- messing with video transparency as well from the same post
- main issue right now is getting the correct save settings
- changed x
linspace
value to have the previous frame- added conditional where at
i=0
,i
would start at0
instead of-1
when calling thex_df[i-1]
andx_df[i]
dataframe entry
- added conditional where at
x_df = df["Time (ms)"]
y_df = df["Velocity (m/s)"]
x_max = int(np.ceil(df["Time (ms)"].max()))
y_max = int(np.ceil(df["Velocity (m/s)"].max()))
time_seconds = x_df.iloc[-1]/1000
framerate = 60
totalFrames = len(df)
# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
fig.set_size_inches(18.5, 10.5)
fig.savefig('test2png.png', dpi=100)
# transparency settings for the figure
#fig.patch.set_alpha(0.)
ax = plt.axes(xlim=(0, x_max), ylim=(0, y_max))
ax.set(title='Velocity vs Time',
ylabel="Velocity",
xlabel="Time (ms)")
# transparency settings for the plot area
#ax.patch.set_facecolor('#ababab')
#ax.patch.set_alpha(0)
plt.plot(x_df, y_df)
line, = plt.plot([], [], linestyle='none', marker = 'o', ms = 10, color='r')
# initialization function: plot the background of each frame
def init():
line.set_data([], [])
return line,
# animation function. This is called sequentially
def animate(i):
x = x_df[i]
#print("x")
#print(x)
y = y_df[i]
line.set_data(x, y)
return line,
# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init,frames = totalFrames, interval=0, blit=True)
# save the animation as an mp4. This requires ffmpeg or mencoder to be
# installed. The extra_args ensure that the x264 codec is used, so that
# the video can be embedded in html5. You may need to adjust this for
# your system: for more information, see
# http://matplotlib.sourceforge.net/api/animation_api.html
anim.save('dot_tracking_animation_test.mp4', fps=framerate, extra_args=['-vcodec', 'libx264'])
plt.show()
Figuring out Keyerror
While running Dot Tracking Along the Velocity Curve
I got a consistent keyerror
when plotting i. The following code blocks helped me debug the values of i for each iteration.
I later realized that the keyerror
was caused by my totalFrames calculation, which took the last value of the data (milliseconds converted to seconds), multiplied it by the framerate, and was rounded to get total number of frames. Before changing to use the length of the dataframe, the totalFrames
was 1333.98
, which was rounded to 1334. The keyerror
occured because animate(i)
uses the frames
variable as a index. Once animate reached the length of the dataframe (1319
) the animation stopped, but in the animation.FuncAnimation()
function continued to pass values in for i
. When computing for x
, this caused a keyerror
since animation.FuncAnimation()
would continue to feed values in for i
, but in the dataframe no such values existed.
To prevent this from happening, I added a conditional that when we reached the end of the dataframe, we would use the last value of the dataframe.
x_df = df["Time (ms)"]
y_df = df["Velocity (m/s)"]
for i in x_df:
x = x_df[i]
#print("i = ",i,",", "a = ",a)
y = y_df[i]
print("i = ", i, "x = ", x, ",", "y = ",y)
print()
df.iloc[1318]
#len(df)
display(x_df)
display(framerate * time_seconds)
totalFrames
fig = plt.figure()
fig.set_size_inches(18.5, 10.5)
fig.savefig('test2png.png', dpi=100)
fig, ax = plt.subplots()
ax.plot(x_df, y_df)
plt.plot(x_df[0:100], y_df[0:100])
plt.show()
!jupyter nbconvert Animating-Velocity-Graph.ipynb --to html