Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

matplotlib linked x axes with autoscaled y axes on zoom

How can I create a stack of plots with linked (shared) x axes that autoscale the y axes of all "slave" plots during zoom? For example:

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()

When I zoom in ax1, this updates the x axes of ax2 as well (so far so good), but I also want the y axes of ax2 to autoscale based on the now visible data range. All autoscale settings are on (as is the default). It didn't help to manually set the autoscale settings after creating ax2:

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

print ax2.get_autoscaley_on()
-> True

Did I miss something?

like image 866
Stefan Avatar asked Jun 18 '12 16:06

Stefan


People also ask

How do I zoom in x axis MatPlotLib?

The point under your mouse when you begin the zoom remains stationary, allowing you to zoom to an arbitrary point in the figure. You can use the modifier keys 'x', 'y' or 'CONTROL' to constrain the zoom to the x axis, the y axis, or aspect ratio preserve, respectively.


1 Answers

After studying the gory details of matplotlib's axes.py, it appears that there are no provisions to autoscale an axes based on a view of the data, so there is no high-level way to achieve what I wanted.

However, there are 'xlim_changed' events, to which one can attach a callback:

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)

Unfortunately, this is a pretty low-level hack, which will break easily (other objects than Lines, reversed or log axes, ...)

It appears not possible to hook into the higher level functionality in axes.py, since the higher-level methods do not forward the emit=False argument to set_xlim(), which is required to avoid entering an infinite loop between set_xlim() and the 'xlim_changed' callback.

Moreover, there appears to be no unified way to determine the vertical extent of a horizontally cropped object, so there is separate code to handle Lines, Patches, Collections, etc. in axes.py, which would all need to be replicated in the callback.

In any case, the code above worked for me, since I only have lines in my plot and I am happy with the tight=True layout. It appears that with just a few changes to axes.py one could accommodate this functionality much more elegantly.

Edit:

I was wrong about not being able to hook into the higher-level autoscale functionality. It just requires a specific set of commands to properly separate x and y. I updated the code to use high-level autoscaling in y, which should make it significantly more robust. In particular, tight=False now works (looks much better after all), and reversed/log axes shouldn't be a problem.

The one remaining issue is the determination of the data limits for all kinds of objects, once cropped to a specific x extent. This functionality should really be built-in matplotlib, since it may require the renderer (for example, the code above will break if one zooms in far enough that only 0 or 1 points remain on screen). The method Axes.relim() looks like a good candidate. It is supposed to recalculate the data limits if the data have been changed, but presently handles only Lines and Patches. There could be optional arguments to Axes.relim() that specify a window in x or y.

like image 167
Stefan Avatar answered Oct 14 '22 07:10

Stefan