Matplotlib链接的X轴,在缩放时与自动调整的Y轴相关

17

我如何创建一个堆叠的图表,共享x轴,并在缩放时自动调整所有“从属”图表的y轴范围?例如:

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

当我缩放 ax1 时,这会同时更新 ax2 的 x 轴(目前没问题),但我还希望 ax2 的 y 轴根据现在可见的数据范围自动调整。 所有的自动缩放设置都已开启(默认情况下也是如此)。在创建 ax2 后手动设置自动缩放设置并没有帮助:

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

print ax2.get_autoscaley_on()
-> True

我有什么遗漏吗?


2
y轴正在自动缩放,但是自动缩放考虑的是数据的完整范围,而不仅仅是当前缩放窗口中的范围。在这种情况下,您需要(半)手动设置。 - Joe Kington
@JoeKington:是的,这就是发生的事情。我可能会认为这种行为不符合最小惊讶原则。人们期望“自动缩放”应该适用于当前可见的数据,而不是一些远离屏幕的区域。 - Stefan
2个回答

26

经过研究matplotlib的axes.py的详细信息,发现没有提供根据数据视图自动缩放轴的方法,因此没有高级方法可以实现我想要的效果。

但是,有“xlim_changed”事件,可以附加回调函数:

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)

很遗憾,这是一种相当低级的hack方法,很容易出现问题(除了线条,还有反转或对数轴等其他对象...)

似乎无法钩入axes.py中的更高级别功能,因为更高级别的方法不将emit=False参数转发给set_xlim(),这需要避免set_xlim()和“xlim_changed”回调之间进入无限循环。

此外,在水平裁剪对象的垂直范围上确定统一的方法似乎不存在,因此在axes.py中处理Lines、Patches、Collections等需要单独的代码,这些都需要在回调中进行复制。

无论如何,上面的代码对我有效,因为我的绘图中只有线条,并且我对tight=True布局感到满意。看起来只需对axes.py进行少量更改,就可以更优雅地实现此功能。

编辑:

我错了,不能钩入更高级别的自动缩放功能。只需要一组特定的命令就可以正确分离x和y。我更新了代码以在y中使用高级自动缩放,这应该使它更加稳健。特别是,tight=False现在可用(看起来效果好多了),反转/对数轴不应该成为问题。

唯一剩下的问题是确定所有种类对象在裁剪到特定x范围后的数据限制。这个功能真的应该内置于matplotlib中,因为它可能需要渲染器(例如,如果缩放到屏幕上只剩下0或1个点,上面的代码将会出现问题)。Axes.relim()方法看起来是一个很好的候选者。它应该重新计算数据限制如果数据已被更改,但目前仅处理Lines和Patches。可以为Axes.relim()指定窗口中的可选参数x或y。


+1 发现 xlim_changed。我认为这是未记录的,不是吗? - bmu
@bmu:是的,我在阅读axes.py时偶然发现了它,试图找到一个可以挂钩的地方。幸好它在那里,否则我们会在重载Axes和Figure上花费很多时间... - Stefan
有任何更新吗?我需要做同样的事情,想知道现在是否存在更简洁的解决方案。 - Jason S
据我所知,axes.py仍然不支持本地化。然而,我一直在使用上面的代码变体,每天都能得到相当满意的结果。它被包装在Figure的子类中,还具有细节级别功能和上下文菜单。非常实用,但是代码太多了,无法在此处放置。 - Stefan
@Stefan,你在哪里发布了(子)类?我对Matplotlib的内部机制不是很清楚,我一直在尝试使用你的代码来更新包含每个次级轴的子图网格。有一些问题我还没有解决,所以如果你能分享一点你的魔法就太好了! - Luis E.

0

我不知道这个的正确协议,但是最近我使用了这个答案来重新调整一些时间序列数据,其中有一些填充。以下是我所做的更改,以使其能够实现。我敢打赌,在8年后肯定有更简单的方法可以做到这一点...

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()
            if np.issubdtype(x.dtype, np.datetime64):
                # convert dates to numbers so searchsorted works
                x = matplotlib.dates.date2num(x)
            # 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))

        for c in a.collections:
            for p in c.get_paths():
                vertices = p.vertices
                x, y = vertices[:, 0], vertices[:, 1]
                # x won't be sorted when you pull path vertices
                yc = y[(x >= xlim[0]) & (x <= xlim[1])]
                ylim = min(ylim[0], np.nanmin(yc)), max(ylim[1], np.nanmax(yc))

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

        # 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接