18
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-12

はじめに

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

各種情報

動作確認バージョン

  • rustc : 1.51.0
  • cargo : 1.51.0

ソース構成

Rustのソースは以下の2つ

  • main.rs : Plottersを使ってグラフ描画する処理を記述
  • data.rs : グラフ表示用の時系列データを定義
.
├── Cargo.toml
└── src
    ├── data.rs <- 関数を定義しているモジュール
    └── main.rs <- main関数のみ記載

Cargo.tomlの中身

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

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

[dependencies]
plotters = "0.3.1"

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

plottersのバージョンを0.3.3以上でビルドする場合には、こちらのドキュメントにあるように 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のライフタイム注釈については詳しく説明しないが、これを入れると文字列を「文字列リテラル」として静的領域に格納してくれる。
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.rsmain関数でグラフを描画する。手順は以下の4つ。

手順

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

以下はソースの中身

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

グラフのカスタマイズ

折れ線に点を加える

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

  // 点グラフの定義&描画
  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

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

  // 点グラフの定義&描画
  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」のように変換する関数を定義する。

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

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

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

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

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

再度ビルド実行すると、日付のフォーマットが変わっていることがわかる。
plot.png

x軸ラベルの間隔を変更

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

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

plot.png
実際に試してみると、ラベルの数は必ずしもx_labelsの引数で指定した数とは一致しないようで、想定よりも少なかったりする…。
ドキュメントx_labelsの説明を見ると下記のようにある。

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

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

参考サイト

18
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?