Matplotlib 是 python 中非常常用的绘图模块。 matplotlib 支持使用类似于 matlab 的指令式语言绘图,也支持使用面向对象的调用方法绘图。其绘图方法简单,且与 numpy, pandas 等科学计算库配合良好,因此被广泛使用。但是,在 matplotlib 的使用过程中,插入中文并控制字体是一件比较麻烦的事,需要特殊配置才能正常显示。本文详细介绍在 matplotlib 中使用中文的方法。

首先,我们来绘制一个简单的图,包含一条正弦曲线和一条余弦曲线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)
fig = plt.figure(figsize=(4, 3))
ax = fig.gca()
ax.plot(x, y, label='sin(x)')
ax.plot(x, z, label='cos(x)')
ax.set_xlim((-10, 10))
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.legend()
fig.tight_layout()
fig.savefig('fig1.png', dpi=150)

运行程序,会发现生成了一个正弦曲线图,如下图所示。

在matplotlib中使用中文,无中文时

上面的代码使用的是面向对象的方法绘制的。当然,也可以使用 matlab 的命令流风格绘制。这里不再赘述。

现在代码里没有出现中文,一切正常。如果直接使用中文,我们来看一下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)
fig = plt.figure(figsize=(4, 3))
ax = fig.gca()
ax.plot(x, y, label='正弦曲线')
ax.plot(x, z, label='余弦曲线')
ax.set_xlim((-10, 10))
ax.set_xlabel('相位角(rad)')
ax.set_ylabel('三角函数值')
ax.legend()
fig.tight_layout()
fig.savefig('fig2.png', dpi=150)

生成的图片如下图所示。

在matplotlib中使用中文,中文不显示

里面的文字无法显示,使用一个方框代表一个文字。同时,在控制台中会输出以下信息

1
2
3
RuntimeWarning: Glyph 20313 missing from current font.
font.set_text(s, 0, flags=flags)
...

这个信息的意思是,在调用 font.set_text 时,发现在当前的字体集中,找不到相应字的字体。

那么当前使用的字体集是什么呢?可以在 ipython 中,使用以下命令,打印 matplotlib 的设置参数

1
2
from matplotlib import rcParams
print(rcParams)

从控制台的输出中可以看到,有几项以 font 开头的与字体有关。

font.family 是指使用的字体组。每一个字体组对应一系列字体,这些字体有共同的特征。常用的字体组有三个,分别是 serif ,即衬线字体,另一个是 sans-serif ,即无衬线字体,最后一个是 monospace ,即等宽字体。三种字体的适用场景这里不再赘述。程序默认的为无衬线字体,适合屏幕输入。对于打印论文,一般使用的是衬线字体。

对于每一个字体组,都有一个对应的字体列表。比如,在我的 MacBook 中,衬线字体的字体列表是这样定义的

1
2
3
4
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']

熟悉 HTML 编程的读者看到这一列表一定会感到使熟悉。在 HTML 中,有一个字体 fallback 机制,浏览器会先尝试使用字体列表中的第一个字体渲染文字。如果第一个字体文件中没有对应的文字,则会 fallback 到下一个字体中再次查找。这也是为什么在浏览器中,使用特殊字体时,对于一些生辟字依然可以渲染,但是字体不同。但是,需要特别注意的是,matplotlib 中没有类似的 fallback 机制 。尽管也使用了一个字体列表,但是 matplotlib 使用的机制是,从列表的第一项开始在系统中查找字体,如果存在这一字体,则使用这一字体渲染,而不会再使用任何后续字体。只有系统中不存在这一字体时,才会沿列表向后寻找可用字体。因此,无法使用类似于 Word 的中文和西文字体分别渲染的功能,也无法使用 HTML 中查找多种字体功能。

上述讨论表明,无法通过在字体列表后面加入字体的方法来添加中文支持。那么如何解决这一问题呢?

首先,我们尝试直接使用中文字体。先要确定使用什么字体。如果使用的是 MacBook ,可以在 Launchpad 的“其它” -> “字体列表”中,找到可以使用的中文字体对应的英文名。在我的电脑中,宋体的字体名是 “Simsun (founder extended)”。在 Windows 中,也可以从“字体设置”中找到宋体,点击它,从元数据中可以看到其字体文件名为 “SIMSUN.TTC” ,即字体名为 “SIMSUN”。那么就可以把字体名传入 matplotlib 。建立以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams

rcParams['font.family'] = 'serif'
rcParams['font.serif'] = 'Simsun (founder extended)'
x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)
fig = plt.figure(figsize=(4, 3))
ax = fig.gca()
ax.plot(x, y, label='正弦曲线')
ax.plot(x, z, label='余弦曲线')
ax.set_xlim((-10, 10))
ax.set_xlabel('相位角(rad)')
ax.set_ylabel('三角函数值')
ax.legend()
fig.tight_layout()
fig.savefig('fig3.png', dpi=150)

绘制的结果如下图所示:

在matplotlib中使用中文,仅使用中文字体

可以看出,汉字都可以正常显示了。但是有一个问题,就是图中的英文和数字字体也使用了宋体。宋体的英文和数字都比较丑陋,在科技文献中,一般不会使用,而是使用 “Times New Roman” 字体代替。这里就遇到了一个麻烦, matplotlib 中目前无法同时使用两个字体,这时就只能使有一种比较麻烦的方式“曲线救国”了。

一种解决的思路是,使用英文更好看的中文字体,比如华文宋体 “STSONG” ,Adobe宋体 “ADOBESONGSTD” 等。但是这些字体与传统的宋体和 Times New Roman 还是有一定区别的。如果对字体要求比较严格,则行不通。

另一种解决思路是,在全局使用英文字体 “Times New Roman”, 而对于出现中文的地方,使用临时的中文字体。但是对于中英文混排,使用临时中文字体也无济于事。这时需要借助 LaTeX 渲染器,使用数学字体中的 “stix” 字体来完成混排。它与 “Times New Roman” 的相似度非常高,可以起到“以假乱真”的效果。

下面是进行中英文混排的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams

rcParams['font.family'] = 'serif'
rcParams['font.serif'] = 'Times New Roman'
rcParams['mathtext.fontset'] = 'stix'
ch_font = 'Simsun (founder extended)'
x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)
fig = plt.figure(figsize=(4, 3))
ax = fig.gca()
ax.plot(x, y, label='正弦曲线')
ax.plot(x, z, label='余弦曲线')
ax.set_xlim((-10, 10))
ax.set_xlabel('相位角($\mathrm{rad}$)', fontname=ch_font)
ax.set_ylabel('三角函数值', fontname=ch_font)
lbs = ax.legend().get_texts()
for lb in lbs:
lb.set_fontproperties(ch_font)
ax.set_title('使用中文和 $\mathrm{English}$', fontname=ch_font)
ax.annotate('最大值为 $\mathrm{1}$', xy=(3.1416 / 2, 1), xytext=(-20, -30),
textcoords="offset points", arrowprops={'arrowstyle': '->'},
fontname=ch_font)
fig.tight_layout()
fig.savefig('fig4.png', dpi=150)

上面的代码中,先将全局字体设为 “Times New Roman”,将数学字体设为 “stix” 。然后指定使用的中文字体 ch_font 为 “Simsun (founder extended)” 。这一字体是 MacBook 用户使用的。对于 Windows 用户,将其变为 “SIMSUN”。然后在绘制字体时,对有中文的部分设置临时字体。在调用 set_xlabel 设置坐标轴时,传入 fontname= 参数,设置其字体。对于“相位角(rad)”这一中英混排部分,使用 LaTeX 表达式把英文部分包裹住(即前后各加一个 $ 符号)。由于 LaTeX 中默认渲染字体为斜体,因此需要使用 \mathrm{} 命令,使字符变成正体。

在添加图例时,legend() 方法返回的不是文字,而是一个图例对象。我们使用其 get_texts 方法,获得图例中所用的所有文字组成的列表。然后遍历这一列表,使用 set_fontproperties 方法设置其字体。

对于其它文字操作,大多都有与 fontnamefontproperties 有关的操作,使用类似的方式基本都可以解决。可以查阅 matplotlib 的文档。这里举了一个使用 set_title 添加标题和一个使用 annotate 添加注释的例子。

下面来看这一段代码生成的效果。

在matplotlib中使用中文

值得指出的是,在 MacBook 中的宋体,与 Windows 中的宋体也有不同。因此此图中的宋体看起来也有一些奇怪。如果使用 Windows 设备,则与常见的宋体完全相同。

以上,就是在 matplotlib 中使用中英文混排的方法。希望在未来版本的 matplotlib 中,也可以引入字体的 fallback 机制,设置字体就不会这么麻烦了。