ASP.NET Core MVCでグラフを表示したい【Chart.js】

#### はじめに
ASP.NET、C#、JavaScript初心者です。
趣味で作成中の.NETアプリケーションでグラフを実装したいと思い、
いろいろ調べるのに時間がかかったためまとめました。
グラフの実装は初めてで、Chart.jsは初めて使いました。

■使用技術・バージョン
・ASP.NET Core MVC（.NET6）
・Chart.js　v4.1.2

#### グラフライブラリの選定
「.NET6　グラフ」で検索して以下のものが出てきました。
- ScottPlot・・・WPF専用？使えず
- OxyPlot・・・WPF専用？試していない
- LiveChart・・・WPF専用？試していない
- Chartコントロール・・・WindowsForm専用？使えず
- Chartヘルパー・・・ヘルパーが使えず

.NETのフレームワークはMVCしかやったことがないため、
WPFってなんだ？なぜWPFしか出てこないんだって感じでした。。
（正直フレームワーク多すぎてよく分かっておらず、
「ASP.NET Core MVC　グラフ」で検索すべきだったと思いました）

どれもMVCではできなさそうだったため、
フレームワーク関係なくできそうなChart.jsに決めました。

#### 手順
***① Chart.js公式ドキュメントをコピペしてとりあえず表示させる***
Chart.jsはバージョンにより書き方が違うそうです。
v3系と明記している記事はいくつか見つかるのですが、
v4系と明記してあるものが見つからなかったため公式を参考にするしかありませんでした。

https://www.chartjs.org/docs/latest/getting-started/
```
<div>
  <canvas id="myChart"></canvas>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<script>
  const ctx = document.getElementById('myChart');

  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
      datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderWidth: 1
      }]
    },
    options: {
      scales: {
        y: {
          beginAtZero: true
        }
      }
    }
  });
</script>
```


***② DBから取得したデータを使う（C#と連携させる）***
JavaScriptとC#をどうやって連携したらいいのか分かりませんでした。
Blazorの記事は見つかって、JavaScriptと連携するにはBlazorがいいのか？と思ったりしましたが…

ドンピシャな参考記事が見つかっていなかったため、phpでやっている記事を参考に、
labelsとdataにViewModelの値を渡せればいいのではないか？と考えました。
```
// 抜粋
// @Model.StudyDateの中は"'yyyy/MM/dd HH:mm', 'yyyy/MM/dd HH:mm', …'"というようなデータ
labels: [@Model.StudyDate],
```

