sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF OxyPlot の使いかたの基本まとめ

この記事は、C# その2 Advent Calendar 2018 の11日目の記事です。

OxyPlot とは

f:id:shikaku_sh:20181204152255p:plain:w400
OxyPlot

グラフを描画するためのライブラリです。
拡大や縮小(Pan)、範囲指定(Zoom)が標準で備わっているグラフを作れる、 WPF らしいデータバインディングを利用した記述ができるなどの特徴がある、数少ない無料のグラフライブラリでもあります。
NuGet でもそこそこの DL 数 (2018年12月現在で 369K) を誇る一方で、リファレンスは 2015 年ごろまでしか更新がありません。

OxyPlot's documentation は URL が変更になっていました。GitHub を参照したほうがよいかも。

動的な処理は可能ですが、そこまで得意ではないです。そっちを期待するなら LiveCharts になると思います。
ともかく、ここ数年は利用していましたので、ポイントを紹介。

バージョンと作成するタイプ

今回の作成に利用したのは以下のバージョンです。

  • OxyPlot.Wpf - v1.0.0 (安定版)
  • OxyPlot.Core - v1.0.0

OxyPlot は WPF だとグラフの作り方が2通りある気がします。これが実に面倒です。
とても似たような書き方をしているので、サンプルを探しているときに混乱する可能性が極めて高い。実は、使用しているクラスが名前空間で別々だったり、ちょっと名前が違ったりしています。

  1. OxyPlot.Wpf.PlotView タイプの作り方
  2. OxyPlot.Wpf.Plot タイプの作り方

基本的におすすめは(私のおすすめ的な意味)「1」の作り方です。
公式の情報が「1」で書かれているもののほうが多いことと、おそらく「2」だと用意されていないコントロールがあると思います。(例は FunctionSeries が存在しない?)
ただし、一方的に駄目かというとそうでもなくて、データ追加したときに自動的にグラフを更新するのは「2」だったりと取捨選択のように思います。

とりあえずグラフを表示してみる(1のやりかた)

NuGet から OxyPlot.Wpf を追加してから MainWindow.xaml を次のように編集します。
「1」の PlotView のやりかたは「CS ファイルのコーディング主体」です。 C# のコード部の行数が増える印象です。

<Window x:Class="OxyPlotSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OxyPlotSample"
        xmlns:oxy="http://oxyplot.org/wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <oxy:PlotView DataContext="{Binding Sample1}" Model="{Binding Model}" Controller="{Binding Controller}" />
    </Grid>
</Window>

つぎにコード部を書いてみます。

// MainWindow
public OxyPlotView_Model Sample1 { get; } = new OxyPlotView_Model();

public MainWindow()
{
    InitializeComponent();

    this.DataContext = this;

    Sample1.Init();
}
// OxyPlotView_Model のクラス

public PlotModel Model { get; } = new PlotModel();
public PlotController Controller { get; } = new PlotController();

public OxyPlot.Axes.TimeSpanAxis X { get; } = new OxyPlot.Axes.TimeSpanAxis();
public OxyPlot.Axes.LinearAxis Y { get; } = new OxyPlot.Axes.LinearAxis();
public OxyPlot.Series.LineSeries LineSeries { get; private set; }
public OxyPlot.Series.FunctionSeries FunctionSeries { get; private set; }

public ObservableCollection<TestData> Samples { get; private set; }

