C#
WPF
OxyPlot

【更新あり】OxyPlotでグラフを表示させる際にハマったことまとめ

概要

 WinFormsでは、グラフ表示と言えばChartコントロールでした。
 WPFでもWindowsFormsHostを使えば表示できますが、データビハインドでゴリゴリしなきゃならない点が美しくありませんよね?
  C#:Chartコントロールを使う
  https://qiita.com/nocd5/items/064a783240c1e8590169
 そこで、WPFその他に使えるグラフ描画ライブラリOxyPlotを使おう……と思ったのですが、その際に試行錯誤した記憶をメモしておきます。

表示させたいデータについて

 ここで表示させるデータは、「時刻―数値」のペアが並んだ時系列データです。C#風に書くとこんな感じ。

// 時系列データを表す型をusing宣言
using TimeSeriesData = Dictionary<DateTime, int>;
// 更にそれを種類ごとに記録するためのDictionary
// 例:
// supplyData["燃料"][DateTime.Parse("2017/11/22 10:20:30")] = 12345
// supplyData["資金"][DateTime.Parse("2017/11/23 11:21:31")] = 23456
Dictionary<string, TimeSeriesData> supplyData;

 supplyDataには「種類―時系列データ」のペアが含まれていますが、種類数は動的に決まります。折れ線グラフで言えば、「何本折れ線が入るのかは実行時まで分からない」といったイメージです。

最初の壁:資料が少ない!

解説Webサイトが少ない

 まあとりあえずググるよね、と思って見つけたサイトはこんな感じです。
  OxyPlotでグラフを表示する(WPF) - SDD(Sleep-Driven Development)
  グラフ描画ライブラリ「OxyPlot」の導入方法 - ぴよぴよエンジニアの日記
  OxyPlot を使い WPF アプリケーションでグラフを表示する。 - 自分の歩いた道に落ちてるコード
 一見豊富に見えますが、どれも「導入法とチュートリアル」といった感じで、後述する壁(折れ線本数の動的制御)に答えるものではありませんでした。というわけで次に調べるのは公式サイトのレファレンスなのですが、

公式レファレンスサイトがガバガバ

  Welcome to OxyPlot’s documentation! — OxyPlot 2015.1 documentation
 今2017年ですよね? 2015年じゃないですよね?
 ……というのは「(いい意味で)枯れたライブラリなんだろう」ということでスルーしていましたが、問題はAPIやプロパティ一覧的なものが全然ないということです。例えば「X軸やY軸に設定するプロパティについての詳細」を知りたい際は
  Axes — OxyPlot 2015.1 documentation
を読むことになりますが、このページにおける「Position」や「Minimum/Maximum」などの詳細がキッチリと書かれていないんですよね。例えばChartコントロールのAxisに関するレファレンス

image.png

こんな感じで分かりやすいことを考えると……。また、MajorStepプロパティやIntervalLengthプロパティなどは左上の検索欄に入力して検索しても出てこないという体たらく。こ れ は ひ ど い

image.png

公式レファレンスPDFの所在地

 というわけでWebサイトがさっぱり役に立たないのがOxyPlotなのですが、ググってすぐ見つけたマニュアルPDFはまだ役に立ちました。
  OxyPlot Documentation Release 2015.1
 ただこのリンク先のサイトはどういったものなのかがイマイチよく分からず……トップページによると「ドキュメントの類を配布するためのサイト」らしいのですが、そもそも自前のサイトあるんだからそっちに書けよOxyPlot

次の壁:折れ線の本数をXAML上で動的に制御できない!

 前述のように、今回表示したいグラフの線は可変です。これが固定でしたら、次のコードのようにXAML全開で分かりやすくなったところでした(前述の「チュートリアル」サイトはみんなこんな感じ)

<oxy:Plot Title="グラフのタイトル">
    <!-- グラフにプロットするブツ(LineSeriesなら折れ線グラフ、BarSeriesなら棒グラフなど) -->
    <oxy:Plot.Series>
        <oxy:LineSeries ItemsSource="{Binding DataList1}" Title="線グラフ1"/>
        <oxy:LineSeries ItemsSource="{Binding DataList2}" Title="線グラフ2"/>
        <oxy:LineSeries ItemsSource="{Binding DataList3}" Title="線グラフ3"/>
    </oxy:Plot.Series>
    <!-- グラフの軸。Position="Right"なら第二Y軸と思いねぇ -->
    <oxy:Plot.Axes>
        <oxy:LinearAxis Position="Left" Title="X軸"/>
        <oxy:LinearAxis Position="Bottom" Title="Y軸"/>
    </oxy:Plot.Axes>
