OxyPlotの使い方 ScatterPoint & PolarChart

WPFでChartグラフを表示するためのライブラリーに OxyPlot があります。OxyPlot には 散布図用に ScatterPoint があり、点ごとにサイズと色が指定できます。要素数制限付きの ObservableCollection と合わせて、リアルタイム表示用に残像的なイメージのアニメーションサンプルを作ってみました。

OxyPlot の基本はこれを参考にしてください。

feynman.hatenablog.com

OxyPlot アプリケーションの基本構成

Chart は Blend もしくは VisualStudio の XAMLデザイナーで作成して、ViewModel の座標データとバインドする事により、point を追加すると Chart の更新が自動で行われる様にしました。

f:id:feynman911:20191004160839j:plain

基本的な構成は次の様になっています。

f:id:feynman911:20191007102109j:plain

Model

  • 素数制限付きの ScatterPointCollection クラスを定義

ViewModel

  • ScatterPointCollection ScatterPoints を作成
  • DispatcherTimer で一定時間ごとに ScatterPoint を追加する為の設定
  • DispatcherTimer の起動/停止用の StartFlag プロパティーを設定

View

  • View にPlotを貼り付ける
    Plot の PlotAreaBorderThickness を 0 にして枠線を消す
    Plot の PlotType を Polar にする

  • Plot の Axes に 軸を追加
    MagnitudeAxis:PolarChartの中心からの距離軸
    AngleAxis:PolarChartの角度軸
    LinearColorAxis:ScatterPoint の Value の値に応じて色を付ける為の軸

  • Plot の Series に ScatterSeries を追加
    ScatterSeries の ItemSource に ViewModel の ScatterPoints をバインド

  • View に Checkbox を貼り付けて、ViewModel の StartFlag とバインド

素数制限付きの ScatterPointCollection

ObservableCollection を継承して ScatterPoint 用の ScatterPointCollection を作成します。

ScatterPointCollection に
素数の上限用のプロパティ Limit と Size
上限と下限用に SizeMax, SizeMin,
Value の上限と下限用に ValueMax, ValueMin
を追加します。

Size と Value は、要素の順番で変化させます。

public class ScatterPointCollection : ObservableCollection<ScatterPoint>
{
    protected bool SetProperty<T>(ref T field, T value, 
          [CallerMemberName]string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        return true;
    }

    protected virtual void Shrink(int index)
    {
        while (Limit < Count) RemoveAt(0);
        for(int i = 0;i < Count; i++)
        {
            var item = Items[i];
            item.Size = (SizeMax - SizeMin) * (i + 1) / Count + SizeMin;
            item.Value = (ValueMax-ValueMin)*(i+1)/(double)Count+ValueMin;
            Items[i] = item;
        }
    }

    private double sizeMax = 1.0;
    public double SizeMax
    {
        get { return sizeMax; }
        set { SetProperty(ref sizeMax, value); }
    }

    private double sizeMin = 1.0;
    public double SizeMin
    {
        get { return sizeMin; }
        set { SetProperty(ref sizeMin, value); }
    }

    private double valueMax = 1.0;
    public double ValueMax
    {
        get { return valueMax; }
        set { SetProperty(ref valueMax, value); }
    }

    private double valueMin = 0.0;
    public double ValueMin
    {
        get { return valueMin; }
        set { SetProperty(ref valueMin, value); }
    }

    private int limit = 50;
    public int Limit
    {
        get { return limit; }
        set
        {
            if (value < 1) value = 1;
            if (SetProperty(ref limit, value))
            {
                Shrink(Count);
            }
        }
    }

    protected override void InsertItem(int index, ScatterPoint item)
    {
        base.InsertItem(index, item);
        Shrink(index);
    }
}

ScatterPointCollection と DispatcherTimer の追加

ViewModel に ScatterPoints を追加します。

ScatterPoints は ObservableCollection を継承しているので、UIスレッド以外からは操作できません。

なので、Timer は DispatcherTimer を使用してUIスレッドからPointを追加するようにします。

public MainWindowViewModel()
{
    ScatterPoints.Limit = 50;
    ScatterPoints.SizeMax = 5.0;
    ScatterPoints.SizeMin = 1.0;
    timer.Interval = TimeSpan.FromMilliseconds(25) ;
    timer.Tick += new EventHandler(MovingPoint);
}

