Matplotlib中有没有办法制作不连续的坐标轴?

92

我正在尝试使用pyplot创建一个具有不连续x轴的图表。通常情况下,这是这样绘制的:

(数值)----//----(后面的数值)

//表示你跳过了(数值)和(后面的数值)之间的所有内容。

我没有找到任何关于此的示例,所以我想知道是否可能实现。我知道您可以将数据连接起来,例如金融数据,但我想使轴上的跳跃更加明显。目前,我只是使用子图,但我真的很希望最终能在同一张图表上呈现所有内容。


1
请参考matplotlib文档:破碎轴 - Trenton McKinney
7个回答

87
保罗的回答是完成此操作的完美方法。
但是,如果您不想制作自定义转换,可以使用两个子图来创建相同的效果。
不必从头开始组合示例,matplotlib示例中有一个由Paul Ivanov编写的优秀示例(它只在当前git提示中,因为它是几个月前才提交的。 它还没有在网页上)。
这只是对此示例的简单修改,使其具有不连续的x轴而不是y轴。 (这就是为什么我将此帖子设置为CW)
基本上,您只需执行以下操作:
import matplotlib.pylab as plt
import numpy as np

# If you're not familiar with np.r_, don't worry too much about this. It's just 
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)

# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

plt.show()

enter image description here

为了添加破折线轴线的效果,我们可以这样做(再次修改自Paul Ivanov的示例):

import matplotlib.pylab as plt
import numpy as np

# If you're not familiar with np.r_, don't worry too much about this. It's just 
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)

# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

# This looks pretty good, and was fairly painless, but you can get that
# cut-out diagonal lines look with just a bit more work. The important
# thing to know here is that in axes coordinates, which are always
# between 0-1, spine endpoints are at these locations (0,0), (0,1),
# (1,0), and (1,1). Thus, we just need to put the diagonals in the
# appropriate corners of each of our axes, and so long as we use the
# right transform and disable clipping.

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d),(-d,+d), **kwargs) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal

kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal

# What's cool about this is that now if we vary the distance between
# ax and ax2 via f.subplots_adjust(hspace=...) or plt.subplot_tool(),
# the diagonal lines will move accordingly, and stay right at the tips
# of the spines they are 'breaking'

plt.show()

enter image description here


15
我也说不出更好的了 ;) - Paul Ivanov
3
只有在子图比例为1:1时,制作“//”效果的方法才能正常工作。你知道如何使它能够适用于任何比例,例如使用GridSpec(width_ratio=[n,m])吗? - Frederick Nord
太棒了。通过小的修改,这可以适用于任意数量的x轴部分。 - Christian Madsen
Frederick Nord是正确的。此外,“/”效果不会抑制正常的勾号,这在美学上是不协调的。 - ifly6

31

我看到有很多人对此功能提出了建议,但没有迹象表明它已经被实现。这里是一个可行的解决方案,目前可以使用。它将阶跃函数变换应用于x轴。虽然代码很长,但它相当简单,因为大部分都是样板定制比例尺的内容。我没有添加任何图形来指示断点的位置,因为那是一个风格问题。祝你完成工作顺利。

from matplotlib import pyplot as plt
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
import numpy as np

