把实验数据或函数图像先用外部绘图软件画好,再作为图片贴进文档,其实并非必要。LaTeX 可以 直接在文档中由数据生成图表。核心工具是建立在 TikZ/PGF 之上的 pgfplots。它可以从函数、坐标或数据文件绘制折线图、散点图、柱状图,甚至 3D 曲面,并且字体和线宽都与正文一致。本页以 pgfplots 的 axis 环境和 \addplot 为中心,接着介绍把外部 gnuplot 当作计算器调用,以及把 R 或 Python(matplotlib) 绘制的图导入为 TikZ 代码。
为什么在文档中排版图表
把电子表格或绘图软件生成的 PNG 贴进去,通常会和文档不协调:轴标签的字体不同于正文,公式变成模糊的位图,放大后线条会发锯齿。数据一改,还得重新生成并贴图。pgfplots 反过来让 LaTeX 自己排版图表。坐标轴数字和图例使用与正文相同的字体,曲线是矢量图,任意缩放都保持平滑,标签里还可以直接写 $\sin x$ 这样的 真正数学公式。
第二个优点是 与数据分离。与其把坐标硬写在正文里,不如让 pgfplots 读取 .dat 或 .csv 数据文件;之后只需更新数据并重新编译,图表就会随之变化。同一组数值可在论文、幻灯片和附录中反复使用,也不会出现转录错误。这与“从数据生成表格”的思路完全相同(见相关页面),pgfplots 还附带用于表格的姊妹宏包 pgfplotstable。
pgfplots 构建在 TikZ/PGF 之上,因此整幅图位于 tikzpicture 环境中,也可以自由混用 TikZ 自身的坐标、节点和装饰。TikZ 本身的基础(\draw、坐标系、节点)留给 “TikZ basics” 页面;这里专注于绘制图表。
pgfplots 的骨架 — axis 与 \addplot
首先在导言区加载它,并声明 兼容级别。写 \pgfplotsset{compat=1.18} 会固定该版本的默认行为(坐标轴外观、刻度放置方式等),避免将来 pgfplots 更新后图形悄悄改变。新文档通常应指定已安装版本允许的尽量新的编号。
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}正文中,在一个 tikzpicture 里放置一个 axis 环境,然后在其中每调用一次 \addplot 就叠加一条曲线。坐标轴、刻度、网格和图例通过 axis 的选项设置;每条曲线的颜色和标记通过 \addplot 的选项设置。先看一个最小例子。
\begin{tikzpicture}
\begin{axis}[
xlabel = {$x$},
ylabel = {$f(x)$},
title = {A parabola},
grid = major,
]
\addplot[blue, domain=-3:3, samples=100] {x^2};
\addlegendentry{$x^2$}
\end{axis}
\end{tikzpicture}这会在 −3 ≤ x ≤ 3 区间内用 100 个点采样抛物线 y = x²,并把它画成一条平滑的蓝色曲线。坐标轴带有 x 和 f(x) 标签,顶部有标题 “A parabola”,grid=major 沿主刻度绘制浅色网格,\addlegendentry 添加图例项 “x²”。下面依次看要点。
- 标签和标题用花括号包住。 像
xlabel={$x$}这样用{...}包起来,就能安全地在里面写数学式或逗号。 ^表示幂;三角函数默认使用角度。\addplot {x^2}中的表达式由 pgf 的内部解析器求值。\sin等函数的参数按 度 解释,因此若要传入弧度,请写成sin(deg(x)),用deg()包起来。domain和samples。 函数图会在domain=a:b区间内取samples=N个点进行采样(默认大致是 −5:5、25 点)。曲线显得有棱角时,提高samples。- 图例有两种写法。 可以在每条曲线后放
\addlegendentry{…},也可以在axis选项中统一写legend entries={A,B,...}。位置用legend pos=north west等设置。
下面汇总常用的 axis 选项。它们写在 axis 的 [...] 中,用逗号分隔。
| 选项 | 作用 |
|---|---|
xlabel= / ylabel= | x 轴和 y 轴标签;像 xlabel={$x$} 这样用花括号包住,可写数学式 |
title= | 放在图表上方的标题 |
xmin= xmax= ymin= ymax= | 固定可见范围(坐标轴上下限) |
grid= | 绘制网格线:grid=major 沿主刻度,grid=both 也沿次刻度 |
legend pos= | 图例位置:north west 等;outer north east 可放到坐标轴外 |
xtick= | 明确刻度位置(如 xtick={0,1,2});xtick=data 让刻度对齐数据点 |
width= / height= | 设置图的尺寸 |
ybar | 生成竖向柱状图(后述);横向条形图用 xbar |
三种提供数据的方法 — 函数、坐标、文件
\addplot 大致有三种提供数据的方式。第一种就是前面看到的 来自数学表达式(函数):在 {...} 中写表达式,pgfplots 会在 domain 范围内采样并生成曲线。
\addplot[red, domain=0:2*pi, samples=200] {sin(deg(x))};这会绘制从 0 到 2π 的正弦曲线。由于 x 是弧度值,关键在于用 deg() 在传给按角度工作的 \sin 之前把它转换成度。
第二种是 来自内联坐标:在 coordinates {...} 中列出 (x,y) 对,pgfplots 会按顺序把点连成折线(或只显示标记,形成散点图)。实测值只有几个时很方便。
\addplot[mark=*, blue] coordinates {
(0,0) (1,1) (2,4) (3,9) (4,16)
};这里用蓝线连接五个点,并在每个点上放置实心圆标记(mark=*)。如果想去掉线只保留点,用 only marks;线型可用 dashed 等改变。
第三种,也是实际工作中最重要的一种,是 来自数据文件。table {filename} 会读取以空白分隔的文本文件。第一行是列名(表头),默认情况下 pgfplots 会把 第 1 列作为 x、第 2 列作为 y 来绘制。假设有如下 data.dat。
x y
0 0.0
1 0.8
2 0.9
3 0.1
4 -0.8
5 -1.0\begin{tikzpicture}
\begin{axis}[xlabel={$x$}, ylabel={$y$}, grid=major]
\addplot[mark=square, teal] table {data.dat};
% 列名で明示するなら:
% \addplot table[x=x, y=y] {data.dat};
\end{axis}
\end{tikzpicture}如果要明确指定,可像 table[x=x, y=y] {data.dat} 这样 写出列名(列名区分大小写)。逗号分隔的 CSV 使用 table[col sep=comma]{...};以 # 或 % 开头的行会作为注释跳过。需要整理数据或派生计算列时,可以使用姊妹宏包 pgfplotstable(见相关页面 “Tables from data”)。
柱状图、对数轴与 3D
同一个 axis/\addplot 框架也可以改变数据的呈现方式。要做 柱状图,只需给 axis 加上 ybar(竖向柱)。叠加多个 \addplot 调用时,它们会自动横向错开,形成分组柱状图。
\begin{tikzpicture}
\begin{axis}[
ybar,
xlabel = {Year}, ylabel = {Count},
symbolic x coords = {2023, 2024, 2025},
xtick = data,
]
\addplot coordinates {(2023,40) (2024,55) (2025,72)};
\end{axis}
\end{tikzpicture}这里用 symbolic x coords 把年份(字符串)作为 x 轴标签,并用 xtick=data 让刻度对齐各数据点。若要横向条形图,使用 xbar。
对数轴 只需把 axis 换成不同的环境名即可:双对数用 loglogaxis,只有 x 轴取对数用 semilogxaxis,只有 y 轴取对数用 semilogyaxis。内部 \addplot 的写法不变。
\begin{tikzpicture}
\begin{loglogaxis}[xlabel={$x$}, ylabel={$y$}]
\addplot[domain=1:1000, samples=50] {1/x};
\end{loglogaxis}
\end{tikzpicture}绘制 3D 时使用 \addplot3,axis 会自动变成三维坐标轴。曲面指定 surf,线框网格指定 mesh,函数用 x 和 y 两个变量书写。来自坐标或数据文件的 3D 图也同样通过 \addplot3 处理。
\begin{tikzpicture}
\begin{axis}[xlabel={$x$}, ylabel={$y$}, zlabel={$z$}]
\addplot3[surf, samples=30, domain=-3:3]
{exp(-x^2 - y^2)};
\end{axis}
\end{tikzpicture}这会在 30×30 网格上采样钟形高斯曲面 z = e^(−x²−y²),并将其绘制为彩色曲面。视角可用 view={azimuth}{elevation} 旋转。
把 gnuplot 用作计算器
pgfplots 自带的解析器很方便,但面对复杂表达式或大量采样点时可能力不从心。写 \addplot gnuplot {...} 时,数值计算会交给外部程序 gnuplot,它计算坐标;随后 pgfplots 读回结果并绘制。换句话说,就是把 gnuplot 当作“桌面计算器”使用。
\begin{tikzpicture}
\begin{axis}[xlabel={$x$}, ylabel={$y$}]
\addplot[blue] gnuplot[domain=0:10] {sin(x)};
\end{axis}
\end{tikzpicture}有两点重要注意事项。第一,必须使用 --shell-escape:因为 LaTeX 会启动外部命令(gnuplot),必须像 pdflatex --shell-escape document 这样 允许执行外部命令 后编译才会运行(也称为 -write18)。处理过程中会生成供 gnuplot 使用的中间文件,计算结果写入其中再被读回。
第二,语法不同。传给 gnuplot 的表达式由 gnuplot 自己的数学引擎求值,因此幂运算符不是 pgfplots 的 ^,而是 gnuplot 风格的 `**;三角函数默认使用 **弧度**。所以同一条 “sin 曲线”,用内置解析器写 {sin(deg(x))},经 gnuplot 则写 {sin(x)}。出于安全原因,--shell-escape` 默认关闭,因此只在需要时启用更安全。
导入 R 与 Python 的图
如果你已经在 R 或 Python 中完成分析,想直接复用其中的图,但又不想接受贴图的缺点(字体不一致、公式模糊),可以让这些工具导出 TikZ/pgfplots 代码。这样图表会作为文档的一部分排版,字体和数学公式都与正文一致。
在 R 中,tikzDevice 宏包提供一种“图形设备”,可把 R 的标准图形输出(包括 base plots 和 ggplot2)写成 TikZ 代码。用 tikz() 打开设备,运行平常的绘图代码,再用 dev.off() 关闭,就会得到 .tex 文件。由于它在放置文字时会向 LaTeX 查询字符串宽度和字体度量,输出能准确匹配正文的字体,也可以在轴标签中写 LaTeX 数学式。设置 standAlone=TRUE 时,会输出一个可单独编译的完整文档。
library(tikzDevice)
tikz("plot.tex", width = 4, height = 3)
plot(cars$speed, cars$dist,
xlab = "Speed", ylab = "Distance")
dev.off()在 Python(matplotlib)中,tikzplotlib(旧名 matplotlib2tikz)会把用 matplotlib.pyplot 创建的图转换成 pgfplots 代码。用 tikzplotlib.save("figure.tex") 写出文件,在 LaTeX 端用 \input{figure.tex} 读入。注意 tikzplotlib 目前已停止维护;其 fork matplot2tikz 正作为后继版本开发。新项目可考虑 matplot2tikz(API 基本相同)。
import matplotlib.pyplot as plt
import tikzplotlib # or: import matplot2tikz
plt.plot([0, 1, 2, 3], [0, 1, 4, 9])
plt.xlabel("$x$")
plt.ylabel("$x^2$")
tikzplotlib.save("figure.tex")无论哪条路径,输出的 .tex 内部都会使用 pgfplots,因此文档端也需要 \usepackage{pgfplots} 和 \pgfplotsset{compat=...}。能够手工微调生成的代码,也是图片文件没有的优点。
性能与外部化
pgfplots 很漂亮,但面对 大量数据点会变慢并消耗内存。包含数万点的散点图可能导致编译时间很长,甚至碰到 TeX 的内存限制。可以采取几种办法。
- 抽稀数据点。 用
each nth point=k每 k 个点画一个,或用filter discard if not等过滤器丢弃范围外数据。 - 外部化(externalization)。 使用
\usepgfplotslibrary{external}和\tikzexternalize,每幅图只单独编译一次为 PDF,以后直接插入,正文重编译会更轻(需要--shell-escape)。 - 使用内存更大的引擎。
LuaLaTeX的内存限制更宽松,适合大图。 - 在上游预先绘制。 如果图仍然太重,导入 R、Python 或 gnuplot 绘制的结果(上一节)可能更现实。
需要 --shell-escape 的功能,例如外部化或 gnuplot,在 CI 或容器中构建时也必须启用。Docker / CI 的设置请参见相关页面。