もっと詳しく

個人的にはWPF用のグラフ作成ライブラリとして一押しのScottPlotですが、最近Pythonの記事に集中していたせいで、ScottPlot のバージョンアップをフォローしきれていませんでした。

以前のデモプログラムに最新版のScotPlot Nuget でインストールしたところ、結構仕様が変わっていて修正が必要でした。

そこで、今回は ScottPlot 4.1 用に書き直したデモプログラムの紹介と、 旧バージョン(4.0)との違いについて触れたいと思います。

旧バージョンの仕様について確認されたい方は、こちらの記事をご一読ください。

また、ScottPlotの公式ページにはグラフのサンプルとソースコードが掲載されています。

この記事を読んだ後で、さらに詳しい情報が必要な方は、公式ページが非常に参考になります。

デモプログラムの概要

今回のデモプログラムの画面は次の様になります。

タブが3つあって、各タブに4グラフ、合計12種類のグラフを表示します。

具体的には、下記のグラフが表示されます。

ScottPlotの構造

ScotPlot は、ScottPlot.Plot でインスタンスを生成し、そのインスタンスに対して折線グラフや棒グラフを追加していきます。

各グラフの太さや色、表示する軸(X,Yども2軸まで表示可能)は、グラフのメソッドを読んだ時に返されるオブジェクトに対して行います。

plt = new ScottPlot.Plot(600, 400)
var sig = plt.AddSignal(DataGen.Sin(51, mult: 1));
sig.YAxisIndex = 0;
chart.Render();

ScottPlot 4.0 までは、グラフ作成メソッドに色や太さなどの引数があったのですが、ScottPlot 4.1 から上記の仕様変更が行われたため、引数がかなり省略されています。

下図は、ScottPlotクラスの内部構造のイメージ図となります。

ちなみに、デモプログラムでは ScottPlotをコントロールとして画面に張り付け、”uxChart” という名を付けており、この場合は以下のような記述になります。

plt = uxChart.Plot;
var sig = plt.AddSignal(DataGen.Sin(51, mult: 1));
sig.YAxisIndex = 0;
chart.Render();

ScottPlot 4.1 のインストール方法

今回は Visual Studio 2022 を前提とした解説になりますが、Visual Studio の過去バージョン(2019,2017)をお使いの方でも共通です。

まず、NuGetから「scottplot」というキーワードで検索し、「ScottPlot.WPF」を自分のプロジェクトにインストールします。

WindowsFormで利用されたい方は、その下の「ScottPlot.WinForms」をお選びください。

デモプログラムの解説

参考までにデモプログラムのソースコードを公開しますが、ScottPlotは入っていません。

リビルドを行う場合は、事前にNuGetを使ってインストールをお願いします。

デモプログラムのソースコードダウンロードURL

デモプログラムのソースコードはこちらからダウンロードできます。

XAMLのソースコード

XAMLのソースコードは以下の通りです。

<Window x:Class="ScotPlotTest.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:ScotPlotTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="1320">
    <Grid>
        <TabControl>
            <TabItem Header="チャート1">
                <Grid>
                    <WpfPlot x:Name="uxChart1" HorizontalAlignment="Left" Height="315" Margin="27,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart2" HorizontalAlignment="Left" Height="315" Margin="345,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart3" HorizontalAlignment="Left" Height="315" Margin="664,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart4" HorizontalAlignment="Left" Height="315" Margin="987,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <Button Content="描画" HorizontalAlignment="Left" Margin="1212,10,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click" Height="21"/>
                </Grid>
            </TabItem>
            <TabItem Header="チャート2">
                <Grid>
                    <Button Content="描画" HorizontalAlignment="Left" Margin="1212,10,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
                    <WpfPlot x:Name="uxChart5" HorizontalAlignment="Left" Height="315" Margin="27,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart6" HorizontalAlignment="Left" Height="315" Margin="345,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart7" HorizontalAlignment="Left" Height="315" Margin="664,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart8" HorizontalAlignment="Left" Height="315" Margin="987,53,0,0" VerticalAlignment="Top" Width="300"/>
                </Grid>
            </TabItem>
            <TabItem Header="チャート3">
                <Grid>
                    <Button Content="描画" HorizontalAlignment="Left" Margin="1212,10,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
                    <WpfPlot x:Name="uxChart9" HorizontalAlignment="Left" Height="315" Margin="27,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart10" HorizontalAlignment="Left" Height="315" Margin="345,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart11" HorizontalAlignment="Left" Height="315" Margin="664,53,0,0" VerticalAlignment="Top" Width="300"/>
                    <WpfPlot x:Name="uxChart12" HorizontalAlignment="Left" Height="315" Margin="987,53,0,0" VerticalAlignment="Top" Width="300"/>
                </Grid>
            </TabItem>
        </TabControl>

    </Grid>
