グラフ・プロット(PGFPlots / gnuplot)

グラフを画像として貼り込んだ論文は、たいてい一目で分かります。曲線が汚いからではなく、軸の数字だけ書体が違うからです。LaTeX でこれを根本から直す道具が pgfplots で、グラフを画像として受け取るのではなく、数値から本文と同じ組版エンジンで組み上げます。このページでは pgfplotsaxis\addplot.dat ファイルの読み込み、gnuplot に計算を任せる経路、そして R や Python で描いた図をフォントごと持ち込む方法を扱います。途中で TeX Live 2024 で実際に出たエラーメッセージをそのまま引用します——検索でここに辿り着く人の多くは、まさにそのエラーを見ているはずだからです。

なぜ貼り込んだ画像は文書から浮くのか

理由は「解像度」ではなく 書体と線幅 です。表計算ソフトの既定はサンセリフ体、論文の本文はセリフ体。読者は書体名を意識しなくても、その不一致だけは感じ取ります。加えて、画像を版面に合わせて縮小すると線幅も一緒に縮み、図の中の罫線だけが本文の罫線より細くなります。pgfplots はグラフの座標だけを受け取り、目盛りの数字も凡例も 本文と同じフォント・同じ数式組版 で置くので、この二つのずれが原理的に起きません。軸ラベルに $\sin x$ と書けば、本文の数式とまったく同じ字形になります。

もう一つ、実務で効いてくるのが データとの分離 です。座標を本文に直書きせず .dat.csv を読ませておけば、測定をやり直したときは ファイルを差し替えて再コンパイルするだけ で図が追従します。論文・スライド・付録が同じ数値ファイルを見ているので、転記ミスという事故がそもそも起きません。表側にも同じ発想の姉妹パッケージ pgfplotstable があります。pgfplots 自体は TikZ/PGF の上 に作られているので、図全体は tikzpicture の中に置かれ、TikZ のノードや装飾もそのまま混ぜられます。TikZ そのもの(\draw、座標系、なぜコンパイルが遅いのか)は TikZ のページの担当です。

\pgfplotsset{compat=1.18} を書かないとどうなるか

警告が出て、しかも 図の見た目が古い既定のまま になります。TeX Live 2024 同梱の pgfplots 1.18.1 で compat を書かずにコンパイルすると、ログにこう出ます——"Package pgfplots Warning: running in backwards compatibility mode (unsuitable tick labels; missing features). Consider writing \pgfplotsset{compat=1.18} into your preamble."。警告文が使うべき番号まで教えてくれるので、そのまま従えば済みます。

latex
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}   % pin the release whose defaults you want

なぜこんな仕掛けがあるのかというと、pgfplots は版を重ねるたびに 既定値そのもの を変えてきたからです。目盛りラベルの書き方や軸の余白の取り方が変われば、同じソースから出てくる図も変わります。compat はその「どの版の常識で組むか」を宣言する鍵で、5 年前の原稿を再コンパイルしても図が動かないことを保証します。TeX Live 2024 に入っているのは pgfplots 1.18.1(2021 年 5 月 15 日) なので、この環境で指定できる最新は compat=1.18 です。新規文書では必ず 1 行入れておき、既存文書では 勝手に上げない のが定石——上げた瞬間に図が微妙にずれる可能性があります。

axis\addplot —— 動く最小のグラフ

tikzpicture の中に axis 環境 を一つ置き、その中で \addplot を呼ぶたびに曲線が一本ずつ重なります。役割分担は明快で、軸まわり(ラベル・目盛り・格子・凡例の位置)は axis のオプション曲線ごとの見た目(色・マーカー・線種)は \addplot のオプションです。この二層構造さえ掴めば、あとはオプション名を引くだけになります。

latex
\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 の放物線 y = x² を 100 点で標本化し、青い滑らかな曲線として描きます。grid=major は主目盛りに沿った薄い格子、\addlegendentry は凡例の 1 行です。細かいところを二つだけ。ラベルと題は必ず波括弧で囲むこと——xlabel={$x$} のようにすれば、中にカンマや ] があってもオプション解析が壊れません。凡例は曲線ごとに \addlegendentry{…} を置く書き方と、axis 側に legend entries={A,B,...} とまとめる書き方の二通りがあり、位置は legend pos=north west などで指定します。

