Matplotlib双刻度共享原点的坐标轴

40

我需要在Matplotlib中叠加两个具有不同Y轴刻度的数据集。 数据包含正负值。 我希望两个轴共享一个原点,但Matplotlib默认情况下不会对齐这两个比例尺。

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()

ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
plt.show()

我想可以使用.get_ylim().set_ylim()这两个函数来计算对齐两个刻度尺的值。是否有更简单的解决方案?

上面示例的输出

8个回答

62

使用 align_yaxis() 函数:

import numpy as np
import matplotlib.pyplot as plt

def align_yaxis(ax1, v1, ax2, v2):
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
    _, y1 = ax1.transData.transform((0, v1))
    _, y2 = ax2.transData.transform((0, v2))
    inv = ax2.transData.inverted()
    _, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
    miny, maxy = ax2.get_ylim()
    ax2.set_ylim(miny+dy, maxy+dy)


fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()

ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))

align_yaxis(ax1, 0, ax2, 0)
plt.show()

在此输入图片描述


1
这怎么可能是被接受的答案?它几乎肯定会截断数据。 - Douglas James Bock

25
为了确保y轴范围不变(以避免数据点偏移图表之外),并平衡调整两个y轴,我对@HYRY的答案进行了一些补充:

为了确保y轴范围不变(以避免数据点偏移图表之外),并平衡调整两个y轴,我对@HYRY的答案进行了一些补充:

def align_yaxis(ax1, v1, ax2, v2):
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
    _, y1 = ax1.transData.transform((0, v1))
    _, y2 = ax2.transData.transform((0, v2))
    adjust_yaxis(ax2,(y1-y2)/2,v2)
    adjust_yaxis(ax1,(y2-y1)/2,v1)

def adjust_yaxis(ax,ydif,v):
    """shift axis ax by ydiff, maintaining point v at the same location"""
    inv = ax.transData.inverted()
    _, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
    miny, maxy = ax.get_ylim()
    miny, maxy = miny - v, maxy - v
    if -miny>maxy or (-miny==maxy and dy > 0):
        nminy = miny
        nmaxy = miny*(maxy+dy)/(miny+dy)
    else:
        nmaxy = maxy
        nminy = maxy*(miny+dy)/(maxy+dy)
    ax.set_ylim(nminy+v, nmaxy+v)

1
请您在if/else语句上添加注释,谢谢。 我发现这种方法仍然会截断数据。 - FLab
很难在没有看到数据的情况下解决问题(因此找出为什么数据被截断)。你能提供更多信息吗?也许可以进行调试并建议编辑?(如果您进行了编辑,请在此处通知我,以便我可以接受它 - 代码编辑通常不被接受!) - drevicko
今天晚些时候我会准备一个可重现的例子。如果您能解释if/else的逻辑和重新缩放,那就太好了。谢谢。 - FLab
好的,if语句本质上是确定minymaxy的绝对值哪个更大(当它为负数时,abs(miny)才更大)。换句话说,哪个距离0点更远(实际上是距离某个值v点更远,因为你可以在某个值v处对齐)。 - drevicko
@devicko:在这里展示一个可重现的例子很困难,所以我创建了一个新的问题https://stackoverflow.com/questions/51766031/matplotlib-shared-origin-for-double-axis-without-cutting-data - FLab

19

当绘制以下两个点序列时,@drevicko的答案对我无效:

l1 = [0.03, -0.6, 1, 0.05]
l2 = [0.8,  0.9,  1,  1.1]
fig, ax1 = plt.subplots()
ax1.plot(l1)
ax2 = ax1.twinx()
ax2.plot(l2, color='r')
align_yaxis(ax1, 0, ax2, 0)

在此输入图片描述

...这是我的版本:

def align_yaxis(ax1, ax2):
    """Align zeros of the two axes, zooming them out by same ratio"""
    axes = (ax1, ax2)
    extrema = [ax.get_ylim() for ax in axes]
    tops = [extr[1] / (extr[1] - extr[0]) for extr in extrema]
    # Ensure that plots (intervals) are ordered bottom to top:
    if tops[0] > tops[1]:
        axes, extrema, tops = [list(reversed(l)) for l in (axes, extrema, tops)]

    # How much would the plot overflow if we kept current zoom levels?
    tot_span = tops[1] + 1 - tops[0]

    b_new_t = extrema[0][0] + tot_span * (extrema[0][1] - extrema[0][0])
    t_new_b = extrema[1][1] - tot_span * (extrema[1][1] - extrema[1][0])
    axes[0].set_ylim(extrema[0][0], b_new_t)
    axes[1].set_ylim(t_new_b, extrema[1][1])

