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

# 概要
　WinFormsでは、グラフ表示と言えば[Chartコントロール](https://msdn.microsoft.com/ja-jp/library/kx2ttx0a.aspx)でした。
　WPFでも[WindowsFormsHost]()を使えば表示できますが、データビハインドでゴリゴリしなきゃならない点が美しくありませんよね？
　　[C#:Chartコントロールを使う](https://qiita.com/hart_edsf/items/013479474090a4e681f3)
　　[https://qiita.com/nocd5/items/064a783240c1e8590169](https://qiita.com/nocd5/items/064a783240c1e8590169)
　そこで、WPFその他に使えるグラフ描画ライブラリ[OxyPlot](http://www.oxyplot.org)を使おう……と思ったのですが、その際に試行錯誤した記憶をメモしておきます。

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

```csharp
// 時系列データを表す型を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)](http://crocus7724.hatenablog.jp/entry/2016/02/22/233500)
　　[グラフ描画ライブラリ「OxyPlot」の導入方法 - ぴよぴよエンジニアの日記](http://santea.hateblo.jp/entry/2016/01/31/231011)
　　[OxyPlot を使い WPF アプリケーションでグラフを表示する。 - 自分の歩いた道に落ちてるコード](http://www.ownway.info/Blog/2014/08/post-42.html)
　一見豊富に見えますが、どれも**「導入法とチュートリアル」**といった感じで、後述する壁(折れ線本数の動的制御)に答えるものではありませんでした。というわけで次に調べるのは公式サイトのレファレンスなのですが、

## 公式レファレンスサイトがガバガバ
　　[Welcome to OxyPlot’s documentation! — OxyPlot 2015.1 documentation](http://docs.oxyplot.org/en/latest/)
　**今2017年ですよね？　2015年じゃないですよね？**
　……というのは「(いい意味で)枯れたライブラリなんだろう」ということでスルーしていましたが、問題は**APIやプロパティ一覧的なものが全然ない**ということです。例えば「X軸やY軸に設定するプロパティについての詳細」を知りたい際は
　　[Axes — OxyPlot 2015.1 documentation](http://docs.oxyplot.org/en/latest/models/axes/index.html)
を読むことになりますが、このページにおける「Position」や「Minimum/Maximum」などの詳細がキッチリと書かれていないんですよね。例えば[ChartコントロールのAxisに関するレファレンス](https://msdn.microsoft.com/ja-jp/library/system.windows.forms.datavisualization.charting.axis.aspx)は

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/5693387a-6649-8f37-d8a8-7660520fc7b5.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/d35cd2c7-ba8f-6af4-eb6f-683c55e88163.png)

## 公式レファレンスPDFの所在地
　というわけで**Webサイトがさっぱり役に立たない**のがOxyPlotなのですが、ググってすぐ見つけたマニュアルPDFはまだ役に立ちました。
　　[OxyPlot Documentation Release 2015.1](https://media.readthedocs.org/pdf/oxyplot/latest/oxyplot.pdf)
　ただこのリンク先のサイトはどういったものなのかがイマイチよく分からず……[トップページ](https://readthedocs.org)によると「ドキュメントの類を配布するためのサイト」らしいのですが、そもそも**自前のサイトあるんだからそっちに書けよOxyPlot**。

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

```xml
<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`を使うと、前述のコードは次のようになります。

```xml
<!-- XAML側 -->
<oxy:PlotView Model="{Binding GraphModel}"/>
```

```csharp
// 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](https://qiita.com/maruh/items/035a39a2a01102ce248f)

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

# おまけ

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

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

```csharp
// 軸の最小・最大値
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](http://docs.oxyplot.org/en/latest/models/axes/DateTimeAxis.html)

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

```csharp
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`型の`YAxisType`に`AxisType.Secondary`を代入することで示していました。
　一方OxyPlotでは、Y軸に`Key`を設定し、同じ文字列をSeriesの`YAxisKey`に設定すればOKです。

```csharp
// 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](http://docs.oxyplot.org/en/latest/models/series/PieSeries.html)
　　　　[Xamarin FormsとOxyplotでPCLなグラフをタダで描く - Qiita](https://qiita.com/e_fujikawa/items/7aa3aa597c0b4458ca8a)

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

- 積み上げ棒の長辺(積み上がる方向)の軸を`LinearAxis`、それぞれの積み上げ棒の種類の軸を`CategoryAxis`とします
- `CategoryAxis`の`ItemsSource`プロパティに`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本づつ組み上げるのではなく、 **積み上げ棒の各成分毎に追加することを繰り返す**感じになります

```csharp
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);
```