Rustの基礎 - グラフ

提供: MochiuWiki : SUSE, EC, PCB

📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)

概要

ここでは、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でグラフを作成する基本的な流れを以下に示す。

  1. まず、描画先のバックエンドを作成する。
  2. 次に、描画領域(DrawingArea)を定義して、その中にチャートを構築する。
  3. 最後に、データ系列を描画する。


このフローは、他のプロットライブラリと比べると手続き的であるだが、細かい制御が可能になっている。

以下の例では、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アプリケーションで、動的なグラフ表示を実現できる。


eframe + egui_plotライブラリ

eframe + egui_plotとは

eframeは、Rustで書かれたクロスプラットフォーム対応のGUIフレームワークである。
eframeは、eguiというイミディエートモードGUIライブラリの上に構築されており、シンプルなAPIを提供している。

イミディエートモードGUIとは、従来のリテインドモードGUIとは異なるアプローチである。
リテインドモードでは、ウィジェットの状態をGUIフレームワーク側が保持するのに対し、イミディエートモードでは、アプリケーション側が状態を管理し、毎フレームUIを再構築する。
この設計により、状態管理がシンプルになり、UIとアプリケーションロジックの結合が緩くなるというメリットがある。

egui_plotは、eframe (egui) 上で動作するプロット専用のライブラリである。
このライブラリを使用することで、インタラクティブなグラフをGUIアプリケーション内に埋め込むことができる。
ユーザはマウスでグラフをズームしたり、パンしたりすることができ、リアルタイムでデータが更新されるようなアプリケーションに適している。

eframe + egui_plotの組み合わせは、インタラクティブなデスクトップアプリケーションを作成する場合に特に適している。
また、eframeはWebAssemblyへのコンパイルも容易であり、同じコードでデスクトップアプリケーションとWebアプリケーションの両方を作成できる点もメリットである。
データを可視化しながらユーザの入力に応じて動的に更新するような対話型のアプリケーションを構築する場合に威力を発揮する。

eframe (egui) について詳細を知りたい場合は、公式ドキュメントを参照すること。
また、公式Webサイトでは、Webブラウザ上で動作するデモも提供されており、実際の動作を確認できる。

eframe + egui_plotで作成するグラフ : 基本

まず、アプリケーションの状態を保持する構造体を定義する。
この構造体は、グラフのデータやUI要素の状態など、アプリケーション全体の情報を保持する役割を持つ。

次に、この構造体に対して eframe::App トレイトを実装する。
このトレイトの update メソッドが毎フレーム呼び出されるため、この中でUIとグラフを描画する処理を記述する。

最後に、main メソッドで eframe::run_native を呼び出してアプリケーションを起動する。

イミディエートモードの特性により、UIの状態とアプリケーションの状態が明確に分離されるため、デバッグも容易になる。

以下の例では、0から10までの範囲でsin関数のグラフを描画している。

 use eframe::egui;
 use egui_plot::{Line, Plot, PlotPoints};
 
 fn main() -> Result<(), eframe::Error> {
    // ウィンドウのオプションを設定
    // 初期ウィンドウサイズを800x600ピクセルに設定
    let options = eframe::NativeOptions {
       initial_window_size: Some(egui::vec2(800.0, 600.0)),
       ..Default::default()
    };
 
    // アプリケーションを起動
    // 第1引数 : ウィンドウタイトル
    // 第2引数 : オプション
    // 第3引数 : アプリケーション構造体を生成するクロージャ
    eframe::run_native(
       "Rust グラフ表示の例",
       options,
       Box::new(|_cc| Box::<MyApp>::default()),
    )
 }
 
 // アプリケーションの状態を保持する構造体
 struct MyApp {
    // この例では状態を持たないが、通常はここにデータを保持する
 }
 
 impl Default for MyApp {
    fn default() -> Self {
       Self {}
    }
 }
 
 // eframe::Appトレイトを実装
 // updateメソッドが毎フレーム呼び出される
 impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
       // 中央パネルを作成してその中にUIを配置
       egui::CentralPanel::default().show(ctx, |ui| {
          ui.heading("eframe & egui_plot グラフ表示");
 
          // グラフデータの生成
          // 0から10までの範囲を100ステップで分割し、sin関数の値を計算
          let points: PlotPoints = (0..100)
             .map(|i| {
                let x = i as f64 * 0.1;
                let y = (x * 0.5).sin();
                [x, y]
             })
             .collect();
 
          // データから折れ線グラフを作成
          let line = Line::new(points);
 
          // プロットを表示
          // view_aspectで縦横比を設定 (この例では横が縦の2倍)
          Plot::new("my_plot")
             .view_aspect(2.0)
             .show(ui, |plot_ui| {
                plot_ui.line(line);
             });
       });
    }
 }