</Window>

C#のソースコード

C#側のソースコードは以下の通りです。

using ScottPlot;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ScotPlotTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Random rand = new Random();
            //折れ線グラフのデータ作成
            double[] xs = Enumerable.Range(0, 100).Select(i => (double)i).ToArray();
            double[] ys = Enumerable.Range(0, 100).Select(i => (double)(i + rand.Next(0,100) * ((rand.Next(1,2) == 1) ? 1 : -1))).ToArray();
            double[] ys1 = Enumerable.Range(0, 100).Select(i => Math.Sin(i * 0.1)).ToArray();
            double[] ys2 = Enumerable.Range(0, 100).Select(i => 2 + Math.Cos(i * 0.1)).ToArray();
            double[] ys3 = Enumerable.Range(0, 100).Select(i => 4 + Math.Sin(i * 0.1) * Math.Cos(i * 0.1)).ToArray();
            double[] ys4 = Enumerable.Range(0, 100).Select(i => 1000 + Math.Sin(i * 0.1) * Math.Cos(i * 0.1)).ToArray();

            //グラフの描画
            DrawLine(uxChart1,"折れ線グラフ",xs, ys);

            List<(string legend, double[] xs, double[] ys,int axis)> linedata = new List<(string legend, double[] xs, double[] ys,int axis)>();
            linedata.Add(("legend1", xs, ys1,0));
            linedata.Add(("legend2", xs, ys2,0));
            linedata.Add(("legend3", xs, ys3,0));
            linedata.Add(("legend4", xs, ys4,1));

            DrawLine(uxChart2, "折れ線グラフ(複数)", linedata);

            //散布図
            DrawScatt(uxChart3, "散布図", xs, ys);

            //円グラフ
            double[] values = { 778, 43, 283, 76, 184 };
            string[] labels = { "C#", "JAVA", "Python", "F#", "PHP" };
            DrawPie(uxChart4, "円グラフ", labels, values);

            //縦棒グラフ
            DrawColumn(uxChart5, "縦棒グラフ", labels, values);

            //横棒グラフ
            DrawBar(uxChart6, "横棒グラフ", labels, values);

            //グルーピングのデータ作成
            string[] items = { "コア数", "クロック数", "スレッド数", "キャッシュ", "価格" };
            List<(string, double[])> datas = new List<(string, double[])>();
            datas.Add(("Core-i7", new double[] { 8, 3.6, 16, 64, 70 }));
            datas.Add(("Core-i5", new double[] { 6, 2.8, 12, 32, 50 }));
            datas.Add(("Core-i3", new double[] { 4, 2.0, 8, 16, 30 }));
            datas.Add(("Pentium", new double[] { 2, 1.8, 4, 8, 10 }));
            datas.Add(("Celeron", new double[] { 2, 1.5, 4, 8, 5 }));

            //棒グラフ(グルーピング)
            DrawGroupColumn(uxChart7, "グループ", items, datas);

            //積み上げグラフ
            DrawStack(uxChart8, "積み上げ", items, datas);

            //レーダーチャート
            DrawRadar(uxChart9, "レーダー", items, datas);

            //箱ひげ図
            DrawPopulations(uxChart10, "箱ひげ", ys);

            //散布図と回帰直線
            DrawRegression(uxChart11, "回帰", xs, ys);

            ///ヒストグラム
            DrawHistgram(uxChart12, "ヒストグラム", ys,20);

        }

        /// <summary>
        /// 折れ線グラフ
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="xs"></param>
        /// <param name="ys"></param>
        private void DrawLine(WpfPlot chart, string title, double[] xs,double[] ys)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            chart.Plot.AddSignalXY(xs,ys);
            chart.Render();
        }

        /// <summary>
        /// 折れ線グラフ(複数)
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="datas"></param>
        private void DrawLine(WpfPlot chart, string title, List<(string legend,double[] xs, double[] ys,int axis)> datas)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);

            foreach (var data in datas)
            {
                var sig = chart.Plot.AddSignalXY(data.xs, data.ys, label: data.legend);
                sig.YAxisIndex = data.axis;
            }
            //data.axis に1つでも1以上があれば2軸を表示
            chart.Plot.YAxis2.Ticks(datas.Count(i=>i.axis > 0) > 0);
            
            chart.Plot.Legend(location: Alignment.UpperRight);
            chart.Render();
        }

        /// <summary>
        /// 散布図
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="xs"></param>
        /// <param name="ys"></param>
        private void DrawScatt(WpfPlot chart,string title, double[] xs, double[] ys)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            chart.Plot.AddScatter(xs, ys, lineWidth: 0);
            chart.Render();
        }

        /// <summary>
        /// 円グラフ
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="labels"></param>
        /// <param name="values"></param>
        private void DrawPie(WpfPlot chart, string title, string[] labels,double[] values)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            var pie = chart.Plot.AddPie(values);
            pie.SliceLabels = labels;
            pie.ShowLabels = true;
            chart.Render();
        }

        /// <summary>
        /// 縦棒グラフ
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="labels"></param>
        /// <param name="ys"></param>
        private void DrawColumn(WpfPlot chart, string title, string[] labels, double[] ys)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            chart.Plot.AddBar(ys,DataGen.Consecutive(ys.Length));
            chart.Plot.XTicks(labels);
            chart.Render();

        }

        /// <summary>
        /// 横棒グラフ
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="labels"></param>
        /// <param name="ys"></param>
        private void DrawBar(WpfPlot chart, string title, string[] labels, double[] ys)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            var bar = chart.Plot.AddBar(ys,DataGen.Consecutive(ys.Length));
            bar.Orientation = ScottPlot.Orientation.Horizontal;
            chart.Plot.YTicks(labels);
            chart.Render();

        }

        /// <summary>
        /// 箱ひげ図
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="ys"></param>
        private void DrawPopulations(WpfPlot chart, string title, double[] ys)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            chart.Plot.AddPopulation(new ScottPlot.Statistics.Population(ys));
            chart.Plot.XAxis.Ticks(false);
            chart.Render();
            
        }

        /// <summary>
        /// 散布図と回帰直線
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="xs"></param>
        /// <param name="ys"></param>
        private void DrawRegression(WpfPlot chart,string title, double[] xs, double[] ys)
        {

            var model = new ScottPlot.Statistics.LinearRegressionLine(xs, ys);

            chart.Plot.Clear();
            chart.Plot.Title((title != "") ? title : "Y = {model.slope:0.0000}x + {model.offset:0.0}\nR² = {model.rSquared:0.0000}");
            chart.Plot.AddScatter(xs, ys, lineWidth: 0);
            chart.Plot.AddLine(model.slope, model.offset, (xs.Min(), xs.Max()), lineWidth: 2);
            chart.Render();
        }

        /// <summary>
        /// 縦棒グラフ(グループ)
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="labels"></param>
        /// <param name="values"></param>
        private void DrawGroupColumn(WpfPlot chart, string title, string[] labels, List<(string legend,double[] ys)> values)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);

            var datas = Enumerable.Range(0, values[0].ys.Length).Select(y => Enumerable.Range(0, values.Count).Select(x => values[x].ys[y]).ToArray()).ToArray();
            chart.Plot.AddBarGroups(values.Select(i => i.legend).ToArray(), labels, datas,null);
            chart.Plot.Legend(location: Alignment.UpperRight);
            chart.Render();
        }

        /// <summary>
        /// 積み上げグラフ
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="labels"></param>
        /// <param name="values"></param>
        private void DrawStack(WpfPlot chart, string title, string[] labels, List<(string legend, double[] ys)> values)
        {
            //チャートエリアのクリア
            chart.Plot.Clear();

            //タイトルの設定
            chart.Plot.Title(title);

            //X軸のデータを生成
            var x = Enumerable.Range(0, labels.Length).Select(i => (double)i).ToArray();

            //積み上げ用配列の初期化と合計計算
            var sum = values.Select(i => i.ys.Sum()).ToArray();

            //積み上げグラフ作成(トップから順に描画)
            for(int n = 0;n < values.Count;n ++)
            {
                var bar = chart.Plot.AddBar(sum.ToArray(),x);
                bar.Label = labels[n];
                Enumerable.Range(0, sum.Length).Select(i => sum[i] = sum[i] - values[i].ys[n]).ToArray();
            }

            //凡例と軸の設定
            chart.Plot.Legend(location: Alignment.UpperRight);
            chart.Plot.XTicks(values.Select(i=>i.legend).ToArray());

            //レンダリング
            chart.Render();
        }

        /// <summary>
        /// レーダーチャート
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="labels"></param>
        /// <param name="values"></param>

        private void DrawRadar(WpfPlot chart, string title, string[] labels, List<(string legend, double[] ys)> values)
        {
            chart.Plot.Clear();
            chart.Plot.Title(title);
            double[,] plots = new double[values.Count,values[0].ys.Length];

            Enumerable.Range(0, plots.GetLength(0)).Select(i => Enumerable.Range(0, plots.GetLength(1)).Select(j => plots[i, j] = values[i].ys[j]).ToArray()).ToArray();

            var radar = chart.Plot.AddRadar(plots);
            radar.CategoryLabels = labels;
            radar.OutlineWidth = 3;
            radar.GroupLabels = values.Select(i => i.legend).ToArray();
            chart.Plot.Legend(location: Alignment.UpperRight);
            chart.Plot.Grid();
            chart.Render();
        }

        /// <summary>
        /// ヒストグラム
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="ys"></param>
        /// <param name="backet"></param>
        private void DrawHistgram(WpfPlot chart, string title, double[] ys,int backet = 10)
        {
            double max = ys.Max();
            double min = ys.Min();
            var bin = Math.Abs(max - min) / backet;

            (double[] counts, double[] binEdges) = ScottPlot.Statistics.Common.Histogram(ys,min: min, max:max, binSize: bin);
            double[] leftEdges = binEdges.Take(binEdges.Length - 1).ToArray();

            chart.Plot.Clear();
            chart.Plot.Title(title);
            var bar = chart.Plot.AddBar(values: counts,positions: leftEdges);
            bar.BarWidth = bin;
            chart.Render();
        }


        // https://scottplot.net/cookbook/4.1/

    }
}