オプション働き
xlabel= / ylabel=横軸・縦軸のラベル。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=図の仕上がり寸法。width=\linewidth で版面幅にそろう
ybar / xbar縦棒/横棒グラフにする。\addplot を重ねると自動で群棒になる

domainsamples の既定値、そして sin が合わない理由

既定は 区間 −5:5 を 25 点、そして 三角関数は度数法 です。前者は覚えておくと曲線が角ばる理由がすぐ分かりますし、後者は pgfplots でいちばん多い「グラフの形がおかしい」の原因です。面白いのは、この二つの既定値が pgfplots のものではないこと——pgfplots 側の /pgfplots/samples/.initial は空で、値は土台の PGF から降りてきます。TeX Live 2024 の tikz.code.tex には \def\tikz@plot@samples{25}\def\tikz@plot@domain{-5:5} が並んでいて、これが実体です。

度数法の話は数字で見るのがいちばん早いでしょう。TeX Live 2024 で \pgfmathparse{sin(1)} を評価すると 0.01746\pgfmathparse{sin(deg(1))}0.84143 になります。前者は「1 度の正弦」、後者は「1 ラジアンの正弦」で、deg()deg(1) = 57.29578 というラジアン→度の換算器です。つまり \addplot {sin(x)} と書くと横軸 1 のところに sin 1° が来てしまい、周期が 360 の見慣れない波が出ます。正しくは sin(deg(x)) です。

latex
% wrong: pgfmath reads the argument as degrees
\addplot[domain=0:2*pi, samples=200] {sin(x)};
% right: convert radians to degrees first
\addplot[red, domain=0:2*pi, samples=200] {sin(deg(x))};

\addplot にデータを渡す三通り —— 関数・座標・.dat ファイル

式を {...} に書くcoordinates {...} に点を並べるtable {ファイル名} でファイルを読むの三つです。前二つは前節で見ました。実務でいちばん使うのは三つめで、空白区切りのテキストなら先頭行を列名(ヘッダ)とみなし、既定では 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
latex
\begin{tikzpicture}
  \begin{axis}[xlabel={$x$}, ylabel={$y$}, grid=major]
    \addplot[mark=square, teal] table {data.dat};
    % naming the columns explicitly is the safer habit:
    % \addplot table[x=x, y=y] {data.dat};
  \end{axis}
\end{tikzpicture}

列名は 大文字小文字を区別します。ヘッダが x なのに table[x=X, y=y] と書くと、TeX Live 2024 ではこう止まります——"! Package pgfplots Error: Sorry, could not retrieve column 'X' from table '...'. Please check spelling (or introduce name aliases)."。エラー文が「綴りを確認するか別名を導入せよ」と対処法まで書いてくれる、親切な部類の失敗です。カンマ区切りの CSV なら table[col sep=comma, x=x, y=y] {data.csv} とし、#% で始まる行はコメントとして読み飛ばされます。データの整形や計算列が要るときは姉妹パッケージ pgfplotstable の担当です。

棒グラフ・対数軸・\addplot3 の 3D

棒グラフは axisybar を足すだけ対数軸は axis を別の環境名に差し替えるだけ3D は \addplot3 を使うだけです。どれも新しい文法を覚え直す必要がないのが pgfplots の設計上の美点で、\addplot の書き方はそのまま通用します。棒グラフでは、\addplot を複数重ねると自動で横にずれて群(クラスタ)棒になり、年号のような文字列を横軸にしたいときは symbolic x coordsxtick=data を組み合わせます。

latex
\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}

対数軸は、両対数なら loglogaxis、横軸だけなら semilogxaxis、縦軸だけなら semilogyaxis に環境名を替えます。3D は \addplot3 を使うと axis が自動的に三次元になり、曲面は surf、ワイヤフレームは mesh を指定して関数を xy の二変数で書きます。視点は view={方位角}{仰角} で回せます。

latex
\begin{tikzpicture}
  \begin{loglogaxis}[xlabel={$x$}, ylabel={$y$}]
    \addplot[domain=1:1000, samples=50] {1/x};
  \end{loglogaxis}
\end{tikzpicture}

\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}

gnuplot は TeX Live に入っていない