def CustomScaleFactory(l, u):
    class CustomScale(mscale.ScaleBase):
        name = 'custom'

        def __init__(self, axis, **kwargs):
            mscale.ScaleBase.__init__(self)
            self.thresh = None #thresh

        def get_transform(self):
            return self.CustomTransform(self.thresh)

        def set_default_locators_and_formatters(self, axis):
            pass

        class CustomTransform(mtransforms.Transform):
            input_dims = 1
            output_dims = 1
            is_separable = True
            lower = l
            upper = u
            def __init__(self, thresh):
                mtransforms.Transform.__init__(self)
                self.thresh = thresh

            def transform(self, a):
                aa = a.copy()
                aa[a>self.lower] = a[a>self.lower]-(self.upper-self.lower)
                aa[(a>self.lower)&(a<self.upper)] = self.lower
                return aa

            def inverted(self):
                return CustomScale.InvertedCustomTransform(self.thresh)

        class InvertedCustomTransform(mtransforms.Transform):
            input_dims = 1
            output_dims = 1
            is_separable = True
            lower = l
            upper = u

            def __init__(self, thresh):
                mtransforms.Transform.__init__(self)
                self.thresh = thresh

            def transform(self, a):
                aa = a.copy()
                aa[a>self.lower] = a[a>self.lower]+(self.upper-self.lower)
                return aa

            def inverted(self):
                return CustomScale.CustomTransform(self.thresh)

    return CustomScale

mscale.register_scale(CustomScaleFactory(1.12, 8.88))

x = np.concatenate((np.linspace(0,1,10), np.linspace(9,10,10)))
xticks = np.concatenate((np.linspace(0,1,6), np.linspace(9,10,6)))
y = np.sin(x)
plt.plot(x, y, '.')
ax = plt.gca()
ax.set_xscale('custom')
ax.set_xticks(xticks)
plt.show()

输入图像描述


我想现在只能这样了。这将是我第一次尝试自定义轴,所以我们只能看看它会怎样。 - Justin S
你能添加几行代码展示如何使用你的类吗? - Ruggero Turra
@RuggeroTurra 在我的示例中已经全部包含了。你可能只需要滚动到代码块的底部。 - Paul
2
这个例子在我的matplotlib 1.4.3上无法工作:http://imgur.com/4yHa9be。看起来这个版本只识别`transform_non_affine`而不是`transform`。请参考我的补丁:http://stackoverflow.com/a/34582476/1214547。 - Pastafarianist
值得注意的是,如果您想制作一条线图,那么这种方法将会错误地连接断点处的单个线段,而此处的另一个答案(https://dev59.com/JW035IYBdhLWcg3wC7gR#5669301)则不会。 - Gus
显示剩余2条评论

31

查看brokenaxes包:

import matplotlib.pyplot as plt
from brokenaxes import brokenaxes
import numpy as np

fig = plt.figure(figsize=(5,2))
bax = brokenaxes(
    xlims=((0, .1), (.4, .7)),
    ylims=((-1, .7), (.79, 1)),
    hspace=.05
)
x = np.linspace(0, 1, 100)
bax.plot(x, np.sin(10 * x), label='sin')
bax.plot(x, np.cos(10 * x), label='cos')
bax.legend(loc=3)
bax.set_xlabel('time')
bax.set_ylabel('value')

来自brokenaxes的示例


在安装后,无法在Pycharm Community 2016.3.2中导入“from brokenaxes import brokenaxes”。@ben.dichter - Yushan ZHANG
2
有一个错误,我已经修复了。请运行 pip install brokenaxes==0.2 来安装修复后的代码版本。 - ben.dichter
似乎与ax.grid(True)交互不良。 - innisfree
1
破折线能压制刻度吗?或者在水平方向上更接近地格式化轴? - ifly6
1
嗨,Ben,我想要移除y轴,但是我尝试了很多命令,但是与brokenaxes组合使用时无法正常工作,(请注意x轴是断裂轴),谢谢。 - user3737702
显示剩余2条评论

4
一个非常简单的技巧是:
  1. 在坐标轴的脊柱上散布矩形并
  2. 在该位置绘制 "//" 作为文本。
对我来说效果非常好。
# FAKE BROKEN AXES
# plot a white rectangle on the x-axis-spine to "break" it
xpos = 10 # x position of the "break"
ypos = plt.gca().get_ylim()[0] # y position of the "break"
plt.scatter(xpos, ypos, color='white', marker='s', s=80, clip_on=False, zorder=100)
# draw "//" on the same place as text
plt.text(xpos, ymin-0.125, r'//', fontsize=label_size, zorder=101, horizontalalignment='center', verticalalignment='center')

示例绘图:在matplotlib python中伪造破碎/不连续的轴


2
对于那些感兴趣的人,我已经扩展了@Paul的答案并将其添加到matplotlib包装器proplot中。它可以进行轴"跳跃"、"加速"和"减速"
目前还没有办法添加像Joe的答案中那样指示离散跳跃的"十字架",但我计划在未来添加这个功能。我还计划添加一个默认的"刻度定位器",根据CutoffScale参数设置合理的默认刻度位置。

链接已经失效了。 :-( - Thomas Hilger
也许这个可以吗?https://proplot.readthedocs.io/en/stable/ - Thomas Hilger
修复了链接! - Luke Davis

1

针对Frederick Nord的问题,如何在使用不等于1:1比率的网格规范时启用对角线“断裂”线的并行方向,可以根据Paul Ivanov和Joe Kingtons的建议进行以下更改。宽度比例可以使用变量n和m进行变化。

import matplotlib.pylab as plt
import numpy as np
import matplotlib.gridspec as gridspec

x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

n = 5; m = 1;
gs = gridspec.GridSpec(1,2, width_ratios = [n,m])

plt.figure(figsize=(10,8))

ax = plt.subplot(gs[0,0])
ax2 = plt.subplot(gs[0,1], sharey = ax)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace = 0.1)

ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

ax.set_xlim(0,1)
ax2.set_xlim(10,8)

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)