public void Init()
{
    Samples = new ObservableCollection<TestData>
    {
        new TestData{ Time= new TimeSpan(0,0,0), Value=0, Tag="A" },
        new TestData{ Time= new TimeSpan(0,0,1), Value=2, Tag="B" },
        new TestData{ Time= new TimeSpan(0,0,2), Value=4, Tag="C" },
        new TestData{ Time= new TimeSpan(0,0,3), Value=6, Tag="D" },
        new TestData{ Time= new TimeSpan(0,0,4), Value=0, Tag="E" },
        new TestData{ Time= new TimeSpan(0,0,5), Value=2, Tag="F" },
    };

    Model.Title = "PlotView";

    // 軸の初期化
    X.Position = OxyPlot.Axes.AxisPosition.Bottom;
    Y.Position = OxyPlot.Axes.AxisPosition.Left;

    // 線グラフ
    LineSeries = new OxyPlot.Series.LineSeries();
    LineSeries.Title = "Custom";
    LineSeries.ItemsSource = Samples;
    LineSeries.DataFieldX = nameof(TestData.Time);
    LineSeries.DataFieldY = nameof(TestData.Value);

    var a = 1;
    var b = 2;

    // 関数グラフ
    FunctionSeries = new OxyPlot.Series.FunctionSeries
    (
        x => a * x + b, 0, 30, 5, "Y = ax + b"
    );

    Model.Axes.Add(X);
    Model.Axes.Add(Y);
    Model.Series.Add(LineSeries);
    Model.Series.Add(FunctionSeries);

    Model.InvalidatePlot(true);
}

public class TestData
{
    public TimeSpan Time { get; set; }
    public double Value { get; set; }
    public string Tag { get; set; }
}

Init() のメソッドのポイントは Model.InvalidatePlot(true); の実行です。
「1」の PlotView タイプはこれを呼び出さないとグラフの更新(再描画)がありません。なにか操作をしたら、このメソッドを呼び出すようにしましょう。とても重要。

もうひとつのポイントは LineSeries の初期化です。ネット上のサンプルだとデータの追加に LineSeries.Points.Add(); を利用するものもありますが、これだとデータの型が OxyPlot.DataPoint に固定されてしまいます。不便なので、特に利用がなければ ItemsSource を利用したほうが便利だと思います。

f:id:shikaku_sh:20181204152435p:plain:w400

はい。こんな感じで表示されるはずです。

とりあえずグラフを表示してみる(2のやりかた)

NuGet から OxyPlot.Wpf を追加してから MainWindow.xaml を次のように編集します。「2」のやり方は xaml の中である程度グラフを設定している例が多いと思います。
ただし、追加できる Series のタイプが「1」のやりかたよりも少ない気がします。

<Window x:Class="OxyPlotSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OxyPlotSample"
        xmlns:oxy="http://oxyplot.org/wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <oxy:Plot x:Name="plot" DataContext="{Binding Sample2}">
            <oxy:Plot.Axes>
                <oxy:TimeSpanAxis x:Name="x" Position="Bottom" />
                <oxy:LinearAxis x:Name="y" Position="Left" />
            </oxy:Plot.Axes>
            <oxy:LineSeries x:Name="series" ItemsSource="{Binding Samples}"  
                            Title="Custom"
                            DataFieldX="Time" DataFieldY="Value" />
        </oxy:Plot>
    </Grid>
</Window>

ポイントは Model.InvalidatePlot(true); の実行が不要なので、これだけでも(データはないけど)グラフを表示することが可能です。グラフにデータ(直線)を追加してみます。

// MainWindow
public OxyPlot_Model Sample2 { get; } = new OxyPlot_Model();

public MainWindow()
{
    InitializeComponent();

    this.DataContext = this;
    Sample2.Init(plot, x, y, series);
}
// OxyPlot_Model のクラス
public void Init(OxyPlot.Wpf.Plot plot, OxyPlot.Wpf.TimeSpanAxis x, OxyPlot.Wpf.LinearAxis y, OxyPlot.Wpf.LineSeries series)
{
    Plot = plot;
    X = x;
    Y = y;
    LineSeries = series;

    Samples = new ObservableCollection<TestData>
    {
        new TestData{ Time= new TimeSpan(0,0,0), Value=0, Tag="A" },
        new TestData{ Time= new TimeSpan(0,0,1), Value=2, Tag="B" },
        new TestData{ Time= new TimeSpan(0,0,2), Value=4, Tag="C" },
        new TestData{ Time= new TimeSpan(0,0,3), Value=6, Tag="D" },
        new TestData{ Time= new TimeSpan(0,0,4), Value=0, Tag="E" },
        new TestData{ Time= new TimeSpan(0,0,5), Value=2, Tag="F" },
    };

    Plot.Title = "WPF Plot";
}