ソースコードの解説

こちらの記事で紹介したデモプログラムを、ScottPlot 4.1 用に書き直したものなので、メソッドの仕様はほとんど同じです。

1点だけ異なるのが、 DrawLine メソッドです。

ScottPlot 4.1 からはY軸を2軸にまで利用できるようになりました。

そこで、DrawLineメソッドの引数に軸が指定できるようにしています。

linedata.Add(("legend1", xs, ys1,0));
linedata.Add(("legend2", xs, ys2,0));
linedata.Add(("legend3", xs, ys3,0));
linedata.Add(("legend4", xs, ys4,1));
DrawLine(uxChart2, "折れ線グラフ(複数)", linedata);

旧デモプログラムからの変更箇所

もし既に旧バージョンのScottPlotを使っていて、今回最新バージョンに置き換えようと考えている方の参考として、今回私が行った修正点を簡単に解説しておきます。

WpfPlotの plt プロパティが Plot に変わった 

ScottPlot は、WpfPlot の plt プロパティからオブジェクトを取得し、このオブジェクトに備わっている各グラフ描画メソッドを呼び出すことでグラフ描画します。

今回は、このプロパティの名前が Plot に変更されています。

//従来
var plt = uxChart.plt;
//Ver 4.1
var plt = uxChart.Plot;

