ASP.NET Core Blazor と Chart.js で入門する Web アプリ作成 － 実践編：Chart.jsでグラフを描く

「ASP.NET Core Blazor と Chart.js で入門するWebアプリ作成」の3回目。各回記事の内容は以下のようになります。

[導入編：サンプルを動かす][link-1]
[探究編：Blazorの仕組みを理解する][link-2]
実践編：Chart.jsでグラフを描く（当記事）
[Tips編：BlazorやChart.jsのあれこれ][link-4]
[番外編：Ubuntuサーバで公開する][link-5]

[link-1]:https://qiita.com/okatako/items/171f05dfc36d6b27769d
[link-2]:https://qiita.com/okatako/items/c0208510961982df4bdc
[link-3]:https://qiita.com/okatako/items/4dbb7eaee95f4c337d57
[link-4]:https://qiita.com/okatako/items/e50b90afecaf5ddb5f01
[link-5]:https://qiita.com/okatako/items/b04ddd9c4f5631d0cc40

今回の内容は、第1回「導入編」の「作業の流れ」で提示した項目のうち
4. サンプルソースから余計なものを削除してシンプルなアプリに変える
5. Chart.js の導入
6. サーバ側からブラウザ側のJavaScript関数を呼び出す
です。

今回で Chart.js を使った独自アプリを動かすところまで行きます。

**2020/10/23 追記**
System.Text.Json.JsonSerializer によって Newtonsoft.Json.JsonConvert.SerializeObject の機能（`NullValueHandling.Ignore`）が置換できることが判明したので、System.Text.Json.JsonSerializer を使うように変更しました。これに伴い、「[ChartJson クラスの用意](#chartjson-クラスの用意)」に記載の `ChartJson` クラスの Extension の記述が変更になっています。なお、Newtonsoft.Json のインストールの節は、 NuGet の使い方の例として残しておきます。

# サンプルソースをシンプルな作りに変える
前回記事「探究編」で Blazor アプリの構成要素はほぼ理解できたので、今回は導入編で作成したサンプルプロジェクトから不要なものを削除して、いったんシンプルなアプリを作成してみます。その後、Chart.js などいくつかの機能を追加します。

サンプルプロジェクトを作成していない方やすでにいろいろ改造を加えてしまった方は、導入編の「[Blazor フレームワークを用いたサンプルプロジェクトの作成と実行](https://qiita.com/okatako/items/171f05dfc36d6b27769d#blazor-%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8%E5%AE%9F%E8%A1%8C)」を参照して、新規にプロジェクトを作成してください。

## 不要な Razor ファイルの削除
まずは「Weather forecast」画面だけを表示するシンプルなアプリに作りかえます。Home ページと Counter ページは使わないので当該ページの Razor ファイルは削除してしまって構いません。具体的には

- _Pages/Counter.Razor_
- _Pages/Index.Razor_
- _Shared/NavMenu.razor_
- _Shared/SurveyPrompt.razor_

です[^del_cs]。残しておいても問題ないですが、すっきりさせたい方は VS や VS Code でファイルを選択して削除してしまいましょう。もちろん、コンソールを使って del なり rm なりしても構いません。

[^del_cs]: 前回記事で _Counter.razor.cs_ ファイルを作成した場合は、それも不要ファイルになります。

不要ファイルを削除した場合、残る主要ファイルは以下のようになります[^remain_cs]。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/392ebe3d-88d2-5c3a-e062-b3d5f1366a7d.png)

[^remain_cs]: 前回記事で _FetchData.razor.cs_ ファイルを作成した場合は、それも残ります。

## _MainLayout.razor_ の修正
左側のサイドメニューを構成していた _NavMenu.razor_ が不要になったので、_Shared/MainLayout.razor_ を修正して、 `@Body` だけを残します。

```html:MainLayout.razor
@inherits LayoutComponentBase

<div class="main">
    <div class="content px-4">
        @Body
    </div>
</div>
```
## _FetchData.razor_ の修正
_Pages/FetchData.razor_ を修正してルートページにします。先頭の `@page` のパスを `"/"`[^root_path] に修正してください。

[^root_path]: このように `@page "/"` となっていれば、どんなファイル名の _.razor_ ファイルでもルートページになれます。

```html:Pages/FetchData.razor/先頭部
@page "/"
```
もし、 _Pages/Index.razor_ が残っている場合は、その `@page` のパスを `"/"` 以外に変更しておいてください。

## ビルドしてみる
VS であればメニューからビルドできますが、VS Code の場合はコンソールから `dotnet build` でビルドします[^dotnet_build]。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/59f683aa-616d-8cbf-2780-1547bc2a1c80.png)
[^dotnet_build]: `Ctrl + Shift + B` でビルドを実行することもできます。VS Code の場合は Terminal が開いてそこで `dotnet build` が実行されます。
ビルドに成功しました。VS (Code) に戻って F5 で実行してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/4e04067c-6fbf-f001-6b25-47432968a819.png)
意図通り、FetchData画面だけが表示されています。確認したらブラウザを閉じます。VS Code の場合はデバッグ実行も停止してください。