はい。これで表示できました。

f:id:shikaku_sh:20181204152606p:plain:w400

データの変更を通知するコレクション ObservableCollection に対応しているので、あとは Samples にデータを Add() してやるだけで直線を伸ばすことが可能です。

数値解析のようなリアルタイムに直線のデータを伸ばすタイプのグラフは、こっちのほうが簡単かもしれません。

リアルタイムに Series データの追加

直線グラフは、リアルタイムにデータを追加していくことが可能です。
これはおそらく「2」のやり方のほうが得意にしていると思います。それぞれのやり方は次のとおりです。
見たままで、「1」のやり方は必ず Model.InvalidatePlot(true); の呼び出しがつきまといます。「2」は追加するだけでグラフを更新することが可能です。

// 1 PlotView
public void AddItem()
{
    var i = Samples.Count();
    Samples.Add(new TestData { Time = new TimeSpan(0, 0, i), Value = i, Tag = $"ADD:{i}" });

    Model.InvalidatePlot(true);
}

// 2 Plot
public void AddItem()
{
    var i = Samples.Count();
    Samples.Add(new TestData { Time = new TimeSpan(0, 0, i), Value = i, Tag = $"ADD:{i}" });
}

Series (線グラフ) のカスタマイズ

直線グラフは表示できましたが、直線をマウスでクリックすると直線の位置情報が表示されます。

f:id:shikaku_sh:20181204152727p:plain:w400
半端な位置の選択

半端な点と点の間の値も表示してしまうので、これが嫌なら CanTrackerInterpolatePoints を false に設定してあげます。これは「1」「2」で共通です。よく使いそうな設定はこのあたりとか。

プロパティ名 内容
CanTrackerInterpolatePoints 点と点の間の値を表示しないようにする
Smooth 点と点のつなぎ方を曲線にする
StrokeThickness 線の太さ
Color 線の色 (「1」は OxyPlot.OxyColor を利用。「2」は System.Windows.Media.Color を利用。)
TrackerFormatString トラッカーの情報をカスタマイズする

トラッカーの情報変更は、もう少し補足します。
位置情報 (X, Y) 以外にも表示したい情報があるときはこんな感じでカスタマイズします。これだと Tag というプロパティ名の値を表示してくれます。

public void InitSeries()
{
    LineSeries.TrackerFormatString = "{Tag}\n{1} : {2:mm\\:ss\\.f}\n{3} : {4:0.000}";
}

f:id:shikaku_sh:20181204152909p:plain:w400

グラフのマウス操作のカスタマイズ

グラフの操作は、

  • 左クリックでラインを選択して詳細表示ができる
  • 右クリックでグラフを動かすことができる
  • 中クリックで指定範囲選択ができる
  • あとキー操作もいくつか対応

これを変更するには PlotController を設定してあげます。「1」と「2」で設定方法が少しだけ異なります。

// 1 PlotView タイプの初期化
public void InitController()
{
    // グラフのマウス操作およびキー操作の初期化
    Controller.UnbindKeyDown(OxyKey.A);
    Controller.UnbindKeyDown(OxyKey.C, OxyModifierKeys.Control);
    Controller.UnbindKeyDown(OxyKey.C, OxyModifierKeys.Control | OxyModifierKeys.Alt);
    Controller.UnbindKeyDown(OxyKey.R, OxyModifierKeys.Control | OxyModifierKeys.Alt);
    Controller.UnbindTouchDown();

    Controller.UnbindMouseDown(OxyMouseButton.Left);
    Controller.UnbindMouseDown(OxyMouseButton.Middle);
    Controller.UnbindMouseDown(OxyMouseButton.Right);

    Controller.BindMouseDown(OxyMouseButton.Left, PlotCommands.PanAt);
    Controller.BindMouseDown(OxyMouseButton.Middle, PlotCommands.PointsOnlyTrack);
    Controller.BindMouseDown(OxyMouseButton.Right, PlotCommands.ZoomRectangle);
}