原则上,有无限不同的可能性来对齐零点(或其他值,其他提供的解决方案也可以接受):无论您在 y 轴上放置零点的位置在哪里,您都可以缩放两个系列中的每一个,使其适合。我们只是选择这样一个位置,使得在变换后,两者覆盖了相同高度的垂直区间。 换句话说,我们将它们与非对齐图形相比最小化相同因子的大小。 (这并不意味着0位于图形的一半:例如,如果一个图形全为负数而另一个图形全为正数,则会发生这种情况。)

Numpy 版本:

def align_yaxis_np(ax1, ax2):
    """Align zeros of the two axes, zooming them out by same ratio"""
    axes = np.array([ax1, ax2])
    extrema = np.array([ax.get_ylim() for ax in axes])
    tops = extrema[:,1] / (extrema[:,1] - extrema[:,0])
    # Ensure that plots (intervals) are ordered bottom to top:
    if tops[0] > tops[1]:
        axes, extrema, tops = [a[::-1] for a in (axes, extrema, tops)]

    # How much would the plot overflow if we kept current zoom levels?
    tot_span = tops[1] + 1 - tops[0]

    extrema[0,1] = extrema[0,0] + tot_span * (extrema[0,1] - extrema[0,0])
    extrema[1,0] = extrema[1,1] + tot_span * (extrema[1,0] - extrema[1,1])
    [axes[i].set_ylim(*extrema[i]) for i in range(2)]

7
这里的其他答案似乎过于复杂,而且并不一定适用于所有情况(例如,ax1全部为负数,ax2全部为正数)。有两种简单的方法总是有效:
  1. 在两个y轴的中间都放置0
  2. 有点花哨,同时保留了正负比例,请见下文
def align_yaxis(ax1, ax2):
    y_lims = numpy.array([ax.get_ylim() for ax in [ax1, ax2]])

    # force 0 to appear on both axes, comment if don't need
    y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
    y_lims[:, 1] = y_lims[:, 1].clip(0, None)

    # normalize both axes
    y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
    y_lims_normalized = y_lims / y_mags

    # find combined range
    y_new_lims_normalized = numpy.array([numpy.min(y_lims_normalized), numpy.max(y_lims_normalized)])

    # denormalize combined range to get new axes
    new_lim1, new_lim2 = y_new_lims_normalized * y_mags
    ax1.set_ylim(new_lim1)
    ax2.set_ylim(new_lim2)

4

我从上面的内容开始,想出了一个解决方案,可以对齐任意数量的轴:

def align_yaxis_np(axes):
    """Align zeros of the two axes, zooming them out by same ratio"""
    axes = np.array(axes)
    extrema = np.array([ax.get_ylim() for ax in axes])

    # reset for divide by zero issues
    for i in range(len(extrema)):
        if np.isclose(extrema[i, 0], 0.0):
            extrema[i, 0] = -1
        if np.isclose(extrema[i, 1], 0.0):
            extrema[i, 1] = 1

    # upper and lower limits
    lowers = extrema[:, 0]
    uppers = extrema[:, 1]

    # if all pos or all neg, don't scale
    all_positive = False
    all_negative = False
    if lowers.min() > 0.0:
        all_positive = True

    if uppers.max() < 0.0:
        all_negative = True

    if all_negative or all_positive:
        # don't scale
        return

    # pick "most centered" axis
    res = abs(uppers+lowers)
    min_index = np.argmin(res)

    # scale positive or negative part
    multiplier1 = abs(uppers[min_index]/lowers[min_index])
    multiplier2 = abs(lowers[min_index]/uppers[min_index])

    for i in range(len(extrema)):
        # scale positive or negative part based on which induces valid
        if i != min_index:
            lower_change = extrema[i, 1] * -1*multiplier2
            upper_change = extrema[i, 0] * -1*multiplier1
            if upper_change < extrema[i, 1]:
                extrema[i, 0] = lower_change
            else:
                extrema[i, 1] = upper_change

        # bump by 10% for a margin
        extrema[i, 0] *= 1.1
        extrema[i, 1] *= 1.1

    # set axes limits
    [axes[i].set_ylim(*extrema[i]) for i in range(len(extrema))]

