Posted at

WPF OxyPlotで円グラフを作る

More than 1 year has passed since last update.

OxyPlotで円グラフを作ってみたが、少し使いづらかったので使い方と、

ドーナツグラフにすることもできたのでそのやり方についても書いておく。

※導入はNugetからOxyPlot.WpfをインストールすればOK


使い方


基本

PieSeriesにPieSliceを追加



プロパティとして公開しているPlotModelにPieSeriesを追加



PlotViewのModelプロパティにバインドする


ViewModel.cs

    public class ViewModel

{
public PlotModel _PlotModel { get; private set; } = new PlotModel() { Title = "PieChartSample" };

public ViewModel()
{
var series = new PieSeries
{
StrokeThickness = 2.0,
InsideLabelPosition = 0.5,
AngleSpan = 360,
StartAngle = 270,
};

series.Slices.Add(new PieSlice("A型", 7508));
series.Slices.Add(new PieSlice("B型", 6125));
series.Slices.Add(new PieSlice("O型", 4346));
series.Slices.Add(new PieSlice("AB型", 1778));

_PlotModel.Series.Add(series);
}
}



MainWindow.xaml

<Window x:Class="OxyPieChartSample.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:OxyPieChartSample"
xmlns:oxy="http://oxyplot.org/wpf"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<oxy:PlotView Model="{Binding _PlotModel, Mode=OneWay}"/>
</Grid>
</Window>

実行するとこんな感じになる

2017-09-21_14h10_10.jpg

PieSeriesのプロパティ(一部)

・StrokeThickness : 要素間の境界線の太さ。

・InsideLabelPosition : ラベル(要素名)の位置。円の中心が0、円周上が1。

・AngleSpan : 円の描画角度。360で円、180で半円。

・StartAngle : 円の描画開始位置。0を水平としているので270で通常の円グラフになる。

・AreInsideLabelsAngled : trueにするとラベルが円の中心に向けて傾く。

・ExplodedDistance : 要素を分離させる場合の距離(0.05~0.1くらいが妥当)。

・Diameter : 円の表示倍率。

・InnerDiameter : 指定倍率だけ中心から穴が空き、ドーナツ状になる。

PieSliceのプロパティ

・Label : 要素名(コンストラクタで指定)。

・Value : 値(コンストラクタで指定)。

・Fill : 要素の色。指定しなかったら勝手に決まる。

・IsExploded : 要素を分離させるかどうか。

分離させるとこんな感じになる

2017-09-21_14h26_02.jpg

