C# でグラフプロット - OxyPlot, LiveCharts, WinForms Chartコントロール

# はじめに

C# でデータを可視化したいとき、Python の matplotlib や Matlab のような普段使いのグラフプロットライブラリがないのが困りものです。

ざっと調べたところ、商用ライセンスを使わない場合は、選択肢としては次のようなものがあるようです。（数年前からあまり活発に更新されていないものが多いですが・・）

1. Windows Forms 標準の Chart Control
1. [OxyPlot](http://www.oxyplot.org/) や [LiveCharts](https://lvcharts.net/) などのサードパーティーライブラリ

（参考 [電子工作専科-C#プロットライブラリ](https://denshikousakusenka.jimdo.com/%E3%81%9D%E3%81%AE%E4%BB%96/c-plot%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA/)）

ここでは、(1) 標準Chartコントロール、(2) OxyPlot、(3) LiveCharts の使い方を比べてみます。**性能比較ではなく、導入方法のメモとして。**


## WPF や UWP での利用

Windows フォーム標準の Chart コントロールは WPF から使うことも可能なようです（[WPFでChartコントロールを使う](https://qiita.com/nocd5/items/064a783240c1e8590169)）。ただし、WindowsFormsHost を使うため[描画に問題が起こることがある](http://yamaki.hatenablog.com/entry/20111006/1317887610)とか。

OxyPlot や LiveCharts は WPF, UWP, Xamarin 含め広くサポートしていますし、MITライセンスなので使いやすそう。

この記事ではサンプルを Windowsフォームとしますが、WPF 等でもプロットまわりのコーディングの流れは大きくは変わらないようです。[LiveCharts は WPF版も簡単にまとめておきます](#livecharts-wpf%E7%89%88)。


## 以下のサンプルコードについて

**クリックイベントによってデータと棒グラフを更新する**ようなプログラムにします。

環境：

- Visual C# (Visual Studio 2017)
- .NET Framework 4.6.1
- Windows フォームアプリケーション / WPF アプリ

仕様：

- DarkBlue の棒グラフ (Column Chart) 、値は 0 - 0.8 の範囲でランダム（一様乱数）
- プロットエリア内の背景は白、縦線は削除
- X軸にラベル (hogehoge, fuga, foo bar の3つを繰り返し）と 0始まりの index を表示
- Y軸の範囲は常に 0 - 1.0
- クリックイベントで値を更新

![livecharts-column-600.png](https://qiita-image-store.s3.amazonaws.com/0/55780/c0171e26-04e4-c4ba-9c2a-900437e8235a.png)
↑ LiveCharts プロット例（LiveCharts は下から出てくるアニメーションがつきます）

（以下のサンプルコードでは、インデントは読みやすさのために減らしているため参考程度ということで。）


# Windows フォーム標準 Chart コントロール

## 準備と初期化 (標準ChartControl)

1. Chartコントロールを追加
    - ツールボックスを「Chart」で検索（もしくはデータ > Chart）し、Form1 へドラッグ＆ドロップ
2. サイズ調整
    - Form1 のウィンドウに合うように Chartコントロールの表示サイズを拡大
    - Form1 のウィンドウサイズに従ってグラフ表示サイズが変わるように、プロパティ > 配置 > Anchor に Bottom, Right を追加
3. グラフプロパティ調整
    - 凡例が不要であれば、Form1 のプロパティ > グラフ > Legends を選び、メンバーから Legend1 を削除
4. データプロット
    - 以下のコードを Form1.cs へ

```csharp:Form1.cs-標準ChartControl版(初期化)
    // コンストラクタのInitializeComponent()の後かForm1_Load内にて
    chart1.Titles.Add("Test Chart Control");
    chart1.ChartAreas[0].AxisX.LabelStyle.Angle = -90;
    chart1.ChartAreas[0].AxisX.Interval = 1;
    chart1.ChartAreas[0].AxisX.MajorGrid.Enabled = false;
    chart1.ChartAreas[0].AxisY.Maximum = 1.0;
    chart1.ChartAreas[0].AxisY.Minimum = 0.0;
    chart1.Series["Series1"].ChartType = SeriesChartType.Column;
    chart1.Series["Series1"].Color = Color.DarkBlue;

    List<string> texts = new List<string> { "hogehoge", "fuga", "foo bar" };
    Random r = new System.Random();
    for (var i = 0; i < 50; i++)
    {
        chart1.Series["Series1"].Points.AddXY(i, r.Next(80) / 100.0);
        chart1.Series["Series1"].Points[i].AxisLabel
            = String.Format("{0} - {1}", texts[i % texts.Count], i);
    }
```

`ChartAreas["ChartAreas1"]` や `Series[0]` など、添え字と名前のどちらでもOK。

## データ更新とグラフ再描画 (標準ChartControl版)

`chart1` のクリックイベントハンドラを作成します（chart1 プロパティの稲妻マークから見つかるClick，もしくは Form1.cs [デザイン] の chart1 の表示範囲をダブルクリック）。

```csharp:Form1.cs-標準ChartControl(データ更新)
    // クリックイベントハンドラ (chart1_Click) にて
    Console.WriteLine("clicked!");
    Random r = new System.Random();
    foreach (var pointData in chart1.Series["Series1"].Points)
    {
        pointData.SetValueY(r.Next(80) / 100.0);
    }
    chart1.Refresh();  // これが必要
```

これでグラフをクリックするたびに棒グラフが更新されます。

データを追加するときは `AddXY` などの関数を使いますが、これら Add系（の関数でデータ追加を行うと、自動でグラフがリフレッシュされます。

一方、データの値だけ設定するには `SetValueXY` や `SetValueY` などを使いますが、これら SetValue系でデータを置きかえた時や `Points.ElementAt(1).YValues` に直接代入したとき、`Points.Clear()` としたときなどは、自動ではグラフが更新されません。上記のコードのように `chart1.Refresh()` を明示的に呼ぶ必要があります。


# OxyPlot

http://docs.oxyplot.org/en/latest/getting-started/hello-windows-forms.html が参考になります。ただ、ツールボックスでのコントロール追加は説明が省略されているので、[ほかの記事](http://whoopsidaisies.hatenablog.com/entry/2014/02/10/122939)も参考にします。

WPF なら、以下の手順のうち「ツールボックスへコントロールの追加」が「XAMLの編集」になりますが、ウェブでいろいろ見つかるようなので省略します。ここでは Windows Forms を前提としています。

## 準備と初期化 (OxyPlot)

1. パッケージの追加
    1. ツール > NuGet パッケージ マネージャー > ソリューションの NuGet パッケージの管理 > 参照 の検索ボックスを開き「OxyPlot」で検索
    1. 「OxyPlot.WindowsForms」を追加 (v2.0.0 プレリリース版もあるようですがここでは v1.0.0 安定版を使います。WPFならば「OxyPlot.Wpf」などプラットフォーム合ったものを追加します。）
    1. 「OxyPlot.Core」も一緒に追加されます。
2. ツールボックスへコントロールの追加
    1. ツールボックスの適当なところでコンテキストメニュー（右クリック等）を表示
    1. アイテムの選択 > .NET Framework コンポーネント > 参照 で `(ソリューションファイルのあるフォルダ)\packages\OxyPlot.WindowsForms.1.0.0\lib\net45\OxyPlot.WindowsForms.dll` を選択（読み込みに失敗する場合があり、何度か試すとうまく読み込まれるようです。）
    1. PlotView コントロールを使えるようになるので Form1 へ配置し、適当にサイズ調整（[Chart コントロールの設定を参照のこと](https://qiita.com/memakura/items/23f5ca91f1975f8cd156#%E6%BA%96%E5%82%99%E3%81%A8%E5%88%9D%E6%9C%9F%E5%8C%96-%E6%A8%99%E6%BA%96chartcontrol)）
3. データプロット
    - 以下のコードを Form1.cs へ

```csharp:Form1.cs-OxyPlot版(初期化)
// 以下の namespace を追加
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;

...

    // コンストラクタのInitializeComponent()の後かForm1_Load内にて
    var model = new PlotModel {
        Title = "Test ColumnSeries",
        Background = OxyColors.White
    };
    var axisX = new CategoryAxis { Position = AxisPosition.Bottom, Angle = -90 };
    model.Axes.Add(axisX);
    model.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Minimum = 0.0, Maximum = 1.0});
    plotView1.Model = model;

    List<string> texts = new List<string> { "hogehoge", "fuga", "foo bar" };
    Random r = new System.Random();

    var series = new ColumnSeries { FillColor = OxyColors.DarkBlue };
    for (var i = 0; i < 50; i++)
    {
        series.Items.Add(new ColumnItem(r.Next(80) / 100.0));
        axisX.Labels.Add(String.Format("{0} - {1}", texts[i % texts.Count], i));
    }
    model.Series.Add(series);
```

## データ更新とグラフ再描画 (OxyPlot)

`plotView1` のクリックイベントハンドラを追加し、クリックするたびにデータ更新とグラフ再描画を行うようにします。

```csharp:Form1.cs-OxyPlot版(データ更新)
    // クリックイベントハンドラ (plotView1_Click) にて
    Console.WriteLine("clicked!");
    Random r = new System.Random();
    
    var series = new ColumnSeries { FillColor = OxyColors.DarkBlue };
    for (var i = 0; i < 50; i++)
    {
        series.Items.Add(new ColumnItem(r.Next(80) / 100.0));
    }
    plotView1.Model.Series.Clear();  // データを直接置きかえる方法が分からないので
    plotView1.Model.Series.Add(series);
    plotView1.Model.InvalidatePlot(true);
```

データを直接置きかえる方法が分からなかったので、いったん `Clear()` しています。
グラフを再描画させるためには、`Model.InvalidatePlot(true)` が必要なようです。

# LiveCharts

LiveCharts も、OxyPlot と同様に、NuGet でパッケージを導入します。

参考（LiveChartsドキュメント）：

- [Basic Column](https://lvcharts.net/App/examples/v1/wf/Basic%20Column)
- [Filtering](https://lvcharts.net/App/examples/v1/wpf/Filtering%20data)

**いったんデータを紐づけておけば、再描画は自動で行われるようです。あとで使いやすいようにプロパティにしておきます。**

## 準備と初期化 (LiveCharts)

1. パッケージの追加
    - NuGet から「LiveCharts.WinForms」を選択します。現時点での安定版は v0.9.7.1 でした。「LiveCharts」「LiveCharts.Wpf」（いずれも v0.9.7）も一緒にインストールされるようです。
2. ツールボックスへコントロールの追加
    - [OxyPlot と同様に](#%E6%BA%96%E5%82%99%E3%81%A8%E5%88%9D%E6%9C%9F%E5%8C%96-oxyplot)コントロールを追加します。`(ソリューションファイルのあるフォルダ)\packages\LiveCharts.WinForms.0.9.7.1\lib\net45\LiveCharts.WinForms.dll` を選びます。
    - 「CartesianChart」を選んで配置します。
3. データプロット
    - 以下のコードを Form1.cs へ


```csharp:Form1.cs-LiveCharts版(初期化)
// 以下の namespace を追加
using LiveCharts;
using LiveCharts.Wpf;  // .Wpf は必要 / .WinForms は必要に応じて

...

  // Form1クラスのプロパティとして以下を追加
  public ChartValues<double> DataValues { get; set; }
  public List<string> Labels { get; set; }

...

    // コンストラクタのInitializeComponent()の後かForm1_Load内にて
    Labels = new List<string>();
    // (*1)
    cartesianChart1.AxisX.Add(new Axis {
        Labels = Labels,  // Labelsプロパティと紐づける
        Separator = new Separator { Step = 1, IsEnabled = false },
        LabelsRotation = -90
    });
    cartesianChart1.AxisY.Add(new Axis { MinValue = 0.0, MaxValue = 1.0 });
    cartesianChart1.BackColor = Color.White;
    // cartesianChart1.DisableAnimations = true;  // アニメーションを切るならtrue

    List<string> texts = new List<string> { "hogehoge", "fuga", "foo bar" };
    Random r = new System.Random();

    var valarray = new double[50];
    for(var i=0; i < valarray.Length; i++)
    {
        valarray[i] = r.Next(80) / 100.0;
        Labels.Add(String.Format("{0} - {1}", texts[i % texts.Count], i));
    }
    DataValues = new ChartValues<double>(valarray);
    // (*2)
    cartesianChart1.Series.Add(new ColumnSeries {
        Values = DataValues,  // DataValuesプロパティと紐づける
        Fill = System.Windows.Media.Brushes.DarkBlue
    });
```

グラフの軸や背景色の設定などは，(\*1) のように、`cartesianChart1` から `AxisX` などのプロパティを介して行っています。また、(\*2) の `cartesianChart1.Series` は SeriesCollection型ですが、LiveCharts では複数の Series (データ系列) をこの SeriesCollection クラスで管理します。

図の様々なプロパティをまとめて管理するのが Windowsフォーム版特有の CartesianChartクラスで、`cartesianChart1` はそのオブジェクトです（namespace は LiveCharts.WinForms）。つまり、Windowsフォーム版は、WPF版の様々なプロパティを [CartesianChart がラップする形](https://github.com/Live-Charts/Live-Charts/blob/master/WinFormsView/CartesianChart.cs)です。CartesianChart 以外にもいくつかのタイプの Chart があります。
一方で、[後述のように](#livecharts-wpf%E7%89%88) 、WPF 版ではこれらのプロパティを直接 XAML で Binding していきます。

`Labels` や `DataValues` のデータを更新すると、グラフも自動で再描画されます。

`ChartValues` の `Add` を何度も呼ぶことは望ましくなく、先に配列に値を入れておいて `AddRange` などで一度に `ChartValues` にいれる方がよい ([Performance Tips](https://lvcharts.net/App/examples/v1/wf/Performance%20Tips)) というので、いったん配列に入れています。（上のコードでは `AddRange` ではなくコンストラクタで渡しています。）

なお、グラフ全体にタイトルをつける方法は見当たりませんでした。


## データ更新とグラフ再描画 (LiveCharts)

LiveCharts `cartesianChart1` のクリックイベントは取れないようだったので、データをクリックした際に発生する `DataClick` イベントを追加し、クリックするたびにデータ更新とグラフ再描画を行うようにします。

```csharp:Form1.cs-LiveCharts版(データ更新)
    // イベントハンドラ cartesianChart1_DataClick にて
    Console.WriteLine("clicked!");
    Random r = new System.Random();
    var n = DataValues.Count;
    DataValues.Clear();
    DataValues.AddRange(new double[n].Select(_ => r.Next(80) / 100.0));
```

棒グラフをクリックすると、たしかに自動でリフレッシュされます。毎回、にょきっと下からグラフが伸びてくるアニメーションです。

乱数配列の生成は LINQ の Select にしています。以下のように、`Values[i]` に順次代入しても動くようですが、パフォーマンス的にはよろしくないのかも（このくらいのデータサイズなら許される？）

```csharp
    for (var i = 0; i < cartesianChart1.Series[0].Values.Count; i++)
    {
        cartesianChart1.Series[0].Values[i] = r.Next(80) / 100.0;
    }
```

## LiveCharts WPF版

最後に LiveCharts を WPF で使う方法を簡単にまとめておきます。

NuGet パッケージのインストールを行った後に、XAMLを編集します。
AxisX や SeriesCollection などの各種プロパティを Binding しておきます。

```xml:MainWindow.xaml
<Window x:Class="TestPlot.MainWindow"
    ...(省略)...
    xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
  <Grid>
    <lvc:CartesianChart Series="{Binding SeriesCollection}" DataClick="CartesianChart_DataClick">
      <lvc:CartesianChart.AxisX>
        <lvc:Axis Labels="{Binding Labels}" LabelsRotation="-90">
          <lvc:Axis.Separator>
            <lvc:Separator IsEnabled="False" Step="1"></lvc:Separator>
          </lvc:Axis.Separator>
        </lvc:Axis>
      </lvc:CartesianChart.AxisX>
      <lvc:CartesianChart.AxisY>
        <lvc:Axis MinValue="0.0" MaxValue="1.0"></lvc:Axis>
      </lvc:CartesianChart.AxisY>
    </lvc:CartesianChart>
  </Grid>
</Window>
```

C# のコードですが、[Windows Forms 版のコード](#%E6%BA%96%E5%82%99%E3%81%A8%E5%88%9D%E6%9C%9F%E5%8C%96-livecharts)に対する差分を示します。(\*1) は XAML側で行ったので削除できます。一方で (\*2) は SeriesCollection も自分で用意しておきます。SeriesCollection のプロパティも追加します。

```csharp:MainWindow.xaml.cs-WPF版(WinfForm版との差分)
  public SeriesCollection SeriesCollection { get; set; }  // 追加するプロパティ
  
...

    // WinForm版の(*1)は削除．(*2)を以下のように変更
    SeriesCollection = new SeriesCollection
    {
        new ColumnSeries
        {
            Values = DataValues,  // DataValuesプロパティと紐づける
            Fill = Brushes.DarkBlue
        }
    };

    DataContext = this; // Binding用
```

## LiveCharts のパフォーマンス

自分では未検証なので以下は参考情報として。

LiveCharts はデータ点が1000点ぐらいならいけるが1万点は重く、一方で OxyPlot ではいけるという [Qiita の記事](https://qiita.com/7of9/items/aee55440df24c00c27e8)があります。ただ、パフォーマンスチューニング前の結果のようでした。

[LiveCharts の作者自身が書いているように](https://lvcharts.net/Home/About)、LiveCharts は、OxyPlot をよりモダンな UI にしたいというモチベーションから作成されているので、見た目も凝っていて、アニメーションも豊富なようです。
したがって、パフォーマンスを上げるためには、（先の [Qiita の記事](https://qiita.com/7of9/items/aee55440df24c00c27e8)にもリンクがありますが）[ドキュメント (Performance Tips)](https://lvcharts.net/App/examples/v1/wf/Performance%20Tips) に従って、いくつか設定変更するのがよいでしょう。
アニメーションの Disable を含め、どのプロパティをオフにするとよいかや、何度も `Add` しないなど、チューニングの Tips がいろいろ紹介されています。


さらには、有料版ならば1千万点でも問題ないとか（[LiveCharts Geared](https://lvcharts.net/licensing/pricing)・・70ドル弱なので他の商用ライブラリよりはだいぶ良心的かな）。

パフォーマンスを気にするなら、LiveCharts はデフォルトのままではなく設定を工夫と・・



# まとめ

普段使いのチャートライブラリはどれがよいか、悩ましいところです。

OxyPlot と LiveCharts は、[GitHub のフォーク数では LiveCharts がやや高く](https://dotnet.libhunt.com/compare-oxyplot-vs-live-charts)、一方で [Google trends では OxyPlot の方がやや高め](https://trends.google.co.jp/trends/explore?q=livecharts,oxyplot)ようです。ドキュメントの充実度では LiveCharts でしょう。

適当なライブラリは目的次第かと思いますが、個人的には LiveCharts は相性が良い感じ。

Windows に限定した場合は、標準Chartコントロールで行けるところまでいって、ズームとかヒートマップとかアニメーションとか必要になってから OxyPlot もしくは LiveCharts を考えるのもよさそう。