次に、別の Razor コンポーネントを用意し、 _FetchData.razor_ からはそのコンポーネントを呼び出すようにしてみます。

_Shared/Chart.razor_ ファイル[^other-dir]を作成し、以下の内容で置き換えてください。

[^other-dir]: _Shared_ 以外の別の名前のフォルダを作成して、そこに _Chart.razor_ ファイルを作ることもできます。その場合は、 _\_Imports.razor_ に `@using MyBlazorApp.{FOLDER_NAME}` を追加してください。

```html:Chart.razor
<h1>Chartの世界にようこそ！</h1>

ここには Chart.js を使ったグラフが表示されます。
```
イメージは次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/d168c918-7f05-a8ea-e9c6-051f7377d86f.png)
`@page`命令がない _.razor_ ファイルは部品になる[^blazor_buhin]んでしたね。_FetchData.razor_ を修正して `Chart` コンポーネントを参照するようにします。下記2行以外は全て削除してください。（_FetchData.razor.cs_ がある場合はファイルを削除してください）

[^blazor_buhin]: 前回記事の[SurveyPrompt.razor](https://qiita.com/okatako/private/c0208510961982df4bdc#surveypromptrazor)の節を参照。

```html:FetchData.razor
@page "/"

<Chart />
```
イメージは次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/50f2a442-3245-edde-5c63-8e7114c37e9d.png)
修正をすべて保存したら F5 で実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/697c47cb-7e30-27fe-f1f1-8bb7f6ffd564.png)
OKです。[前回記事][link-2]でがんばって Blazor の仕組みを（ある程度ですが）理解した甲斐がありました。以下では、この Chart コンポーネントにグラフを表示させていくことにします[^chart_razor_1]。

[^chart_razor_1]: 今回はあえて _Chart.razor_ という別のコンポーネントを作成しましたが、_FetchData.razor_ に実装しても構いません。

# Chart.js の導入
プロジェクトに Chart.js を導入します。これは静的なファイルなので配置場所は `wwwroot`配下になります。

## Visual Studio での Chart.js の導入
まず Chart.js を配置するためのフォルダを作成しておきます[^mkdir_lib]。ソリューションエクスプローラで `wwwroot` を右クリックし、「追加」>「新しいフォルダー」と選択していきます。
[^mkdir_lib]: コンソール操作が得意な方は、`mkdir wwwroot/lib` で作成しても構いません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/7dd688c2-7a34-f491-afd9-f06ef613ce9d.png)
フォルダ名は何でもよいのですが、今回は "lib" にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/8b2bebef-1243-fd27-5f4e-266ad7f24de1.png)
_lib_ フォルダに chart.js を導入します。`lib` を右クリックし、「追加」>「クライアント側のライブラリ」と選択していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/bba87158-3cb1-00a5-a23f-59e337c74c11.png)

下図のような画面が表示されるので、ライブラリ名に「Chart.js」と入力して、表示された候補から「Chart.js」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/765bc6e2-f175-8ff6-069c-2c9af6844232.png)
以下のような画面に変わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/775691b3-9e1d-6db6-d2b9-2d46051a8bd9.png)
「インストール」をクリックして完了です。_wwwroot/lib_ の下に _Chart.js_ フォルダが作成されて、その下に _.css_ や _.js_ が置かれているのを確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/481c5e7e-12ed-d6b7-3778-80e77a324749.png)

## VS Code での Chart.js の導入
npm を使える方はそちらでインストールするのが最も簡単だと思います。

