放浪軍師のアプリ開発局

Xamarin.Formsを使ってAndroid,iOS,UWP,WPFで動くアプリを開発したりしています。他にもいろいろやってます。尚、このブログはわからないところを頑張って解決するブログであるため、正しい保証がありませんのでご注意ください。

Blazor WebAssembly で グラフを表示してみよう

こんにちは、放浪軍師です。最近ブログ更新頻度が高いね!やったぜ!

Blazor WebAssembly で グラフを表示したい

さて、現在目標としている燻製監視アプリですが、温度の推移をグラフで表示したいなぁと思っていまして、フロントで採用予定の Blazor WebAssembly でグラフを書く方法について調べてみましたので紹介したいと思います。

Blazorise と ChartJs.Blazor

Blazor WebAssembly でのグラフ表示は現在、Blazorise.Charts と ChartJs.Blazor の2種類が主流なようですが、このうち ChartJs.Blazor はちょっとごたごたしていてメンテナーが不在の状態が続いているようです。そのため、Blazorise.Charts を使用したいと思います。

Blazorise

Blazorise は Blazor 上に構築されたコンポーネントライブラリです。GitHub での活動も活発に行われている様子で安心ですね。

blazorise.com

今回使うグラフは Blazorise のうちの一部の機能という事になります。公式ページを見ながら早速やってみましょう。

blazorise.com

環境

Microsoft Visual Studio Community 2019 Version 16.8.2
Blazorise.Charts 0.9.3.6
Microsoft.AspNetCore.Components.WebAssembly 5.0.5

GitHub

作成したサンプルです。

github.com

注意点

Blazorise の最新版は現在 .NET5 でしか動きません。プロジェクトを作成する際には .NET5 を必ず選ぶようにしましょう。(1時間ハマる)

blazorise.com

NugetPackge をインストール

プロジェクトを作成したら以下をインストールします。

www.nuget.org

wwwroot/index.html

<!-- Blazorise.Charts -->以下の部分を追加します。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazoriseChartsTry</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazoriseChartsTry.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <!-- Blazorise.Charts -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
    <script src="_content/Blazorise.Charts/blazorise.charts.js"></script>
</body>

</html>

Program.cs

//Blazorise.Charts以下の部分を追加します。

using Blazorise;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace BlazoriseChartsTry {
    public class Program {
        public static async Task Main(string[] args) {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            //Blazorise.Charts
            builder.Services.AddBlazorise(options => {
                options.ChangeTextOnKeyPress = true;
            }).AddEmptyProviders();

            await builder.Build().RunAsync();
        }
    }
}

※ちなみに公式ページではこの部分は古い形で紹介されているので注意が必要です。(1時間ハマる) blazorise.com

_Imports.razor

@using Blazorise.Chartsを追加します。これにより他の .razor で using しなくても使えるようになるようですね。

Views/LineChartPage.razor

基本的に公式そのままなのですが一点。buttonに関しては公式の<Button Clicked="@(async () => await HandleRedraw())">Redraw</Button>は使えなかったので変更しています。理由は謎です。

@page "/LineChartPage"
<h3>LineChartPage</h3>

<button @onclick="@(async () => await HandleRedraw())">Redraw</button>

<LineChart @ref="lineChart" TItem="double" />

@code{
    LineChart<double> lineChart;

    protected override async Task OnAfterRenderAsync(bool firstRender) {
        if (firstRender) {
            await HandleRedraw();
        }
    }

    async Task HandleRedraw() {
        await lineChart.Clear();
        await lineChart.AddLabelsDatasetsAndUpdate(Labels, GetLineChartDataset());
    }

    LineChartDataset<double> GetLineChartDataset() {
        return new LineChartDataset<double> {
            Label = "# of randoms",
            Data = RandomizeData(),
            BackgroundColor = backgroundColors,
            BorderColor = borderColors,
            Fill = true,
            PointRadius = 2,
            BorderDash = new List<int> { }
        };
    }

    string[] Labels = { "Red", "Blue", "Yellow", "Green", "Purple", "Orange" };
    List<string> backgroundColors = new List<string> { ChartColor.FromRgba(255, 99, 132, 0.2f), ChartColor.FromRgba(54, 162, 235, 0.2f), ChartColor.FromRgba(255, 206, 86, 0.2f), ChartColor.FromRgba(75, 192, 192, 0.2f), ChartColor.FromRgba(153, 102, 255, 0.2f), ChartColor.FromRgba(255, 159, 64, 0.2f) };
    List<string> borderColors = new List<string> { ChartColor.FromRgba(255, 99, 132, 1f), ChartColor.FromRgba(54, 162, 235, 1f), ChartColor.FromRgba(255, 206, 86, 1f), ChartColor.FromRgba(75, 192, 192, 1f), ChartColor.FromRgba(153, 102, 255, 1f), ChartColor.FromRgba(255, 159, 64, 1f) };

    List<double> RandomizeData() {
        var r = new Random(DateTime.Now.Millisecond);

        return new List<double> { r.Next(3, 50) * r.NextDouble(), r.Next(3, 50) * r.NextDouble(), r.Next(3, 50) * r.NextDouble(), r.Next(3, 50) * r.NextDouble(), r.Next(3, 50) * r.NextDouble(), r.Next(3, 50) * r.NextDouble() };
    }
}