</oxy:Plot>

 ただ、前述の公式レファレンスPDFを読んだ限りでは、グラフ線の数を動的に制御するにはoxy:Plotでは駄目で、oxy:PlotViewを使う必要があるようです。……このことに気づくまでは、oxy:Plot.Seriesを入力時にそこからプロパティがIntelliSenseで出てこないことに困惑していました(それぐらい対応してくれよ……)。

結局どう対処したか?

 oxy:PlotViewを使うと、前述のコードは次のようになります。

<!-- XAML側 -->
<oxy:PlotView Model="{Binding GraphModel}"/>
// Model側
private PlotModel graphModel;
    public PlotModel GraphModel {
        get => graphModel;
        set {
            graphModel = value;
            NotifyPropertyChanged();
        }
    }
}

// PlotModelを作成して上書きする
var newGraphModel = new PlotModel();
newGraphModel.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom, Title = "X軸" });
newGraphModel.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Title = "Y軸" });
foreach (var dataList in DataListList) {
    var lineSeries = new LineSeries();
    foreach (var plot in dataList) {
        lineSeries.Points.Add(new DataPoint(plot.Key, plot.Value));
        lineSeries.Title = "線グラフ";
        newGraphModel.Series.Add(lineSeries);
    }
}
// グラフの要素を画面に反映する
newGraphModel.InvalidatePlot(true);
GraphModel = newGraphModel;

 ポイントとしては、

  • ModelだけBinding
  • Modelの中身はC#コードでゴリゴリ対処

ということですね。ゴリゴリ部分の感触はChartコントロールに似ていたので難しくありませんでしたが、MVVMとは何だったのか感があってちょっと残念です。InvalidatePlotメソッドを呼ぶ部分は次のページを参考にしました。
  OxyPlotのコントロール(PlotView)をリアルタイムにアップデートする方法 - Qiita

まとめ

 とりあえず使い方には慣れましたが、レファレンスがガバガバだと調べ事に苦労しますねorz
 次にグラフ描画が必要な場合は、LiveChartsを使ってみて感触を確かめようと思います。

おまけ

時刻をどうプロットするか

 冒頭で説明したDictionary<string, TimeSeriesData> supplyDataですが、これを表示させる場合はちょっとした工夫が必要です。つまり、「時刻」をどうプロットするかということです。というわけでサンプルコードを置いておきます。

// 軸の最小・最大値
DateTime graphMin, graphMax;
// 時刻表示向けの軸としてDateTimeAxisがあるのでそれを初期化する
var dateTimeAxis = new DateTimeAxis{
    // 最小値を設定
    Minimum = DateTimeAxis.ToDouble(graphMin),
    // 最大値を設定
    Maximum = DateTimeAxis.ToDouble(graphMin),
    // 表記を書式文字列で設定
    // 下記例では「2017/11/21」といった風になる
    StringFormat = "yyyy/MM/dd"
};
// DateTimeAxis.ToDoubleは、ChartコントロールにおけるDateTime#ToOADateのようなもの
DateTime plotX;
double plotY;
var plotData = new DataPoint(DateTimeAxis.ToDouble(plotX), plotY);

 なお、書式文字列に使用する書式一覧は流石に公式レファレンスに書いていました。
 DateTimeAxis — OxyPlot 2015.1 documentation

目盛りの間隔は?罫線は?

 目盛りの間隔は、大きなもの(Major)と小さなもの(Minor)の2種類が存在します。これはExcelなどでおなじみの設定方法でしょう。その設定方法についてもサンプルコードを置いておきます。