グラフ描画メソッドの名前が変わった

従来のグラフ描画メソッドは、PlotBar、PlotScatter、PlotLineなど、先頭にPlotが付いていました。

Ver 4.1 からは AddBar、AddScatter、AddLine の様に先頭がAddに代わっています。

//従来
chart.plt.PlotSignalXY(data.xs, data.ys, label: data.legend);
//Ver 4.1
chart.Plot.AddSignalXY(data.xs, data.ys, label: data.legend);

グラフ描画メソッドの引数が変わった

冒頭にも説明しましたが、今までは幅や色などグラフのデザインを指定する場合は、各グラフ描画メソッドの引数を使っていました。

Ver 4.1 からは、引数で指定するのではなく、グラフ描画メソッドが返すオブジェクトに対してデザインを指定します。

この仕様変更により引数が見直されており、全てではありませんが多くのメソッドにおいて、引数の名前や引数の並び変わったり、廃止されています。

//従来(第1引数にX軸データを指定)
chart.plt.PlotBar(xs,ys);
//Ver 4.1(第2引数にX軸データを指定)
chart.Plot.AddBar(ys,xs);

Legendのロケーション指定が変わった

凡例の表示を行うLegendメソッドには、凡例をグラフのどこに表示するかを指定する location 引数があります。

