Nimでmatplotlibが使いたい（matplotnim, nimpy）

# はじめに

[ggplotnim](https://scinim.github.io/getting-started/basics/basic_plotting.html), [nim-plotly](https://github.com/SciNim/nim-plotly), [graph](https://github.com/stisa/graph), [gnuplot.nim](https://qiita.com/YagiEL39/items/be050672b2a49757d6de)等々、Nimにもグラフ描画ライブラリは複数あります。今回は私が使い慣れているPythonの`matplotlib`をNimで使う方法をまとめます。

# matplotnim, nimpyとは

[`matplotnim`](https://github.com/ruivieira/matplotnim)はmatplotlibのNim版ラッパーライブラリです。
[`nimpy`](https://github.com/yglukhov/nimpy)は、PythonからNim、並びにNimからPythonを呼ぶことを容易にするライブラリです。使用方法は[Nim初心者が始めるNimとnimpy](https://qiita.com/k4saNova/items/5bb67d1cb40ba90431af)などを参考にしてください。

# 実行環境

Windows11 22H2
nim 1.6.10

# matplotnim使用方法（エラー解消方法）
:::note warn
matplotnim自体がnimpyを用いているため、結局Pythonのインストールが必要となります。また、matplotnim公式もnimpyがあるので、自身が冗長であると言っています。 ~~matplotnimを使用するメリットは、import文を使うことでよりPythonしてる感が出る程度です。~~ nimpyだけを用いる方法も[後述](#nimpyで同等の処理をする)します。
:::

まず、nimbleからmatplotnimをダウンロードしましょう。
```
nimble install matplotnim
```
なおプログラム例は以下のようになります。
```nim
import matplotnim, nimpy # pltからの.呼出でnimpyが要る

let x = @[1, 2, 3, 4]
let y = @[5.5, 7.6, 11.1, 6.5]

discard plt.figure()
discard plt.plot(x, y)
discard plt.show()
```
さっそく実行すると、以下のエラーが出ました。
```
Python path configuration:
  PYTHONHOME = (not set)
  PYTHONPATH = (not set)
  program name = 'python'
  isolated = 0
  environment = 1
  user site = 1
  import site = 1
  sys._base_executable = 'c:\\code\\Nim\\matplotnim_test.exe'
  sys.base_prefix = 'C:\\anaconda3'
  sys.base_exec_prefix = 'C:\\anaconda3'
  sys.platlibdir = 'lib'
  sys.executable = 'c:\\code\\Nim\\matplotnim_test.exe'
  sys.prefix = 'C:\\anaconda3'
  sys.exec_prefix = 'C:\\anaconda3'
  sys.path = [
    'C:\\anaconda3\\python39.zip',
    '.\\DLLs',
    '.\\lib',
    'c:\\code\\Nim',
  ]
Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
Python runtime state: core initialized
ModuleNotFoundError: No module named 'encodings'

Current thread 0x00006220 (most recent call first):
<no Python frame>
Error: execution of an external program failed: 'c:\code\Nim\matplotnim_test.exe '
```
[同様のエラー](https://stackoverflow.com/questions/38132755/importerror-no-module-named-encodings)から、`PYTHONHOME`と`PYTHONPATH`の環境変数を設定していないことが原因と知りました。[Windows10のAnacondaで仮想環境にインストールしたパッケージがimportできない](https://qiita.com/ground0state/items/52c5945329e9823c8c93)等を参考にして、`PYTHONPATH`には自作のライブラリを格納したパス、`PYTHONHOME`には標準の Python libraries のパスを指定します。

```
PYTHONHOME=C:\anaconda3
PYTHONPATH=C:\anaconda3\Library\bin
```
私は上記のように設定しましが、Pythonのインストール状況によってまちまちだと思います。また、今回はmatplotlibを呼び出したいだけなので`PYTHONPATH`の設定だけで動作すると思われます。

再起動して実行すると今度はこんなエラーが...
```
This application failed to start because it could not find or load the Qt platform plugin "windows"
in "".

Reinstalling the application may fix this problem.
Error: execution of an external program failed: 'c:\code\Nim\matplotnim_test.exe '
```
そもそもPython側で実行すると普通に動きます。
```python
import matplotlib.pyplot as plt
print(plt.__file__) # C:\anaconda3\lib\site-packages\matplotlib\pyplot.py

x = [1, 2, 3, 4]
y = [5.5, 7.6, 11.1, 6.5]

plt.figure()
plt.plot(x, y)
plt.show()
```
Nim側とPython側では違うmatplotlibを呼び出しているかとも思いましたが、同じでした。
```python:searchPlace.py
def searchPlace(lib):
  return lib.__file__ # アンダーライン始まり・2連続はNimだと多分無理
```
```nim
import matplotnim, nimpy

let place = pyImport("searchPlace").callMethod("searchPlace", plt).to(string)
echo place # C:\anaconda3\lib\site-packages\matplotlib\pyplot.py
```
調べたところ、[同様のエラー](https://qiita.com/sukoppu1102/items/f5c00b520da0fa5d2ff3)を発見したので同じように`QT_PLUGIN_PATH`を設定して再起動。
```
QT_PLUGIN_PATH=C:\anaconda3\Library\plugins
```
するとやっと表示することができました！
![figure1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120826/2e01c971-408a-a8b8-f79d-dea36e27e67e.png)

# nimpyで同等の処理をする
ところで、matplotnimの実装は
```nim: matplotnim.nim
import nimpy

let plt* = pyImport("matplotlib.pyplot")
```
以上です。実際matplotnimをインストールする必要はあまり感じません。
先の章でmatplotlibをインストールしていなければ、nimpyをインストールしましょう。
```
nimble install nimpy
```
先ほどのプログラムを少し変更して実行します。エラー対応は先の章の通りにしましょう。
```nim
import nimpy
let plt = pyImport("matplotlib.pyplot")

let x = @[1, 2, 3, 4]
let y = @[5.5, 7.6, 11.1, 6.5]

discard plt.figure()
discard plt.plot(x, y)
discard plt.show()
```
こちらでも実行できました！
![figure1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120826/20289cfb-af20-6caf-8819-fdbccfdd0d2d.png)

seq型が通るのであれば、ArraymancerのTensor型は通るのかと試しましたが、一度seq型にしないとダメなようです。

```nim
import nimpy, arraymancer
let plt = pyImport("matplotlib.pyplot")

let x = [1, 2, 3, 4].toTensor
let y = [5.5, 7.6, 11.1, 6.5].toTensor

discard plt.figure()
discard plt.plot(x.toSeq1D(), y.toSeq1D())
discard plt.show()
```

## (更新) Windows起動時に表示されるメッセージへの対処法

`QT_PLUGIN_PATH`を設定すると、Windows起動時に以下のエラーメッセージが表示されてしまいます。
![Ot_error.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120826/83941f01-941b-d79c-6b7c-bd411084c82b.jpeg)

そのため、以下のようにプログラム上で環境変数を設定して、プログラム動作時だけ機能するようにしましょう。

```nim
import nimpy, arraymancer
let plt = pyImport("matplotlib.pyplot")

let x = [1, 2, 3, 4].toTensor
let y = [5.5, 7.6, 11.1, 6.5].toTensor

# import os # Nim上での設定は反映されないらしい
# putEnv("QT_PLUGIN_PATH", r"C:\anaconda3\Library\plugins")

let os = pyImport("os")
os.environ["QT_PLUGIN_PATH"] = r"C:\anaconda3\Library\plugins"
# discard os.putenv("QT_PLUGIN_PATH", r"C:\anaconda3\Library\plugins") # 若しくは

discard plt.figure()
discard plt.plot(x.toSeq1D(), y.toSeq1D())
discard plt.show()
```

# まとめ
nimpyを使えばPythonのライブラリを簡単に使えることがわかりました。
描画系ライブラリは他にもあるので、自分に合うものを試していきたいです。