不連続なグラフを綺麗に書く

# Abstract

matplotlibって便利ですよね。私も様々な場面で活用しています。
本記事では不連続なグラフの図を作るためのtipsを紹介しようと思います。(意外とこの知見がネットにまとまって落ちていなかったので…)

目標は次の図のような、不連続点を綺麗に描画することです。
![result-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/9f9198a8-d43b-7cf8-e6ed-d8de405e01c4.png)

# 問題設定

本記事では次のグラフを作成します。

```math
y = \mathrm{sign}(\sin(x))
```

ただし$\mathrm{sign}(x)$は符号関数であり、次のように定義されます。

```math
\mathrm{sign}(x) = \begin{cases}
1  & \text{if $x > 0$}\\
0  & \text{if $x = 0$}\\
-1 & \text{if $x < 0$}
\end{cases}
```

まず普通にプロットすると次のようになります。
※以降もそうですが、薄い線は$\sin(x)$です。

![base.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/bc80aea0-c063-ed42-1022-a4bbc6090761.png)

<details><summary>ソースコードはこちら</summary><div>

```python
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-np.pi, np.pi, 1000)

y = np.sign(np.sin(x))
plt.plot(x, y)

plt.plot(x, np.sin(x), alpha=0.2)

plt.xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi],
           labels=[r"$- \pi$", r"$- \frac{\pi}{2}$", r"$0$", r"$\frac{\pi}{2}$", r"$\pi$"])
plt.yticks([-1, 0, 1])
plt.show()

```

</div></details>

matplotlibは当然ですが、不連続なんて認識しません。
認識して勝手に線を消す方が厄介まであります。

# 解法1

matplotlibは`np.inf, np.nan`といった値を無視して描画しません。それを利用したのが次の図とコードです。

![result-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/eb5c1d3e-288c-e219-7d12-03b3eaa18bc8.png)


```python
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-np.pi, np.pi, 1000)
dx = ( x[-1] - x[0] ) / 1000

y = np.sign(np.sin(x))
# 不連続な点付近を`np.nan`で上書きすることで、グラフから消す。
y[np.gradient(y, dx) > 1] = np.nan
plt.plot(x, y)

plt.plot(x, np.sin(x), alpha=0.2)

plt.xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi],
           labels=[r"$- \pi$", r"$- \frac{\pi}{2}$", r"$0$", r"$\frac{\pi}{2}$", r"$\pi$"])
plt.yticks([-1, 0, 1])
plt.show()
```

今回の関数は$\sin(x)$が0になるタイミングで不連続になるので、$\sin(x) = 0$となるような点を描画から除外します。解析的には$x = -\pi, 0, \pi$ですが、この値がプログラム中の`x`に含まれているとは限りません。なのでここでは勾配が一定以上になったら除外といった形で対応しました。

そこまで悪くはないですが、やはり含まない点は◯で明示したいです。
なお$y = 1 / x$といった無限大に発散するパターンでは同様の手法で対応できます。

![result-1-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/53c92079-aa4b-1063-6f19-c901ab259cbe.png)

```python
import numpy as np
import matplotlib.pyplot as plt


x = np.linspace(-1, 1, 1000)

y = 1 / x
# 不連続な点付近を`np.nan`で上書きすることで、グラフから消す。
threshold = 100
y[y < -threshold] = np.nan
y[y > threshold] = np.nan
plt.plot(x, y)

plt.show()
```

ここでは適当な値を閾値として、その値より大きい・小さいもの`np.nan`で上書きしています。
もちろん、勾配から判定してもよいです。

# 解法2

今回の関数は次のように明確に書くことができます。

```math
\mathrm{sign}(\sin(x)) = \begin{cases}
1  & \text{if $0 < x < \pi$}\\
0  & \text{if $x = -\pi, 0, \pi$}\\
-1 & \text{if $-\pi < x < 0$}
\end{cases}
```

なのでこれを丁寧に描画します。
そして`facecolor(s)`というオプションで◯を再現します。

![result-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/9f9198a8-d43b-7cf8-e6ed-d8de405e01c4.png)


```python

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-np.pi, np.pi, 1000)

LINE_COLOR = "blue"
FACE_COLOR = plt.rcParams['axes.facecolor']     # 現在のAxesの背景色を取得

exclude_point_options = dict(
    facecolors=FACE_COLOR,        # これで点の塗りつぶしが背景色になります
    edgecolors=LINE_COLOR,       # 点の線の色です
    s=25,       # 調整してください
    zorder=2        # 他の線と被ったときに上側に描画してもらうためです
)
include_point_options = dict(
    facecolors=LINE_COLOR,
    edgecolors=LINE_COLOR,       # 点の線の色です
    s=exclude_point_options["s"],
    zorder=exclude_point_options["zorder"]
)

for k in range(2):
    segment_length = np.pi
    x_seg = np.linspace(x[0] + k*segment_length, x[0] + (k+1)*segment_length, 1000)
    y_seg = np.ones_like(x_seg) * (-1)**(k+1)
    plt.plot(x_seg, y_seg, color=LINE_COLOR, zorder=exclude_point_options["zorder"] - 1)        # 端っこの点が線の上になるようにzorderを調整
    plt.scatter([x_seg[0], x_seg[-1]], [y_seg[0], y_seg[-1]], **exclude_point_options)

zeros = np.array([- np.pi, 0, np.pi])
plt.scatter(zeros, np.zeros_like(zeros), **include_point_options)

plt.plot(x, np.sin(x), alpha=0.2)

plt.xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi],
           labels=[r"$- \pi$", r"$- \frac{\pi}{2}$", r"$0$", r"$\frac{\pi}{2}$", r"$\pi$"])
plt.yticks([-1, 0, 1])
plt.show()

```

重要になるのは
1. 色の固定
1. `zorder`を設定
1. `facecolors, edgecolors`を設定

になります。
色の固定をしておかないと、自動で色が割り振られてしまい区間ごとに色が変わってしまいます。
また`zorder`を点が大きくなるように設定しないと、線が被ってしまいます。
`facecolors, edgecolors`を設定することで、意図した色に設定しています。
なお`facecolors = "none"`とすると透明になります。透明にしてしまうと線が被ってしまうので今回は背景の色に合わせるという形で調整しています。

# 参考

https://teratail.com/questions/212942

http://nolinknolife.livedoor.blog/archives/16513660.html