放浪軍師のXamarin.Formsアプリ開発局

Xamarin.Forms+Prism+ReactivePropertyで素人がAndroidやUWPのアプリを右往左往しながら開発している様を発信していきます。性質上間違いも多いのでご注意ください。

グラフを描けるライブラリ OXYPlot を学ぶ

放浪軍師の Xamarin.Forms によるアプリ開発
今回は OXYPlot を学んでみました。
まず、初めて訪問された方は以下をお読みください。
www.gunshi.info

グラフの描画

ちょっと間が空きましたが、以前業務上で展示会に出品した際に使用したグラフを簡単に描ける OXYPlot というグラフ描画ライブラリを紹介しようと思います。グラフというのは様々な場面で使用する事があるので是非とも身に付けたいですね。今回は折れ線グラフと円グラフを簡単に紹介しますが他にも種類があるみたいです。今回は、以下のようなサンプルを用意してみました。

Android
f:id:roamschemer:20181120004536g:plain:h400

UWP(Windows 10)
f:id:roamschemer:20181120004626g:plain:h400

左側に線グラフ。右側に円グラフを表示するようにしています。ボタンを押すとランダムでデータが投げ込まれグラフが更新されます。
線グラフの場合最低2点無いと線を引かないので注意してください。

参考ページ

例のごとく他のページを色々と参考にさせて頂いております。まず、以下くりごはんさんの記事とその中で公開されている GitHub サンプルを見てもらうのが判りやすいと思います。
www.kurigohan.com
他にもググると結構ページが出てきます。 Xamarin だけじゃなく WPF でも比較的よく使われているみたいですね。この記事内にも描かれていますが注意点として、プレリリース版のv1.1.0-unstable0011を選択しないとUWPで動きませんのでNugetパッケージを適用する際は注意する事と、各プラットフォームで Xamarin.Forms.Forms.Init() を検索してその下に PlotViewRenderer.Init(); を記述する事ですね。大体2時間ぐらいハマります。先日のJXUGもくもく会で、制限時間の全てを犠牲にしたのは秘密

グラフを更新するのに苦戦

単純にグラフ一発表示であれば上記で問題ないのですが、展示会ではリアルタイムで取得する値を動的にグラフ化する必要があって、そこでちょっと苦戦しました。この際参考になったのは@YSRKENさんの以下の記事です。いつもお世話になっております!
qiita.com
ハマったのはグラフの点を入力した後に InvalidatePlot(true) でグラフを描画できるのですが、二度目には必ず例外を吐くというところでした。これに関しては更新の度に使い捨ての PlotModel を作成し、InvalidatePlot(true)したそれを本命の PlotModel に代入する…という形で回避しています。ちょっと言葉で説明すると解りにくいですね。実際には以下コードやGitHubを参考にしてみてください。尚、毎回全打点を再描画する事にはなりますが、かなり軽いようで毎秒10点×数時間とか廻しても問題ありませんでした。勿論状況次第では描画する回数を減らすとか工夫が必要だと思います。

円グラフのプロパティ

データを投げ込むと自動で割合を算出してくれたりしてかなり高性能な印象です。円グラフの細かいプロパティに関しては@kuro4さんの以下記事が参考になると思います。各項目の色は、指定しなければ自動で色分けしてくれるみたいです。便利!
qiita.com

サンプルコード

いつものように Prism + ReactiveProperty を使用して MVVM で書いています。…多分。 MVVM まだ若干自信ない

LinePlotModel

線グラフ用のModelです。用途もだいぶ違うと思われるので、このように分けた方が扱いやすいと思います。

using OxyPlot;
using OxyPlot.Series;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;

namespace OXYPlotTest.Models
{
    /// <summary>
    /// OXYPlotを使った線チャート
    /// </summary>
    public class LinePlotModel : BindableBase
    {
        /// <summary>
        /// PlotModelのプロパティ
        /// </summary>
        public PlotModel PlotModel
        {
            get => plotModel;
            set => SetProperty(ref plotModel, value);
        }
        private PlotModel plotModel;

        /// <summary>
        /// 線の座標
        /// </summary>
        public ObservableCollection<DataPoint> dataPointList { get; private set; } = new ObservableCollection<DataPoint>();

        /// <summary>
        /// グラフのタイトル
        /// </summary>
        private string title;
        
        /// <summary>
        /// グラフの色
        /// </summary>
        private OxyColor lineColor;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="title">グラフタイトル</param>
        /// <param name="lineColor">グラフの色</param>
        public LinePlotModel(string title, OxyColor lineColor)
        {
            this.title = title;
            this.lineColor = lineColor;
            PlotModel = new PlotModel();
        }

        /// <summary>
        /// ランダムに数字を与える(実験用)
        /// </summary>
        public void RandomPush()
        {
            var seed = Environment.TickCount;
            var rnd = new System.Random(seed);
            Push(rnd.Next(0, 10000), rnd.Next(0, 10000));
        }

        /// <summary>
        /// 指定した数字を与える
        /// </summary>
        public void Push(double x,double y)
        {
            dataPointList.Add(new DataPoint(x, y));
        }

