The ultimate matplotlib tutorial (1)
1 Overview
在开始之前先声明一下本文宗旨:
- 本文目标是画布以及子图框架的搭建,并不涉及具体图形,比如散点图,条形图等。
- 画图就像堆积木,一点点『添加』最终成型。所以在 matplotlib 中,我推荐使用
add
和set
的写法。一点点添加元素,直到最终输出图形。 - 本文一个图形对应一段完整的代码(除了导入第三方库)。
- 初学者请先坚决抛弃
pyplot
交互模块(explicit interfaces),或许会有些不便,但对 matplotlib 整体框架的把握有非常大的好处。 - 本文之所以不得不使用
pyplot
是因为要输出图形,但在 Sec. 8 中给出了不使用pyplot
的写法。
Matplotlib 中有 124 个 模块, 604 个类,4899 个方法,840 个属性。我们不可能全部将其掌握,只能从更高的层次上整体把握其思想。
在 matplotlib 中图形的最高层次是 figure
(先不管 Artist
这种复杂的概念), 所有的元素都在 figure
中叠加。figure
上可以在不同的位置添加不同的子图(subplot
),然后子图中有坐标轴(xaxis
, yaxis
),坐标轴上有轴标签(xlabel
, ylabel
)、刻度线(xtick
, ytick
)和刻度线标签(xticklabels
,yticklabels
),别忘了还有图例(legend
)。最后给子图分配合适的位置,整个 figure
就算是搭起来了。
接下来要做的是对以上元素进行细致的设置,每一个元素都有非常多细致的设置,掌握一些常用的命令,我认为足够做出出版级别(publication-quality)的图,当然了,这也是官方文档的说法。
在开始之前先导入要用的程序包,在此再啰嗦一句,初学者请抛弃那种便利但混杂的plt
写法:
2 Figure
想象我们作图时需要什么?最重要的当然是作图的载体,也就是纸。在 matplotlib 作图时同样如此,只不过在此大家习惯称之为画布(figure
),画布本身又含有许多元素,比如整个画布的大小(figsize
);还有你是选择白纸还是黑纸,对应到 matplotlib 中便是背景色(facecolor
)了;画布有边框(frameon
)吧,边框是线,线有自己的固有属性,比如颜色(edgecolor
)和线宽(linewidth
),至此画布的物理状态已经被确定了。
画布设置好后,就可以画图了,matplotlib 中一张画布可以有多张子图(add_subplot
),既然有多张子图,就会涉及到子图的定位和排列问题,官方文档 也给出了很多示例。在介绍多子图之前,先介绍单图的情况,即一张画布只有一张图形:
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)
ax1.plot(x, y)
plt.show()
2.1 General settings
子图框架设置好以后,便可对其标题(title
)、边框(frameon
)、背景(facecolor
)、坐标轴标签(xlabel
, ylabel
)、坐标轴范围(xlim
, ylim
, xmargin
, ymargin
)、刻度线(xticks
, yticks
)、刻度线标签(xticklabels
, yticklabels
)、网格(grid
)、图例(legend
)做出细致的设置了。设置方法分为两种:
- 直接在
add_subplot()
时设置。 - 采用
set_title()
之类的方法设置。
前者更直接方便,但不可设置其属性,只能使用默认值,后者反之。
在子图的设置中,标题、边框、背景、坐标轴标签、坐标轴范围、网格的设置相对比较简单,下面一起说明。刻度线、图例则之后分别说明。
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)
ax1 = fig.add_subplot(
111,
title="Subplot1",
frameon=True, # 坐标轴边框
facecolor="white", # 背景色
xlabel="$x$", # 坐标轴标签
ylabel="$y$",
xlim=(-10, 10), # 坐标轴范围
ylim=(-1, 1),
xticks=[-10, -6, -2, 2, 6, 10],
xticklabels=["-ten", "-six", "-two", "two", "six", "ten"],
xmargin=0.1, # 自动根据数据范围扩充坐标轴范围,会覆盖 xlim
ymargin=0.1,
)
ax1.grid(which="both", color="green", linestyle="--", linewidth=0.5)
ax1.plot(x, y, color="red")
ax1.plot(x, z, color="green")
plt.show()
Fig. 2 中虽然给出了grid
的简单示例,但一般不推荐大家使用该元素。
2.2 Tick
我觉得 tick
是整个图形中最复杂的部分,它包括刻度线样式和刻度线标签字体样式(tick_params
)的定义之外,还包括对刻度线的定位(locator
)和刻度线标签的显示(formatter
)两个重要的部分。
此处列出地已基本足够科技论文出图的元素,更细致的设置请参考官方文档。
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
# 对主刻度线进行设置
ax1.tick_params(
axis="both", # 同时对横纵轴设置
which="major", # 设置针对主刻度
direction="inout", # 刻度线方向
length=6, # 长度
width=2, # 宽度
color="red", # 刻度线颜色
labelsize=18, # 刻度标签字体大小
pad=20,
)
ax1_xmajor, ax1_ymajor = 4, 0.5
ax1_xmajor_locator = mpl.ticker.MultipleLocator(ax1_xmajor) # 设置主刻度间隔
ax1_ymajor_locator = mpl.ticker.MultipleLocator(ax1_ymajor)
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
ax1.yaxis.set_major_locator(ax1_ymajor_locator)
# 对副刻度线进行设置
ax1.tick_params(
axis="both", which="minor", direction="in", length=4, width=1, color="black"
)
ax1_xminor_locator = mpl.ticker.MultipleLocator(ax1_xmajor / 2) # 设置副刻度间隔
ax1_yminor_locator = mpl.ticker.MultipleLocator(ax1_ymajor / 2)
ax1.xaxis.set_minor_locator(ax1_xminor_locator)
ax1.yaxis.set_minor_locator(ax1_yminor_locator)
x = np.linspace(-10, 10, 500)
y = np.sin(x)
z = np.cos(x)
ax1.plot(x, y)
ax1.plot(x, z)
plt.show()
Locator
控制刻度线位置的元素叫locator
,也就是定位器。大部分情况下,默认的定位器是无法满足需求的,要想更细致地调节刻度显示,就需要重写 set_major_locator()
方法。这部分是我觉得在整个 matplotlib 中的一个难点,所以在此尽可能写的详细一些。
一般根据默认设置,就可以得到一个还不错的刻度线显示,但是如果要更加精细地对齐控制,就需要阅读一些源码了。作为示例,看一下 IndexLocator(Locator)
的源码:
class IndexLocator(Locator):
"""
Place a tick on every multiple of some base number of points
plotted, e.g., on every 5th point. It is assumed that you are doing
index plotting; i.e., the axis is 0, len(data). This is mainly
useful for x ticks.
"""
def __init__(self, base, offset):
"""Place ticks every *base* data point, starting at *offset*."""
self._base = base
self.offset = offset
def set_params(self, base=None, offset=None):
"""Set parameters within this locator"""
if base is not None:
self._base = base
if offset is not None:
self.offset = offset
def __call__(self):
"""Return the locations of the ticks"""
dmin, dmax = self.axis.get_data_interval()
return self.tick_values(dmin, dmax) # Log at WARNING level if *locs* is longer than `Locator.MAXTICKS`.
def tick_values(self, vmin, vmax):
return self.raise_if_exceeds(
np.arange(vmin + self.offset, vmax + 1, self._base))
方法:
set_params(self, base=None, offset=None)
: 获取起始位置和偏移。__call__(self)
: 如果超过刻度线太多,抛出异常提示。tick_values(self, vmin, vmax)
: 返回刻度线列表。
其中重点看一下np.arange(vmin + self.offset, vmax + 1, self._base)
,这一句的意思就是根据提供的起始值,按照步长扩展到整个刻度线列表。
接下来演示一下其它几种定位器。
NullLocator
不显示刻度线:
FixedLocator
显示给定的刻度线:
AutoLocator
从 [1, 2, 2.5, 5, 10]
以及其倍数中自动寻找合适的步长:
MultipleLocator
以给定步长生成刻度线:
LinearLocator
和 MultipleLocator
相反,LinearLocator
是根据给定的刻度线数自动算步长:
LogLocator
当图形和对数有关时就需要用到 LogLocator
了:
Formatter
定位器选好后,就可以在特定的刻度线上显示特定的刻度线标签了,这也是非常重要的一部分内容。
NullFormatter
NullFormatter()
直接将刻度线标签设置为空,但是刻度线依旧显示:
FixedFormatter
FixedFormatter(seq)
需要和 FixedLocator()
搭配使用,一切都是自己指定:
FormatStrFormatter
FormatStrFormatter(fmt)
是一个比较有意思的功能,可以在原刻度线显示的基础上添加特定的前缀或后缀,用 %d
表示原刻度标签,比如在此基础上给它一个漂亮国的到乐前缀:
PercentFormatter
PercentFormatter(xmax, decimals, symbol, is_latex)
中 xmax
为显示的最大值,decimals
为显示的精度,symbol
为百分比号的显示形式,is_latex
是否为 LaTex 格式。事实上将百分比号换成其它符号也可以。
LogFormatter
LogFormatter(base, labelOnlyBase, minor_thresholds, linthresh)
中 base
是对数底数
FuncFormatter
2.3 Legend
legend
的设置十分丰富,必要时建议查阅官方文档进行细致地调控。在此仅列出一些重要参数,特别要注意的是,legend
要放在 label
后面,否则无法正常显示(其原因也不难理解,知道标签后才能给出图例):
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$", ylim=(-1.5, 2.5))
x = np.linspace(-10, 10, 500)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) + np.cos(x)
y4 = np.sin(x) - np.cos(x)
ax1.plot(x, y1, label="$\sin(x)$", color="red")
ax1.plot(x, y2, label="$\cos(x)$", color="green")
ax1.plot(x, y3, label="$\sin(x)+\cos(x)$", color="blue")
ax1.plot(x, y4, label="$\sin(x)-\cos(x)$", color="orange")
ax1.legend(
loc="upper right", # 位置
frameon=True, # 边框
edgecolor="black", # 边框颜色
borderpad=2, # 边框和图例的距离
ncol=2, # 一行有几列
)
plt.show()
2.4 Spines
Spines 的中文翻译是轴脊线,它控制着子图的边框设置,比如边框的显示(visuable
),颜色(color
),线宽(linewidth
)等,更多的设置参考官方文档。
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
ax1.spines["left"].set_color("red") # 左轴颜色为红色
ax1.spines["left"].set_linewidth(5) # 左轴线宽为红色
ax1.spines["right"].set_visible(False) # 设置右轴不可见
ax1.spines["top"].set_visible(False)
x = np.linspace(-10, 10, 500)
y = np.sin(x)
ax1.plot(x, y)
实际上坐标轴只能设置为不可见,即 set_visible(False)
,不可以实际删除。
3 Subfigure
以上在单图之外,我们往往需要的是整齐排列的子图,(2, 2)
排列也好 (2, 3)
排列也好,总之想要得到的是规整的子图排列。假如想要的是 4 个子图,排列为 (2, 2)
,此时就可以使用 add_subplot(22i)
直接添加,其中 i
是按横向数第 i
个子图,matplotlib 会很贴心地按照 4 等分将子图自动定位:
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")
x = np.linspace(0, 10, 100)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()
3.1 Subplots_adjust
从 Fig. 6 中可看出默认子图间横纵都有间隔,既然有间隔,那就可以调,在 matplotlib 中用 subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=None)
调节子图边框(不包含刻度线、刻度线标签和坐标轴标签)和整个画布的关系,基准点为画布的左下。比如:
left = 0.1
:子图区域的最左边框和画布左边框的间距为画布宽度的 \(0.1\)。right = 1
:子图区域的最右边框和画布右边框重合。bottom = 0.1
: 子图区域的最下边框和画布最下边框的间距为画布高度的 \(0.1\)。wspace = 0.25
:子图间的横向间距为子图平均宽度的 \(0.25\)。hspace = 0.25
:子图间的纵向间距为子图平均高度的 \(0.25\)。
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
# 设置画布整体布局,以边框为边界
fig.subplots_adjust(left=0.1, right=1, bottom=0.1, top=1, wspace=0.25, hspace=0.25)
ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")
x = np.linspace(0, 10, 100)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()
# fig.savefig(fname='figure.pdf')
right=1, top=1
表示的是子图右边框和上边框之外完全无空白,但是 Fig. 7 却没有正确显示出来,这可能是交互模式下的显示问题,如果使用 savefig()
命令则可正确显示。
3.3 Gridspec
至此,我们基本上已经清楚地了解规则多子图的画法,但有时我们的子图排列并不总是规则的,此时就要用到 matplotlib 的 add_gridspec()
,从字面上便可理解,它是给画布添加一个大格子,大格子包含了(n, m)
个小格子,根据小格子索引分配各个子图的位置和大小:
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
gs = fig.add_gridspec(2, 2) # 大格子分为 2*2 的小格子
ax0 = fig.add_subplot(gs[0, :], xlabel="$x$", ylabel="$y$") # 格子索引从 0 开始,`0:` 表示整行
ax10 = fig.add_subplot(gs[1, 0], xlabel="$x$", ylabel="$y$") # 第 2 行的第 1 列
ax11 = fig.add_subplot(gs[1, 1], xlabel="$x$", ylabel="$y$")
在子图的排列不规则时,可以先在纸上画出所有的小格子,然后严格按照小格子确定子图的位置和大小。
4 Text and annotate
有时候会特别需要一些文字标注,对图形加以解释。此时有两种标注方法:
text()
:仅有文本,无箭头。除文本外需要提供文本锚点。annotate()
:有文本和箭头指向。不仅需要提供文本(text
)和文本锚点(textxy
),还需要给出箭头的终止位置(xy
)。
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-((x - mu) ** 2)) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)
ax1.text(x=-2, y=0.07, s="Text: This is a Gaussian distribution") # text 标注
ax1.annotate(
xytext=(-2, 0.025), # 文本位置
text="Annotate: This is a Gaussian distribution",
xy=(-4, 0.025), # 箭头终点
arrowprops=dict(arrowstyle="->"),
) # 含有箭头的标注
ax1.plot(x, y)
plt.show()
5 Axhline and axvspan
有时也需要对图形划出一条线,以作为某种参考。比如想标注出 \(x \ge 0.025\) (\(y \ge 0.025\)),以清楚地了解哪些 \(x\) 落入该范围,此时就要用到 axhline()
(axvline()
;同样的有时候想标注的不仅是条线,而是一片区域,此时就要用到 axhspan()
或 axvspan()
。
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-((x - mu) ** 2)) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)
ax1.axhline(
y=0.025, # 横线的高度,即 y 值
xmin=0.1, # 横线从 x 轴的哪里开始,比例
xmax=0.9, # 横线从 x 轴的哪里结束,比例
color="red",
linestyle="-",
)
ax1.axvline(x=-2) # 纵向
ax1.axvspan(
xmin=-2, # 填充起始位置
xmax=2, # 填充终止位置
ymin=0.025, # 填充从 x 轴哪里开始,比例
ymax=0.05,
facecolor="green",
)
ax1.plot(x, y)
plt.show()
6 LaTex
在 matplotlib 中也对 LaTex 进行了完整的支持,这也意味着可以直接调用 LaTex 丰富的宏包进行高质量输出,比如常用的 \usepackage{siunix}
进行单位输出,在设置之前请先确认下电脑是否已经安装 LaTex。下面介绍一下其用法:
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(
111,
xlabel="$x$",
ylabel="$y$",
title="{\\ttfamily This is a Gaussian distribution (monospaced font).}",
)
sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-((x - mu) ** 2)) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)
ax1.text(x=-2, y=0.07, s="siunix: $\SI{10}{g/L}$") # 测试宏包是否可用
ax1.text(x=-2, y=0.05, s="mhchem: $\ce{C + O2 -> CO2}$")
ax1.text(
x=-2,
y=0.03,
s="$f(x)=\\frac{1}{\sigma \sqrt{2 \pi}} e^{-\\frac{(x-\mu)^{2}}{2 \sigma^{2}}}$",
)
ax1.plot(x, y)
plt.show()
ValueError:
\SI{10}{g/L}
^
Unknown symbol: \SI, found '\' (at char 0), (line:1, col:1)
使用如下命令获取 matplotlib 配置文件路径:
获取 matplotlibrc 配置文件路径后可对其进行设置,但我觉得尽量还是不要对其修改。下面我给出一个 LaTex 的修改例子:
7 Savefig
至此,除了具体的图形展示(条形图,散点图,频率直方图等),你已经清楚了从创建画布,到整个图形的框架设置的完整流程。接下来只需保存即可,在此可以对图形框架进行很多设置,但大多都和之前的设置重复。还是建议严格的按照以上的设置流程,规范作图步骤。另外,科技写作图形推荐矢量图格式 .pdf
,具体矢量图和位图的区别可自行学习。
如果前面每一步都设置好了,图片的保存就显得十分简单:
8 Say no to pyplot
Pyplot 模块给 matplotlib 提供了交互模式,为 matplotlib 的学习提供了便利,但同时又给初学者带来了无尽的困扰。
它来回穿插在 matplotlib 的各个类和方法之间,使代码看起来杂乱无章。所以我建议初学者暂且拒绝 pyplot,踏踏实实一步步创建类,调用方法来完成图形,下面给出一个完全抛弃 pyplot
的作图流程:
fig = Figure(figsize=(8, 6),
facecolor='pink',
frameon=True,
edgecolor='green',
linewidth=2)
ax1 = fig.add_subplot(111, xlabel='$x$', ylabel='$y$')
# 设置刻度线
ax1_xmajor = 1
ax1_xmajor_locator = mpl.ticker.MultipleLocator(
ax1_xmajor) # 直接调用 MultipleLocator 类
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-(x - mu)**2) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)
ax1.plot(
x,
y,
label=
'$f(x)=\\frac{1}{\sigma \sqrt{2 \pi}} e^{-\\frac{(x-\mu)^{2}}{2 \sigma^{2}}}$'
)
ax1.legend(loc='best')
fig.savefig('Gaussian distribution.pdf')
9 Elements
Matplotlib 中有许多独立的元素可以进行精细的设置,以下列举出常用的元素和该元素常用的设置。
9.1 Marker
在绘制散点图的时候,有个参数为marker
,具体可参考官方文档
Marker可设置的属性有:
s
:大小c
:颜色marker
:种类linewidth
:线宽(边缘线)edgecolor
:边缘线颜色alpha
:透明度
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
np.random.seed(19680801)
N = 100 # 数据个数
r0 = 0.6 # 分类界限
x = 0.9 * np.random.rand(N)
y = 0.9 * np.random.rand(N)
area = (20 * np.random.rand(N)) ** 2 # 0 to 10 point radii
c = np.sqrt(area) # 颜色
r = np.sqrt(x**2 + y**2)
area1 = np.ma.masked_where(r < r0, area) # 小于 area
area2 = np.ma.masked_where(r >= r0, area) # 大于等于 area
ax1.scatter(x, y, s=area1, marker="^", c=c) # 大小 # marker # 颜色
ax1.scatter(x, y, s=area2, marker="o", c=c)
# Show the boundary between the regions:
theta = np.arange(0, np.pi / 2, 0.01)
ax1.plot(r0 * np.cos(theta), r0 * np.sin(theta))
plt.show()
9.2 Line and arrow
线的形状一般有:
-
--
-.
:
- custom
除了以上,还可以根据自己的喜好自定义样式。
还可以设置不同的虚线结合方式,具体参考set_dash_capstyle(s)
set_dashes([6, 2])
:6pt line, 2pt break.set_dashes([2, 2, 10, 2])
:2pt line, 2pt break, 10pt line, 2pt break.
Line 可设置的属性有:
linestyle
:形状linewidth
:宽度color
:颜色
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")
x = np.linspace(0, 10, 100)
ax1.plot(x, np.sin(x), linestyle="--")
ax1.plot(x, np.cos(x), linestyle="-.")
ax1.plot(x, np.sin(x) + 0.1, linestyle=":")
ax1.plot(x, np.cos(x) + 0.1, dashes=[2, 2, 10, 2])
plt.show()
Arrow 和 Line 相比只是多加了一个箭头,箭头样式参考官方文档,其余用法和 Line 类似。
9.3 Font
在包括 LaTex 的排版中,字体分为正文字体和数学字体,这两个可以分别设置。在 matplotlib 中同样如此。 相对于LaTex, 虽然 matplotlib 的字体设置更加丰富(参考官方文档),但我还是更推荐学习下 LaTex 的字体内容。
在介绍具体地操作之前,先整理下字体的种类都有哪些(列举一些我喜欢的)。
正文字体
字体族
- 衬线字体(Serif):Bitter,Palatino,New York,Times
- 非衬线字体(Sans-serif):Arial,Avenir,Seravek
- 等宽字体(Monospace):Source Code Pro,Monaco
字体形状
- 直立(Upright shape)
- 意大利(Italic shape)
- 倾斜(Slanted shape)
- 小型大写(Small captials shape)
字体系列
- 中等(Medium series)
- 加宽加粗(Bold extended series)
数学字体
数学字体一般使用 Times,这部分的介绍在刘海洋的 LaTex 入门中写的比较详细,请自行阅读,事实上也推荐系统地学习下这本书。
直接给出 LaTex 命令:
\mathrm{}
:衬线\mathsf{}
:非衬线\mathtt{}
:等宽\mathit{}
:变量,数学模式下默认该斜体\mathbb{}
:空心字体,比如实数域 \(\mathbb{R}\)\mathcal{}
:常用于具体运算符,比如损失函数 \(\mathcal{L}\)
Matplotlib 的可用字体
在设置字体之前,先看一下哪些字体可用:
设置中文字体
在默认情况下是不支持中文字体的,此时需要根据各自电脑中安装的字体手动设置:
plt.rcParams['font.family'] = ['sans-serif'] # 设置正文字体族为 sans-serif,此处可自行设置
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 根据字体族,设置中文字体
当然了,任何字体都可以按照以上方式自行设置。比如:
plt.rcParams['font.family'] = ['serif'] # 设置正文字体族为 sans-serif
plt.rcParams['font.sans-serif'] = ['Times', 'Palatino'] # 衬线
plt.rcParams['font.serif'] = ['Arial', 'Helvetica'] # 非衬线
plt.rcParams['font.monospace'] = ['Source Code Pro', 'Monaco'] # 等宽
plt.rcParams['font.size']=10 # 字体大小
以上是非 LaTex 环境的字体设置,LaTex 的字体设置请参考 Sec. 6 部分。
各元素字体配置
在 matplotlib 中,字体的设置可以打包成一个字典,但凡需要设置字体的元素直接添加 dict()
即可,要用到字体设置的元素一般有(可根据以下方式命名,方便管理)以下这些,更多具体的设置请参考官方文档:
fontproperties
- 坐标轴标签(
xlabel_font
):ax1.set_xlabel(), ax1.set_ylabel()
- 子图标题(
subplot_font
):ax1.set_title()
- 带箭头指向的标注(
annotate_font
):ax1.annoatate()
prop
- 图例(
legend_font
):ax1.legend()
fontdict
- 刻度线标签(
xtick_font
):ax1.set_xticklabels(), ax1.set_yticklabels()
- 标注(
text_font
):ax1.text()
字体的设置本身不复杂,但是根据以上层级,首先各自参数名不同,其次也来自不同的类,比较容易混淆。其中 fontproperties
和 prop
为FontProperties
实例,fontdict
为 Text
实例,因其参数名称可能不同,具体设置请谨慎查阅官方文档。
text_font = {
'family': 'Source Code Pro', # 可指定字体族,如 sans-serif, serif, monospace, 也可直接指定可用字体
'fontsize': 12, # 大小
'color': 'green', # 字体颜色
'rotation': 'horizontal', # 旋转角度,也可 vertical 和具体角度
'bbox': { # 文字盒子设置
'boxstyle': 'round',
'facecolor': 'pink',
'edgecolor': 'black',
'linewidth': 1
}
}
ax1.text(x=2, y=0, s='Test, Test', fontdict=text_font) # 非数学公式表达
ax1.text(x=2, y=1, s='$xy,\\mathrm{xy},\\mathbb{R},\\mathcal{L}$', fontdict=text_font) # 数学表达
9.4 Color
Matplotlib 支持的颜色格式有如下几种:
- RGB:
(0.1, 0.2, 0.3)
- 十六进制:
#0f0f0f
- 十六进制:用
#abc
as#aabbcc
- 字符串代替浮点数:
0
as black;1
as white;0.8
as light blue. - 单字符串作为基本颜色:
b
as blue;g
as green;r
as red;c
as cyan;m
as magenta;y
as yellow;k
as black;w
as white. - X11/CSS4:
aquamarine
- xkcd:
xkcd:sky blue
- Tableau 10:
tab: blue
;tab: orange
- CN:
C0
,默认有个颜色盘,C0
指的是其第 1 个颜色
Colormap
Colorbar
Colorbar 是数据标量值到颜色的映射,比如在热度图中就需要 Colorbar 对数据值进行展示。将已有的一个 Axes
对象传入给 colorbar
方法,则可自动根据 Axes
的数据生成 colorbar, 先感性地感受下什么是 colorbar:
# import matplotlib.pyplot as plt
# import matplotlib as mpl
fig = plt.figure(figsize=(6, 1))
ax1 = fig.add_subplot(111)
fig.subplots_adjust(bottom=0.5) # 设置子图到下边界的距离
cmap = mpl.cm.spring
norm = mpl.colors.Normalize(vmin=5, vmax=10)
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
cax=ax1, # 传入的是一个子图对象
orientation="horizontal",
label="spring",
)
plt.show()
调节 colorbar 两边样式:
fig, ax = plt.subplots(4, 1, figsize=(6, 4), constrained_layout=True)
cmap = mpl.cm.viridis
bounds = [-1, 2, 5, 7, 12, 15]
for i, extend in (0, "neither"), (1, "min"), (2, "max"), (3, "both"):
norm = mpl.colors.BoundaryNorm(bounds, cmap.N, extend=extend)
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
cax=ax[i],
extendfrac="auto",
orientation="horizontal",
label=f"extend={extend}",
)
plt.show()
下面介绍下 figure
的 colorbar
方法
10 Tricks
10.1 Bbox_inches and pad_inches
图形有边界框,bbox_inches
就是控制该边框的参数,如果将其设置为 tight
,即可刚好把子图所有元素包围。注意,该命令可能会覆盖 subplots_adjust
的设置。
但这并不能得到一张四周无空白的图形,因为还有个 pad_inches
参数在控制上步骤得到的内边框到整个画布外边框的距离,默认值是 \(0.1\),将其设置为 \(0\) 即可得到一个四边无边框的最终图形:
fig = plt.figure(
figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")
x = np.linspace(0, 10, 100)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
# fig.savefig(
# fname='Gaussian distribution.pdf',
# bbox_inches='tight', # 尝试找出一个边框,使其刚好能框出子图部分所有元素(包括坐标轴标签等),一般设置为 tight 即可。需要注意的是该命令只能用于保存图片,不能用于显示。
# pad_inches=0) # 所保存图形周围的填充量,默认是 0.1
plt.show()
pad_inches=0
表示的是右边和上边完全无空白,只能在 savefig()
命令下正确显示。 实际使用中,在 Fig. 17 添加fig.savefig()
命令。
还需要注意的一个细节是,虽然pad_inches=0
可以使右边和上边完全无空白,但因为锚点缘故,会裁掉一部分边框,如果希望完全无空白且边框不被裁,那边需要更深入地了解 anchor
这一元素,若觉得麻烦,则可以给其一个很小的数值即可,比如pad_inches=0.01
。
10.2 Tight_layout
根据官网文档描述,tight_layout()
使坐标轴长度成为了弹性体,根据坐标轴标签、刻度线标签是否重叠重新分配其与坐标轴的空间,以确保其能完全显示。具体而言,它调节的是:
- 子图间的横向和纵向间距(
h_pad=1 ,w_pad=1
),单位是默认字体的比例 - 整个子图区域和画布的间距(
pad=0
或rect=(0, 0, 1, 1)
)
从上面的描述可以看出,与 bbox_inches()
和 pad_inches()
相比,tight_layout()
增加了对子图之间更细节的设置:
fig = plt.figure(
figsize=(8, 6),
facecolor="pink",
frameon=True,
edgecolor="green",
tight_layout=dict(
h_pad=1, w_pad=1, rect=(0, 0, 1, 1) # 单位为 fontsize 大小
), # 包含子图的所有元素,比如坐标轴标签等
linewidth=2,
)
ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")
x = np.linspace(0, 10, 100)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()
# fig.savefig('Gaussian distribution.pdf', bbox_inches='tight', pad_inches=0)
对比 Fig. 18 和 Fig. 6 ,可以清楚地看出重叠部分已被重新调整。当然了,你也可以选择像 Fig. 7 一样调整。但明显 tight_layout()
更加简洁。
pad_inches=0
表示的是右边和上边完全无空白,只能在 savefig()
命令下正确显示。 实际使用中,在 Fig. 18 添加fig.savefig()
命令。
在实际使用中,我也最推荐这种 tight_layout()
,bbox_inches
和 pad_inches
结合的方法,而不太推荐 subplots_adjust()
。
至此,我们几乎可以将子图排列整齐,且完美显示出来。但注意,我们至此并没有涉及到任何 anchor
的内容,如果精细地控制图形的每一点设置,anchor
是一个绕不开的内容。但无论如何,我认为到此为止已经可以得到一张接近完美的画布了。
11 Default setting
在 matplotlib 中,运行如下代码即可获取全部可自定义参数:
默认参数的设置格式如下:
一共有 304 个参数可设置,下面仅列举常用参数。
11.1 figure
figure.dpi: 100.0 # 图像dpi,但是我一般喜欢直接输出 pdf
figure.figsize: [6.4, 4.8] # 画布大小
figure.frameon: True # 显示画布边框
figure.edgecolor: white # 边框颜色
figure.facecolor: white # 画布背景
figure.titlesize: large # 标题字体大小
figure.titleweight: normal # 标题字重
figure.raise_window: True # 弹出图形窗口
figure.autolayout: False
figure.constrained_layout.h_pad: 0.04167
figure.constrained_layout.hspace: 0.02
figure.constrained_layout.use: False
figure.constrained_layout.w_pad: 0.04167
figure.constrained_layout.wspace: 0.02
figure.subplot.left: 0.125
figure.subplot.right: 0.9
figure.subplot.top: 0.88
figure.subplot.bottom: 0.11
figure.subplot.hspace: 0.2 # 子图间高度
figure.subplot.wspace: 0.2 # 子图间宽度
11.2 axes
axes.edgecolor: black # 图形边框
axes.facecolor: white # 图形背景
axes.grid: False # 网格
axes.grid.axis: both # 网格方向
axes.grid.which: major # 网格位置:major, minor
axes.labelcolor: black # 标签颜色
axes.labelpad: 4.0 # 标签离轴偏离
axes.labelsize: medium # 标签大小
axes.labelweight: normal # 标签字重
axes.linewidth: 0.8 # 轴线宽
axes.spines.bottom: True # 显示下轴线
axes.spines.left: True # 显示左轴线
axes.spines.right: True # 显示右轴线
axes.spines.top: True # # 显示上轴线
axes.titlecolor: auto # 图形标题颜色
axes.titlelocation: center # 图形标题位置
axes.titlepad: 6.0 # 图形标题偏离
axes.titlesize: large # 图形标题大小
axes.titleweight: normal # 图形标题字重
axes.xmargin: 0.05 #坐标轴根据 x 数据值左边扩展 0.05
axes.ymargin: 0.05
axes.zmargin: 0.05
11.3 xaxis and xtick
xaxis.labellocation: center # 标签位置
xtick.alignment: center # 对齐
xtick.bottom: True # 刻度线位置
xtick.color: black # 颜色
xtick.direction: out # 方向
xtick.labelbottom: True # 标签位置
xtick.labelcolor: inherit # 标签颜色
xtick.labelsize: medium # 标签字重
xtick.labeltop: False # 标签位置
xtick.major.bottom: True # 主刻度线位置
xtick.major.pad: 3.5 # 主刻度线偏离
xtick.major.size: 3.5 # 主刻度线长度
xtick.major.top: True
xtick.major.width: 0.8 # 主刻度线宽度
xtick.minor.bottom: True
xtick.minor.pad: 3.4
xtick.minor.size: 2.0
xtick.minor.top: True
xtick.minor.visible: False # 显示副刻度线
xtick.minor.width: 0.6
xtick.top: False
11.4 yaxis and ytick
yaxis.labellocation: center
ytick.alignment: center_baseline
ytick.color: black
ytick.direction: out
ytick.labelcolor: inherit
ytick.labelleft: True
ytick.labelright: False
ytick.labelsize: medium
ytick.left: True
ytick.major.left: True
ytick.major.pad: 3.5
ytick.major.right: True
ytick.major.size: 3.5
ytick.major.width: 0.8
ytick.minor.left: True
ytick.minor.pad: 3.4
ytick.minor.right: True
ytick.minor.size: 2.0
ytick.minor.visible: False
ytick.minor.width: 0.6
ytick.right: False
11.5 font
font.family: ['sans-serif'] # 使用衬线字体还是非衬线字体
font.sans-serif: ['DejaVu Sans', 'Bitstream Vera Sans', 'Computer Modern Sans Serif', 'Lucida Grande', 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica', 'Avant Garde', 'sans-serif'] # 非衬线字体集
font.serif: ['DejaVu Serif', 'Bitstream Vera Serif', 'Computer Modern Roman', 'New Century Schoolbook', 'Century Schoolbook L', 'Utopia', 'ITC Bookman', 'Bookman', 'Nimbus Roman No9 L', 'Times New Roman', 'Times', 'Palatino', 'Charter', 'serif'] # 非衬线字体集
font.monospace: ['DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Computer Modern Typewriter', 'Andale Mono', 'Nimbus Mono L', 'Courier New', 'Courier', 'Fixed', 'Terminal', 'monospace'] # 等宽字体集
font.size: 10.0 # 字体大小
font.style: normal # 字体风格:normal, italic, oblique
font.weight: normal # 字重:normal, bold, lighterm bolder
11.6 grid
11.7 legend
legend.loc: best # 图例位置
legend.frameon: True # 显示边框
legend.framealpha: 0.8 # 边框透明度
legend.edgecolor: 0.8 # 边框颜色
legend.facecolor: inherit # 背景颜色
legend.title_fontsize: None # 图例标题的字体大小
legend.borderpad: 0.4 # 边框偏离
legend.columnspacing: 2.0 # 列间隔
legend.fontsize: medium # 图例字体大小
legend.labelcolor: None # 图例标签颜色
legend.labelspacing: 0.5 # 图例和标签间隔
legend.fancybox: True # 边框阴影
11.8 savefig
11.9 scatter
11.10 lines
lines.linestyle: - # 样式
lines.linewidth: 1.5 # 宽度
lines.color: C0 # 颜色
lines.dashdot_pattern: [6.4, 1.6, 1.0, 1.6] # dashdot 样式
lines.dashed_pattern: [3.7, 1.6] # dashed 样式
lines.dotted_pattern: [1.0, 1.65] # dotted 样式
lines.marker: None
lines.markersize: 6.0 # 线上样本点大小
lines.markeredgecolor: auto # 线上样本点边缘颜色
lines.markeredgewidth: 1.0 # 线上样本点边框宽度
lines.markerfacecolor: auto # 线上样本点背景色
lines.scale_dashes: True
11.11 text
11.12 mathtext
11.13 others
``yml markers.fillstyle: full # 填充样式