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.
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. –
@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