2012-06-18 15 views
16

¿Cómo puedo crear una pila de tramas con ejes x (vinculados) que autoescalen los ejes y de todos los trazados "esclavos" durante el zoom? Por ejemplo:matplotlib vinculados x ejes con ejes y de escalado automático en el zoom

import matplotlib.pyplot as plt 
fig = plt.figure() 
ax1 = fig.add_subplot(211) 
ax2 = fig.add_subplot(212, sharex=ax1) 
ax1.plot([0,1]) 
ax2.plot([2,1]) 
plt.show() 

Cuando hago zoom AX1, esto actualiza los ejes x de ax2 así (hasta ahora tan bueno), pero también quiero que los ejes y de ax2 a AutoScale basado en el rango de datos ahora visibles . Todas las configuraciones de autoescala están encendidas (como es el predeterminado). No ayudó establecer manualmente la configuración de autoescala después de crear ax2:

ax2.autoscale(enable=True, axis='y', tight=True) 
ax2.autoscale_view(tight=True, scalex=False, scaley=True) 

print ax2.get_autoscaley_on() 
-> True 

¿Echo de menos algo?

+2

El eje y se está escalando automáticamente, pero el autoescalado tiene en cuenta el rango _full_ de los datos, no solo el rango en la ventana de zoom actual. Tendrá que configurar cosas (semi) manualmente, en este caso. –

+0

@JoeKington: Sí, esto es lo que sucede. Podría argumentar que este comportamiento no corresponde al Principio de Menos Asombro. Uno de ellos sería que la "escala automática" debería aplicarse a los datos visibles actualmente, no a una región lejana a la pantalla. – Stefan

Respuesta

21

Después de estudiar los detalles sangrientos de matplotlib's axes.py, parece que no hay disposiciones para autoescalar un eje basado en una vista de los datos, por lo que no hay una forma de alto nivel para lograr lo que quería.

Sin embargo, hay eventos 'xlim_changed', a la que se puede adjuntar una devolución de llamada:

import numpy as np 

def on_xlim_changed(ax): 
    xlim = ax.get_xlim() 
    for a in ax.figure.axes: 
     # shortcuts: last avoids n**2 behavior when each axis fires event 
     if a is ax or len(a.lines) == 0 or getattr(a, 'xlim', None) == xlim: 
      continue 

     ylim = np.inf, -np.inf 
     for l in a.lines: 
      x, y = l.get_data() 
      # faster, but assumes that x is sorted 
      start, stop = np.searchsorted(x, xlim) 
      yc = y[max(start-1,0):(stop+1)] 
      ylim = min(ylim[0], np.nanmin(yc)), max(ylim[1], np.nanmax(yc)) 

     # TODO: update limits from Patches, Texts, Collections, ... 

     # x axis: emit=False avoids infinite loop 
     a.set_xlim(xlim, emit=False) 

     # y axis: set dataLim, make sure that autoscale in 'y' is on 
     corners = (xlim[0], ylim[0]), (xlim[1], ylim[1]) 
     a.dataLim.update_from_data_xy(corners, ignore=True, updatex=False) 
     a.autoscale(enable=True, axis='y') 
     # cache xlim to mark 'a' as treated 
     a.xlim = xlim 

for ax in fig.axes: 
    ax.callbacks.connect('xlim_changed', on_xlim_changed) 

Por desgracia, esto es un truco muy bajo nivel, que se rompen con facilidad (otros objetos que líneas, revirtió o log de los ejes, ...)

Parece que no es posible conectar la funcionalidad de nivel superior en axes.py, ya que los métodos de nivel superior no envían el argumento emitir = Falso a set_xlim(), que es obligatorio para evitar ingresar un bucle infinito entre set_xlim() y la devolución de llamada 'xlim_changed'.

Además, parece que no hay una forma unificada para determinar la extensión vertical de un objeto recortado horizontalmente, por lo que hay un código separado para manejar Líneas, Patches, Colecciones, etc. en axes.py, que todos deberían ser replicado en la devolución de llamada.

En cualquier caso, el código anterior funcionó para mí, ya que solo tengo líneas en mi trazado y estoy contento con el diseño tight = True. Parece que con solo unos pocos cambios en axes.py uno podría acomodar esta funcionalidad mucho más elegantemente.

Editar:

que estaba equivocado acerca de no ser capaz de conectar a la funcionalidad de escala automática de nivel superior. Solo requiere un conjunto específico de comandos para separar correctamente x e y. Actualicé el código para usar el autoescalado de alto nivel en y, lo que debería hacerlo significativamente más robusto. En particular, tight = False ahora funciona (se ve mucho mejor después de todo), y los ejes inverted/log no deberían ser un problema.

El único problema restante es la determinación de los límites de datos para todo tipo de objetos, una vez recortada a una extensión x específica. Esta funcionalidad debería estar incorporada en matplotlib, ya que puede requerir el renderizador (por ejemplo, el código anterior se romperá si uno amplía lo suficiente como para que solo 0 o 1 punto permanezcan en la pantalla). El método Axes.relim() parece un buen candidato. Se supone que debe volver a calcular los límites de datos si los datos se han cambiado, pero actualmente solo maneja Líneas y parches. Podría haber argumentos opcionales para Axes.relim() que especifiquen una ventana en x o y.

+0

+1 para "descubrir" 'xlim_changed'. Creo que esto no está documentado, ¿no es así? – bmu

+0

@bmu: Sí, lo encontré leyendo axes.py, tratando de encontrar un lugar para engancharme. Lo bueno es que está ahí, de lo contrario nos divertiríamos muchísimo sobrecargando Axes y Figure ... – Stefan

+0

¿Alguna actualización? Necesito hacer lo mismo + me preguntaba si existe una solución más limpia ahora. –

Cuestiones relacionadas