Gráfica de actualización dinámica en matplotlib

114

Estoy creando una aplicación en Python que recopila datos de un puerto serie y traza un gráfico de los datos recopilados frente a la hora de llegada. La hora de llegada de los datos es incierta. Quiero que el gráfico se actualice cuando se reciban los datos. Busqué cómo hacer esto y encontré dos métodos:

  1. Limpia la trama y vuelve a dibujar la trama con todos los puntos.
  2. Anime la trama cambiándola después de un intervalo particular.

No prefiero el primero, ya que el programa se ejecuta y recopila datos durante mucho tiempo (un día, por ejemplo) y volver a dibujar el gráfico será bastante lento. El segundo tampoco es preferible ya que la hora de llegada de los datos es incierta y quiero que el gráfico se actualice solo cuando se reciban los datos.

¿Hay alguna forma en la que pueda actualizar el gráfico simplemente agregando más puntos solo cuando se reciben los datos?

Shadman Anwer
fuente
2
Posible duplicado del trazado en tiempo real en el bucle while con matplotlib
Trevor Boyd Smith

Respuestas:

138

¿Hay alguna manera en la que pueda actualizar la trama simplemente agregando más puntos a ella ...

Hay varias formas de animar datos en matplotlib, dependiendo de la versión que tenga. ¿Has visto los ejemplos de libros de cocina de matplotlib ? Además, consulte los ejemplos de animación más modernos en la documentación de matplotlib. Finalmente, la API de animación define una función FuncAnimation que anima una función en el tiempo. Esta función podría ser simplemente la función que utiliza para adquirir sus datos.

Básicamente, cada método establece la datapropiedad del objeto que se está dibujando, por lo que no requiere borrar la pantalla o la figura. La datapropiedad puede simplemente extenderse, por lo que puede mantener los puntos anteriores y seguir agregando a su línea (o imagen o lo que sea que esté dibujando).

Dado que dice que la hora de llegada de sus datos es incierta, su mejor opción probablemente sea hacer algo como:

import matplotlib.pyplot as plt
import numpy

hl, = plt.plot([], [])

def update_line(hl, new_data):
    hl.set_xdata(numpy.append(hl.get_xdata(), new_data))
    hl.set_ydata(numpy.append(hl.get_ydata(), new_data))
    plt.draw()

Luego, cuando reciba datos del puerto serie, simplemente llame update_line.

Chris
fuente
¡Finalmente! He estado buscando una respuesta a este +1 :) ¿Cómo hacemos que la trama se cambie de escala automáticamente? ax.set_autoscale_on (True) no parece funcionar.
Edward Newell
13
Encontré la respuesta: llame a ax.relim () luego ax.autoscale_view () después de actualizar los datos pero antes de llamar a plt.draw ()
Edward Newell
El enlace al libro de cocina de Matplotlib ( scipy.org/Cookbook/Matplotlib/Animations ) parece estar roto ( aparece un error "Prohibido")
David Doria
21
Dado que no hay una llamada a show (), el gráfico nunca aparece en la pantalla. Si llamo a show (), bloquea y no realiza las actualizaciones. ¿Me estoy perdiendo de algo? gist.github.com/daviddoria/027b5c158b6f200527a4
David Doria
2
enlace a una respuesta independiente similar pero diferente con código que puede ejecutar (esta respuesta tiene la idea general correcta pero el código de ejemplo no se puede ejecutar)
Trevor Boyd Smith
44

Para hacer esto sin FuncAnimation (por ejemplo, si desea ejecutar otras partes del código mientras se produce el gráfico o desea actualizar varios gráficos al mismo tiempo), llamar drawpor sí solo no produce el gráfico (al menos con el qt backend).

Lo siguiente funciona para mí:

import matplotlib.pyplot as plt
plt.ion()
class DynamicUpdate():
    #Suppose we know the x range
    min_x = 0
    max_x = 10

    def on_launch(self):
        #Set up plot
        self.figure, self.ax = plt.subplots()
        self.lines, = self.ax.plot([],[], 'o')
        #Autoscale on unknown axis and known lims on the other
        self.ax.set_autoscaley_on(True)
        self.ax.set_xlim(self.min_x, self.max_x)
        #Other stuff
        self.ax.grid()
        ...

    def on_running(self, xdata, ydata):
        #Update data (with the new _and_ the old points)
        self.lines.set_xdata(xdata)
        self.lines.set_ydata(ydata)
        #Need both of these in order to rescale
        self.ax.relim()
        self.ax.autoscale_view()
        #We need to draw *and* flush
        self.figure.canvas.draw()
        self.figure.canvas.flush_events()

    #Example
    def __call__(self):
        import numpy as np
        import time
        self.on_launch()
        xdata = []
        ydata = []
        for x in np.arange(0,10,0.5):
            xdata.append(x)
            ydata.append(np.exp(-x**2)+10*np.exp(-(x-7)**2))
            self.on_running(xdata, ydata)
            time.sleep(1)
        return xdata, ydata

d = DynamicUpdate()
d()
Zah
fuente
¡Si! ¡Finalmente una solución que funciona con Spyder! Lo que me faltaba era gcf (). Canvas.flush_events () después del comando draw ().
np8
Basado en este gran ejemplo, escribí un pequeño módulo de Python que permite el trazado repetitivo: github.com/lorenzschmid/dynplot
lorenzli
1
¡Un hermoso ejemplo!
vvy
Claro, conciso, versátil, flexible: esta debería ser la respuesta aceptada.
pfabri
Para usar esto en un Jupyter Notebook , debe agregar el %matplotlib notebookcomando mágico después de su declaración de importación matplotlib.
pfabri
3

Aquí hay una forma que permite eliminar puntos después de un cierto número de puntos trazados:

import matplotlib.pyplot as plt
# generate axes object
ax = plt.axes()

# set limits
plt.xlim(0,10) 
plt.ylim(0,10)

for i in range(10):        
     # add something to axes    
     ax.scatter([i], [i]) 
     ax.plot([i], [i+1], 'rx')

     # draw the plot
     plt.draw() 
     plt.pause(0.01) #is necessary for the plot to update for some reason

     # start removing points if you don't want all shown
     if i>2:
         ax.lines[0].remove()
         ax.collections[0].remove()
NDM
fuente
2

Sé que llego tarde para responder a esta pregunta, pero para tu problema, puedes consultar el paquete "joystick". Lo diseñé para trazar un flujo de datos desde el puerto serie, pero funciona para cualquier flujo. También permite el registro de texto interactivo o el trazado de imágenes (además del trazado de gráficos). No es necesario que hagas tus propios bucles en un hilo separado, el paquete se encarga de ello, solo da la frecuencia de actualización que desees. Además, el terminal permanece disponible para controlar los comandos mientras se traza. Consulte http://www.github.com/ceyzeriat/joystick/ o https://pypi.python.org/pypi/joystick (use pip install joystick para instalar)

Simplemente reemplace np.random.random () por su punto de datos real leído desde el puerto serie en el siguiente código:

import joystick as jk
import numpy as np
import time

class test(jk.Joystick):
    # initialize the infinite loop decorator
    _infinite_loop = jk.deco_infinite_loop()

    def _init(self, *args, **kwargs):
        """
        Function called at initialization, see the doc
        """
        self._t0 = time.time()  # initialize time
        self.xdata = np.array([self._t0])  # time x-axis
        self.ydata = np.array([0.0])  # fake data y-axis
        # create a graph frame
        self.mygraph = self.add_frame(jk.Graph(name="test", size=(500, 500), pos=(50, 50), fmt="go-", xnpts=10000, xnptsmax=10000, xylim=(None, None, 0, 1)))

    @_infinite_loop(wait_time=0.2)
    def _generate_data(self):  # function looped every 0.2 second to read or produce data
        """
        Loop starting with the simulation start, getting data and
    pushing it to the graph every 0.2 seconds
        """
        # concatenate data on the time x-axis
        self.xdata = jk.core.add_datapoint(self.xdata, time.time(), xnptsmax=self.mygraph.xnptsmax)
        # concatenate data on the fake data y-axis
        self.ydata = jk.core.add_datapoint(self.ydata, np.random.random(), xnptsmax=self.mygraph.xnptsmax)
        self.mygraph.set_xydata(t, self.ydata)

t = test()
t.start()
t.stop()
Guillaume S
fuente