Python por ejemplo
Tugurium/Python

Python, por ejemplo

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.

matplotlib_04_01_anim.py
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.

matplotlib_04_02_anim.py
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.

matplotlib_04_03_anim.py
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.

matplotlib_04_04_anim.py
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.

matplotlib_04_05_anim.py
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

Gráficos animados

    • Gráficos animados