Common Lispの計算結果を
GNUplotで図示する

  • Common LispとGNUplotを組み合わせてグラフを描く方法を紹介します。
  • Lispで計算した結果をテキストファイルに書き出し、GNUplotがそれを読み込んで描画します。
  • 点グラフ・折れ線グラフ・棒グラフなど、plotコマンドのオプションで描画スタイルを切り替えられます。
  • LispからGNUplotのスクリプトファイルを生成すれば、データ計算から描画までを一連の処理として自動化できます。

関連記事

1. GNUplotでリストをグラフにする

Lispで計算した結果の傾向を考えるために、グラフで眺めたいときがあります。
そのたびに、わざわざExcelを起動するのは面倒です。
そこで、コマンドラインからグラフを描画するために、GNUplotを使ってみることにしました。

円に含まれる格子点の数を x ごとに数えたら、(当たり前ですが)引き伸ばされた円が出てきた
円に含まれる格子点の数を x ごとに数えたら、(当たり前ですが)引き伸ばされた円が出てきた

GNUplotはコマンド1行でグラフが出る軽量ツールです1
点列をファイルで用意にすれば、「Lispで計算して、GNUplotで描く」という役割分担もできます。

ちなみに、Common LispからGNUplotを直接操作するライブラリとしてeazy-gnuplotも試してみたのですが、私の環境によるのかgnuplotが起動しませんでした2
あと、LispからGNUplotを直接制御したい場合は、clgplotも選択肢の一つみたいです3

1.1. GNUplotのインストール

gnuplotは、macOSならHomebrewで1行で入ります4

brew install gnuplotCode language: Bash (bash)
1.1. GNUplotのインストール

インストールが終わったらターミナルで gnuplot と打つと対話モードに入ります。
たとえば、次を打ってみてください。

plot sin(x)
1.1. GNUplotのインストール

ウィンドウが開いて正弦波が表示されました。

対話モードを終了するには quit を打ちます。

1.2. 描画バックエンドを指定する(set terminal)

何も表示されない場合は、描画バックエンドでウィンドウを明示すると解決することが多いです5

set terminal qt
plot sin(x)Code language: JavaScript (javascript)

画像として保存したい場合は、先頭にこれを加えます。

set terminal png
set output "out.png"Code language: JavaScript (javascript)

もちろん、SVGでも書き出せます。

set terminal svg background "white"
set output "output.svg"Code language: JavaScript (javascript)
1.2. 描画バックエンドを指定する(set terminal)

1.3. スクリプトを用意しておく

対話モードで慣れたら、スクリプトを用意しておきます。

set terminal svg background "white"
set output "output.svg"

plot sin(x)Code language: JavaScript (javascript)

あとはシェルから1行で実行できます。

gnuplot plot.gpCode language: Bash (bash)

たとえば、コマンドラインから
gnuplot plot.gp && open output.svg
などと実行すれば、手元でスクリプトを編集しながらSVGを表示できます。

2. Lispで点列を作ってGNUplotに渡す

plotは、式や点の座標を描画します

たとえば、y = x^2 のような数式もGNUplot単体で描画できます。

set terminal svg background "white"
set output "output.svg"

plot x**2Code language: JavaScript (javascript)
Gnuplot Produced by GNUPLOT 6.0 patchlevel 4 0 10 20 30 40 50 60 70 80 90 100 -10 -5 0 5 10 x**2 x**2

ただ、計算ロジックが複雑になるケースは、Lispを経由したくなります。
たとえばフィボナッチ数列、リストのフィルタリング、再帰的な計算などはGNUplotの式言語では書きにくい気がします。

もちろん、GNUplotはLispの関数を直接呼べません。
そこで、Lispで計算した結果をテキストファイルに書き出して、GNUplotがそれを読む、という2段階で進めます。

自分で傾向をつかむためのグラフには十分です。

2.1. 点列を書き出す(list -> file)

たとえば、y = x^2 上の点をLispで取ってみます。