Ver 4.1 では、この location 引数の指定が enum Location から enum Alignment に変更されています。

//従来
chart.plt.Legend(location: Location.upperRight);
//Ver 4.1
chart.Plot.Legend(location: Alignment.UpperRight);

これは Legend に限ったものではなく、その他のメソッドにおいても同様です。

Y第2軸が指定できるようになった

これは大変うれしい拡張機能です。

指定の方法は簡単で、グラフ描画メソッドの戻り値にあるYAxis2に、表示したい軸を設定し、最後に 2軸表示用のメソッドを呼ぶだけです。

//折線グラフ描画メソッドを呼び出し、返されるオブジェクトのYAxisIndexに軸の番号=0を指定
var sig1 = chart.Plot.AddSignalXY(~);
sig1.YAxisIndex =0;
//折線グラフ描画メソッドを呼び出し、返されるオブジェクトのYAxisIndexに軸の番号=1を指定
var sig2 = chart.Plot.AddSignalXY(~);
sig2.YAxisIndex =1;
//Y2軸を表示
chart.Plot.YAxis2.Ticks(true);

実は、X軸、Y軸とも2軸まで指定できるようになっています。

ただ、今までにX軸を2軸つかうグラフは見たことが無いので、今回は取り扱いませんでしたが、XAxisIndexに使いたいX軸を指定し、XAxis2.Tick(true) とするだけです。

HistGram用のデータ集計を行うメソッドが変わった

まず名前空間の階層が次の様に変わっています。

ScottPlot.Statictics.Histogram ⇒ ScottPlot.Statistics.Common.Histogram

また、データの最大、最小、ビンサイズを指定するようになっていて、バケット数(階級をいくつに分割するか)やバーの太さなどの引数が無くなりました。

//従来
var hist = new ScottPlot.Statistics.Histogram(ys,binCount:backet);
chart.plt.PlotBar(hist.bins, hist.counts, barWidth: hist.binSize, outlineWidth: 0);
//Ver 4.1
(double[] counts, double[] binEdges) = ScottPlot.Statistics.Common.Histogram(ys,min: min, max:max, binSize: bin);
double[] leftEdges = binEdges.Take(binEdges.Length - 1).ToArray();
var bar = chart.Plot.AddBar(values: counts,positions: leftEdges);
bar.BarWidth = bin;

まとめ

今回は ScottPlot 4.1 をWPFで使う方法について、デモプログラムを交えて解説しました。

こまごまとした仕様変更が行われているため、旧バージョンから乗り換える場合はそれなりの修正が必要となります。

しかし、この記事を読んでいただければ、大枠で変更のポイントが分かるので、そんなに時間は掛からないのではないかと思います。

新しいグラフも追加されているので、それらを使いたい場合、もしくは2軸が使いたい場合は、是非ScottPlot 4.1 をご活用下さい。