Rustの基礎 - グラフ
概要
ここでは、RustでGUIアプリケーションを作成し、グラフを表示する方法を記載する。
Rustでのグラフ描画について
Rustには、グラフを描画するための複数のライブラリとアプローチが存在する。
主なアプローチとしては、以下に示すようなものがある。
- plotters
- Rustネイティブのプロット作成ライブラリ
- eframe + egui_plot
- GUIフレームワークとプロット機能の組み合わせ
- eframe (egui) は、Rustで書かれたイミディエートモードGUIライブラリであり、WebAssemblyにも対応している。
- gtk-rs + Cairo
- GTKとCairoを使用した描画
eframe (egui) はRustの標準ライブラリではないため、プロジェクトのCargo.tomlに依存関係を追加する必要がある。
以下に示すように、Cargo.tomlの[dependencies]セクションに追加する。
[dependencies]
eframe = "0.24"
egui_plot = "0.24"
依存関係を追加した後、以下に示すコマンドを実行してライブラリをダウンロードしてビルドする。
cargo build
eframe(egui)について詳しく知りたい場合は、GitHubリポジトリ や 公式ドキュメントを参照すること。
plottersライブラリ
plottersライブラリとは
plottersは、Rustで書かれたデータ可視化ライブラリである。
このライブラリの大きな特徴は、バックエンドに依存しない設計となっている点である。
つまり、同じコードで、画像ファイル(PNG、JPEG)、ベクターグラフィックス(SVG)、HTMLキャンバス、ビットマップバッファ等、様々な出力形式にグラフを描画できる。
plottersは、科学技術計算や統計分析の分野で特に威力を発揮する。
グラフを画像ファイルやSVGとして出力する場合に適している。
複雑な統計グラフ、ヒートマップ、3Dプロット、複数のサブプロットの配置等、高度な可視化要件にも対応できる柔軟性を持っている。
また、パフォーマンスにも優れており、大量のデータポイントを効率的に描画できる。
その他、バッチ処理でレポートを生成、大量のデータを一度に可視化する場合は、plottersが向いている。
Cargo.tomlに以下に示す依存関係を追加する。
[dependencies]
plotters = "0.3"
plottersについて詳しく知りたい場合は、GitHubリポジトリ や 公式ドキュメントを参照すること。
また、Plotters Developer's Guideには、実践的なチュートリアルと豊富なサンプルコードが掲載されている。
plottersで作成するグラフ : 基本
plottersでグラフを作成する基本的な流れを以下に示す。
- まず、描画先のバックエンドを作成する。
- 次に、描画領域(DrawingArea)を定義して、その中にチャートを構築する。
- 最後に、データ系列を描画する。
このフローは、他のプロットライブラリと比べると手続き的であるだが、細かい制御が可能になっている。
以下の例では、sin関数とcos関数のグラフをPNG画像として出力している。
use plotters::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// PNGファイルへの描画バックエンドを作成
// 画像サイズは800x600ピクセル
let root = BitMapBackend::new("output.png", (800, 600))
.into_drawing_area();
// 背景を白色で塗りつぶす
root.fill(&WHITE)?;
// チャートを構築する
// x軸の範囲: -3.14から3.14(-πからπ)
// y軸の範囲: -1.5から1.5
let mut chart = ChartBuilder::on(&root)
.caption("Sin波とCos波のグラフ", ("sans-serif", 40).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(-3.14f64..3.14f64, -1.5f64..1.5f64)?;
// メッシュ (グリッド線) を描画
chart.configure_mesh().draw()?;
// Sin波のデータを生成して描画
// -π〜πまでを100分割してプロット
chart
.draw_series(LineSeries::new(
(-314..314).map(|x| {
let x = x as f64 / 100.0;
(x, x.sin())
}),
&RED,
))?
.label("y = sin(x)")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
// Cos波のデータを生成して描画
chart
.draw_series(LineSeries::new(
(-314..314).map(|x| {
let x = x as f64 / 100.0;
(x, x.cos())
}),
&BLUE,
))?
.label("y = cos(x)")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));
// 凡例を描画
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
// バックエンドに描画内容を書き込む
root.present()?;
println!("グラフをoutput.pngに保存しました");
Ok(())
}
注目すべき点は、ChartBuilderを使ってチャートの設定を段階的に構築している部分である。
captionでタイトルを設定、marginで余白を指定、ラベル領域のサイズを設定している。
このようなビルダーパターンは、Rustで広く仕様されている設計手法である。
また、draw_seriesメソッドで系列を描画した後、labelとlegendメソッドをチェーンすることにより、凡例に表示される情報を設定できる。
plottersで作成するグラフ : データ分析
統計分析では、外部ファイルからデータを読み込む場面が頻繁に発生する。
CSVファイルを扱うために、CSVクレートを使用する。
そのため、Cargo.tomlに以下に示す設定を追加する。
[dependencies]
plotters = "0.3"
csv = "1.3"
serde = { version = "1.0", features = ["derive"] }
以下の例では、月ごとの売上データを記録したCSVファイルを読み込み、棒グラフとして可視化している。
複数の系列 (今年度と前年度の比較等) を表示することにより、データの傾向を把握しやすくなる。
"month","current_year","previous_year"
"1","120","100"
"2","135","110"
"3","150","125"
"4","145","130"
"5","160","140"
"6","175","155"
"7","180","160"
"8","185","165"
"9","190","170"
"10","195","175"
"11","200","180"
"12","210","185"
use plotters::prelude::*;
use serde::Deserialize;
use std::error::Error;
// CSVの行を表す構造体
#[derive(Debug, Deserialize)]
struct SalesData {
month: u32,
current_year: f64,
previous_year: f64,
}
fn main() -> Result<(), Box<dyn Error>> {
// CSVファイルを読み込む
let mut reader = csv::Reader::from_path("data.csv")?;
let mut data: Vec<SalesData> = Vec::new();
for result in reader.deserialize() {
let record: SalesData = result?;
data.push(record);
}
// 描画の準備
let root = BitMapBackend::new("sales_chart.png", (1024, 768))
.into_drawing_area();
root.fill(&WHITE)?;
// チャートの構築
let mut chart = ChartBuilder::on(&root)
.caption("月別売上比較(今年度 vs 前年度)", ("sans-serif", 50))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(0f64..13f64, 0f64..250f64)?;
// メッシュの設定と描画
chart
.configure_mesh()
.x_desc("月")
.y_desc("売上 (万円)")
.x_labels(12)
.y_labels(10)
.draw()?;
// 今年度のデータを棒グラフとして描画
chart.draw_series(
data.iter().map(|d| {
let x = d.month as f64;
// 棒の幅を0.3に設定し、x座標から0.15左にオフセット
Rectangle::new(
[(x - 0.15, 0.0), (x + 0.15, d.current_year)],
BLUE.filled(),
)
}),
)?
.label("今年度")
.legend(|(x, y)| Rectangle::new([(x, y - 5), (x + 15, y + 5)], BLUE.filled()));
// 前年度のデータを棒グラフとして描画
// 今年度の棒と重ならないように、少し右側にオフセット
chart.draw_series(
data.iter().map(|d| {
let x = d.month as f64 + 0.3;
Rectangle::new(
[(x - 0.15, 0.0), (x + 0.15, d.previous_year)],
RED.filled(),
)
}),
)?
.label("前年度")
.legend(|(x, y)| Rectangle::new([(x, y - 5), (x + 15, y + 5)], RED.filled()));
// 凡例を描画
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
root.present()?;
println!("売上グラフをsales_chart.pngに保存しました");
Ok(())
}
serdeクレートのDeserializeトレイトを使用して、CSVの各行を構造体に自動的にマッピングしている。
これにより、型安全にデータを扱うことができ、コンパイル時にデータ構造の誤りを検出することができる。
棒グラフの描画では、Rectangleを使用して各月の売上を矩形として表現している。
今年度と前年度のデータを並べて表示するために、x座標を少しずつずらして配置している。
このように細かい調整が可能なのが、plottersの柔軟性の高さを示している。
また、configure_meshメソッドで軸のラベルや目盛りの数を詳細に制御できる。
x_descとy_descで軸の説明を追加し、x_labelsとy_labelsで目盛りの数を指定している。
これらの設定により、グラフが読みやすくなる。
plottersで作成するグラフ : Webアプリケーション
plottersのメリットの1つは、バックエンドを切り替えるだけで、異なる出力形式に対応できる点である。
SVGはベクター形式のため、拡大しても画質が劣化せず、Webページへの埋め込みにも適している。
BitMapBackendをSVGBackendに変更するだけで、その他はほとんど変更せずに済むという点である。
use plotters::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// SVGファイルへの描画バックエンドを作成
let root = SVGBackend::new("output.svg", (800, 600))
.into_drawing_area();
root.fill(&WHITE)?;
let mut chart = ChartBuilder::on(&root)
.caption("Sin波とCos波のグラフ(SVG版)", ("sans-serif", 40))
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(-3.14f64..3.14f64, -1.5f64..1.5f64)?;
chart.configure_mesh().draw()?;
// Sin波を描画
chart
.draw_series(LineSeries::new(
(-314..314).map(|x| {
let x = x as f64 / 100.0;
(x, x.sin())
}),
&RED,
))?
.label("y = sin(x)")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
// Cos波を描画
chart
.draw_series(LineSeries::new(
(-314..314).map(|x| {
let x = x as f64 / 100.0;
(x, x.cos())
}),
&BLUE,
))?
.label("y = cos(x)")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
root.present()?;
println!("グラフをoutput.svgに保存しました");
Ok(())
}
生成されたSVGファイルは、Webブラウザで直接開いて表示できる。
また、HTMLファイルに埋め込むことで、Webページ上でインタラクティブな要素として利用することも可能である。
さらに、plottersはWebAssemblyにも対応しており、CanvasBackendを使用することにより、Webブラウザ上のHTMLキャンバス要素に直接描画することができる。
これにより、完全にRustで記述したWebアプリケーションで、動的なグラフ表示を実現できる。
その他
Rustでグラフ描画を行う場合、言語の特性として以下に示す事柄に注意する。
Rustは所有権システムとライフタイムという独自の概念を持っており、これによりメモリ安全性とスレッド安全性が保証される。
GUIアプリケーションにおいても、この恩恵を受けることができ、実行時エラーやメモリリークのリスクを大幅に減らすことができる。
また、Rustはゼロコスト抽象化を提供するため、高レベルな記述でありながら、C/C++に匹敵するパフォーマンスを実現できる。
これは、大量のデータをプロットする場合や、リアルタイムでグラフを更新する場合に特に有利である。
Rustのエコシステムは活発に発展しており、新しいライブラリやツールが日々登場している。
公式のクレートレジストリであるcrates.io や docs.rsのドキュメントを定期的に確認することを推奨する。