Matplotlib 轴具有两个尺度共享原点

问题描述 投票:0回答:9

我需要在 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()
两个对齐两个比例来执行一些计算。有没有更简单的解决方案?

Output from the sample above

matplotlib scale axis
9个回答
63
投票

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

enter image description here


25
投票

为了确保维持 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)

22
投票

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

enter image description here

...所以这是我的版本:

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

这里的其他答案似乎过于复杂,并不一定适用于所有场景(例如 ax1 都是负数,ax2 都是正数)。有 2 种始终有效的简单方法:

9
投票

始终将 0 放在两个 y 轴的图表中间

    有点花哨,在某种程度上保留了正负比,见下文
  1. 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 个随机系列的示例(您可以看到 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)

子图正如我所期望的那样

这可能不是您正在寻找的,但这帮助我将整数排列在两个不同的垂直轴上:

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

这个功能效果最好

0
投票
from mpl_axes_aligner import align align.yaxes(ax1, org1, ax2, org2, pos)

https://matplotlib-axes-aligner.readthedocs.io/en/latest/align_usage.html

© www.soinside.com 2019 - 2024. All rights reserved.