YewでChart.jsのグラフを表示する

都内勤務ソフトウェアエンジニア。このブログが、誰かの役に立てていれば嬉しい。

2021/12/17

title

この記事はYewChart.jsのグラフを表示する過程を書いていきます。動機は、Importing functions from JS - The wasm-bindgen Guideを見て、jsのクラスを簡単にrust/wasmへ持ってこれると知ったためです。

ソースはgithubから取得できます。

事前準備

必要に応じて準備をお願いします。

rustup

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

wasm-pack

$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

wasm32-unknown-unknown

$ rustup target add wasm32-unknown-unknown

cargo-make

$ cargo install --force cargo-make

miniserve

$ cargo install --locked miniserve

プロジェクト作成

$ cargo new yew-chartjs-tutorial --lib
$ cd yew-chartjs-tutorial

Cargo.tomlを以下に修正します。

Cargo.toml
[package]
name = "yew-chartjs-tutorial"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
yew = "0.19.3"
wasm-bindgen = "0.2.78"

yewは1.0になるまで破壊的変更が余儀なくされるため、リファクタリングなど対応が必要になるかもしれません。

Note: Yew is not 1.0 yet. Be prepared to do major refactoring due to breaking API changes.

Yew About

次に、Makefile.tomlをCargo.tomlと同じ階層に作成します。

$ tree -L 1          
.
├── Cargo.lock
├── Cargo.toml
├── Makefile.toml
├── src
└── target
Makefile.toml
[tasks.build]
command = "wasm-pack"
args = ["build", "--target", "web", "--no-typescript", "--dev", "--out-name", "wasm", "--out-dir", "./static"]

[tasks.serve]
command = "miniserve"
args = ["./static", "--index", "index.html"]

cargo-makeをインストールしていると、makersコマンドが使えます。ビルドする場合は、makers build、ローカルにテストサーバを設定する場合は、makers serveを行います。ですがその前にlib.rsを修正します。

$ which makers
/Users/hibi221b/.cargo/bin/makers

lib.rsを以下に修正します。Yew Build a sample appとの差異は次の4つになります。

1: lib.rsを修正している
2: use wasm_bindgen::prelude::*;の追加
3: main関数にpubを追加
4: main関数に#[wasm_bindgen(start)]を追加

lib.rs
use wasm_bindgen::prelude::*;
use yew::prelude::*;

enum Msg {
    AddOne,
}

struct Model {
    value: i64,
}

impl Component for Model {
    type Message = Msg;
    type Properties = ();