Shell を使ってインストールする場合は、VS での導入のときにもちらっと名前が出ていますが [cdnjs][link-cdnjs] というサイトからファイルを個別にダウンロードします。最新版のダウンロードURLについては、[cdnjs][link-cdnjs] で確認してください。

[link-cdnjs]:https://cdnjs.com/libraries/Chart.js

### Power Shell を使う場合
VS Code のメニューから「Terminal」>「New Terminal」と選択してください。画面下部に Power Shell が開きます。プロジェクトフォルダにいることを確認したら、下記のコマンド列をまとめてコピペで実行してください[^ps-foreach]。（VSに合わせたフォルダ構成[^chart_js_dir]にしています）

[^ps-foreach]: 本来は foreach を使べきだと思いますが、筆者は Power Shell に詳しくないので、このような形に・・・。とりあえずはコマンド全体をTERMINALにコピペして実行してください。
[^chart_js_dir]: 必ずしもこのようなフォルダ構成にする必要はないのですが、説明の都合上、 VS 場合に合わせています。

```powershell:～\Dev\dotnet\MyBlazorApp
mkdir -p wwwroot/lib/Chart.js
cd wwwroot/lib/Chart.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js -OutFile Chart.min.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js -OutFile Chart.bundle.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js -OutFile Chart.bundle.min.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css -OutFile Chart.css
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js -OutFile Chart.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css -OutFile Chart.min.css

```

### Git Bash を使う場合
Git Bash を使える場合は、下記を実行してください。

```shell-session:~/Dev/dotnet/MyBlazorApp
mkdir -p wwwroot/lib/Chart.js
cd wwwroot/lib/Chart.js
for x in css min.css js min.js bundle.js bundle.min.js; do
    curl https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.$x --output Chart.$x
done
```

実行が終わったら、 VS Code の Explorer で _lib/Chart.js_ 配下を確認しておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/1e7aa22c-a07c-a362-b131-65c5c4d84898.png)

## Chart.js を参照する
### _\_Host.cshtml_ を修正して _Chart.css_ と _Chart.min.js_ を参照する
_Pages/\_Host.cshtml_ を開き、以下のように `<head>` の末尾に `<link rel="stylesheet" href="lib/Chart.js/Chart.css" />` と挿入し、 _Chart.css_ が参照できるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/46571106-3faa-7a50-6508-ff8b91687a95.png)
さらに `<body>` の末尾に `<script src="lib/Chart.js/Chart.min.js" asp-append-version="true"></script>` と挿入し、ルートページにアクセスした際に Chart.js の本体を読み込むようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/6f0532b7-7c3f-6ab1-4487-d204799206b4.png)

## Chartの canvas を用意する
グラフが描画される場所（canvas）を用意します。_Shared/Chart.razor_ を開き、下記コードを追加してください。

```html:Chart.razor
<div class="chart-container" style="position: relative; height:400px; width:600px">
    <canvas id="myChart"></canvas>
</div>
```
`canvas` はHTMLの既存要素で、以下のようなものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/abdb42ce-1aee-079f-995b-d4b2150dcafc.png)
Chart.js は、この`canvas`にグラフを描画するわけです。その際、`canvas` 要素に付与した`id`を参照します。上記では`myChart` という id を設定しています。

## Chart() を呼び出す関数を用意する
ここで初めて JavaScript のコードが登場します。そして JavaScript が必要になるのはここだけとなります。

_\_Host.cshtml_ を開き、`<body>` ブロックの末尾に次のコードを挿入してください。

```javascript:_Host.cshtml
    <script>
        function renderChart(chartJson) {
            if (window.chartObj) {
                console.log('destroy chartObj');
                window.chartObj.destroy();
            }
            console.log("json=" + chartJson);
            var ctx = document.getElementById('myChart');
            window.chartObj = new Chart(ctx, JSON.parse(chartJson));
        };
    </script>
```
画像では以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/4a2f0e1b-6ead-51c5-b936-435a133c1336.png)
ここでは `renderChart()` という関数を定義し、そこから Chart.js が提供する `Chart()` を呼んでいます。

`renderChart()` は C# から呼び出されることを想定しており、引数として JSONオブジェクトを文字列化したデータを受け取ります。それを parse してJSONオブジェクトに戻したものを `Chart()` に渡します。

`Chart()` は、canvas 要素（ _Chart.razor_ で定義した、 id が `myChart` となっている canvas）とグラフデータを表すJSONオブジェクトを引数に取ります。`new Chart(・・・)` するだけでグラフは描かれるのですが、複数回呼び出されたときに前回作成したグラフを破棄するために `chartObj` という変数に保存しています。