以下是4个随机序列的示例(您可以看到离散范围在四个独立的y轴标签集上): aligned axes


1

我需要将两个子图对齐,但不是在它们的零点上。其他解决方案对我来说并不完全适用。

我的程序主代码如下。子图没有对齐。此外,我只改变align_yaxis函数,保持所有其他代码不变。

import matplotlib.pyplot as plt

def align_yaxis(ax1, v1, ax2, v2):
  return 0

x  = range(10)
y1 = [3.2, 1.3, -0.3, 0.4, 2.3, -0.9, 0.2, 0.1, 1.3, -3.4]
y2, s = [], 100
for i in y1:
    s *= 1 + i/100
    y2.append(s)

fig = plt.figure()
ax1 = fig.add_subplot()
ax2 = ax1.twinx()

ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
ax1.bar(x, y1, color='tab:blue')
ax2.plot(x, y2, color='tab:red')

fig.tight_layout()
align_yaxis(ax1, 0, ax2, 100)
plt.show()

未对齐的子图图片

使用@HYRY的解决方案,我得到了对齐的子图,但第二个子图超出了图形。你看不到它。

def align_yaxis(ax1, v1, ax2, v2):
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
    _, y1 = ax1.transData.transform((0, v1))
    _, y2 = ax2.transData.transform((0, v2))
    inv = ax2.transData.inverted()
    _, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
    miny, maxy = ax2.get_ylim()
    ax2.set_ylim(miny+dy, maxy+dy)

没有第二个子图的图片

使用@drevicko的解决方案,我也得到了对齐的图表。但是现在第一个子图已经不在图片中了,第一个Y轴也很奇怪。

def align_yaxis(ax1, v1, ax2, v2):
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
    _, y1 = ax1.transData.transform((0, v1))
    _, y2 = ax2.transData.transform((0, v2))
    adjust_yaxis(ax2,(y1-y2)/2,v2)
    adjust_yaxis(ax1,(y2-y1)/2,v1)

def adjust_yaxis(ax,ydif,v):
    """shift axis ax by ydiff, maintaining point v at the same location"""
    inv = ax.transData.inverted()
    _, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
    miny, maxy = ax.get_ylim()
    miny, maxy = miny - v, maxy - v
    if -miny>maxy or (-miny==maxy and dy > 0):
        nminy = miny
        nmaxy = miny*(maxy+dy)/(miny+dy)
    else:
        nmaxy = maxy
        nminy = maxy*(miny+dy)/(maxy+dy)
    ax.set_ylim(nminy+v, nmaxy+v)

没有第一个子图的图片

所以我稍微调整了@drevicko的解决方案,得到了我想要的结果。

def align_yaxis(ax1, v1, ax2, v2):
    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
    _, y1 = ax1.transData.transform((0, v1))
    _, y2 = ax2.transData.transform((0, v2))
    adjust_yaxis(ax1,(y2 - y1)/2,v1)
    adjust_yaxis(ax2,(y1 - y2)/2,v2)

def adjust_yaxis(ax,ydif,v):
    """shift axis ax by ydiff, maintaining point v at the same location"""
    inv = ax.transData.inverted()
    _, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
    miny, maxy = ax.get_ylim()

    nminy = miny - v + dy - abs(dy)
    nmaxy = maxy - v + dy + abs(dy)
    ax.set_ylim(nminy+v, nmaxy+v)

我期望的子图外观


1

Tim的解决方案已经适用于多个轴:

import numpy as np

def align_yaxis(axes): 
    y_lims = np.array([ax.get_ylim() for ax in axes])

    # force 0 to appear on all axes, comment if don't need
    y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
    y_lims[:, 1] = y_lims[:, 1].clip(0, None)

    # normalize all axes
    y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
    y_lims_normalized = y_lims / y_mags

    # find combined range
    y_new_lims_normalized = np.array([np.min(y_lims_normalized), np.max(y_lims_normalized)])

    # denormalize combined range to get new axes
    new_lims = y_new_lims_normalized * y_mags
    for i, ax in enumerate(axes):
        ax.set_ylim(new_lims[i])    

0

这可能不是您正在寻找的,但这对我有所帮助,使得两个不同垂直轴上的整数对齐:

ax1.set_ylim(0,4000)
ax2.set_ylim(0,120)
ax2.set_yticks(np.linspace(ax2.get_yticks()[0], ax2.get_yticks()[-1], len(ax1.get_yticks())))


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