しかし、日付項目のシングルクォーテーションがエンコードされグラフ表示されません。
![スクリーンショット (12-1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/433f3269-415d-bd31-824f-ccd85899527a.png)

JSONにしないといけないかといろいろ調べていましたが、
@Html.RawでHTMLエンコードを無効にすればグラフ表示できるようになりました。
参考記事は以下です。試していないですが、以下の方法でJSONデータでできそうです。

https://blog.monslab.net/2018/09/27/asp-net-core%E3%81%A7%E3%82%B0%E3%83%A9%E3%83%95%E3%82%92%E6%8F%8F%E3%81%84%E3%81%9F/

***③ 棒グラフと折れ線グラフを表示させたりカスタムする***


#### 問題
y軸は左と右に二軸表示させたいのですが、以下のようになってしまいました。
※中身のデータが変なのはスルーしてください…
![スクリーンショット (8-1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/2f8815c4-9b77-d5ed-f5bb-67e0f14170fc.png)

複数グラフを表示させている記事を参考にしたのですが、v3系の記事しかなく、
バージョンによる違いがあるのかなと思い、公式ドキュメントを確認しました。
以下にサンプルコードがあり、v3系と変わりなくできるらしいことは確認。
（これを探すのにも苦労しました）
https://www.chartjs.org/docs/latest/axes/cartesian/#axis-position
```
const myChart = new Chart(ctx, {
    type: 'line',
    data: {
        datasets: [{
            data: [20, 50, 100, 75, 25, 0],
            label: 'Left dataset',

            // This binds the dataset to the left y axis
            yAxisID: 'left-y-axis'
        }, {
            data: [0.1, 0.5, 1.0, 2.0, 1.5, 0],
            label: 'Right dataset',

            // This binds the dataset to the right y axis
            yAxisID: 'right-y-axis'
        }],
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
    },
    options: {
        scales: {
            'left-y-axis': {
                type: 'linear',
                position: 'left'
            },
            'right-y-axis': {
                type: 'linear',
                position: 'right'
            }
        }
    }
});
```

いろいろ値を変えて確かめたところ、scalesの中が反映されていないことが分かりました。

#### 結果
途中かっこが足りていませんでした。
scalesの前のanimationやpluginsをコメントアウトしたら理想通りの表示ができたので、optionsの中の書く順番かと思いきや違いました。
そういえば、かっこが足りないっていうエラーが出てたなと（「Uncaught Syntax Error: Unexpected token ')'」）
末行でエラーが出るのでその箇所を修正しており、途中に問題があることに気づきませんでした。
ちなみにanimationとpluginsは公式をコピペしたもので、そこに自分でscalesを足しました。

以下のコードで理想通りのグラフ表示ができました。
※グラフに表示するデータ自体も修正しました
※また、グリッド線を消すのはgridLinesではなくgridでした（1/14修正）
https://www.chartjs.org/docs/latest/axes/styling.html
```index.cshtml
@model Flashcard.ViewModels.HistoriesViewModel

@{
    ViewData["Title"] = "Histories";
}

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<div class="content m-auto p-4" style="max-width:900px">
    <div class="p-3 p-sm-5 border box-shadow">
        @if (Model.StudyDate != null)
        {
                <div class="mb-sm-4 text-center font-120">学習履歴</div>

                <!-- グラフ -->
                <div class="canvas-container">
                    <canvas id="myChart"></canvas>
                </div>
        }
        else
        {
                <div>学習履歴はありません</div>
        }
    </div>
    <div class="row d-md-flex justify-content-md-center mt-3">
        @if (Model.StudyDate != null)
        {
            <div class="col-sm-6 col-md-4">
                <form asp-action="AllDelete">
                    <button type="submit" class="btnCommon btnRed btnBig w-100 box-shadow" onclick="return confirm('履歴を全て削除してよろしいですか');">履歴の削除</button>
                </form>
            </div>
            <div class="col-sm-6 col-md-4 mt-2 mt-sm-0">
                <form asp-action="DownloadFile">
                    <button type="submit" class="btnCommon btnGreen btnBig w-100 box-shadow">Excel出力</button>
                </form>
            </div>
        }
        <div class="col-sm-6 col-md-4 mt-3 mt-md-0">
            <button type="button" class="btnCommon btnGray btnBig w-100 box-shadow" onclick="location.href='./'">戻る</button>
        </div>
    </div>
</div>

<script>
    const ctx = document.getElementById('myChart');
    new Chart(ctx, {
        type: 'bar', // 棒グラフ
        data: {
            labels: [@Html.Raw(@Model.StudyDate)], // x軸に表示する学習日時
            datasets: [{
                label: '正答数(回)',
                data: [@Html.Raw(@Model.CorrectAnswerCount)], // 学習日時ごとの正答数
                order: 2,
                borderColor: '#a6cee3',
                backgroundColor: '#a6cee3',
                hoverBackgroundColor: '#a6cee3',
                borderWidth: 1,
                yAxisID: 'first-y-axis'
            }, {
                label: '正答率(%)',
                type: "line", // 折れ線グラフ
                data: [@Html.Raw(@Model.CorrectAnswerRate)], // 学習日時ごとの正答率
                order: 1, // 折れ線グラフが前面になるように
                borderColor: '#1f78b4',
                backgroundColor: '#1f78b4',
                yAxisID: 'second-y-axis'
            }]
        },
        options: {
            animation: false, // チャートがすぐに表示されるようにアニメーションをオフにする
            responsive: true, // レスポンシブ
            maintainAspectRatio: false,
            plugins: {
                //legend: { // 凡例（今回は表示にしている）
                //    display: false
                //},
                tooltip: { // ツールチップ非表示
                    enabled: false
                },
            }, // このかっこが足りなかったのが問題だった
            scales: {
                'first-y-axis': { // y軸1つ目
                    type: 'linear', // 線形軸
                    position: 'left', // 軸の位置
                    min: 0, // 最小値
                    //max: 10, // 最大値
                    ticks: { // 目盛線
                        stepSize: 5 // 目盛間隔
                    },
                    title: {
                        display: true,
                        text: "正答数(回)"
                    }
                },
                'second-y-axis': { // y軸2つ目
                    type: 'linear',
                    position: 'right',
                    min: 0,
                    max: 100,
                    ticks: {
                        stepSize: 10
                    },
                    title: {
                        display: true,
                        text: "正答率(%)"
                    },
                    grid: { // グリッド線を片方消す
                        drawOnChartArea: false
                    }
                }
            }
        }
    });
    $(function () {
        var container = $('.canvas-container');
        var ctx = $('#myChart');
        ctx.attr('width', container.width());
        ctx.attr('height', 300);
    });
</script>
```
```HistoriesController.cs
using Microsoft.AspNetCore.Mvc;
using Flashcard.Data;
using System.Security.Claims;
using Flashcard.ViewModels;
using Microsoft.EntityFrameworkCore;

namespace Flashcard.Controllers
{
    public class HistoriesController : Controller
    {
        private readonly FlashcardContext _context;

        private readonly Claim _claim;

        HistoriesViewModel viewModel = new HistoriesViewModel();

        public HistoriesController(FlashcardContext context, IHttpContextAccessor httpContextAccessor)
        {
            _context = context;
            _claim = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
        }

        // 初期処理
        // GET: Histories
        public async Task<IActionResult> Index()
        {
            // ログインしていなければログイン画面へ
            if (_claim == null)
            {
                return RedirectToAction("Index", "Account");
            }

            // 履歴を取得する
            int userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
            // 日時の新しいものから10件取得する
            // TakeLastは使えないため降順で10件取ってから、昇順に並べ替えている
            var flashcardContext = _context.Histories
                .Where(h => h.UserId == userId)
                .OrderByDescending(h => h.StudyDate)
                .Take(10);
            var HistoriesList = await flashcardContext.OrderBy(h => h.StudyDate).ToListAsync();

            // グラフデータ作成
            foreach (var item in HistoriesList)
            {
                if (!string.IsNullOrEmpty(viewModel.StudyDate))
                {
                    viewModel.StudyDate = viewModel.StudyDate + ", '" + item.StudyDate.ToString("yyyy/MM/dd") + "'";
                    viewModel.CorrectAnswerCount = viewModel.CorrectAnswerCount + ", " + item.CorrectAnswerCount.ToString();
                    viewModel.CorrectAnswerRate = viewModel.CorrectAnswerRate + ", " + ((double)item.CorrectAnswerCount / (double)item.StudyCount * 100).ToString();
                }
                else
                {
                    viewModel.StudyDate = "'" + item.StudyDate.ToString("yyyy/MM/dd") + "'";
                    viewModel.CorrectAnswerCount = item.CorrectAnswerCount.ToString();
                    viewModel.CorrectAnswerRate = ((double)item.CorrectAnswerCount / (double)item.StudyCount * 100).ToString();
                }
            }

            return View(viewModel);
        }
    }
}
```
```HistoriesViewModel.cs
namespace Flashcard.ViewModels
{
    public class HistoriesViewModel : CommonViewModel
    {

        // ユーザ名
        public string UserName { get; set; }

        // 学習日
        public string StudyDate { get; set; }

        // 正答数
        public string CorrectAnswerCount { get; set; }

        // 正答率
        public string CorrectAnswerRate { get; set; }

    }
}
```

![スクリーンショット (14-1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/a55dc739-3cb7-dbf1-a7e8-dc4718a83ee8.png)

#### 最後に
初歩的なところで詰まってしまいましたが、公式ドキュメントを確認する大切さと、少しずつコメントアウト等して確かめるのがよいかなと思いました。
今回はやらなかったJSONについても学びたいです。