これによりこんな感じでグラフが書けます。

f:id:roamschemer:20210512001327g:plain:w350

うにょうにょ動いて結構いい感じなんですが、これX軸が等間隔なんですよね。

Views/ScatterChartPage.razor

X,Yを指定するグラフは以下のように書きます。

@page "/ScatterChartPage"
<h3>ScatterChartPage</h3>
<p>https://github.com/stsrki/Blazorise/discussions/2127</p>

<button @onclick="@(async () => await HandleRedraw(chart,GetChartDataset))">Redraw</button>
<Chart @ref="chart" TItem="Plot" OptionsObject="@options" />

@code {
    Chart<Plot> chart;

    public struct Plot {
        public Plot(double x, double y) {
            X = x;
            Y = y;
        }
        public double X { get; set; }
        public double Y { get; set; }
    }

    object options = new {
        Scales = new {
            XAxes = new[]
        {
                new
                {
                    Type = "linear",
                    Position = "bottom"
                }
            }
        }
    };

    string[] Labels = { "Red", "Blue", "Yellow", "Green", "Purple", "Orange" };
    List<string> backgroundColors = new List<string> { ChartColor.FromRgba(255, 99, 132, 0.2f), ChartColor.FromRgba(54, 162, 235, 0.2f), ChartColor.FromRgba(255, 206, 86, 0.2f), ChartColor.FromRgba(75, 192, 192, 0.2f), ChartColor.FromRgba(153, 102, 255, 0.2f), ChartColor.FromRgba(255, 159, 64, 0.2f) };
    List<string> borderColors = new List<string> { ChartColor.FromRgba(255, 99, 132, 1f), ChartColor.FromRgba(54, 162, 235, 1f), ChartColor.FromRgba(255, 206, 86, 1f), ChartColor.FromRgba(75, 192, 192, 1f), ChartColor.FromRgba(153, 102, 255, 1f), ChartColor.FromRgba(255, 159, 64, 1f) };

    bool isAlreadyInitialised;

    Random random = new Random(DateTime.Now.Millisecond);

    protected override async Task OnAfterRenderAsync(bool firstRender) {
        if (!isAlreadyInitialised) {
            isAlreadyInitialised = true;

            await Task.WhenAll(
                HandleRedraw(chart, GetChartDataset));
        }
    }

    async Task HandleRedraw<TDataSet, TItem, TOptions, TModel>(Blazorise.Charts.BaseChart<TDataSet, TItem, TOptions, TModel> chart, Func<TDataSet> getDataSet)
        where TDataSet : ChartDataset<TItem>
        where TOptions : ChartOptions
        where TModel : ChartModel {
        await chart.Clear();

        await chart.AddLabelsDatasetsAndUpdate(Labels, getDataSet());
    }

    async Task SetDataAndUpdate<TDataSet, TItem, TOptions, TModel>(Blazorise.Charts.BaseChart<TDataSet, TItem, TOptions, TModel> chart, Func<List<TItem>> items)
        where TDataSet : ChartDataset<TItem>
        where TOptions : ChartOptions
        where TModel : ChartModel {
        await chart.SetData(0, items());
        await chart.Update();
    }

    LineChartDataset<Plot> GetChartDataset() {
        return new LineChartDataset<Plot> {
            Type = "scatter",
            Label = "Scatter Dataset",
            Data = RandomizeData(),
            BackgroundColor = backgroundColors,
            BorderColor = borderColors,
            ShowLine = false
        };
    }

    List<Plot> RandomizeData() {
        return new List<Plot> {
            new Plot(random.Next( 3, 50 ) * random.Next(), random.Next( 3, 50 ) * random.Next()),
            new Plot(random.Next( 3, 50 ) * random.Next(), random.Next( 3, 50 ) * random.Next()),
            new Plot(random.Next( 3, 50 ) * random.Next(), random.Next( 3, 50 ) * random.Next()),
            new Plot(random.Next( 3, 50 ) * random.Next(), random.Next( 3, 50 ) * random.Next()),
            new Plot(random.Next( 3, 50 ) * random.Next(), random.Next( 3, 50 ) * random.Next()),
            new Plot(random.Next( 3, 50 ) * random.Next(), random.Next( 3, 50 ) * random.Next()),
        };
    }
}

XYプロット用の構造体Plotを作成してTItemで指定し、OptionsObject で描画条件を入れます。ちなみにこの方法は公式には載っていなくて、以下 discussions で紹介されています。
(実は今回わからない事があったので、つたない英語ですが質問していたりします。メンテナーさんがすぐに答えてくれました。Thank you so much!!!)

github.com

こんな感じのグラフになります。よいではないかー!!!

f:id:roamschemer:20210512001529g:plain:w350

まとめ

結構簡単にいい感じのグラフが描けるようです。しかも線グラフ以外にも円グラフやらなんやらも行ける様子。ガンガン使っていきましょう!