var linearAxis = new LinearAxis{
    // 大きな間隔を指定
    MajorStep = 5,
    // 小さな間隔を指定
    MinorStep = 1,
    // 大きな間隔で置かれる罫線のスタイル(この場合は実線・黒色)
    MajorGridlineStyle = LineStyle.Solid,
    MajorGridlineColor = OxyColors.Black,
    // 小さな間隔で置かれる罫線のスタイル(この場合は実線・灰色)
    MinorGridlineStyle = LineStyle.Dot,
    MinorGridlineColor = OxyColors.Gray
};

 ちなみにDateTimeにおける「1日」は実数表記だと「1」ですので、例えば1時間毎に区切りたい場合は「1.0/24」を指定することになります。また、OxyColors型やその実態であるOxyColor型はOxyPlot独自のもので、System.Windows.Media.Color型などからキャストできません。

第二Y軸に表示したいんだけど

 Chartコントロールでは、Series型のYAxisTypeAxisType.Secondaryを代入することで示していました。
 一方OxyPlotでは、Y軸にKeyを設定し、同じ文字列をSeriesのYAxisKeyに設定すればOKです。

// Keyに設定する文字列は一意なら何でもいい。それこそ"左軸"・"右軸"とかでもいい
var y1Axis = new LinearAxis{ Position = AxisPosition.Left,  Key = "Primary" };
var y2Axis = new LinearAxis{ Position = AxisPosition.Right, Key = "Secondary" };
// Series作成時に、どちら側の軸に属するかを設定する
var lineSeries1 = new LineSeries();
lineSeries1.YAxisKey = "Primary";
var lineSeries2 = new LineSeries();
lineSeries2.YAxisKey = "Secondary";

円グラフを描きたい

 SeriesとしてPieSeries型を用意し、それのSlicesプロパティにList<PieSlice>型を代入すればOKです。
 PieSliceのコンストラクタは引数として(string 名前, double 数値)を取ります。この「数値」は%の値ではなく素の値で大丈夫です。
  参考:
    PieSeries — OxyPlot 2015.1 documentation
    Xamarin FormsとOxyplotでPCLなグラフをタダで描く - Qiita

積み上げ棒グラフを描きたい

 相変わらず公式ドキュメントがクソの役にも立たないので苦労しましたが、StackOverFlowなどを調べ回った結果、次のポイントに注意すればOKなことが分かりました。

  • 積み上げ棒の長辺(積み上がる方向)の軸をLinearAxis、それぞれの積み上げ棒の種類の軸をCategoryAxisとします
  • CategoryAxisItemsSourceプロパティにIEnumerableな値を代入します。List<string>を入れることが多いでしょう
  • 横軸をCategoryAxisにした場合は、SeriesとしてColumnSeries型の値を追加していきます。逆に縦軸をCategoryAxisにした場合は、BarSeries型を追加する必要があることに注意!
    • この追加するColumnSeries型ないしBarSeries型ですが、Series.Addする前にIsStackedプロパティをtrueにする必要があります
  • 積み上げをどう記述するかですが、積み上げる種別を仮にA・B・C……とした場合、「AにおけるCategoryAxisの項目の値」をSeries.Addした後に「BにおけるCategoryAxisの項目の値」「CにおけるCategoryAxisの項目の値」をSeries.Addしていきます。
    • イメージとしては、積み上げ棒を1本づつ組み上げるのではなく、 積み上げ棒の各成分毎に追加することを繰り返す感じになります
var plotModel = new PlotModel();
// 横軸・縦軸を追加する。今回は縦軸をLinearAxisとした(LabelListはList<string>など、ラベルを表す値)
plotModel.Axes.Add(new LinearAxis {Position = AxisPosition.Left});
plotModel.Axes.Add(new CategoryAxis { Position = AxisPosition.Bottom, ItemsSource = LabelList });
// グラフ要素を追加する
// ループが「積み上げ棒の各成分」→「各積み上げ棒における成分値」となっていることに注意
for (int type = 0; type < Type.Max; ++k) {
    // インスタンスを初期化
    var columnSeries = new ColumnSeries();
    // 積み上げられるようにする
    columnSeries.IsStacked = true;
    // ここに代入しておくと、積み上げ棒の成分の名前(判例)がマウスオーバーで表示できるようになる
    columnSeries.Title = $"{columnLabel[k]}";
    // 成分値を追加していく
    for (int n = 0; n < Data[type].Max; ++n) {
        columnSeries.Items.Add(new ColumnItem(Data[type][n]));
    }
    plotModel.Series.Add(columnSeries);
}
plotModel.InvalidatePlot(true);