Python画截断图

近日,帮女朋友画截断图时,遇到了一些问题,网上很多资料都是互相粘贴,缺少能够解决问题的帖子,经过查看官方api最终解决了问题。
在此记录一下,也希望能够帮助其他有需要的人。

brokenaxes

这个是最方便的一种解决办法,官网地址为Pypi,官网的教程比较简洁

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_simple.png

看着效果好像还不错,但是如果要画柱状图,则如何解决呢?
仿照官方的程序,我们可以写出来以下的程序

import matplotlib.pyplot as plt
import numpy as np
from brokenaxes import brokenaxes
name_list = [ '1', '6', '30', '60']
num_list_scene1 = [1050, 1055, 1060, 1065]
num_list_scene2 = [5, 15, 25, 35]
num_list_scene3 = [10, 10, 10, 10]
num_list_scene4 = [5, 30, 60, 80]

x = list(range(len(num_list_scene1)))
total_width, n = 0.9, 6
width = total_width / n
bax = brokenaxes(ylims=((0, 110), (1050, 1120)), hspace=.03, despine=False)
bax.bar(x, num_list_scene1, width=width, label='scene1', fc='c')
for j in range(len(x)):
    x[j] = x[j] + width
bax.bar(x, num_list_scene2, width=width, label='scene2', fc='#A52A2A')
for j in range(len(x)):
    x[j] = x[j] + width
bax.bar(x, num_list_scene3, width=width, label='scene3', fc='b', )
for j in range(len(x)):
    x[j] = x[j] + width
bax.bar(x, num_list_scene4, width=width, label='scene4',tick_label=name_list, fc='#D2691E')
bax.set_xlabel('x')
bax.set_ylabel('y')
bax.legend(loc = 1)
plt.show()

得到以下图:


brokenaxes_bar.png

这个图我们可以发现存在一定的问题,它的横坐标不对了,我们的“1”显示不出来了!!!
那怎么解决呢?可以知道我们在bar()函数中使用tick_label=name_list是不可行的了,那我们试下plt.xticks函数看看
首先删除bar函数中的tick_label=name_list,然后在plt.show()前面加上plt.xticksx,name_list),得到下图结果

brokenaxes_bar_2.png

好吧,结果更差,不只存在自带的0.0,1.0的刻度,我们想要的刻度还不在正确的位置!

我们通过对比这两个图,可以发现一个很trick的解决办法!
第一个图,没有自带的刻度,但是少了一个我们想要的刻度;第二个图,存在自带的刻度,但是我们想要的刻度全都有!
我们能不能考虑结合下呢?
我们尝试在bar中加上一个空的刻度,然后再显示出来

import matplotlib.pyplot as plt
import numpy as np
from brokenaxes import brokenaxes
name_list = [ '1', '6', '30', '60']
fake_name_list = [ '', '', '', '']
num_list_scene1 = [1050, 1055, 1060, 1065]
num_list_scene2 = [5, 15, 25, 35]
num_list_scene3 = [10, 10, 10, 10]
num_list_scene4 = [5, 30, 60, 80]

x = list(range(len(num_list_scene1)))
total_width, n = 0.9, 6
width = total_width / n
bax = brokenaxes(ylims=((0, 110), (1050, 1120)), hspace=.03, despine=False)
bax.bar(x, num_list_scene1, width=width, label='scene1', fc='c')
for j in range(len(x)):
    x[j] = x[j] + width
bax.bar(x, num_list_scene2, width=width, label='scene2', fc='#A52A2A')
for j in range(len(x)):
    x[j] = x[j] + width
bax.bar(x, num_list_scene3, width=width, label='scene3', fc='b', )
for j in range(len(x)):
    x[j] = x[j] + width
bax.bar(x, num_list_scene4, width=width, label='scene4', tick_label=fake_name_list, fc='#D2691E')
bax.set_xlabel('x')
bax.set_ylabel('y')
bax.legend(loc = 1)
plt.xticks(x,name_list)
plt.show()

brokenaxes_bar_3.png

不显示自带的刻度了,但是对应的刻度小短线还存在!怎么办呢?
我们可以考虑手动调整plt.xticks(x,name_list)x的值,使得这两个短线重叠,问题就可以解决了!
只是我们这只有四个短线,如果我们要画很多个值,找让短线重叠的x值,就会做很多枯燥而又没意义的工作!

因此,这个方案在一些简单的图上还可以应用,复杂的场景就难以解决了!

subplot

matplotlib具有很强的自定义能力,我们可以考虑通过画两个子图,然后拼接成一个,来生成我们的截断图!

import matplotlib.pyplot as plt

name_list = [ '1', '6', '30', '60']
num_list_scene1 = [1050, 1055, 1060, 1065]
num_list_scene2 = [5, 15, 25, 35]
num_list_scene3 = [10, 10, 10, 10]
num_list_scene4 = [5, 30, 60, 80]

f, (ax, ax2) = plt.subplots(2, 1, sharex=True)
x = list(range(len(num_list_scene1)))
total_width, n = 0.9, 6
width = total_width / n
ax.bar(x, num_list_scene1, width=width, label='scene1', fc='c')
for j in range(len(x)):
    x[j] = x[j] + width