    fn create(_ctx: &Context<Self>) -> Self {
        Self {
            value: 0,
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::AddOne => {
                self.value += 1;
                // the value has changed so we need to
                // re-render for it to appear on the page
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        // This gives us a component's "`Scope`" which allows us to send messages, etc to the component.
        let link = ctx.link();
        html! {
            <div>
                <button onclick={link.callback(|_| Msg::AddOne)}>{ "+1" }</button>
                <p>{ self.value }</p>
            </div>
        }
    }
}

#[wasm_bindgen(start)]
pub fn main() {
    yew::start_app::<Model>();
}

ビルドを行います。makers buildでstaticディレクトリが作成され、その中にファイルが自動生成されます。自動生成されたファイルの変更などは行いません。

$ pwd
/Users/hibi221b/Desktop/yew-chartjs-tutorial

$ makers build
...
...
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/hibi221b/Desktop/yew-chartjs-tutorial/./static.
[cargo-make] INFO - Build Done in 1.62 seconds.

$ tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── Makefile.toml
├── src
│   └── lib.rs
├── static
│   ├── package.json
│   ├── wasm.js
│   └── wasm_bg.wasm
└── target
    ├── CACHEDIR.TAG
    ├── debug
    └── wasm32-unknown-unknown

staticディレクトリ内にindex.htmlファイルを作成します。static/wasm.js内でinitがexportされているため、scriptタグにtype="module"をつける必要があります。
HTML にモジュールを適用する
デフォルトエクスポートと名前付きエクスポート

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Yew * Chart.js tutorial</title>
</head>
<body>
    <script type="module"> 
        import init from "./wasm.js";
        init();
    </script>
</body>
</html>

最終的なstaticディレクトリ内構成。.wasmファイルのサイズを小さくする方法もあるようです。
Shrinking .wasm Code Size - Rust and WebAssembly

$ ls -lah static 
total 1968
drwxr-xr-x   7 hibi221b  staff   224B 12 16 21:28 .
drwxr-xr-x  10 hibi221b  staff   320B 12 16 20:54 ..
-rw-r--r--   1 hibi221b  staff     1B 12 16 21:20 .gitignore
-rw-r--r--   1 hibi221b  staff   359B 12 16 21:28 index.html
-rw-r--r--   1 hibi221b  staff   157B 12 16 21:20 package.json
-rw-r--r--   1 hibi221b  staff    20K 12 16 21:20 wasm.js
-rw-r--r--   1 hibi221b  staff   951K 12 16 21:20 wasm_bg.wasm

makers serveでローカルサーバを立て、ブラウザで確認するとyewのチュートリアルアプリが表示されます。

$ pwd
/Users/hibi221b/Desktop/yew-chartjs-tutorial

$ makers serve
miniserve v0.18.0
Serving path /Users/hibi221b/Desktop/yew-chartjs-tutorial/static
Availabe at (non-exhaustive list):
    http://127.0.0.1:8080

yewのチュートリアルアプリ

chromeの場合、デベロッパーツールのSourcesを確認すると、static以下のファイルが読み込まれていることを確認できました。サンプルアプリは+1ボタンを押すと加算された数が、表示される内容になっています。

static/index.htmlにChart.jsのCDN追加

今回は、jsdelivrのCDNをindex.htmlのhead内に追加します。Chart.jsは、そのほかのインストール方法もあります。
Installation | Chart.js

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Yew * Chart.js tutorial</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@3.6.2/dist/chart.min.js"></script>
</head>
<body>
    <script type="module"> 
        import init from "./wasm.js";
        init();
    </script>
</body>
</html>

簡単なラインチャートを描画するjsクラスを作成

Yewで使いたいjsクラスを用意します。myChartクラスは簡単なラインチャートを表示するグラフを描画します。modulesディレクトリを作成、その中のmyChart.jsにクラスを作成します。

$ tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── Makefile.toml
├── modules
│   └── myChart.js
├── src
│   └── lib.rs
├── static
│   ├── index.html
│   ├── package.json
│   ├── wasm.js
│   └── wasm_bg.wasm
└── target
    ├── CACHEDIR.TAG
    ├── debug
    └── wasm32-unknown-unknown
modules/myChart.js
export class MyChart {
    constructor() {
        let labels = [
            'January',
            'February',
            'March',
            'April',
            'May',
            'June',
        ];
        
        let data = {
            labels: labels,
            datasets: [{
                label: 'My First dataset',
                backgroundColor: 'rgb(255, 99, 132)',
                borderColor: 'rgb(255, 99, 132)',
                data: [0, 10, 5, 2, 20, 30, 45],
            }]
        };

        this.config = {
            type: 'line',
            data: data,
            options: {
                responsive: false
            }
        };
    }

    draw() {
        new Chart(
            document.getElementById('myChart'),
            this.config
        )
    }
}

Yewにjsクラスをインポート

Yewのチュートリアルアプリを少し変え、drawボタンを押すとcanvasにラインチャートを描画するアプリを作成します。drawボタンを再度押す場合は、chart was drawnをConsoleに表示、描画させないようにします。lib.rsを以下に修正します。

lib.rs
use wasm_bindgen::prelude::*;
use yew::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

#[wasm_bindgen(module = "/modules/myChart.js")]
extern "C" {
    type MyChart;

    #[wasm_bindgen(constructor)]
    fn new() -> MyChart;

    #[wasm_bindgen(method)]
    fn draw(this: &MyChart);
}

enum Msg {
    Draw,
}

struct Model {
    chart: MyChart,
    is_clicked: bool
}

impl Component for Model {
    type Message = Msg;
    type Properties = ();

    fn create(_ctx: &Context<Self>) -> Self {
        Self {
            chart: MyChart::new(),
            is_clicked: false
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::Draw => {
                if !self.is_clicked {
                    self.chart.draw();
                    self.is_clicked = true;
                    console_log!("self.is_clicked: {}", self.is_clicked);
                } else {
                    console_log!("chart was drawn");
                }

                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        // This gives us a component's "`Scope`" which allows us to send messages, etc to the component.
        let link = ctx.link();
        html! {
            <div>
                <button onclick={link.callback(|_| Msg::Draw)}>{ "draw" }</button>
                <br/>
                <canvas id="myChart" width="400" height="400"></canvas>
            </div>
        }
    }
}

#[wasm_bindgen(start)]
pub fn main() {
    yew::start_app::<Model>();
}

ログを出力せせます。console.log("hogehoge");logとしてインポートし、引数を取れるようにマクロconsole_logを定義しています。console_log!("self.is_clicked: {}", self.is_clicked);のようにself.is_clickedの値を調べたりできます。

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

modules/myChart.jsのMyChartクラスをインポートしています。MyChartは構造体(struct)として使用可能です。コンストラクタ、メソッド、他にはゲッッター、セッターもインポートできるようです。
Importing functions from JS - The wasm-bindgen Guide

#[wasm_bindgen(module = "/modules/myChart.js")]
extern "C" {
    type MyChart;

    #[wasm_bindgen(constructor)]
    fn new() -> MyChart;

    #[wasm_bindgen(method)]
    fn draw(this: &MyChart);
}

createメソッドでMyChart::new()コンストラクタを呼びます。

fn create(_ctx: &Context<Self>) -> Self {
    Self {
        chart: MyChart::new(),
        is_clicked: false
    }
}

updateメソッドで、drawボタンが押されていない場合、self.chart.draw();チャートを描画、console_log!("self.is_clicked: {}", self.is_clicked);デベロッパーツールのConsoleにself.is_clickedの値を表示します。

fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
    match msg {
        Msg::Draw => {
            if !self.is_clicked {
                self.chart.draw();
                self.is_clicked = true;
                console_log!("self.is_clicked: {}", self.is_clicked);
            } else {
                console_log!("chart was drawn");
            }

            true
        }
    }
}

viewメソッドで、buttonタグのonclick属性にコールバックとしてMsg::Drawメッセージを指定することで、チャートを描画するかどうかupdateが行われます。描画領域のcanvasも追加し、ここにチャートが表示されます。

fn view(&self, ctx: &Context<Self>) -> Html {
    // This gives us a component's "`Scope`" which allows us to send messages, etc to the component.
    let link = ctx.link();
    html! {
        <div>
            <button onclick={link.callback(|_| Msg::Draw)}>{ "draw" }</button>
            <br/>
            <canvas id="myChart" width="400" height="400"></canvas>
        </div>
    }
}

Build & Serve

lib.rsを修正したので、makers buildでビルドします。

$ pwd
/Users/hibi221b/Desktop/yew-chartjs-tutorial

$ makers build
...
...
[INFO]: ✨   Done in 1.33s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/hibi221b/Desktop/yew-chartjs-tutorial/./static.
[cargo-make] INFO - Build Done in 2.10 seconds.

ビルド後、staticディレクトリ内にmyChart.jsが生成されており、wasm.js内でMyChartがインポートされていました。

$ tree -L 5 -I target
.
├── Cargo.lock
├── Cargo.toml
├── Makefile.toml
├── modules
│   └── myChart.js
├── src
│   └── lib.rs
└── static
    ├── index.html
    ├── package.json
    ├── snippets
    │  └── yew-chartjs-tutorial-11b9bc144d57b077
    │      └── modules
    │          └── myChart.js
    ├── wasm.js
    └── wasm_bg.wasm

6 directories, 10 files
static/wasm.js
import { MyChart } from './snippets/yew-chartjs-tutorial-11b9bc144d57b077/modules/myChart.js';

ビルドが完了したのでmakers serveをして、任意のブラウザで確認して終了です。

$ pwd
/Users/hibi221b/Desktop/yew-chartjs-tutorial

$ makers serve
miniserve v0.18.0
Serving path /Users/hibi221b/Desktop/yew-chartjs-tutorial/static
Availabe at (non-exhaustive list):
    http://127.0.0.1:8080
© 2020 hibi221b All rights reserved.