private DispatcherTimer timer = new DispatcherTimer();

private ScatterPointCollection scatterPoints = new ScatterPointCollection();
public ScatterPointCollection ScatterPoints
{
    get { return scatterPoints; }
    set { SetProperty(ref scatterPoints, value); }
}

private double dA1 = 10.0;
private double dA2 = 0.7 / 180.0 * Math.PI;
private double ang1 = 0.0;
private double ang2 = 0.0;

/// <summary>
/// DispatcherTimerで実行するポイント追加メソッド
/// UIスレッドで実行される
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MovingPoint(object sender, EventArgs e)
{
    double mag = 50.0 + 40.0 * Math.Sin(ang2);
    double ang = ang1;
    var p = new ScatterPoint(mag, ang);
    ScatterPoints.Add(p);
    ang1 += dA1;
    ang2 += dA2;
}

StartFlag で DispatcherTimer の On/Off を切り替えます。

DispatcherTimer が起動すると メソッド MovingPoint によって ScatterPoint が追加されます。

private bool startFlag = false;
/// <summary>
/// DispatcherTimerのStart/Stop
/// </summary>
public bool StartFlag
{
    get { return startFlag; }
    set {
        if (value == true) timer.Start();
        else timer.Stop();
        SetProperty(ref startFlag, value);
    }
}

View の作成

Plot を貼り付け PlotType を Polar にします。
PlotAreaBorderThickness を 0 にして枠線を消しておきます。

次に OxyPlot の プロパティの Axes をクリックして軸を追加します。
OxyPlot の軸には次のようなものがあります。

f:id:feynman911:20191006213921j:plain

この中から MagnitudeAxis と AngleAxis と LinearColorAxis を追加します。

f:id:feynman911:20191006214317j:plain

それぞれの軸のMaximum, Minimum、Gridの設定を行います。

LinerColorAxis の設定

LinerColorAxis は ScatterPoint の Value の値に応じて色を付けます。

色は LinearColorAxis の GradientStops の設定で行います。
プロパティの GradientStops をクリックし GradientStop を追加します。
GradientStop は 0-100%の間で3点以上必要みたいです。
色の階調は PalletSize で指定します。

f:id:feynman911:20191006231415j:plain

ScatterSeries の設定

Plot の Series に ScatterSeries を追加し、ItemSource に ViewModel の ScatterPoints をバインドします。MarkerType を Circle にします。

f:id:feynman911:20191007001649j:plain

f:id:feynman911:20191007001705j:plain

あとは、View に Checkbox を貼り付けて、ViewModel の StartFlag で DispacherTimer の起動と停止ができるようにすると、サンプルアプリの完成です。

Plot v.s. PlotView & PlotModel

OxyPlot を使用する場合、View に Plot を貼りつけて XAML で Chart を作成して ViewModel のプロパティとバインドする方法と、View に PlotView を貼りつけて表示用の器だけ用意し、ViewModel にPlotModel を作り、コードで Chart を作成する方法の2通りがありますが、その特徴をまとめておきます。

【ViewにPlotを貼りつける場合】

  1. Chart 本体は View 側の Plot
  2. 軸、表示する線の定義を XAML で行う
    XAMLエディターのプロパティーを修正しながらリアルタイムで Chart が確認できるので初心者向き。動的に線を増やしたりできないので、予め多めに作って IsVisible で隠したりする必要がある。
  3. ViewModel の点座標用のプロパティーを Plot の Series にバインドする
  4. Chartの更新はプロパティーの変更通知でできる ObservableCollection を使用すると、点を追加するたびに自動更新される

【View に PlotView を貼りつける場合】

  1. Chart 本体は ViewModel 側の PlotModel
  2. 軸とか表示する線の定義は ViewModel のコードで行う。 慣れている人はコードの方が XAML より簡単にかけるが、確認のたびにビルドしないといけない。コードで記述するので、動的に線を増やしたりできる。
  3. ViewModel の plotModel を PlotView にバインドする
  4. Chartの更新は PlotModel の InvalidatePlot(true) で行う

作成したソースコードの場所

ソースコードは次の場所に置いてあります。

github.com

*参考 - View から ViewModel にBlendのダイアログで選択してバインドするためには、View のデザイン時の DataContext にViewModel が設定されていることが必要です。 (メニューの [形式]-[デザイン時のDataContextの設定] から設定できます)