updateメソッド内でグラフのデータを生成し、Plotウィジェットを使って表示している。
eframeでは、このupdateメソッドが通常は1秒間に60回程度呼び出されるため、動的にデータが変化するような場合でもスムーズにグラフが更新される。

このコードを実行するには、プロジェクトディレクトリで以下に示すコマンドを実行する。

cargo run


また、マウスホイールでズームしたり、ドラッグしてパンできる点も、egui_plotの便利な機能である。

eframe + egui_plotで作成するグラフ : インタラクティブ機能

実用的なアプリケーションでは、ユーザの操作に応じてグラフを更新する機能が求められる。

よく使用される機能を以下に示す。

  • メニューバーを追加して、そこからアプリケーションを終了できるようにする。
  • ボタンを配置して、グラフの再描画とクリアを行えるようにする。
  • ステータスバーを追加して、現在の時刻を表示する。


eframeでは、パネルという概念を使用すればUIのレイアウトを構築する。
TopBottomPanel を使用すれば画面の上部や下部に固定されたパネルを作成でき、CentralPanel を使用すれば残りの領域を埋めるパネルを作成できる。
この仕組みにより、柔軟なレイアウトを簡単に実現することができる。

乱数を生成するために、rand クレートを使用する。
また、現在時刻を取得するために、chrono クレートを使用する。
そのため、Cargo.tomlファイルに以下に示す依存関係を追加すること。

 [dependencies]
 eframe = "0.24"
 egui_plot = "0.24"
 rand = "0.8"
 chrono = "0.4"


 use eframe::egui;
 use egui_plot::{Line, Plot, PlotPoints};
 use rand::Rng;
 use chrono::Local;
 
 fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
       initial_window_size: Some(egui::vec2(800.0, 600.0)),
       ..Default::default()
    };
 
    eframe::run_native(
       "グラフを表示するウィンドウ",
       options,
       Box::new(|_cc| Box::<GraphApp>::default()),
    )
 }
 
 // アプリケーションの状態を保持する構造体
 struct GraphApp {
    // グラフに表示するデータポイントのベクター
    data: Vec<f64>,
    // グラフを表示するかどうかを制御するフラグ
    show_graph: bool,
 }
 
 impl Default for GraphApp {
    fn default() -> Self {
       Self {
          // 初期化時にランダムなデータを生成
          data: Self::generate_random_data(),
          // デフォルトではグラフを表示
          show_graph: true,
       }
    }
 }
 
 impl GraphApp {
    // 25個のランダムな浮動小数点数を生成する関数
    fn generate_random_data() -> Vec<f64> {
       let mut rng = rand::thread_rng();
       (0..25).map(|_| rng.gen::<f64>()).collect()
    }
 
    // グラフを再描画する関数
    // 新しいランダムデータを生成して、グラフを表示状態にする
    fn redraw_graph(&mut self) {
       self.data = Self::generate_random_data();
       self.show_graph = true;
    }
 
    // グラフをクリアする関数
    // 単純に表示フラグをfalseにすることで非表示にする
    fn clear_graph(&mut self) {
       self.show_graph = false;
    }
 }
 
 impl eframe::App for GraphApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
       // 画面上部にメニューバーを配置
       egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
          egui::menu::bar(ui, |ui| {
             ui.menu_button("ファイル", |ui| {
                // 終了ボタンがクリックされたらウィンドウを閉じる
                if ui.button("終了 (Ctrl+Q)").clicked() {
                   ctx.send_viewport_cmd(egui::ViewportCommand::Close);
                }
             });
          });
       });
 
       // 画面下部にステータスバーを配置
       // 現在の日時を1秒ごとに更新して表示
       egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
          let now = Local::now();
          let time_str = now.format("日時 %Y年%m月%d日 %H時%M分%S秒").to_string();
          ui.label(time_str);
       });
 
       // 残りの領域に中央パネルを配置
       egui::CentralPanel::default().show(ctx, |ui| {
          // 水平方向にウィジェットを並べる
          ui.horizontal(|ui| {
             ui.heading("Rust eframe & egui_plot グラフ表示");
 
             // ボタンを右寄せで配置
             // Layout::right_to_leftを使用することで、右から左へ配置される
             ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                // グラフを消すボタン
                if ui.button("グラフを消す").clicked() {
                   self.clear_graph();
                }
                // グラフを打つボタン
                if ui.button("グラフを打つ").clicked() {
                   self.redraw_graph();
                }
             });
          });
 
          // 区切り線を表示
          ui.separator();
 
          // グラフの表示 (show_graphがtrueの場合のみ)
          if self.show_graph {
             // データからプロット用のポイントを生成
             // enumerate()でインデックスを取得し、それをx座標として使用
             let points: PlotPoints = self.data
                .iter()
                .enumerate()
                .map(|(i, &y)| [i as f64, y])
                .collect();
 
             // 赤色の折れ線グラフを作成
             let line = Line::new(points)
                .color(egui::Color32::from_rgb(255, 0, 0));
 
             // プロットを表示
             Plot::new("main_plot")
                .view_aspect(2.0)
                .show(ui, |plot_ui| {
                   plot_ui.line(line);
                });
          }
       });
 
       // 1秒後に再描画をリクエスト
       // これにより、ステータスバーの時刻が1秒ごとに更新される
       ctx.request_repaint_after(std::time::Duration::from_secs(1));
    }
 }


