플롯

실험 데이터나 함수 그래프를 외부 그래프 프로그램에서 그려 이미지로 붙여 넣을 필요는 없습니다. LaTeX에는 문서 안에서 데이터로부터 직접 플롯을 만드는 방법이 있습니다. 주역은 TikZ/PGF 위에 만들어진 pgfplots입니다. 함수, 좌표, 데이터 파일에서 선 그래프와 산점도, 막대그래프, 3D 곡면까지 본문과 같은 글꼴과 같은 선 두께로 그릴 수 있습니다. 이 페이지는 pgfplotsaxis 환경과 \addplot을 중심으로, 외부 gnuplot을 계산기로 호출하는 방법과 R 또는 Python (matplotlib)에서 그린 그래프를 TikZ 코드로 가져오는 방법까지 다룹니다.

왜 문서 안에서 플롯을 조판하는가

스프레드시트나 그림 앱에서 만든 PNG를 붙이면 대개 문서와 어긋납니다. 축 라벨의 글꼴이 본문과 다르고, 수식은 흐릿한 래스터 이미지가 되며, 확대하면 선이 들쭉날쭉해집니다. 데이터를 고치면 그림을 다시 만들고 다시 붙여 넣어야 합니다. pgfplots는 반대로 그래프 자체를 LaTeX가 조판하게 합니다. 축 숫자와 범례는 본문과 같은 글꼴을 쓰고, 곡선은 벡터라 어떤 배율에서도 매끄럽고, 라벨에는 $\sin x$ 같은 진짜 수식을 그대로 넣을 수 있습니다.

두 번째 장점은 데이터와의 분리입니다. 좌표를 본문에 하드코딩하지 않고 pgfplots가 .dat.csv 데이터 파일을 읽게 하면, 데이터를 갱신하고 다시 컴파일하는 것만으로 플롯이 따라옵니다. 논문, 슬라이드, 부록에서 같은 숫자를 여러 번 재사용할 수 있고 전사 오류도 생기지 않습니다. 이는 데이터로부터 표를 만드는 발상(관련 페이지 참조)과 정확히 같으며, pgfplots에는 표를 위한 자매 패키지 pgfplotstable도 포함되어 있습니다.

pgfplotsTikZ/PGF 위에 만들어졌으므로 전체 그림은 tikzpicture 환경 안에 놓이고, TikZ 자체의 좌표, 노드, 장식을 자유롭게 함께 쓸 수 있습니다. TikZ 자체의 기본(\draw, 좌표계, 노드)은 “TikZ basics” 페이지에 맡기고, 여기서는 플롯 작성에 집중합니다.

pgfplots의 뼈대 — axis\addplot

먼저 프리앰블에서 불러오고 호환성 수준을 선언합니다. \pgfplotsset{compat=1.18}처럼 쓰면 그 버전의 기본 동작(축 모양, 눈금 배치 등)에 고정되어, 나중에 pgfplots가 업데이트되어도 그림이 조용히 바뀌지 않습니다. 새 문서에서는 설치된 버전이 허용하는 한 가능한 최신 번호를 지정하는 것이 관례입니다.

latex
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}

본문에서는 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점으로 샘플링하여 매끄러운 파란 곡선으로 그립니다. 축에는 x와 f(x) 라벨이 붙고, 위에는 “A parabola”라는 제목이 있으며, grid=major는 주 눈금을 따라 옅은 격자를 그리고, \addlegendentry는 범례 항목 “x²”를 붙입니다. 핵심을 차례로 보겠습니다.

  • 라벨과 제목은 중괄호로 감싸세요. xlabel={$x$}처럼 {...}로 감싸면 그 안에 수식이나 쉼표를 안전하게 넣을 수 있습니다.
  • ^는 거듭제곱이고, 삼각함수는 기본적으로 도 단위입니다. \addplot {x^2}의 식은 pgf 내부 파서가 평가합니다. \sin 같은 함수는 인수를 로 받으므로 라디안을 넘기려면 sin(deg(x))처럼 deg()로 감쌉니다.
  • domainsamples. 함수 플롯은 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 범위에서 샘플링해 곡선을 만듭니다.