※%が表示されているラベルが他のコントロール等に重なったりPlotViewの境界からはみ出たりすることがある。

 防ぐにはラベルの表示を消すか大きく余白をとるくらいしかなさそう(´・ω・`)


動的に値を変える


Model.cs

    public class Model

{
public Item Item1 { get; private set; } = new Item();
public Item Item2 { get; private set; } = new Item();
public Item Item3 { get; private set; } = new Item();
public Item Item4 { get; private set; } = new Item();

readonly List<Item> itemList = new List<Item>();
public ReadOnlyCollection<Item> ItemList
{
get { return new ReadOnlyCollection<Item>(itemList); }
}

public Model()
{
itemList.Add(Item1);
itemList.Add(Item2);
itemList.Add(Item3);
itemList.Add(Item4);
}
}

public class Item
{
public string Label { get; set; }
public double Value { get; set; }
}


Modelでは要素に表示するLabelと値のValueを持ったItemクラスをプロパティとして公開し、

それらを詰め込んでいるItemListも公開している。

PieSliceをプロパティ化するのが良さそうだが、こいつはLabelとValueがコンストラクタでしかセットできないので

別クラスを用意した。


ViewModel.cs

    public class ViewModel

{
public Model _Model { get; private set; } = new Model();
public PlotModel _PlotModel { get; private set; } = new PlotModel() { Title = "PieChartSample" };
public ReactiveCommand C_PieUpdate { get; private set; } = new ReactiveCommand();

private PieSeries pieSeries = new PieSeries()
{
StrokeThickness = 2.0,
InsideLabelPosition = 0.5,
AngleSpan = 360,
StartAngle = 270,
};
private List<PieSlice> slices = new List<PieSlice>();

public ViewModel()
{
pieSeries.Slices = slices;
_PlotModel.Series.Add(pieSeries);
C_PieUpdate.Subscribe(x => PieUpdate());
}
private void PieUpdate()
{
slices.Clear();
slices.AddRange(_Model.ItemList.Where(x => x.Value > 0).Select(x => new PieSlice(x.Label, x.Value)));
_PlotModel.InvalidatePlot(true);
}
}


ViewModelではModel,PlotModel,ReactiveCommandをプロパティとして公開し、

PieSeriesとListはフィールドに定義している。

PieSeriesのSlicesプロパティはListを参照させているので、要素の変更はListを変更するだけでよくなる。

更新処理はPieUpdate()で行っており、ModelのItemListを取り出してPieSliceを作ってslicesに追加している。

また、値が0以下だと表示がおかしくなるのでWhereではじいている。

さらに、PlotViewはバインドデータを変えただけでは更新されないので、

PlotModel.InvalidatePlot(true);を呼んで更新している。

(詳しくはココ)

※Viewはバインドしてるだけなので省略

実行するとこんな感じで、入力値をグラフに反映させることができる

ReactivePropertyを使えば入力時に即反映させることもできる

2017-09-21_16h03_09.jpg


ドーナツグラフを作る

単純なドーナツグラフはPieSeriesのInnerDiameterを設定すれば作れるが、

2重に重なったものやドーナツグラフの中に円グラフがあるものは機能にないので作れない。

しかし、あれこれ考えているとPlotViewを重ねることで無理やり表現することができた。

※単純に重ねているだけなので、ドーナツグラフ側のトラッカーを表示して円グラフに重なるとトラッカーが隠れる(ヽ´ω`)

 回避策は思いつかなかったが、前面のOpacityを90%とかにすると少しマシになる。

 いい方法があれば教えてください。

作り方は単純で、PlotViewのBackgroundをnullにして重ねて配置し、

前面のPieSeriesにはDiameterを設定して円を小さくし、背面にはInnerDiameterを設定して穴を空けるだけ。

ただ、これをViewModelに作ると結構めんどくさいので、Helperクラスを作ってみた。

    public class OxyPieChartHelper

{
/// <summary>
/// PieSliceリスト
/// </summary>
public List<PieSlice> Slices { get; private set; } = new List<PieSlice>();
/// <summary>
/// 系列
/// </summary>
public PieSeries PieSeries { get; private set; } = new PieSeries() { StartAngle = 270, AngleSpan = 360, StrokeThickness = 4, };
/// <summary>
/// プロットモデル
/// </summary>
public PlotModel PlotModel { get; private set; } = new PlotModel();

public OxyPieChartHelper()
{
//SeriesにSlicesをセット
PieSeries.Slices = Slices;
//PlotModelにSeriesを追加
PlotModel.Series.Add(PieSeries);
}

/// <summary>
/// 要素(PieSlice)を更新する
/// </summary>
/// <param name="slices"></param>
public void UpdateSlices(IEnumerable<PieSlice> slices)
{
Slices.Clear();
Slices.AddRange(slices);
PlotModel.InvalidatePlot(true);
}
}

public class OxyInsideDonutChart : OxyPieChartHelper
{
public OxyInsideDonutChart()
{
PieSeries.InsideLabelPosition = 0.6;
PieSeries.Diameter = 0.7;
}
}

public class OxyOutsideDonutChart : OxyPieChartHelper
{
public OxyOutsideDonutChart()
{
PieSeries.InsideLabelPosition = 0.5;
PieSeries.InnerDiameter = 0.71;
PieSeries.TickHorizontalLength = 0;
}
}