上部にはメニューバーがあり、ファイルメニューから終了を選択することでアプリケーションを閉じることができる。
中央部には、グラフが表示され、右上に配置された2つのボタンで操作できる。
下部にはステータスバーがあり、現在の日時が1秒ごとに更新されて表示される。

[グラフを打つ]ボタンを押下すると、新しいランダムデータが生成され、グラフが再描画される。
この操作は何度でも繰り返すことができ、毎回異なるパターンのグラフが表示される。

[グラフを消す]ボタンを押下すると、グラフが非表示になり、空白の領域だけが残る。

再度、[グラフを打つ]ボタンを押下すれば、新しいグラフが表示される。

このように、eframeを使用することで、状態管理がシンプルなまま、インタラクティブなアプリケーションを作成できる。
updateメソッド内でUIを記述し、ボタンのクリック等のイベントに応じて状態を更新すれば、自動的に画面が更新される。
この直感的なプログラミングモデルが、eframeの大きな利点である。

また、Rustの所有権システムにより、メモリ管理が自動的に行われる。
Rustでは所有権とライフタイムの仕組みにより、メモリリークやダングリングポインタといった問題を、コンパイル時に防ぐことができる。
これは、長時間動作するGUIアプリケーションにとって非常に重要な特性である。


その他

Rustでグラフ描画を行う場合、言語の特性として以下に示す事柄に注意する。

Rustは所有権システムとライフタイムという独自の概念を持っており、これによりメモリ安全性とスレッド安全性が保証される。
GUIアプリケーションにおいても、この恩恵を受けることができ、実行時エラーやメモリリークのリスクを大幅に減らすことができる。

また、Rustはゼロコスト抽象化を提供するため、高レベルな記述でありながら、C/C++に匹敵するパフォーマンスを実現できる。
これは、大量のデータをプロットする場合や、リアルタイムでグラフを更新する場合に特に有利である。

Rustのエコシステムは活発に発展しており、新しいライブラリやツールが日々登場している。
公式のクレートレジストリであるcrates.iodocs.rsのドキュメントを定期的に確認することを推奨する。