        /// <summary>
        /// グラフを更新
        /// </summary>
        public void Renewal()
        {
            var model = new PlotModel { Title = this.title };
            var lineSeries = new LineSeries() { Color = lineColor };
            foreach (var dp in dataPointList)
            {
                lineSeries.Points.Add(dp);
            }
            model.Series.Add(lineSeries);
            model.InvalidatePlot(true);
            PlotModel = model; //グラフ情報をガッツリ代入
        }

    }
}

Renewal() メソッドが注意点で説明した部分になります。ローカルでグラフを作成して、それを本命に代入しています。

PiePlotModel

こちらは円グラフ用Modelです。注意点は先ほどと同じですね。今回はサンプルの為各種プロパティは固定しています。

using OxyPlot;
using OxyPlot.Series;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;

namespace OXYPlotTest.Models
{
    public class PiePlotModel : BindableBase
    {
        /// <summary>
        /// PlotModelのプロパティ
        /// </summary>
        public PlotModel PlotModel
        {
            get => plotModel;
            set => SetProperty(ref plotModel, value);
        }
        private PlotModel plotModel;

        /// <summary>
        /// 線の座標
        /// </summary>
        public ObservableCollection<PieSlice> PieSeriesList { get; private set; } = new ObservableCollection<PieSlice>();

        /// <summary>
        /// グラフのタイトル
        /// </summary>
        private string title;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="title">グラフタイトル</param>
        public PiePlotModel(string title)
        {
            this.title = title;
            PlotModel = new PlotModel();
        }

        /// <summary>
        /// ランダムに数字を与える(実験用)
        /// </summary>
        public void RandomPush()
        {
            var seed = Environment.TickCount;
            var rnd = new System.Random(seed);
            Push(rnd.Next(0, 100), rnd.Next(0, 10000));
        }

        /// <summary>
        /// 指定した数字を与える
        /// </summary>
        public void Push(double x, double y)
        {
            PieSeriesList.Add(new PieSlice("No" + x.ToString(), y));
        }

        /// <summary>
        /// グラフを更新
        /// </summary>
        public void Renewal()
        {
            var model = new PlotModel { Title = this.title };
            var pieSeries = new PieSeries() { StrokeThickness = 0.8, InsideLabelPosition = 0.8, AngleSpan = 360, StartAngle = 270 };
            foreach (var dp in PieSeriesList)
            {
                pieSeries.Slices.Add(dp);
            }
            model.Series.Add(pieSeries);
            model.InvalidatePlot(true);
            PlotModel = model; //グラフ情報をガッツリ代入
        }

    }
}
ViewModel

特に注意点は無いと思います。いつものように ReactiveProperty を使えば楽ちんですね。

using Prism.Navigation;
using Reactive.Bindings;
using OxyPlot;
using OXYPlotTest.Models;
using Reactive.Bindings.Extensions;
using System;

namespace OXYPlotTest.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        //折れ線グラフ
        public ReactiveProperty<PlotModel> LineChart { get; private set; }
        public ReactiveCommand LinePushTapped { get; } = new ReactiveCommand();
        public LinePlotModel linePlotModel = new LinePlotModel("lineChert", OxyColors.Red);

        //円グラフ
        public ReactiveProperty<PlotModel> PieChart { get; private set; }
        public ReactiveCommand PiePushTapped { get; } = new ReactiveCommand();
        public PiePlotModel piePlotModel = new PiePlotModel("pieChert");

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            //折れ線グラフ
            //VM←M
            LineChart = linePlotModel.ObserveProperty(x => x.PlotModel).ToReactiveProperty();
            //Button
            LinePushTapped.Subscribe(_ =>
            {
                linePlotModel.RandomPush();
                linePlotModel.Renewal();
            });

            //円グラフ
            //VM←M
            PieChart = piePlotModel.ObserveProperty(x => x.PlotModel).ToReactiveProperty();
            //Button
            PiePushTapped.Subscribe(_ =>
            {
                piePlotModel.RandomPush();
                piePlotModel.Renewal();
            });

        }

    }
}
View

前述のくりごはんさんの記事にも書いてありますが、意外と厄介なので注意が必要です。なんで表示されないんだ!?って事が結構ありましたね。Gridで以下のように書くとうまくいきますが、他の場合はちょっと苦戦するかもしれません。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:oxy="clr-namespace:OxyPlot.Xamarin.Forms;assembly=OxyPlot.Xamarin.Forms"
             x:Class="OXYPlotTest.Views.MainPage"
             Title="OXYPlotTest">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <oxy:PlotView Grid.Column="0" Grid.Row="0" 
                      Model="{Binding LineChart.Value}" 
                      HorizontalOptions="FillAndExpand"
                      VerticalOptions="FillAndExpand" />
        <Button Grid.Column="0" Grid.Row="1" 
                Text="Push"
                Command="{Binding LinePushTapped}"/>
        <oxy:PlotView Grid.Column="1" Grid.Row="0"
                      Model="{Binding PieChart.Value}" 
                      HorizontalOptions="FillAndExpand"
                      VerticalOptions="FillAndExpand" />
        <Button Grid.Column="1" Grid.Row="1"
                Text="Push"
                Command="{Binding PiePushTapped}"/>
    </Grid>

</ContentPage>
GitHub

何かのお役に立てれば幸い。少しずつ増えてきましたね~。
github.com

まとめ

折れ線グラフは主に業務で使用する事が多そうです。一方の円グラフですが、実はこれを使って乱ちゃんにルーレット機能を追加できないか模索中。多分できると思っていますが果たして…?