不管是论文、博客文章还是 PPT,凡是有数学公式的地方,常常都需要伴有函数图象。绘制插图的工具多到难以计数,找到称心顺手的工具更是难上加难,以下就尝试过的工具给出我的评价,或许能提供一些参考。
PowerPoint
对,没有看错,正是 Office 家族的 PowerPoint,PowerPoint 的绘图工具十分强劲。
一来是具有图形化界面,所见即所得的操作方式很适合完全没有绘图基础的用户,在一顿摸索之后总能画得出不错的图形。二是 PowerPoint 支持多种图片格式,不管是网络上常用的 JPEG,还是用于论文的 SVG,一键就能导出。此外,在 PowerPoint 原生环境中绘制的图形,插入到 PPT 中更是天衣无缝。再者,PowerPoint 是 Windows 系统中的预装软件,开箱即用,十分方便。
在我看来,对于一些简单或是要求不高的图形,PowerPoint 就已经足够满足要求了。但是,若对排版有着较高要求,那么 PowerPoint 就难堪此任了。
对于较为复杂的图形,在 PowerPoint 需要绘制大量图层,操作相当困难,建议还是交给专业的 Adobe Illustrator。再者,PowerPoint 很难直接放大整个图形。例如,一组包含文字与几何形状的图形,直接放大就会造成文字与几何形状间的错位,必须一个个调整。另一大缺点就是 Office 的公式编辑器相当难用,尽管新版的 Office 已经支持了 LaTex 表达式,但是字体等等问题真是一言难尽。令我放弃 PowerPoint 关键是 PowerPoint 不能按公式绘制函数,只能用曲线工具一点点去描图,这就不能满足我的需求了。
TikZ
TikZ 是 LaTex 的绘图宏包,能够通过 LaTex 指令直接在 PDF 中绘制矢量图。TikZ 是在 LaTex 环境中用指令绘图的,因此 TikZ 绘制的图形特别能满足排版强迫症患者,同时绘图风格与 LaTex 文章一致,看起来十分舒服。
我也尝试过使用 TikZ 绘图,我的感觉是操作较为繁琐。例如绘制平面坐标系,竟然需要绘制两根箭头线,再绘制线上的短线作为刻度。或许为了排版美观,这不算什么,「严谨」嘛。那么对会绘图工具来说,最致命的一点莫过于不能导出图片文件了吧,是的,TikZ 竟然只能导出 PDF。我还尝试了各种格式转换工具,总不能导出清晰的 JPEG,不适合放在网页上,遂放弃。
TikZ 还有另一个小问题,也可能是我配置的问题,在我使用 TikZ 绘制函数时,若指定的定义域超过某个值,就会给出错误 Dimension too large
。网络上给出的原因是,TikZ 不支持计算过大的数值,这一点在使用上也让人备感掣肘。
matplotlib
matplotlib 是我最早学习的专业绘图工具,过去几年,我也见证着它越来越完善。随着使用 matplotlib 越多,对于 matplotlib 的样式总会疲倦,于是我尝试了其他绘图工具,最终兜兜转转,又回到了 matplotlib 的怀抱,这大概就是「否定之否定」吧。
matplotlib 是 Python 的绘图包,因为同时依赖于强力的 numpy 包,复杂的运算对它而言是轻而易举,这对于各种数据的处理非常方便。matplotlib 通过代码绘制图像,各种样式自然也可以自定义。matplotlib 的各种优点在此也不再赘述,回到本文的主题,那么如何绘制出教科书式的函数示意图呢?
用以下代码绘制默认样式的函数图像:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(4,3)) # 新建画布
ax = fig.add_subplot(111) # 新建坐标系
fig.add_axes(ax) # 将坐标系添加到画布
x = np.arange(-6, 6, 0.1)
y = lambda x: 1/(1+np.e**(-x))
ax.plot(x, y(x))
plt.show()
matplotlib 的默认样式虽然也很美观,但是与我们想要的教科书样式差别很大,教科书样式的主要特点就是坐标轴位于原点、黑白配色,我的解决方案是使用以下代码的样式:
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.axisartist as axisartist
fig = plt.figure(figsize=(10,4))
ax1 = axisartist.Subplot(fig, 121) # 左侧子图
ax2 = axisartist.Subplot(fig, 122) # 右侧子图
fig.add_axes(ax1)
fig.add_axes(ax2)
axes_list = [ax1, ax2]
for ax in axes_list:
# 隐藏边框
ax.axis[:].set_visible(False)
# 在原点绘制 x, y 轴
ax.axis["x"] = ax.new_floating_axis(0,0)
ax.axis["y"] = ax.new_floating_axis(1,0)
# 设置 x, y 轴的样式
ax.axis["x"].set_axisline_style("-|>", size=1.5)
ax.axis["y"].set_axisline_style("-|>", size=1.5)
# 设置 x, y 轴的箭头设为黑色
ax.axis["x"].line.set_facecolor("black")
ax.axis["y"].line.set_facecolor("black")
# 隐藏坐标轴刻度
ax.set_xticks([])
ax.set_yticks([])
x = np.arange(-6, 6, 0.1)
y = lambda x: 1/(1+np.e**(-x))
ax1.plot(x, y(x), lw=2, c="black")
x = np.arange(-6, 6, 0.1)
y = lambda x: np.e**(-x)/(1+np.e**(-x))**2
ax2.plot(x, y(x), lw=2, c="black")
plt.show()
唯一的不足之处是坐标轴的箭头稍有些肥大,略显怪异,我没有找到改变这个箭头样式的方法,于是只好直接修改默认样式。在 matplotlib.patches
中找到以下代码片段:
@_register_style(_style_list, name="-|>")
class CurveFilledB(_Curve):
"""An arrow with filled triangle head at the end."""
arrow = "-|>"
在后面添加代码修改箭头样式,修改后的代码片段是:
@_register_style(_style_list, name="-|>")
class CurveFilledB(_Curve):
"""An arrow with filled triangle head at the end."""
arrow = "-|>"
# 修改默认箭头样式
def __init__(self, head_length=.75, head_width=.125):
super().__init__(head_length=head_length, head_width=head_width)
Warning 对于 matplotlib.patches
的修改,在matplotlib 更新后会失效,需要重新修改。
最后使用以下代码绘制图像:
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.axisartist as axisartist
def scale_axes(ax, x, y, xscale=0.2, yscale=0.2):
dx = np.max(x) - np.min(x)
ax.set_xlim([np.min(x)-xscale*dx, np.max(x)+xscale*dx])
dy = np.max(y) - np.min(y)
ax.set_ylim([np.min(y)-yscale*dy, np.max(y)+yscale*dy])
fig = plt.figure(figsize=(10,4))
ax1 = axisartist.Subplot(fig, 121)
ax2 = axisartist.Subplot(fig, 122)
fig.add_axes(ax1)
fig.add_axes(ax2)
axes_list = [ax1, ax2]
for ax in axes_list:
ax.axis[:].set_visible(False)
ax.axis["x"] = ax.new_floating_axis(0,0)
ax.axis["y"] = ax.new_floating_axis(1,0)
ax.axis["x"].set_axisline_style("-|>", size=1.5)
ax.axis["y"].set_axisline_style("-|>", size=1.5)
ax.axis["x"].line.set_facecolor("black")
ax.axis["y"].line.set_facecolor("black")
ax.set_xticks([])
ax.set_yticks([])
x = np.arange(-6, 6, 0.1)
y = lambda x: 1/(1+np.e**(-x))
ax1.plot(x, y(x), lw=2, c="black")
scale_axes(ax1, x, y(x))
x = np.arange(-6, 6, 0.1)
y = lambda x: np.e**(-x)/(1+np.e**(-x))**2
ax2.plot(x, y(x), lw=2, c="black")
scale_axes(ax2, x, y(x))
plt.show()
其中我新增了 scale_axes()
用于控制函数图像按比例自动缩放,能够让左右图的坐标轴对齐,风格更统一,这样的图像就非常美观了。
Warning 本文最后更新于 2022 年 09 月 07 日,请确定内容是否过时。