ax.bar(x, num_list_scene2, width=width, label='scene2', fc='#A52A2A')
for j in range(len(x)):
    x[j] = x[j] + width
ax.bar(x, num_list_scene3, width=width, label='scene3', fc='b', align = 'center')
for j in range(len(x)):
    x[j] = x[j] + width
ax.bar(x, num_list_scene4, width=width, label='scene4', fc='#D2691E')

x = list(range(len(num_list_scene1)))
ax2.bar(x, num_list_scene1, width=width, label='scene1', fc='c')
for j in range(len(x)):
    x[j] = x[j] + width
ax2.bar(x, num_list_scene2, width=width, label='scene2', fc='#A52A2A')
for j in range(len(x)):
    x[j] = x[j] + width
ax2.bar(x, num_list_scene3, width=width, label='scene3', fc='b')
for j in range(len(x)):
    x[j] = x[j] + width
ax2.bar(x, num_list_scene4, width=width, label='scene4', tick_label=name_list, fc='#D2691E')

ax.set_ylim(1050, 1120)  # outliers only
ax2.set_ylim(0, 110)  # most of the data

ax.spines['bottom'].set_visible(False)
ax2.spines['top'].set_visible(False)
ax.xaxis.tick_top()
ax.tick_params(labeltop='off')  # don't put tick labels at the top
ax2.xaxis.tick_bottom()
d = .015

kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((-d, +d), (-d, +d), **kwargs)        # top-left diagonal
ax.plot((1 - d, 1 + d), (-d, +d), **kwargs)  # top-right diagonal

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

plt.subplots_adjust(hspace=0.1)
plt.xlabel('x')
plt.ylabel('y')
plt.show()

看着结果似乎还不错,坐标的刻度也显示正常啦


subplot_1.png

只是,由于我们使用subplot画出来的两个图默认大小是一样的,而前文使用brokenaxes画出来的截断上下的长度并不一样。我们这样画出来的图不好看,还需要调整下!
考虑到matplotlib可以使用grid来画图,我们尝试用它来调整下上下两图的比例!

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

name_list = [ '1', '6', '30', '60']
num_list_scene1 = [1050, 1055, 1060, 1065]
num_list_scene2 = [5, 15, 25, 35]
num_list_scene3 = [10, 10, 10, 10]
num_list_scene4 = [5, 30, 60, 80]

a1,a2=2,3
gs = gridspec.GridSpec(2, 1,height_ratios=[a1,a2],hspace=0.1)
ax = plt.subplot(gs[0,0:])
ax2 = plt.subplot(gs[1,0:], sharex=ax)

x = list(range(len(num_list_scene1)))
total_width, n = 0.9, 6
width = total_width / n
ax.bar(x, num_list_scene1, width=width, label='scene1', fc='c')
for j in range(len(x)):
    x[j] = x[j] + width
ax.bar(x, num_list_scene2, width=width, label='scene2', fc='#A52A2A')
for j in range(len(x)):
    x[j] = x[j] + width
ax.bar(x, num_list_scene3, width=width, label='scene3', fc='b', align = 'center')
for j in range(len(x)):
    x[j] = x[j] + width
ax.bar(x, num_list_scene4, width=width, label='scene4', fc='#D2691E')

x = list(range(len(num_list_scene1)))
ax2.bar(x, num_list_scene1, width=width, label='scene1', fc='c')
for j in range(len(x)):
    x[j] = x[j] + width
ax2.bar(x, num_list_scene2, width=width, label='scene2', fc='#A52A2A')
for j in range(len(x)):
    x[j] = x[j] + width
ax2.bar(x, num_list_scene3, width=width, label='scene3', fc='b')
for j in range(len(x)):
    x[j] = x[j] + width
ax2.bar(x, num_list_scene4, width=width, label='scene4', tick_label=name_list, fc='#D2691E')

ax.set_ylim(1050, 1120)  # outliers only
ax2.set_ylim(0, 110)  # most of the data

ax.spines['bottom'].set_visible(False)
ax2.spines['top'].set_visible(False)
ax.xaxis.tick_top()
ax.tick_params(labeltop='off')  # don't put tick labels at the top
ax2.xaxis.tick_bottom()
d = .015

oa1,oa2=(a1+a2)/a1,(a1+a2)/a2
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((-d, +d), (-d*oa1, +d*oa1), **kwargs)        # top-left diagonal
ax.plot((1 - d, 1 + d), (-d*oa1, +d*oa1), **kwargs)  # top-right diagonal

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

plt.xlabel('x')
plt.ylabel('y')
plt.show()

画出来的结果好看多了!

subplot_2.png

不对,还有问题!我们的ylabel不在图片的正中间呀!
在网上查了好久,都说的set_position,但是这个参数很简陋,只有几个位置参数,没法进行详细的定义。最后发现官方的api提供的改变位置的函数set_label_coords
那我们就使用起来吧!
plt.ylabel('y')改为

ax2.set_ylabel("y")
ax2.yaxis.set_label_coords(-0.1,1)

重新画一下图


subplot_3.png

问题解决了!
我们得到了一个完整的图!

参考文献:

  1. 不想开手动挡了
  2. Python 作图实现坐标轴截断
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容