RustのPlottersでグラフ描画を試す

## はじめに

Rustのグラフ描画ツールPlottersで色々試してみた。
英語サイトも含めて現時点ではWebに載っている情報が意外と少なく、[Plottersのドキュメント](https://docs.rs/plotters/0.3.1/plotters/index.html)のみを参考に実装した内容もご紹介する。
はじめに断っておくと、グラフ描画の機能面では例えばPythonの[Matplotlib](https://matplotlib.org/)等、別言語やフレームワークの描画ツールの方が充実しているかもしれない。なので、本記事は「何らかの制約上、Rustでないとグラフ描画できない。私の開発環境ではRustのPlottersがグラフ描画ツールとしてベスト！」という方の参考になれば幸いである。

## 各種情報

### 動作確認バージョン

* rustc : 1.51.0
* cargo : 1.51.0

### ソース構成

Rustのソースは以下の２つ

* main.rs : Plottersを使ってグラフ描画する処理を記述
* data.rs : グラフ表示用の時系列データを定義

```
.
├── Cargo.toml
└── src
    ├── data.rs <- 関数を定義しているモジュール
    └── main.rs <- main関数のみ記載
```

#### Cargo.tomlの中身

最低限、plottersのクレートをインストールすれば動作可能。
今回はx軸を日付データ（Date<Local>型）にしたいため、追加でchronoもインストールする。

```toml:Cargo.toml
[package]
name = "plotters-sample"
version = "0.1.0"
authors = ["auther"]
edition = "2018"

[dependencies]
plotters = "0.3.1"

# 時刻情報を扱うの利用（グラフ表示に必須ではない）
chrono = "0.4.19"
```

:::note warn
plottersのバージョンを0.3.3以上でビルドする場合には、[こちら](https://docs.rs/plotters/0.3.3/plotters/index.html)のドキュメントにあるように libfontconfig 等のパッケージが必要。以下はaptでインストールする例。

```
sudo apt install pkg-config libfreetype6-dev libfontconfig1-dev
```
::: 



## 入力データの準備

プロットするデータは``data.rs``のソース内で設定し、``main.rs``から参照できるようにする。

* x軸が日付、y軸が数値の時系列データ。（トヨタ自動車の株価の各日の終値）
* サンプルソースのため、``get_data``関数内で直書きで設定する。実利用ではcsvファイルやDataBaseから取得することになるはず。
* 日付は最初は文字列スライス型で定義しているので、日付の型``Date<Local>``に変換する関数``parse_time``も作っておく。``&`static``のライフタイム注釈については詳しく説明しないが、これを入れると文字列を「文字列リテラル」として静的領域に格納してくれる。

```rs:data.rs
use chrono::offset::{Local, TimeZone};
use chrono::Date;

/// データの取得（固定値）
pub fn get_data() -> Vec<(&'static str, f32)> {
  return vec![
    ("2021-06-22", 9960.0), ("2021-06-23", 9782.0),
    ("2021-06-24", 9824.0), ("2021-06-25", 9849.0),
    ("2021-06-28", 9842.0), ("2021-06-29", 9740.0),
    ("2021-06-30", 9710.0), ("2021-07-01", 9690.0),
    ("2021-07-02", 9820.0), ("2021-07-05", 9772.0),
    ("2021-07-06", 9794.0), ("2021-07-07", 9734.0),
    ("2021-07-08", 9675.0), ("2021-07-09", 9650.0),
    ("2021-07-12", 9815.0), ("2021-07-13", 9865.0),
    ("2021-07-14", 9869.0), ("2021-07-15", 9832.0),
    ("2021-07-16", 9866.0), ("2021-07-19", 9740.0),
    ("2021-07-20", 9612.0), ("2021-07-21", 9725.0),
    ("2021-07-26", 9829.0), ("2021-07-27", 9871.0),
    ("2021-07-28", 9804.0), ("2021-07-29", 9856.0),
    ("2021-07-30", 9805.0), ("2021-08-02", 10030.0),
  ];
}

/// 日付の文字列をDate型に変換
pub fn parse_time(time_str: &str) -> Date<Local> {
    Local.datetime_from_str(
      &format!("{} 0:0", time_str),
      "%Y-%m-%d %H:%M"
    )
    .unwrap()
    .date()
}
```

## グラフの描画

``main.rs``の``main``関数でグラフを描画する。手順は以下の４つ。

### 手順

* (1) プロット用データの準備
    * dataモジュールから入力データを取得し、横軸用の日付のベクトル、縦軸用のf32型のベクトルに変換する
* (2)  描画先の情報を設定
    * 画像サイズや背景色など、描画キャンバス全般に関する情報を設定する
    * 今回は描画結果を画像ファイルとして出力するため、BitMapBackend型のオブジェクトを定義
* (3) グラフ全般の設定
    * キャプション、x軸、y軸の値範囲、ラベルの余白等、グラフ描画全般に関して設定する
    * 各軸の値の範囲は自動設定できないようなので、``build_cartesian_2d``メソッドで範囲を明示する必要がありそう…。y軸の範囲はf32型のVectorの最大最小値で決めているが、f32型にはNaNが含まれていてminやmaxを求めるのに工夫がいる。（[参考にさせていただいたサイト](https://qiita.com/lo48576/items/343ca40a03c3b86b67cb)）
* (4) グラフの描画
    * (3)で設定した内容をもとに、グラフの軸やグリッド線などを描画する
    * (1)で準備したデータを指定のスタイルで描画する。以下ソースの例では折れ線グラフ（LineSeries）で描画。

以下はソースの中身

```rs:main.rs
use plotters::prelude::*;
use chrono::offset::Local;
use chrono::Date;

mod data;

fn main() -> Result<(), Box<dyn std::error::Error>> {

  /* (1) プロット用データの準備 */

  // データを取得。この時点では(日付,値)のタプルのVector型になっている
  let data = data::get_data();

  /* x軸とy軸で個別のVector型にする */
  
  // x軸 : 日付のVector
  let xs: Vec<Date<Local>> = data.iter()
                                 .map(|(x, _)| data::parse_time(*x))
                                 .collect();
  // y軸: 値のVector
  let ys: Vec<f32> = data.iter()
                         .map(|(_, y)| *y)
                         .collect();


  /* (2) 描画先の情報を設定 */

  let image_width = 1080;
  let image_height = 720;
  // 描画先を指定。画像出力する場合はBitMapBackend
  let root = BitMapBackend::new
    ("plot.png", (image_width, image_height)).into_drawing_area();
    
  // 背景を白にする
  root.fill(&WHITE)?;


  /* (3) グラフ全般の設定 */

  /* y軸の最大最小値を算出
     f32型はNaNが定義されていてys.iter().max()等が使えないので工夫が必要
     参考サイト
     https://qiita.com/lo48576/items/343ca40a03c3b86b67cb */
  let (y_min, y_max) = ys.iter()
                         .fold(
                           (0.0/0.0, 0.0/0.0),
                           |(m,n), v| (v.min(m), v.max(n))
                          );

  let caption = "Sample Plot";
  let font = ("sans-serif", 20);

  let mut chart = ChartBuilder::on(&root)
    .caption(caption, font.into_font()) // キャプションのフォントやサイズ
    .margin(10)                         // 上下左右全ての余白
    .x_label_area_size(16)              // x軸ラベル部分の余白
    .y_label_area_size(42)              // y軸ラベル部分の余白
    .build_cartesian_2d(                // x軸とy軸の数値の範囲を指定する
      *xs.first().unwrap()..*xs.last().unwrap(), // x軸の範囲
      y_min..y_max                               // y軸の範囲
    )?;


  /* (4) グラフの描画 */

  // x軸y軸、グリッド線などを描画
  chart.configure_mesh().draw()?;

  // 折れ線グラフの定義＆描画
  let line_series = LineSeries::new(
                    xs.iter()
                      .zip(ys.iter())
                      .map(|(x, y)| (*x, *y)),
                    &RED
                   );
  chart.draw_series(line_series)?;

  Ok(())
}
```

``cargo run``などでプログラムを実行すると、次のようなグラフが``plot.png``という名前で出力される。
![plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/ca59eb14-6a66-7dc2-d2fd-4cb1bef42ae0.png)

## グラフのカスタマイズ

### 折れ線に点を加える

手順(4)で折れ線を追加した後に、次のようにCircle型のオブジェクトを要素とするイテレータを作ってdraw_series関数を呼ぶことで実現可能。

```rs
  // 点グラフの定義＆描画
  let point_series = xs.iter()
                     .zip(ys.iter())
                     .map(|(x, y)|
                         Circle::new(
                           (*x, *y),
                           4,        // Circleのサイズ
                          &RED,      // 色を指定
                          // ↓円を塗りつぶしたければこちら
                          // ShapeStyle::from(&RED).filled(), 
                         )
                     );
  chart.draw_series(point_series)?;
```
![plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/932e42e4-7a1d-ac69-261d-d425dd142a94.png)

折れ線グラフのLineSeriesと同じ位置づけで「PointSeries」という構造体もあって、上のような点描画が可能。こっちも試してみたらビルドがなかなか通らずに手こずり、以下のように書いてようやく動く。

```rs
  // 点グラフの定義＆描画
  let point_series = PointSeries::<_,_,Circle<_,_>,_>::new(
                     xs.iter()
                       .zip(ys.iter())
                       .map(|(x, y)| (*x, *y)),
                     4,        // Circleのサイズ
                     &RED      // 色を指定
                    );
  chart.draw_series(point_series)?;
```

試してはいないが、Circleの代わりにTextを指定するとグラフ上にテキストを追加することも可能なはず。その場合、上の例で``&RED``で色を指定していた部分がShapeStyleの参照型だったのを、テキスト用の型であるTextStyleの参照型で指定すれば良さそう。


### x軸ラベルの編集

グラフのx軸の日付が「2021-06-22+09:00」となってて長ったらしいので、もっと短くしたい。そんな時は、手順(4)の``  chart.configure_mesh()``で``x_label_formatter``メソッドを追加すれば実現できる。具体的には「x軸の値を整形する関数」を定義してあげて、``x_label_formatter``の引数に定義した関数を指定する。

言葉ではわかりにくいので、実際に実装してみよう。
まず、``date.rs``のソースで、「2021-06-22+09:00」を「2021/06/22」のように変換する関数を定義する。

```rs:data.rs
/// 日付型を文字列に変換する関数を追加
pub fn date2string(date: &Date<Local>) -> String {
  date.format("%Y/%m/%d").to_string()
}
```

続いて、``main.rs``のchart.configure_mesh()部分を次のように変更する。

```rs:main.rs
  chart.configure_mesh()
       .x_label_formatter(&data::date2string) // 追加
       .draw()?;
```

わざわざ関数定義しなくても、クロージャーで書くことも可能。

```rs:main.rs
  chart.configure_mesh()
       .x_label_formatter(&|x: &Date<Local>| x.format("%Y/%m/%d").to_string()) // 追加
       .draw()?;
```

再度ビルド実行すると、日付のフォーマットが変わっていることがわかる。
![plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/bb8abb6d-c07c-65b5-9f96-2cc96e6e4e63.png)

### x軸ラベルの間隔を変更

間隔では指定できないが、``chart.configure_mesh()``の後に、「x軸ラベルの数」を指定する``x_labels``メソッドを追加して変更可能。

```rs:main.rs
  chart.configure_mesh()
       .x_label_formatter(&data::date2string)
       .x_labels(4) // 追加
       .draw()?;
```
![plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/6f20f0cc-7aa1-33da-04dc-8b4f83871060.png)
実際に試してみると、ラベルの数は必ずしも``x_labels``の引数で指定した数とは一致しないようで、想定よりも少なかったりする…。
[ドキュメント](https://docs.rs/plotters/0.3.1/plotters/chart/struct.MeshStyle.html)で``x_labels``の説明を見ると下記のようにある。

> Set how many labels for the X axis at most
> value: The maximum desired number of labels in the X axis

どうやら指定した数はあくまで「表示ラベル数の最大値」の模様。「指定ラベル数の表示が厳しい」とPlottersが判断した場合は、指定よりも少ないラベル数（間隔は広くなる）しか描画されないようなので注意いただきたい。

## 参考サイト

* RustのドローイングライブラリPlottersの紹介
    * https://lab.mo-t.com/blog/rust-plotters
* PlottersのGitHubの実装例
    * https://github.com/38/plotters/tree/master/examples