Gráficos animados
1.1.5 Gráficos animados
En la introducción a este capítulo, señalábamos que el propósito de la visualización de datos es dar sentido a grandes cantidades de información numérica, resaltando patrones y relaciones de manera gráfica. Las animaciones potencian esta capacidad al presentar de manera dinámica la evolución de los datos, permitiendo una comprensión más profunda y fluida de su desarrollo a lo largo del tiempo.
Para animar un gráfico debemos empezar por el gráfico. Sí, lo que parece de cajón, a veces no lo es. El primer paso es conocer la función que vamos a visualizar antes de proceder con la animación. A continuación debemos definir el rango de valores en que se va a mover nuestra animación, determinando el primer y último valor, marcando así el inicio y el fin del movimiento. Las animaciones van a ser, en cierto modo, una película, compuesta por un conjunto de fotogramas (frames), cada uno de ellos una gráfica completa, que presentados uno tras otro (o uno sobre otro), con el retardo temporal adecuado, nos va a crear la ilusión de un movimiento animado.
La forma más sencilla de hacer un gráfico animado en matplotlib es utilizar una de las clases Animation(). Disponemos de:
- Animation(): Clase base para animaciones.
- FuncAnimation(): Subclase de TimedAnimation() que hace una animación llamando repetidamente a una función.
- ArtistAnimation(): Subclase de TimedAnimation() que crea una animación utilizando un conjunto fijo de objetos Artist, elementos visibles de una figura
Vamos a centrarnos en el uso de la clase FuncAnimation(), que realmente no genera animaciones por sí misma, sino que se limita a transformar una serie de gráficos proporcionados a través de otra función, que se encarga de devolver un fotograma en cada llamada, estableciendo la velocidad de reproducción mediante la cantidad de fotogramas por segundo en el intervalo de microsegundos definido. La animación se construye mediante la secuencia de estos fotogramas, cada uno representando un estado específico. La función une y reproduce los gráficos en un flujo continuo.
La firma de FuncAnimation() es:
matplotlib.animation.FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, *, cache_frame_data=True, **kwargs)
- Parámetros
fig: Un objeto figura que contendrá la imagen.
func: Función a llamar para cada fotograma. El primer argumento será el siguiente valor que actualice los datos que van en el gráfico. Cualquier argumento posicional adicional se suministra a través del parámetro fargs.
La firma de la función debe ser: def func(frame, *fargs).
frames: Fuente de datos para pasar a func y a cada fotograma de la animación. Puedes ser un iterable, un entero o una función generadora. Si es un entero, equivale a pasar range(frames).
Si es una función generadora, entonces debe tener la firma: def gen_function().
En todos estos casos, los valores de frames simplemente se pasan a la función suministrada por el usuario y, por tanto, pueden ser de cualquier tipo.
init_func: Función que se llama una vez antes del primer fotograma. Si no se especifica, se utilizarán los resultados de dibujar desde el primer elemento de la secuencia.
La firma de la función debe ser: def init_func().
fargs: Argumentos adicionales a pasar a cada llamada a func.
save_coun: Número de valores de fotogramas a almacenar en cache.
interval: Retraso entre fotogramas en milisegundos. Marca el ritmo de actualización de la animación, cada cuanto tiempo se ejecutará la función de animación y actualizará la figura. Por defecto 200.
repeat_delay: Retardo en milisegundos entre ejecuciones de animación consecutivas. Por defecto 0.
repeat: Si True, la animación se repite cuando se completa la secuencia de fotogramas. Por defecto True.
blit: Si True, cualquier gráfico animado se dibuja encima de cualquier otro, sin importar su zorder. Por defecto False.
cache_frame_data: Si True, se almacenan en caché los datos de los fotogramas. Desactivar la caché puede ser útil cuando los fotogramas contienen objetos grandes. Por defecto True.
Vamos a animar nuestro habitual gráfico del seno.
Preparamos una función de inicialización init(), que crea el fotograma a partir del que tendrá lugar la animación. En este caso la función de inicialización solo crea un fotograma vacío. Esta función debe devolver el objeto que el animador debe actualizar al generar cada cuadro.
Declaramos después una función de animación animate(), que toma un único parámetro que será el número de fotograma con el que calcularemos el trazado de nuestro gráfico. Establecemos la posición actual del punto en (i, sin(i)), y devolveremos la tupla que hace referencia al gráfico generado.
Una vez definidas las funciones creamos un ventana de figura con un solo eje sobre el que dibujaremos la función seno estática. Este gráfico solo tiene sentido aquí para que veamos por dónde va a ir nuestra animación.
Iniciamos la animación creando un objeto que nos servirá para trazar el gráfico modificándolo en cada fotograma de la animación. En el ejemplo lo iniciamos como un punto de color rojo.
Finalmente creamos el objeto de animación, llamando a la función de animación del gráfico, a la que pasamos nuestra función de animación y la de inicialización. Establecemos una animación de 360 fotogramas con un retardo de 20ms entre fotogramas. Indicamos con blit que sólo se vuelvan a dibujar las partes del trazo que han cambiado, así la animación se mostrará mucho más rápido. Este objeto necesita persistir, por lo que debe ser asignado a una variable.
Acabamos el script mostrando el resultado. Y salvando el grafico animado como un fichero de tipo .gif.
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter import math # función de inicialización de la figura def init(): # elimina la lista de imágenes trazo.set_data([], []) return trazo, # función para crear cada fotograma def animate(i): # trazar un único valor trazo.set_data(i, math.sin(math.radians(i))) return trazo, # creación de los valores a visualizar grados = [i for i in range (0, 361, 10)] seno = [math.sin(math.radians(i)) for i in grados] # establecer el área de trabajo fig = plt.figure() # marcas en los ejes X e Y axs = plt.axes(xlim=(0, 360), ylim=(-1, 1)) # dibujar el gráfico axs.plot(grados, seno) # dibujar el primer trazo de la animación # trazo: lista que representa los datos trazados (líneas o marcas) trazo, = axs.plot([0], [0], 'ro') # animar el gráfico anim = FuncAnimation(fig, animate, init_func=init, frames=361, interval=20, blit=True, repeat=True) # mostrar el gráfico plt.show() ##anim.save(r'c:\TestPython\matematicas\matplotlib\sine_wave.gif', ## writer='imagemagick', fps=30) # guardar el gráfico animado file = r'c:\TestPython\matematicas\matplotlib\sine_wave_dot.gif' writergif = PillowWriter(fps=30) # método para guardar bajo Windows anim.save(file, writer=writergif) plt.close()
El gráfico presenta, además del trazado estático de la función seno, el trazado animado de un punto moviéndose según los valores del seno que generamos en cada llamada a la función de animación animate().

