bokeh: 基準線は`Figure.ray`関数で引くのがおすすめ

# 環境
* Python3.10.2
* bokeh 2.4.2


# 背景
グラフを描画する際、下図のように目標値などの基準線を引きたいときがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/99329bd2-5226-6654-4145-69fab6659f63.png)

http://drmorich.blog135.fc2.com/blog-entry-281.html 引用

基準線は`Figure.line`関数より`Figure.ray`関数を使った方が便利です。その理由と使い方を記載します。


# `Figure.ray`関数の使い方
[Figure.ray](https://docs.bokeh.org/en/2.4.3/docs/reference/plotting/figure.html#bokeh.plotting.Figure.ray)関数は半直線を描画します。


以下のコードを実行すると、`y=3`の線が描画されます。

```python
plot = figure()
plot.scatter(x=[1, 10, 100,200], y=[1,2,4,5])
plot.ray(x=0, y=3, color="red")
show(plot)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/7330d589-1058-e1be-6340-6e94b267c6bf.png)

デフォルトでは線の長さは無限です（`length`引数で指定）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/7a0de79c-bcad-83c5-f027-d9fdcaccade1.png)

`angle`引数に角度を指定することも可能です。

```python
dates = [datetime.datetime(2018, 7, 18,0,0,0,0) + datetime.timedelta(days) for days in range(5)]
sales = [100, 110, 300, 280, 270]
plot = figure(x_axis_type="datetime")
plot.line(x=dates, y=sales)
plot.ray(x=datetime.datetime(2018, 7, 20), y=0, angle=math.pi/2, color="red")
show(plot)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/6a68a9e0-0431-c9fd-bfc1-4977422aa591.png)

詳細な使い方を知りたい方は、[User Guide](https://docs.bokeh.org/en/2.4.3/docs/user_guide/plotting.html?#segments-and-rays)も参照ください。


# `Figure.ray`関数を使うメリット
## メリット1：無限の長さの線が引ける
基準線の長さは通常無限です。
`Figure.ray`関数では`length`引数を指定しなければ、長さが無限の直線（正しくは半直線）が引けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/ca900056-b213-ef9c-7ad1-1d0d00294ee9.png)

`Figure.line`関数では必ず2点を指定するので、長さを指定することになります。


## メリット2：デフォルトのグラフ範囲にあまり影響しない
デフォルトのグラフ範囲は、描画対象のデータがすべて表示される範囲です。
したがって、基準線を`Figure.line`関数で引くと、図のようにデフォルトのグラフ範囲が広くなりすぎます。

```python
plot = figure()
plot.scatter(x=[1, 10, 100,200], y=[1,2,4,5])
plot.line(x=[0,1000], y=[3,3], color="red")
show(plot)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/d78cc0d2-ce66-4297-7f8c-354f1deaf479.png)

`Figure.ray`関数では長さを無限にしても、デフォルトのグラフ範囲には影響しません。
ただし`Figure.ray`関数で引いた線の始点は、デフォルトのグラフ範囲で表示されます。


# `Figure.ray`関数のデメリット

## `Figure.ray`関数の`angle`の角度はデータ単位でなくでなく画面単位

以下のコードは、傾きが45°の直線を描画します。

```python
plot = figure()
plot.scatter(x=[1, 10, 100,200], y=[1,2,4,5])
plot.ray(x=0, y=0, angle=math.pi/4, color="red")
show(plot)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/a30004fa-7c3c-376c-4efd-437a3eefbaa5.png)

当然ではありますが、`angle`はデータ単位でなく画面単位です。したがって、`y=x`のような基準線を引くのは難しいです。
`match_aspect=True`を指定してX軸とY軸のスケールを同じにすれば`y=x`の線は引けますが、決して見やすいグラフではありません。また"Box Zoom"機能を使うとX軸とY軸のスケールが変わってしまい、`y=x`の線ではなくなります。

>This setting only sets the initial plot draw and subsequent resets. It is possible for tools (single axis zoom, unconstrained box zoom) to change the aspect ratio.

https://docs.bokeh.org/en/2.4.3/docs/reference/plotting/figure.html#bokeh.plotting.Figure.match_aspect

```python
plot = figure(match_aspect=True)
plot.scatter(x=[1, 10, 100,200], y=[1,2,4,5])
plot.ray(x=0, y=0, angle=math.pi/4, color="red")
show(plot)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37903/b0a39509-030d-b362-9c04-34c9f999f2d2.png)


bokehのフォーラムでコアチームメンバに教えてもらいました。

https://discourse.bokeh.org/t/how-do-i-add-line-y-x-with-ray-method-i-could-not-set-gradient-of-line-to-45-in-data-units/9464



# まとめ
* 基準線を水平または垂直に引く場合は、`Figure.ray`関数を使うと便利。半直線が引ける。
* `Figure.ray`関数の`angle`引数は画面単位なので、`y=x`のような傾きのある直線を引くのが難しい。