### テスト描画する
グラフ描画に必要な JavaScript はこれだけでよいのですが、一応、テスト用の JavaScript も追加してテストしてみましょう。以下のコード[^stringify]をさらに `<body>` ブロックの末尾に挿入してください（コピペしましょう）。

[^stringify]: Chart()関数は JSON オブジェクトを受け取るのですが、それを呼び出している renderChart() が C# から呼び出される都合上、JSONオブジェクトを文字列に変換して渡しています。

<details><summary>**テストコードの内容**（クリックして開いてください）</summary><div>

```javascript
    <script>
        function renderChartTest() {
            renderChart(
                JSON.stringify({
                    "type": "bar",
                    "data": {
                        "labels": ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
                        "datasets": [
                            {
                                "label": "# of Votes",
                                "data": [12, 19, 3, 5, 2, 3],
                                "backgroundColor": [
                                    "rgba(255, 99, 132, 0.2)",
                                    "rgba(54, 162, 235, 0.2)",
                                    "rgba(255, 206, 86, 0.2)",
                                    "rgba(75, 192, 192, 0.2)",
                                    "rgba(153, 102, 255, 0.2)",
                                    "rgba(255, 159, 64, 0.2)"
                                ],
                                "borderColor": [
                                    "rgba(255, 99, 132, 1)",
                                    "rgba(54, 162, 235, 1)",
                                    "rgba(255, 206, 86, 1)",
                                    "rgba(75, 192, 192, 1)",
                                    "rgba(153, 102, 255, 1)",
                                    "rgba(255, 159, 64, 1)"
                                ],
                                "borderWidth": 1
                            }
                        ]
                    },
                    "options": {
                        "scales": {
                            "yAxes": [
                                {
                                    "ticks": {
                                        "beginAtZero": true
                                    }
                                }
                            ]
                        }
                    }
                })
            );
        }
    </script>
```
</div>

Chartコンポーネントにこのテスト関数を呼び出すボタンを追加します。

```html:Chart.razor
<button class="btn btn-primary btn-sm py-0" onclick="renderChartTest()">Click me</button>
```
最終的に _Chart.razor_ ファイルの内容は以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/89a0ae3b-6d0d-ea9e-d9c6-1955f311e190.png)
ここまで準備ができたら、F5 を押してデバッグ実行してみます。次のように「Click me」というボタンが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/4d25b122-55b1-7c84-3439-cb9d26427739.png)
このボタンをクリックします。問題がなければ次のようなグラフが描画されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/f93c7ac3-9b71-9a15-3c61-ceccd9cbaf45.png)
ボタンをクリックするたびにグラフがアニメーション付きで再描画されます。ここでブラウザで F12 を押してください。開発画面が開くのでコンソールをクリックします。以下のように、`renderChart()` 関数に記述した `console.log()` によりログが出力されているのが分かると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/b929a253-177f-94b5-0317-073164247c96.png)

ボタンを押して何も起きなかった場合も、ブラウザで F12 を押して開発画面を開いてください。コンソールに何かエラーが表示されていると思います。以下は実際に筆者の環境で発生したエラー画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/8415aec7-8a13-9008-10eb-b7f8980f28fb.png)
この情報を見て、がんばってエラーをつぶしましょう。

# サーバ側からJavaScript関数を呼び出す
先ほどのテストでは、JavaScript を使ってブラウザ側でグラフデータを用意し、`renderChart()` を呼んでいました。つまり、サーバ側は一切かかわっていませんでした。

ここからが本題です。Chartコンポーネントで「Click me」ボタンが押されたら、サーバ側でグラフデータを作成し、それを引数にしてブラウザ側の JavaScript 関数 `renderChart()` を呼ぶようにしてみましょう。