Punto moviendose a lo largo del seno
Disponemos de diferentes formatos para guardar la animación: gif, mp4, avi, mov, etc.
Dependiendo del formato, los parámetros para guardar el gráfico animado son diferentes.
Las opciones más comunes para guardar las animaciones son: ImageMagick y PillowWriter.
Aunque ImageMagick es la opción más común, al menos bajo Unix, puede causar problemas en entornos Windows. La solución pasa por usar PillowWriter, como hemos hecho en el script.
En el siguiente ejemplo modificamos la función animate(), generando una serie de valores entre 0 y el número de fotograma que proporciona la función de animación, con lo que tendremos, fotograma a fotograma, una serie cada vez mayor de puntos.
En este ejemplo no hemos hecho uso de la función de inicialización, nos basta con el primer trazado que hacemos antes de proceder con la función de animación.
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation import math # función para crear cada fotograma def animate(i): # generar una serie de valores y trazarlos x = [n for n in range (0, i, 10)] y = [math.sin(math.radians(n)) for n in x] trazo.set_data(x, y) return trazo, # creación de los valores a visualizar grados = [i for i in range (0, 361, 10)] seno = [math.sin(math.radians(i)) for i in grados] # establecer el área de trabajo fig = plt.figure() # marcas en los ejes X e Y axs = plt.axes(xlim=(0, 360), ylim=(-1, 1)) # dibujar el gráfico axs.plot(grados, seno) # dibujar el primer trazo de la animación # trazo: lista que representa los datos trazados (líneas o marcas) trazo, = axs.plot([0], [0], 'ro') # animar el gráfico anim = FuncAnimation(fig, animate, frames=362, interval=20, blit=True, repeat=False) # mostrar el gráfico plt.show()
El gráfico presenta, además del trazado estático de la función seno, el trazado animado de una serie de puntos correspondiente a las coordenadas de los puntos (x, y) calculados.

Puntos animados sobre el trazado de seno
En el siguiente ejemplo trazamos la animación de la curva de la función seno exclusivamente, para ello modificamos la función animate() calculando cada uno de los puntos (x, y), y los vamos guardando en sendas listas para su trazado.
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter import math def animate(i): # generar un valor y trazarlo x.append(i) y.append(math.sin(math.radians(i))) trazo.set_data(x, y) return trazo, # establecer el área de trabajo fig = plt.figure() # marcas en los ejes X e Y axs = plt.axes(xlim=(0, 360), ylim=(-1, 1)) # valores a trazar x, y = [], [] # dibujar el primer trazo de la animación # trazo: lista que representa los datos trazados (líneas o marcas) trazo, = axs.plot(x, y) # animar el gráfico anim = FuncAnimation(fig, animate, frames=361, interval=20, blit=True, repeat=False) # mostrar el gráfico plt.show()
En el gráfico resultante vemos como avanza el trazado de la curva según vamos generando más puntos.

Trazado animado de la función seno
En el siguiente ejemplo no definimos marcas para los ejes, por lo que el gráfico y el trazado irán modificándose según vayamos añadiendo puntos en cada fotograma.
Vamos a pasar a la función animate() las listas de valores (x, y) mediante parámetros extras con fargs, con la única intención de ver su uso.
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter import math # función para crear cada fotograma def animate(i, xx, yy): # generar un valor y acumularlo xx.append(i*10) yy.append(math.sin(math.radians(i*10))) # trazar el gráfico plt.plot(xx, yy) # establecer el área de trabajo fig = plt.figure() # no se establecen valores para las marcas de los ejes # valores a trazar x, y = [], [] # animar el gráfico anim = FuncAnimation(fig, animate, fargs=(x, y), frames=72, interval=300, repeat=False) # mostrar el gráfico plt.show()
El resultado es la animación siguiente.

Trazado incremental animado de la función seno
Y por último vamos ver como animar más de un trazado en el mismo gráfico.
En la función animate(), vamos a calcular datos para dos curvas, el seno y el coseno, trazando a continuación cada uno de los conjuntos de valores.
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter import math # función para crear cada fotograma def animate(i): # generar un valor y acumularlo x1.append(i*10) y1.append(math.sin(math.radians(i*10))) y2.append(math.cos(math.radians(i*10))) # trazar el gráfico plt.plot(x1, y1, 'b') plt.plot(x1, y2, 'r') # establecer el área de trabajo fig = plt.figure() # marcas en los ejes X e Y axs = plt.axes(xlim=(0, 360), ylim=(-1, 1)) # valores a trazar x1, y1, y2 = [], [], [] # animar el gráfico anim = FuncAnimation(fig, animate, frames=37, interval=300, repeat=False) # mostrar el gráfico plt.show()
El resultado son dos curvas moviéndose a la par.

Trazado animado de las funciones seno y coseno