// 2 Plot タイプの初期化
public void InitController()
{
    // グラフのマウス操作およびキー操作の初期化
    var controller = Plot.ActualController;

    controller.UnbindKeyDown(OxyKey.A);
    // 以下同じ
}

縦軸と横軸の設定

これは「1」も「2」もほぼ同じように設定することが可能です。
注意は、レンジの指定(MinimumRange, MaximumRange)をしておくことを推奨します。大量のデータを表示するグラフに対して、極端な拡大縮小をしたときにメモリーオーバーフローのエラーを出すことがあります。

public void InitAxisX()
{
    X.Position = AxisPosition.Bottom;
    X.MajorGridlineColor = OxyColor.FromArgb(0xCC, 0xE3, 0xE3, 0xE3);
    // X.MajorGridlineColor = System.Windows.Media.Color.FromArgb(0xCC, 0xE3, 0xE3, 0xE3);
    X.MajorGridlineStyle = LineStyle.Solid;
    X.MajorGridlineThickness = 1;
    X.MinorGridlineColor = OxyColor.FromArgb(0x99, 0xE3, 0xE3, 0xE3);
    X.MinorGridlineStyle = LineStyle.Dot;
    X.MinorGridlineThickness = 1;

    var min = 0;
    var absMin = -10;
    var max = 100;
    var absMax = 120;

    // 表示領域
    X.Minimum = min;
    X.AbsoluteMinimum = absMin;
    X.Maximum = max;
    X.AbsoluteMaximum = absMax;

    var minRange = 10;
    var maxRange = 200;

    // 表示サイズの最大最小
    X.MinimumRange = minRange;
    X.MaximumRange = maxRange;

    Model.InvalidatePlot(true);
}

画像にして保存

OxyPlot は BMP 形式の画像を出力するメソッドを用意してくれています。
話が早いですね。

// 1 PlotView
public void SaveImage()
{
    using (var stream = new FileStream("output_1.png", FileMode.Create))
    {
        var width = 800;
        var height = 600;

        OxyPlot.Wpf.PngExporter.Export(Model, stream, width, height, OxyColor.FromArgb(0xff, 0xff, 0xff, 0xff));
    }
}

// 2 Plot
public void SaveImage()
{
    var source = Plot.ToBitmap();
    // plot.SaveBitmap("output.png"); // BMP形式

    using (var stream = new FileStream("output_2.png", FileMode.Create))
    {
        var encoder = new PngBitmapEncoder(); // その他の形式

        encoder.Frames.Add(BitmapFrame.Create(source));
        encoder.Save(stream);
    }
}

Y 軸のテキストが少し切れてしまうことがあるようです。手っ取り早い対応方法は、プロットに余白を設定して、余裕を与えることです。
これも「1」と「2」で対応が異なり、「1」だと Margin を追加して「2」だと Padding を追加してあげてください。

<oxy:PlotView Margin="12" />
<oxy:Plot Padding="20" />

これでキレイに画像を出力できました。

サンプルのプログラム

f:id:shikaku_sh:20181204153224p:plain
PlotView と Plot の違いをみるサンプル

「1」の PlotView と「2」の Plot の違いと、以下の操作ができるサンプルを公開しておきます。

  • データの途中追加
  • Series の途中変更
  • Axis の途中変更
  • Controller の途中変更
  • 画像の保存

「OxyPlotSample」というサンプルをあげています。