## Newtonsoft.Json のインストール
**注：** 冒頭の「追記」のところでも触れていますが、Newtonsoft.Json は使わなくなりました。したがってこの節は飛ばして次の「[ChartJson クラスの用意](#chartjson-クラスの用意)」に移ってください。本節は NuGet の使い方の例として残しておきます。

この後で Chart.js 用のグラフデータとなるJSON構造を表す `ChartJson` という C# のクラスを用意します。

`ChartJson` データは C# クラスのインスタンスなので、そのままでは `Chart()` の引数に渡せません。いったん文字列にシリアライズしてから `renderChart()`関数に渡し、さらにそこで JSON データに戻してから `Chart()` に渡すことにします。

C# クラスのインスタンスを JSON 構造の文字列にシリアライズしてくれる Newtonsoft.Json というパッケージがあるので、NuGet を使ってインストールしておきます。

### Visual Studio の場合
下図のように「依存関係」のところで右クリックし、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/579b6dd4-9be0-4c83-c153-b3b4291749c1.png)
NuGet画面が開いたら、「参照」のところで "Newtonsoft.Json" と入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/f84d1227-ee2d-4f62-892b-ca9b0be278f2.png)
表示された "Newtonsoft.Josn" を選択し、「インストール」をクリックしてインストールしてください。

### VS Code の場合
まず VS Code に NuGet パッケージマネージャをインストールする必要があります。未インストールの方は、Extensions で "nuget" を検索し、"NuGet Package Manager" をインストールしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/5a0eac0d-53d0-d91b-c52e-27ce8619942e.png)
NuGet がインストールできたら、 Explorer で _MyBlazorApp.csproj_ を開き、そこで右クリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/ccedfbed-25cf-39b8-00d9-48b80eccd178.png)
「Command Palette...」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/1c1560f1-b757-de8e-8520-a0ad6d524ffb.png)
入力窓が開くので、"Add Package" を入力し Enter を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/ef77d210-c099-6f51-2afd-7ab92e1ee15f.png)
新しく開いた入力窓で "Newtonsoft.Json" と入力し Enter を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/6ca36f49-cb15-ff53-b1b4-5f1ad5b65765.png)
候補が表示されるので、先頭の "Newtonsoft.Json" を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/9144e823-ff0d-4a42-4833-330661bade5d.png)
バージョンを聞かれるので先頭の最新版を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/ebdf2390-6bb5-913f-6485-358f1eb51e73.png)
_.csproj_ にパッケージ参照が挿入されました。保存すると「restore するか」と聞かれるかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/7891cafa-ebd6-4af3-fb67-9520b199e113.png)
その場合は「Restore」をクリックしてください。また `dotnet build` を実行しておいてください。

## ChartJson クラスの用意
Chart.js 用のグラフデータとなるJSON構造を表すクラスを用意しておきます。_Data_[^models_1] フォルダの下に _ChartJson.cs_ というファイルを作成し、以下の内容で書き換えてください。

[^models_1]: MVCの観点からは _Models_ というフォルダを作成して、その下に配置すべきかもしれません。

<details><summary>**_ChartJson.cs_ の内容**（クリックして開いてください）</summary><div>