OxyInsideDonutChartが円グラフでOxyOutsideDonutChartがドーナツグラフになる。

3重4重…にしたかったらOxyPieChartHelperを継承してサイズを調整すればOK。

これを使ってドーナツグラフを実装してみる。


DonutModel.cs

 public class DonutModel

{
public DonutItem Item1 { get; private set; } = new DonutItem();
public DonutItem Item2 { get; private set; } = new DonutItem();
public DonutItem Item3 { get; private set; } = new DonutItem();
public DonutItem Item4 { get; private set; } = new DonutItem();

readonly List<DonutItem> itemList = new List<DonutItem>();
public ReadOnlyCollection<DonutItem> ItemList
{
get { return new ReadOnlyCollection<DonutItem>(itemList); }
}

public DonutModel()
{
itemList.Add(Item1);
itemList.Add(Item2);
itemList.Add(Item3);
itemList.Add(Item4);
}
}

public class DonutItem
{
public string Label { get; set; }
public double Value { get; set; }
public CategoryEnum Category { get; set; }
}

static class CategoryEnumHelper
{
private static readonly List<CategoryEnum> categoryList = new List<CategoryEnum>();
public static ReadOnlyCollection<CategoryEnum> CategoryList
{
get { return new ReadOnlyCollection<CategoryEnum>(categoryList); }
}

static CategoryEnumHelper()
{
categoryList.AddRange(Enum.GetValues(typeof(CategoryEnum)).Cast<CategoryEnum>());
}
}

public enum CategoryEnum
{
マニューバー,
ブラスター,
チャージャー,
ローラー,
フデ,
スロッシャー,
スピナー,
シューター,
シェルター,
}


Modelは「動的に値を変える」とこでの例にenumでカテゴリを追加しただけ。


DonutViewModel.cs

public class DonutViewModel

{
public DonutModel _DonutModel { get; private set; } = new DonutModel();
public ReadOnlyCollection<CategoryEnum> _Categories { get; } = CategoryEnumHelper.CategoryList;
public OxyOutsideDonutChart _OutsidePie { get; private set; } = new OxyOutsideDonutChart();
public OxyInsideDonutChart _InsidePie { get; private set; } = new OxyInsideDonutChart();
public ReactiveCommand C_PieUpdate { get; private set; } = new ReactiveCommand();

public DonutViewModel()
{
C_PieUpdate.Subscribe(x => PieUpdate());
}
private void PieUpdate()
{
//リストのソート
var sortedList = _DonutModel.ItemList
.Where(x => x.Value > 0)
.OrderBy(x => x.Category);
//内側のグラフの更新
_InsidePie.UpdateSlices(sortedList.Select(x => new PieSlice(x.Label, x.Value)));
//外側のグラフの更新
_OutsidePie.UpdateSlices(_Categories.Select(x => new PieSlice(
x.ToString(),
SumMatch(
sortedList,
y => y.Category == x,
y => y.Value)))
.Where(x => x.Value > 0));
}
/// <summary>
/// コレクション内で条件に一致するものから指定した値を合計する
/// </summary>
/// <typeparam name="T">コレクションの所持する型</typeparam>
/// <param name="targetList">コレクション</param>
/// <param name="condition">条件</param>
/// <param name="key">合計する値</param>
/// <returns></returns>
private double SumMatch<T>(IEnumerable<T> targetList,Func<T,bool> condition,Func<T,double> key)
{
return targetList.Where(x => condition(x)).Sum(x => key(x));
}
}


ViewModelではHelperで定義した円・ドーナツグラフをプロパティとして公開している。

更新処理では、円グラフの要素の位置をドーナツグラフに合わせるため、リストをカテゴリでソートしている。

Viewはバインドするだけ。

実行するとこんな感じ

2017-09-22_16h55_45.jpg


おわりに

私が使いそうな範囲でいろいろいじってみた。

他に使えることとか見つけれたら随時追加する予定(`・ω・´)

また、「こんなことできるよ」とかあれば教えてください。