函数绘图工具的选择

Category 碎碎念

不管是论文、博客文章还是 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 日,请确定内容是否过时。