```cs:Data/ChartJson.cs
using System;
using System.Linq;
using System.Text.Json;

namespace MyBlazorApp.Data
{
    /// <summary>
    /// Chart.js の Chart() 関数に渡す JSONデータを表すクラス
    /// </summary>
    public class ChartJson
    {
        public string type { get; set; }
        public Data data { get; set; }
        public Options options { get; set; }

        public ChartJson RoundData()
        {
            if (data != null) data.RoundData();
            return this;
        }
    }
    public class Data
    {
        public string[] labels { get; set; }
        public Dataset[] datasets { get; set; }

        public void RoundData()
        {
            if (datasets != null) {
                foreach (var dataset in datasets) {
                    dataset.RoundData();
                }
            }
        }
    }

    public class Dataset
    {
        public string label { get; set; }
        /// <summary> double に解釈されるべきデータ </summary>
        public double?[] data { get; set; }
        public string backgroundColor { get; set; }
        public string borderColor { get; set; }
        public int order { get; set; } = 1;
        public string type { get; set; } = "bar";
        public bool fill { get; set; } = false;
        public int radius { get; set; } = 3;
        public double[] borderDash { get; set; }
        public string yAxisID { get; set; } = "y-1";

        public static Dataset CreateBar(string label, double?[] data, string color)
        {
            return new Dataset { label = label, data = data, borderColor = color, backgroundColor = color, order = 3 };
        }

        public static Dataset CreateLine(string label, double?[] data, string bdColor, string bgColor, string yAxis = "y-1")
        {
            return new Dataset { label = label, data = data, borderColor = bdColor, backgroundColor = bgColor, order = 2, type = "line", yAxisID = yAxis };
        }

        public static Dataset CreateLine2(string label, double?[] data, string bdColor, string bgColor)
        {
            return CreateLine(label, data, bdColor, bgColor, "y-2");
        }

        private static double[] _dashParam = new double[] { 10, 3 };

        public static Dataset CreateDashLine(string label, double?[] data, string color)
        {
            return new Dataset { label = label, data = data, borderColor = color, backgroundColor = color, order = 2, type = "line", radius = 0, borderDash = _dashParam };
        }

        public static Dataset CreateDataset(string type, string label, double?[] data, string color, string bgColor, int order)
        {
            return new Dataset { type = type, label = label, data = data, borderColor = color, backgroundColor = bgColor, order = order };
        }

        public void RoundData()
        {
            if (data != null) {
                data = data.Select(x => x._round(3)).ToArray();
            }
        }
    }

    public class Options
    {
        public int AnimationDuration {
            get { return animation.duration; }
            set { animation.duration = value; }
        }

        public bool maintainAspectRatio { get; set; } = false;

        public Animation animation { get; set; } = new Animation();

        public Scales scales { get; set; }

        public static Options Plain()
        {
            return new Options {
                scales = new Scales {
                    yAxes = new[] {
                        new Yaxis{
                            ticks = new Ticks()
                        }
                    }
                }
            };
        }

        public static Options CreateTwoAxes(Ticks leftTicks = null, Ticks rightTicks = null)
        {
            return new Options {
                scales = new Scales {
                    yAxes = new[] {
                            new Yaxis{ id = "y-1", position = "left", ticks = leftTicks ?? new Ticks() },
                            new Yaxis{ id = "y-2", position = "right", ticks = rightTicks ?? new Ticks() },
                    }
                }
            };
        }
    }

    public class Animation
    {
        public int duration { get; set; } = 1000;
    }

    public class Scales
    {
        public Yaxis[] yAxes { get; set; }
    }

    public class Yaxis
    {
        public string id { get; set; } = "y-1";
        public string position { get; set; } = "left";
        public Ticks ticks { get; set; }
    }

    public class Ticks
    {
        public bool beginAtZero { get; set; } = true;
        public double? min { get; set; }
        public double? max { get; set; }
        public double? stepSize { get; set; }

        public Ticks(double? maxVal = null, double? stepVal = null, double? minVal = null)
        {
            if (maxVal.HasValue) {
                max = maxVal.Value;
                min = minVal.HasValue ? minVal.Value : 0;
                stepSize = stepVal.HasValue ? stepVal.Value : maxVal.Value / 10;
            }
        }
    }

    /// <summary>
    /// ヘルパー拡張メソッド
    /// </summary>
    public static class Extensions
    {
        public static double? _round(this double? d, int digits = 0)
        {
            return d.HasValue ? Math.Round(d.Value, digits) : d;
        }

        private static JsonSerializerOptions SerializerOpt = new JsonSerializerOptions { IgnoreNullValues = true };

        public static string _toString(this ChartJson json)
        {
            return json.RoundData()._jsonSerialize();
        }

        public static string _jsonSerialize<T>(this T json)
        {
            return json == null ? "" : JsonSerializer.Serialize<T>(json, SerializerOpt);
        }

        public static T _jsonDeserialize<T>(this string jsonStr)
        {
            return JsonSerializer.Deserialize<T>(jsonStr);
        }
    }
}
```
</div>

`ChartJson` クラスは、Chart() に与える JSONデータの構造に合わせたクラス構成[^absent-value]になっています。このクラスには `_toString()` という拡張メソッドを用意してあります。`_toString()` を呼び出すと ChartJson インスタンスを JSON文字列に変換することができます[^serializeObject]。

[^absent-value]: `datasets[].data` が `double?[]` という null許容型になっているのは、欠損値を null として扱えるようにするためです。次回記事で説明予定。

[^serializeObject]: `_toString()` で使われている `System.Text.Json.JsonSerializer.Serialize()` は、引数として `IgnoreNullValues = true` を指定しています。この場合、 `double?` のような Nullable型の値が null だったときには、その属性そのものが出力されなくなります。たとえば `Ticks` クラスはデフォルトで `beginAtZero` だけが出力されることになります。

## ChartJson データを返す関数を用意する

