はじめに
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もインストールする。
[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
のライフタイム注釈については詳しく説明しないが、これを入れると文字列を「文字列リテラル」として静的領域に格納してくれる。
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
関数でグラフを描画する。手順は以下の4つ。
手順
- (1) プロット用データの準備
- dataモジュールから入力データを取得し、横軸用の日付のベクトル、縦軸用のf32型のベクトルに変換する
- (2) 描画先の情報を設定
- 画像サイズや背景色など、描画キャンバス全般に関する情報を設定する
- 今回は描画結果を画像ファイルとして出力するため、BitMapBackend型のオブジェクトを定義
- (3) グラフ全般の設定
- キャプション、x軸、y軸の値範囲、ラベルの余白等、グラフ描画全般に関して設定する
- 各軸の値の範囲は自動設定できないようなので、
build_cartesian_2d
メソッドで範囲を明示する必要がありそう…。y軸の範囲はf32型のVectorの最大最小値で決めているが、f32型にはNaNが含まれていてminやmaxを求めるのに工夫がいる。(参考にさせていただいたサイト)
- (4) グラフの描画
- (3)で設定した内容をもとに、グラフの軸やグリッド線などを描画する
- (1)で準備したデータを指定のスタイルで描画する。以下ソースの例では折れ線グラフ(LineSeries)で描画。
以下はソースの中身
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
という名前で出力される。
グラフのカスタマイズ
折れ線に点を加える
手順(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)?;
折れ線グラフの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」のように変換する関数を定義する。
/// 日付型を文字列に変換する関数を追加
pub fn date2string(date: &Date<Local>) -> String {
date.format("%Y/%m/%d").to_string()
}
続いて、main.rs
のchart.configure_mesh()部分を次のように変更する。
chart.configure_mesh()
.x_label_formatter(&data::date2string) // 追加
.draw()?;
わざわざ関数定義しなくても、クロージャーで書くことも可能。
chart.configure_mesh()
.x_label_formatter(&|x: &Date<Local>| x.format("%Y/%m/%d").to_string()) // 追加
.draw()?;
再度ビルド実行すると、日付のフォーマットが変わっていることがわかる。
x軸ラベルの間隔を変更
間隔では指定できないが、chart.configure_mesh()
の後に、「x軸ラベルの数」を指定するx_labels
メソッドを追加して変更可能。
chart.configure_mesh()
.x_label_formatter(&data::date2string)
.x_labels(4) // 追加
.draw()?;
実際に試してみると、ラベルの数は必ずしも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が判断した場合は、指定よりも少ないラベル数(間隔は広くなる)しか描画されないようなので注意いただきたい。
参考サイト
- RustのドローイングライブラリPlottersの紹介
- PlottersのGitHubの実装例