(defun square (x)
  (* x x))

(defun make-points (func-obj xmin xmax step)
  (loop for x from xmin to xmax by step
        collect (list x (apply func-obj (list x)))))Code language: Lisp (lisp)

この関数を実行して、結果をdata.txtに書き出します6

(defun write-points (points filename)
  (with-open-file (out filename
                       :direction :output
                       :if-exists :supersede)
    (dolist (p points)
      (format out "~a ~a~%" (first p) (second p)))))

(write-points (make-points #'square -10 10 1)
	      "data1.txt")Code language: Lisp (lisp)

すると、中身はこのように x y の座標が一行ずつ並びます。

-10 100
-9 81
-8 64
...
8 64
9 81
10 100

2.2. GNUplotで描く(file -> plot using)

GNUplotには、データファイルをもとに描画する機能があります。

plot "~/data.txt" using 1:2 with linespointsCode language: JavaScript (javascript)
Gnuplot Produced by GNUPLOT 6.0 patchlevel 4 0 20 40 60 80 100 -10 -5 0 5 10 “~/data.txt” using 1:2 “~/data.txt” using 1:2

using 1:2 は「1列目をx、2列目をy」という指定です7
with linespoints で点をつないだ線グラフになります。

点だけ描きたいなら with points
線だけなら with lines を使います。

2.3. あれこれ整えてSVGで書き出す

数学で見慣れたグラフにするには、軸の表示などにけっこう手を加える必要がありました。

Gnuplot Produced by GNUPLOT 6.0 patchlevel 4 -2 -1 0 1 2 3 4 5 6 7 -4 -3 -2 -1 0 1 2 3 4 x y O “~/data1.txt” using 1:2

以下のようなプロットスクリプト plot.gpを用意して、gnuplot plot.gp コマンドで実行しました。

# 出力設定
set terminal svg background "white"
set output "output.svg"

# 見た目(枠・凡例)
unset border
set nokey

# スケール(縦横比固定)
set size ratio -1

# 範囲(変数で管理)
xmin = -4.5
xmax =  4.5
ymin = -2.5
ymax =  7.5

set xrange [xmin:xmax]
set yrange [ymin:ymax]

# 軸用の補助値(少し外側まで伸ばす)
xaxis_min = xmin - 0.5
xaxis_max = xmax + 0.5
yaxis_min = ymin
yaxis_max = ymax

# 軸(矢印付き)と軸ラベル
set label "x" at xaxis_max,0 offset 0.5,0
set arrow from xaxis_min,0 to xaxis_max,0 head filled size screen 0.02,15 lw 1

set label "y" at 0,yaxis_max offset 0,0.5
set arrow from 0,yaxis_min to 0,yaxis_max head filled size screen 0.02,15 lw 1

# 原点
set xtics add ('' 0)
set ytics add ('' 0) offset -1, 0
set label "O" at 0,0 offset -1.5,-1

# 目盛(軸上に表示)
set xtics axis
set xtics 1
set ytics axis
set ytics 1


# データ描画 pt:point type, ps: point size
plot "~/data2.txt" using 1:2 with linespoints pt 7 ps 0.5Code language: PHP (php)

Lispで書き出すデータは、0.1刻みにするようにしました。

(write-points (make-points #'square -10 10 0.1)
	      "data2.txt")Code language: PHP (php)

3. 複数のグラフを書く

GNUplotで複数の線を描くには、plot コマンドにカンマで並べます。

plot "~/data1.txt" using 1:2 with linespoints pt 7, \
     "~/data2.txt" using 1:2 with linespoints pt 7
Code language: JavaScript (javascript)
Gnuplot Produced by GNUPLOT 6.0 patchlevel 4 -2 -1 0 1 2 3 4 5 6 7 -4 -3 -2 -1 0 1 2 3 4 x y O “~/data1.txt” using 1:2 “~/data2.txt” using 1:2

data1.txt, data2.txtは、Common Lispで

(write-points (make-points #'square -10 10 1)
	      "data3.txt")
(write-points (make-points
	       (lambda (x) (* x x x)) -10 10 1)
	      "data4.txt")Code language: PHP (php)

Lisp側でそれに対応させるなら、write-points を2ファイル分呼ぶか、1ファイルに複数列を書き出します。

たとえば、3列目を別の線にする場合は using 句を変えることができます。

plot "data.txt" using 1:2 with lines, \
     "data.txt" using 1:3 with lines
Code language: JavaScript (javascript)

3.1. リストから棒グラフにする(with boxes)

Lispの計算結果が1次元リストの場合、添字をx軸にするのが手軽です。

(defun write-seq (seq filename)
  (with-open-file (out filename
                       :direction :output
                       :if-exists :supersede)
    (loop for y in seq
          for x from 0
          do (format out "~a ~a~%" x y))))Code language: Lisp (lisp)
(write-seq '(3 1 4 1 5 9 2 6) "data.txt")Code language: Lisp (lisp)

GNUplot側は、with boxesを使うと、棒グラフ(ヒストグラム)にできます。

set boxwidth 0.8
set style fill solid 0.5
set xtics 0,1

plot "data.txt" using 1:2 with boxesCode language: CSS (css)

3.2. Lispからスクリプトファイルを作る(.gp)

毎回GNUplotを手打ちするのは面倒なので、描画設定ごとファイルに書き出しておくと便利です。

(defun write-gnuplot-script (data-file script-file)
  (with-open-file (out script-file
                       :direction :output
                       :if-exists :supersede)
    (format out "set title \"result\"~%")
    (format out "set grid~%")
    (format out "set xrange [-10:10]~%")
    (format out "plot ~s using 1:2 with lines~%" data-file)))Code language: Lisp (lisp)
(write-points (make-points -10 10) "data.txt")
(write-gnuplot-script "data.txt" "plot.gp")Code language: Lisp (lisp)

4. まとめ

Lisp側はリスト処理とファイル書き出しだけ担当すれば、GNUplotのシンプルな機能でグラフを作ることができます。

テキストファイルを経由するだけでグラフは描けます。

  1. GNUplotは1986年から開発が続くコマンドライン型のグラフ描画ツールです。Linux・Windows・macOSなど多くのプラットフォームで動作し、ソースコードは無償で配布されています。 – gnuplot homepage
  2. eazy-gnuplotはGitHub上で公開されているCommon Lisp向けのGNUplotラッパーです。作者の開発環境はSBCL 1.2.1 on Linux(2015年頃)で、QtターミナルはGNUplot 4.6以降でのみ対応とされています。 – eazy-gnuplot README
  3. 内部的に一時ファイルとスクリプトファイルを生成してGNUplotを呼び出す仕組みで、QuicklispからロードできCommon Lispのリストを直接渡してプロットできます。 – clgplot on GitHub
  4. HomebrewはmacOS向けのパッケージ管理システムです。2023年2月時点で、Homebrewが提供するgnuplotのボトル(ビルド済みバイナリ)にはQt5サポートが含まれています。 – gnuplot INSTALL
  5. qt が使えない環境では aqua を試してください。aqua はmacOSのAquaTerm(別途インストールが必要なネイティブ表示アプリ)を使うターミナルです。qt ターミナルはGNUplot 4.6以降で対応しており、macOSのHomebrewビルドでは現在デフォルトで有効化されています。 – gnuplot documentation
  6. :if-exists :supersede は、同名のファイルがすでに存在する場合に上書きするオプションです。CLHSの定義では「既存ファイルと同名の新しいファイルを作成する」動作とされており、実行するたびにファイルが更新されます。 – CLHS: Function OPEN
  7. using 句はGNUplotのデータ列指定オプションで、u と省略できます。列番号のほか、列を使った式(例:using 1:($2*2))も書けます。 – GNUPLOT – A Brief Manual and Tutorial