latex
\addplot[red, domain=0:2*pi, samples=200] {sin(deg(x))};

이는 0부터 2π까지 사인 곡선을 그립니다. x는 라디안 값이므로, 도 단위로 동작하는 \sin에 전달하기 전에 deg()로 도로 변환하는 것이 핵심입니다.

둘째는 인라인 좌표에서입니다. coordinates {...}(x,y) 쌍을 나열하면 pgfplots가 그 점들을 순서대로 이어 선 그래프를 만들거나, 마커만으로 산점도를 만듭니다. 측정값이 몇 개일 때 간편합니다.

latex
\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가 있다고 합시다.

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};
    % 列名で明示するなら:
    % \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 틀에서도 데이터를 보이는 방식을 바꿀 수 있습니다. 막대그래프axisybar(세로 막대) 옵션을 주기만 하면 됩니다. 여러 \addplot 호출을 겹치면 자동으로 가로로 어긋나 클러스터 막대가 됩니다.

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}

여기서는 연도(문자열)를 x축 라벨로 쓰기 위해 symbolic x coords를 사용하고, xtick=data로 각 데이터점에 눈금을 맞춥니다. 가로 막대로 만들고 싶다면 xbar를 사용합니다.

로그 축axis를 다른 환경 이름으로 바꾸기만 하면 됩니다. 양쪽 로그는 loglogaxis, x축만 로그는 semilogxaxis, y축만 로그는 semilogyaxis입니다. 내부의 \addplot 작성법은 변하지 않습니다.

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

3D에는 \addplot3를 사용하며, axis는 자동으로 3차원이 됩니다. 곡면은 surf, 와이어프레임은 mesh를 지정하고, 함수는 xy 두 변수로 씁니다. 좌표나 데이터 파일에서 오는 3D도 같은 방식으로 \addplot3가 처리합니다.

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

이는 종 모양의 가우스 곡면 z = e^(−x²−y²)를 30×30 격자로 샘플링해 색이 있는 곡면으로 그립니다. 시점은 view={azimuth}{elevation}으로 돌릴 수 있습니다.

gnuplot을 계산기로 사용하기

pgfplots 자체 파서는 편리하지만 복잡한 식이나 많은 샘플에는 약할 수 있습니다. \addplot gnuplot {...}라고 쓰면 수치 계산을 외부 프로그램 gnuplot에 맡기고, gnuplot이 계산한 좌표를 pgfplots가 다시 읽어 그립니다. 사실상 gnuplot을 “계산기”로 사용하는 셈입니다.

latex
\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(deg(x))}, gnuplot 경유에서는 {sin(x)}라고 씁니다. --shell-escape`는 보안상 기본적으로 꺼져 있으므로 필요할 때만 켜는 것이 안전합니다.

R과 Python 그래프 가져오기

이미 R이나 Python에서 분석을 마쳤고 그 그래프를 그대로 쓰고 싶지만, 이미지 붙여넣기의 흠(글꼴 불일치, 흐릿한 수식)은 피하고 싶을 수 있습니다. 이럴 때는 각 도구가 TikZ/pgfplots 코드를 내보내게 하면, 그래프가 문서의 일부로 조판되어 글꼴과 수식이 본문과 맞습니다.

R에서는 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)에서는 tikzplotlib(이전 이름 matplotlib2tikz)이 matplotlib.pyplot으로 만든 그림을 pgfplots 코드로 변환합니다. tikzplotlib.save("figure.tex")로 내보내고, LaTeX 쪽에서는 \input{figure.tex}로 가져옵니다. 다만 tikzplotlib는 현재 유지보수가 중단되었고, 포크인 matplot2tikz가 후속으로 개발되고 있습니다. 새 작업이라면 matplot2tikz를 고려하는 것이 좋습니다(API는 거의 같습니다).

python
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에서 그린 결과를 가져오는 방식(이전 절)이 더 현실적일 수 있습니다.

외부화나 gnuplot처럼 --shell-escape가 필요한 기능은 CI나 컨테이너에서 빌드할 때도 활성화해야 합니다. Docker / CI 설정은 관련 페이지를 참조하세요.