_Data/WeatherForecastService.cs_ を開き、 `WeatherForecastService` クラスの中味（9行目～23行目）を以下の内容で置き換えます[^WeatherForecastService]。

[^WeatherForecastService]: 元の気温データ生成ロジックをそのまま使って、セ氏とカ氏の2つのグラフ用のデータを作成しています。

<details><summary>**`WeatherForecastService` クラスの内容**（クリックして開いてください）</summary><div>

```cs:Data/WeatherForecastService.cs
        public Task<ChartJson> GetForecastAsync(DateTime startDate)
        {
            var rng = new Random();
            return Task.Run(() => {
                var forecast = getForecast(startDate);
                return new ChartJson {
                    type = "bar",
                    data = new Data {
                        labels = forecast.Select(x => x.Date.ToString("MM月dd日")).ToArray(),
                        datasets = new Dataset[] {
                                Dataset.CreateBar("摂氏", forecast.Select(x => (double?)x.TemperatureC).ToArray() , "royalblue"),
                                Dataset.CreateBar("華氏", forecast.Select(x => (double?)x.TemperatureF).ToArray(), "darkblue"),
                            },
                    },
                    options = Options.Plain(),
                };
            });
        }

        private WeatherForecast[] getForecast(DateTime startDate)
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
            }).ToArray();
        }
```
</div>

いったんここで `dotnet build` を実行し、ビルドに成功することを確認しておいてください。



## _Chart.razor_ を修正してグラフ描画
ここまででお膳立ては整ったので、ボタンがクリックされたらサーバ側のコードを実行し、返ってきたJSONデータでグラフ描画をしてみましょう。_Chart.razor_ を次のように修正してください。

まず次のコードを追加します。

```html:Shared/Chart.razor
@using MyBlazorApp.Data
@inject WeatherForecastService ForecastService
@inject IJSRuntime JSRuntime
@code{
    public async Task RenderNewChart()
    {
        var json = await ForecastService.GetForecastAsync(DateTime.Now);
        var jsonStr = json._toString();
        await JSRuntime.InvokeAsync<string>("renderChart", jsonStr);
    }
}
```
ボタンがクリックされたら、ここで定義されたメソッド `RenderNewChart` を呼ぶように修正します。

```html:Shared/Chart.razor
<button class="btn btn-primary btn-sm py-0" @onclick="RenderNewChart">Click me</button>
```
修正後のイメージは下図のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/f20b59eb-2dfb-a51a-75f7-a9800f06356c.png)
F5 を押して実行してみましょう。「Click me」ボタンをクリックして、次のようなグラフが描画されれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/63dca1f2-2787-2f66-abe3-8ac11611b161.png)
このグラフは、Excelのグラフでいうところの「集合縦棒」グラフになっています。一つの日付のところに2本の棒グラフが立っていて、左側がセ氏、右側がカ氏の温度を示しています。

### `RenderNewChart()` の説明
_Chart.razor_ に追加した部分を再掲します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121763/b4be3ff2-f9e5-1f9e-659c-bae1cf3a9963.png)
11行目： _Data_フォルダにあるクラスを参照します。この `@using` を _\_Imports.razor_ に入れてしまえば、ここでの `@using` 宣言は不要になります。
12行目： 元々 _Startup.cs_ の `ConfigureServices()` で `services.AddSingleton<WeatherForecastService>()` しているので、このように `@inject` によりシステム側で生成したシングルトンインスタンスを注入してもらうことができます。
13行目： `IJSRuntime JSRuntime` をInjectしています。このオブジェクトを使うとサーバ側のC#コードからブラウザ側の JavaScript を呼び出せるようになります。
15行目： ここから `RenderNewChart()` メソッドの定義となります。ブラウザで「Click me」をクリックすると、サーバ側のこのメソッドが呼び出されることになります。
17行目： Injectされた `WeatherForecastService` インスタンスの `GetForecastAsync()` を呼び出して `ChartJson`データを作成して返してもらいます。
18行目： ChartJson クラスの拡張メソッド `_toString()` により、ChartJson データをJSON文字列に変換します。
19行目： 変換されたJSON文字列を引数として、 _\_Host.cshtml_ で定義した JavaScript関数 `renderChart()` を呼び出します。`IJSRuntime` や `InvokeAsync` については次回記事で説明します。

今回の記事はここまでとなります。次回は Blazor や Chart.js のいろいろな Tips 的な話を取り上げたいと思います。