これは重要な前提なので先に書きます。\addplot gnuplot {...} を使うには、gnuplot を自分で別途インストールしなければなりません。TeX Live 2024 のバイナリディレクトリを覗くと asy(Asymptote)や mpost(MetaPost)は入っていますが、gnuplot は入っていません。gnuplot は TeX とは無関係に開発されている独立したプロットプログラムで、Homebrew や各ディストリビューションのパッケージマネージャから入れる必要があります。

では何のために呼ぶのでしょうか。pgfplots 内蔵のパーサは TeX の上で動く関係で、複雑な式や大量の標本には向きません。\addplot gnuplot {...} と書くと、その数値計算を gnuplot に外注し、返ってきた座標だけを pgfplots が描画します。仕組みは驚くほど素朴で、TeX が gnuplot 用のスクリプトをファイルに書き出し、gnuplot がそれを実行して 数表をファイルに書き、TeX がそれを読み戻します。TeX Live 2024 で \addplot[blue] gnuplot[domain=0:10] {sin(x)}; を処理すると、生成された .gnuplot ファイルにはこの指示が並んでいました(ジョブ名は job に置き換えています)——set table "job.pgf-plot.table"; set format "%.7e"; set samples 25; set dummy x; plot [x=0:10] sin(x);

この一行から二つのことが読み取れます。第一に、samples の既定 25 がそのまま gnuplot 側にも渡っていること。第二に、式が gnuplot の文法のまま 渡されていること——だから冪は pgfplots^ ではなく gnuplot 流の ** を使い、三角関数は ラジアン が既定です。同じ正弦曲線でも、内蔵パーサなら {sin(deg(x))}、gnuplot 経由なら {sin(x)} と書き分けます。そして TeX が外部プログラムを起動するため、--shell-escape が必須 です(-write18 とも呼ばれます)。

latex
% compile with:  pdflatex --shell-escape document
\begin{tikzpicture}
  \begin{axis}[xlabel={$x$}, ylabel={$y$}]
    % gnuplot syntax: radians, and ** for powers
    \addplot[blue] gnuplot[domain=0:10] {sin(x)};
  \end{axis}
\end{tikzpicture}

gnuplot が入っていない環境で実行すると、TeX Live 2024 はこう報告します——"! Package pgfplots Error: Sorry, the gnuplot-result file 'job.pgf-plot.table' could not be found. Maybe you need to enable the shell-escape feature? For pdflatex, this is '>> pdflatex -shell-escape'."。gnuplot が本当に無い場合は、その直前にシェルの "sh: gnuplot: command not found" も出ます。ここで注目すべきは、エラー文が続けて「gnuplot ファイルに対して手で gnuplot <file>.gnuplot を実行してもよい」と教えていることです。.table ファイルさえあれば pgfplots は描けます——実際、手で書いた .table を置いて再コンパイルしたところ、gnuplot 不在のまま図が出ました。gnuplot を入れられない CI では、生成済みの .table をリポジトリに含めてしまうのが現実的な回避策になります。

R と Python の図を、フォントごと持ち込む

解析はすでに R や Python で済んでいる、けれど PNG を貼るのは避けたい——そんなときは、各ツールに TikZ/PGF のコードを書き出させて \input するのが答えです。図が文書の一部として組まれるので、書体も数式も本文とそろいます。ただしどれも TeX Live には含まれない外部ツール なので、それぞれの言語のパッケージ管理から入れる必要があります。

R では CRAN の tikzDevice が、R 標準のグラフィックス出力(base プロットも ggplot2 も)を TikZ コードとして書き出すグラフィックスデバイスを提供します。tikz() でデバイスを開き、いつもの作図コードを走らせ、dev.off() で閉じると .tex が得られます。文字を置くときに LaTeX に問い合わせて文字列の幅とフォント計量を取る のが特徴で、だからこそ出力が本文の書体に正確に合い、軸ラベルに LaTeX の数式を書けます。standAlone=TRUE にすれば単体でコンパイルできる完結した文書として出せます。

R
library(tikzDevice)
tikz("plot.tex", width = 4, height = 3)
plot(cars$speed, cars$dist,
     xlab = "Speed", ylab = "Distance")
dev.off()