on = (n+m)/n; om = (n+m)/m;
ax.plot((1-d*on,1+d*on),(-d,d), **kwargs) # bottom-left diagonal
ax.plot((1-d*on,1+d*on),(1-d,1+d), **kwargs) # top-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d*om,d*om),(-d,d), **kwargs) # bottom-right diagonal
ax2.plot((-d*om,d*om),(1-d,1+d), **kwargs) # top-right diagonal

plt.show()

0

这是一种针对x轴中断的hacky但相当不错的解决方案。

该解决方案基于https://matplotlib.org/stable/gallery/subplots_axes_and_figures/broken_axis.html,它消除了在脊柱上方定位中断的问题,这个问题可以通过如何使用matplotlib绘制点使其出现在脊柱的上方?来解决。

from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt

def axis_break(axis, xpos=[0.1, 0.125], slant=1.5):
    d = slant  # proportion of vertical to horizontal extent of the slanted line
    anchor = (xpos[0], -1)
    w = xpos[1] - xpos[0]
    h = 1

    kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12, zorder=3,
                linestyle="none", color='k', mec='k', mew=1, clip_on=False)
    axis.add_patch(Rectangle(
        anchor, w, h, fill=True, color="white",
        transform=axis.transAxes, clip_on=False, zorder=3)
    )
    axis.plot(xpos, [0, 0], transform=axis.transAxes, **kwargs)

fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
axis_break(ax, xpos=[0.1, 0.12], slant=1.5)
axis_break(ax, xpos=[0.3, 0.31], slant=-10)

axis break pyplot

如果你想替换一个轴标签,可以使用以下方法:
from matplotlib import ticker

def replace_pos_with_label(fig, pos, label, axis):
    fig.canvas.draw()  # this is needed to set up the x-ticks
    labs = axis.get_xticklabels()
    labels = []
    locs = []
    for text in labs:
        x = text._x
        lab = text._text

        if x == pos:
            lab = label

        labels.append(lab)
        locs.append(x)
        
    axis.xaxis.set_major_locator(ticker.FixedLocator(locs))
    axis.set_xticklabels(labels)

fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
replace_pos_with_label(fig, 0, "-10", axis=ax)
replace_pos_with_label(fig, 6, "$10^{4}$", axis=ax)
axis_break(ax, xpos=[0.1, 0.12], slant=2)

broken axisi with new labels


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