Intro to Dynamic Visualization with Python — Animations and Interactive Plots | by Naveen Venkatesan | Towards Data Science

Sometimes, you would like to create a dynamic graphic that either changes with time like a video, or adapts based on an interactive user input. These visualizations do a great of job of really showing how an output can change with its inputs. In this article, I will present the same data as a static plot, an animation, and an interactive plot. In keeping with the physical science theme of my previous articles, the data I am plotting will be from one of the most widely used equations in solid-state physics: the Fermi-Dirac distribution, which describes the occupancy of electrons in solids. The equation is shown below, which relates the fraction of states occupied at energy E as a function of the Fermi energy and temperature.
Our first plot will be a static plot in which will have curves of f(E) at different temperatures. First, we import required packages:
# Import packagesimport matplotlib as mplimport matplotlib.pyplot as pltimport numpy as np
Since we are going to calculate the Fermi-Dirac distribution many times, we should write a function to do this calculation for us:
# Fermi-Dirac Distributiondef fermi(E: float, E_f: float, T: float) -> float:    k_b = 8.617 * (10**-5) # eV/K    return 1/(np.exp((E - E_f)/(k_b * T)) + 1)
Now we can start plotting the data! First I am going to edit some of the general plot parameters:
# General plot parametersmpl.rcParams['font.family'] = 'Avenir'mpl.rcParams['font.size'] = 18mpl.rcParams['axes.linewidth'] = 2mpl.rcParams['axes.spines.top'] = Falsempl.rcParams['axes.spines.right'] = Falsempl.rcParams['xtick.major.size'] = 10mpl.rcParams['xtick.major.width'] = 2mpl.rcParams['ytick.major.size'] = 10mpl.rcParams['ytick.major.width'] = 2
We create our figure and add our axes object to it:
# Create figure and add axesfig = plt.figure(figsize=(6, 4))ax = fig.add_subplot(111)
To supply the varying temperature data to our Fermi-Dirac function, we generate an array of values between 100 K and 1000 K using numpy.linspace:
# Temperature valuesT = np.linspace(100, 1000, 10)
For each of these temperature values, we need to map a different color to its resulting curve. We will generate colors from the coolwarm colormap since we are in fact dealing with ichanging temperatures. Since we have generated 10 temperature values above, we will extract 10 colors from the coolwarm colormap.
# Get colors from coolwarm colormapcolors = plt.get_cmap('coolwarm', 10)
The simplest way to plot our data is to loop through all the temperature values and plot the corresponding curve each time. To generate x-axis values, we again use numpy.linspace to create an array of 100 evenly-spaced values between 0 and 1. Additionally, we are using a fixed Fermi energy value of 0.5 eV for all calculations.
# Plot F-D datafor i in range(len(T)):    x = np.linspace(0, 1, 100)    y = fermi(x, 0.5, T[i])    ax.plot(x, y, color=colors(i), linewidth=2.5)
The final element our plot needs is a method by which to distinguish the different colored temperature curves. To do this, we will create a legend by first creating a list of labels, and then passing them to the axes.legend method.
# Add legendlabels = ['100 K', '200 K', '300 K', '400 K', '500 K', '600 K',           '700 K', '800 K', '900 K', '1000 K']ax.legend(labels, bbox_to_anchor=(1.05, -0.1), loc='lower left',           frameon=False, labelspacing=0.2)
labelspacing — the vertical spacing between legend entries (default is 0.5)
Finally, after adding axis labels, we are presented with the following plot:
Now say we wanted to present the same data as above, but as a video — how would we do this? It turns out that we can do this very easily with matplotlib! We must import the following to make this functionality available to us:
# Import animation packagefrom matplotlib.animation import FuncAnimation
If we are using a Jupyter Notebook, we should also change the backend that matplotlib uses to render its figures in order to allow for interactive plots.
# Change matplotlib backend%matplotlib notebook
For our animation we need to do the following:
(1) Store a reference to the plotted curve as a variable
(2) Use a function call with this variable to keep updating the plot data
We will first plot empty arrays and store this plot as a variable called f_d. Additionally, we will add a text annotation that displays the temperature of the current plot, so we will store a variable reference to this as well. We are aligning the top right corner of the text annotation to the top right corner of our axes object.
# Create variable reference to plotf_d, = ax.plot([], [], linewidth=2.5)# Add text annotation and create variable referencetemp = ax.text(1, 1, '', ha='right', va='top', fontsize=24)
Now to the workhorse of our animation — the animation function. This function will take the input of an index i and update the plot each time it is called. It will also update the text annotation with the current temperature and (just for fun!) change the color of the plot and text based on the same colors from the coolwarm colormap.
# Animation functiondef animate(i):    x = np.linspace(0, 1, 100)    y = fermi(x, 0.5, T[i])    f_d.set_data(x, y)    f_d.set_color(colors(i))    temp.set_text(str(int(T[i])) + ' K')    temp.set_color(colors(i))
set_data(x, y) — set the new x and y data for the plot
Now to the make the magic happen, we use the following line of code:
# Create animationani = FuncAnimation(fig=fig, func=animate, frames=range(len(T)), interval=500, repeat=True)
fig — the figure passed to the animation function
func — the animation function for the plot
frames — an array, starting at 0, representing the frames of the animation. In this case, we pass one with a length equal to the number of temperatures we are animating over (this is also the index i that gets passed to func).
interval — delay between frames in milliseconds
repeat — whether to repeat the animation at its conclusion
There it is! Now, if your axis labels or part of your plot is cut off, you can try adding the following line of code to ensure that all elements are within view of the figure.
# Ensure the entire plot is visiblefig.tight_layout()
Now to save our animation, we use the following:
# Save and show animationani.save('AnimatedPlot.gif', writer='imagemagick', fps=2)
writer — the animation writer program — to generate .gif files, I use ImageMagick
fps — frames per second of the animation (since we only have 10 frames, I am using an fps value of 2 to imitate the interval delay of 500 ms earlier)
Finally, if we want to let the user play around with input parameters to observe their change on the output, we can make an interactive plot. We start by importing required packages.
# Import slider packagefrom matplotlib.widgets import Slider
We again start with creating a figure and axis object to hold our plot. However, this time, we adjust the size of the plot to make room for the sliders that we will add.
# Create main axisax = fig.add_subplot(111)fig.subplots_adjust(bottom=0.2, top=0.75)
figure.subplots_adjust() takes inputs for top, bottom, left, and right to indicate where to draw the four corners of the axis bounding box. In this case, we are making sure the top does not exceed 0.75 so we can place sliders on top of the plot.
Sliders start out just like any other axes object. Here, we are adding both above the plot (one to vary the Fermi energy, and one to vary the temperature). Also, since we changed the global plot settings to remove the right and top spines, we will add them back here for the sliders.
# Create axes for slidersax_Ef = fig.add_axes([0.3, 0.85, 0.4, 0.05])ax_Ef.spines['top'].set_visible(True)ax_Ef.spines['right'].set_visible(True)ax_T = fig.add_axes([0.3, 0.92, 0.4, 0.05])ax_T.spines['top'].set_visible(True)ax_T.spines['right'].set_visible(True)
Now, we have to turn those axes objects into sliders:
# Create sliderss_Ef = Slider(ax=ax_Ef, label='Fermi Energy ', valmin=0, valmax=1.0,              valfmt=' %1.1f eV', facecolor='#cc7000')s_T = Slider(ax=ax_T, 'Temperature ', valmin=100, valmax=1000,              valinit=100, valfmt='%i K', facecolor='#cc7000')
ax — axes object to convert to slider
label — the slider label that goes to the left of the slider
valmin — minimum value of the slider
valmax — maximum value of the slider
valfmt — the string to display as the value of the slider, positioned to the right. %1.1f is a float with 1 decimal point and %i is an integer
facecolor — the color to fill in the slider
Now that we have created our sliders, let us plot the “default” dataset that will show when the figure is first loaded (Fermi energy of 0.5 eV and temperature of 300 K):
# Plot default datax = np.linspace(-0, 1, 100)Ef_0 = 0.5T_0 = 100y = fermi(x, Ef_0, T_0)f_d, = ax.plot(x, y, linewidth=2.5)
Just like in the animated plot case, we now will define the update function that will change the data as the sliders are updated. This update function takes the current values of the sliders, changes the data in the plot, and re-draws the figure.
# Update valuesdef update(val):    Ef = s_Ef.val    T = s_T.val    f_d.set_data(x, fermi(x, Ef, T))    fig.canvas.draw_idle()s_Ef.on_changed(update)s_T.on_changed(update)
Slider.on_changed(func) calls the updating func when the slider value is changed. At last, when we show our plot we get this cool interactive figure!
I hope this article showed how you can potentially make your data visualizations dynamic with animations and interactive sliders. Thank you for reading — I appreciate any comments and feedback. All the examples shown in this article can be found at this Github repository. You can follow me on Twitter or connect with me on LinkedIn for more articles and updates.