Python 側には二つの道があり、混同されがちです。一つめは matplotlib に標準で入っている pgf バックエンドで、追加パッケージが要りません。matplotlib.use("pgf") としてから savefig("figure.pgf") すると .pgf ファイルが出ます。ここが肝心なのですが、これが吐くのは pgfplots のコードではなく素の PGF です。matplotlib 3.11.0 で実際に生成したファイルを調べると \addplotaxis 環境も一つも無く、中身は \pgfqpoint\pgfpathlineto\pgfsetstrokecolor といった低水準の描画命令の羅列でした。つまり座標軸は「pgfplots が組んだ軸」ではなく「線として描かれた軸」です。文書側で必要なのも \usepackage{pgfplots} ではなく \usepackage{pgf} だけです。

python
import matplotlib
matplotlib.use("pgf")
import matplotlib.pyplot as plt

matplotlib.rcParams.update({
    "pgf.texsystem": "pdflatex",   # default here is xelatex
    "font.family": "serif",
    "text.usetex": True,
    "pgf.rcfonts": False,
})

fig, ax = plt.subplots(figsize=(4, 3))
ax.plot([0, 1, 2, 3], [0, 1, 4, 9])
ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$x^2$")
fig.savefig("figure.pgf")

ここに 落とし穴が一つ あります。生成された .pgf は冒頭のコメントで「プリアンブルに \usepackage{pgf} を入れよ」と案内してくるのですが、その通りにして \input{figure.pgf} すると、TeX Live 2024 では止まります——"! Undefined control sequence. <recently read> \mathdefault"。原因は、matplotlib が目盛りラベルを \mathdefault{...} で包んで書き出すのに、その定義を .pgf の中に入れてくれない ことです。matplotlib の backend_pgf.py は自分で PDF を作るときだけ内部プリアンブルに定義を注ぎ込みます。対処は 1 行、プリアンブルに \providecommand{\mathdefault}[1]{#1} を書くだけで通ります。なお pgf.texsystem の既定は xelatex なので、pdfLaTeX で本文を組むなら上の例のように明示しておくのが安全です。

latex
\documentclass{article}
\usepackage{pgf}
\usepackage{lmodern}
% matplotlib wraps tick labels in \mathdefault but never defines it
\providecommand{\mathdefault}[1]{#1}
\begin{document}
\input{figure.pgf}
\end{document}

Python のもう一つの道が tikzplotlib(旧称 matplotlib2tikz)で、こちらは matplotlib の図を pgfplots のコード に変換します。tikzplotlib.save("figure.tex") で書き出し、文書側では \usepackage{pgfplots}\pgfplotsset{compat=...} を読み込んだ上で \input{figure.tex} します。素の PGF ではなく軸そのものが axis 環境になるので、生成後に LaTeX 側で目盛りや凡例を手直しできる のが pgf バックエンドとの決定的な違いです。ただし tikzplotlib は現在メンテナンスが止まっており、フォークの matplot2tikz が後継として開発されています(API はほぼ同じ)。どちらも PyPI からの導入で、TeX Live には含まれません。

点が多すぎてコンパイルが終わらないとき

まず 点を間引く こと。これがいちばん効きます。pgfplots の計算はすべて TeX のマクロ展開の上で行われるので、数万点の散布図は素直に遅くなり、TeX のメモリ上限に当たることさえあります。しかも紙面に刷ったとき、隣り合う点の距離が印刷解像度を下回っていれば、その点は見えていません。以下、効き目の大きい順に。

  • 点を間引く。 each nth point=k で k 点ごとに描き、filter discard if not などで範囲外を捨てます。散布図なら見た目はほとんど変わりません。
  • 外部化(externalization)。 \usepgfplotslibrary{external}\tikzexternalize を使うと、各図が一度だけ別 PDF にコンパイルされ、以後は貼り込むだけになります。本文の再コンパイルが劇的に軽くなる代わりに、--shell-escape が要ります。
  • メモリの大きいエンジン。 lualatex は TeX のメモリ制限が緩く、大きな図で行き詰まりにくくなります。
  • そもそも前段で描いておく。 どうしても重いなら、R・Python・gnuplot 側で描いた結果を取り込む(前節)ほうが現実的です。

外部化と gnuplot はどちらも --shell-escape を要求するので、CI やコンテナでビルドするなら そこでも明示的に有効化 しておかないと、手元では通ったビルドが向こうで落ちます。これは事故として非常に多いので、Docker / CI の設定は関連ページで確認しておくとよいでしょう。