テストの実行
WPF を使用してグラフを生成する
James McCaffrey
一連のテスト関連データからグラフを生成することは、ソフトウェア開発においてよく行われる作業です。私の経験では、データを Excel のスプレッドシートにインポートし、Excel に組み込まれているグラフ作成機能を使用して手動でグラフを生成する方法が最も一般的です。この方法はほとんどの状況で有効ですが、基盤となるデータが頻繁に変更される場合、手動でグラフを作成するのはすぐに面倒になります。今月のコラムでは、Windows Presentation Foundation (WPF) テクノロジを使用して、このプロセスを自動化する方法を紹介します。どのようなグラフを作成するかについては、図 1 を参照してください。このグラフは、未解決のバグと解決済みのバグの数を日付順に示しており、単純なテキスト ファイルからデータを読み取る短い WPF プログラムを使用して、実行時に生成しています。
図 1 プログラムから生成されるバグ数を示すグラフ
青の線で結ばれた赤丸で表される未解決バグの数は、開発作業の初期に急激に増加し、その後時間の経過とともに減少しています。この情報は、ゼロ バグ バウンスが達成される日を予想する場合に役立つ可能性があります。解決済みバグの数 (緑の線で結ばれた三角形のマーカー) は、着実に増加しています。
しかし、この情報は有用であっても、運用環境では開発リソースに限りがあることが多く、このようなグラフを手動で生成する手間は割に合わない可能性があります。ただし、これから説明する手法を使用すると、このようなグラフを迅速かつ簡単に作成できます。
ここからは、図 1 のグラフを生成した C# コードを紹介しながら、詳しく説明します。このコラムでは、C# のコーディングについて中級レベルの知識と WPF についてのごく基本的な知識が読者にあることを前提にしています。ただし、どちらの知識もまったくない場合でも、それほど苦労せずにご理解いただけると思います。きっとこの手法は、皆さんにとって、面白くて便利な新しいスキルになるでしょう。
プロジェクトの準備
まず、Visual Studio 2008 を起動し、WPF アプリケーション テンプレートを使用して新しい C# プロジェクトを作成します。ここでは [新しいプロジェクト] ダイアログ ボックスの右上の領域にあるドロップダウン コントロールから、.NET Framework 3.5 ライブラリを選択しました。このプロジェクトに「BugGraph」という名前を付けます。WPF プリミティブを使用してグラフをプログラムから生成できますが、今回は Microsoft Research ラボによって開発された便利な DynamicDataDisplay ライブラリを使用しました。
このライブラリは、CodePlex オープン ソース ホスト サイトから無料でダウンロードできます (codeplex.com/dynamicdatadisplay、英語)。BugGraph プロジェクトのルート ディレクトリにこのライブラリのコピーを保存し、プロジェクト名を右クリックして [参照の追加] をクリックし、ルート ディレクトリにあるこのライブラリの DLL ファイルをポイントして、この DLL への参照をプロジェクトに追加します。
次に、ソース データを作成します。運用環境では、Excel スプレッドシート、SQL データベース、または XML ファイルにデータが保存されていることが考えられます。ここでは、説明を簡単にするため、単純なテキスト ファイルを使用します。Visual Studio のソリューション エクスプローラー ウィンドウで、プロジェクト名を右クリックして表示されるコンテキスト メニューの [追加] をポイントし、[新しい項目] をクリックします。次に、[テキスト ファイル] を選択し、ファイル名を「BugInfo.txt」に変更して、[追加] ボタンをクリックします。ダミー データを次に示します。
01/15/2010:0:0 02/15/2010:12:5 03/15/2010:60:10 04/15/2010:88:20 05/15/2010:75:50 06/15/2010:50:70 07/15/2010:40:85 08/15/2010:25:95 09/15/2010:18:98 10/15/2010:10:99
各行のコロンで区切られた最初のフィールドは日付を、2 番目のフィールドはその日の時点で未解決であったバグの数を、3 番目のフィールドは解決済みのバグの数を示しています。この後すぐに説明しますが、DynamicDataDisplay ライブラリは、ほとんどの種類のデータを扱えます。
次に、Window1.xaml ファイルをダブルクリックして、プロジェクトの UI 定義を読み込みます。グラフ作成ライブラリ DLL への参照を追加し、次のように WPF の表示領域の Height、Width、および Background の各属性を少し変更します。
xmlns:d3="https://research.microsoft.com/DynamicDataDisplay/1.0"
Title="Window1" WindowState="Normal" Height="500" Width="800" Background="Wheat">
この後、重要なプロット オブジェクトを追加します (図 2 参照)。
図 2 重要なプロット オブジェクトの追加
<d3:ChartPlotter Name="plotter" Margin="10,10,20,10">
<d3:ChartPlotter.HorizontalAxis>
<d3:HorizontalDateTimeAxis Name="dateAxis"/>
</d3:ChartPlotter.HorizontalAxis>
<d3:ChartPlotter.VerticalAxis>
<d3:VerticalIntegerAxis Name="countAxis"/>
</d3:ChartPlotter.VerticalAxis>
<d3:Header FontFamily="Arial" Content="Bug Information"/>
<d3:VerticalAxisTitle FontFamily="Arial" Content="Count"/>
<d3:HorizontalAxisTitle FontFamily="Arial" Content="Date"/>
</d3:ChartPlotter>
ChartPlotter 要素が、メインの表示オブジェクトです。このオブジェクトの定義に、日付型の横軸と整数型の縦軸の宣言を追加しています。DynamicDataDisplay ライブラリの既定の軸の型は、浮動小数点数 (C# の場合は倍精度浮動小数点型 (double)) です。したがって、この型の場合は、明示的な軸の宣言は必要ありません。また、見出しのタイトルの宣言と軸のタイトルの宣言も追加しています。図 3 に、ここまでのデザインを示します。
図 3 BugGraph プログラムのデザイン
ソースの作業に移る
プロジェクトの静的部分を構成できたら、ソース データを読み取り、プログラムからグラフを生成するコードを追加できます。ソリューション エクスプローラー ウィンドウで Window1.xaml.cs をダブルクリックし、コード エディターに C# ファイルを読み込みます。図 4 に図 1 のグラフを生成したプログラムの完全なソース コードを掲載します。
図 4 BugGraph プロジェクトのソース コード
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media; // Pen
using System.IO;
using Microsoft.Research.DynamicDataDisplay; // Core functionality
using Microsoft.Research.DynamicDataDisplay.DataSources; // EnumerableDataSource
using Microsoft.Research.DynamicDataDisplay.PointMarkers; // CirclePointMarker
namespace BugGraph
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Window1_Loaded);
}
private void Window1_Loaded(object sender, RoutedEventArgs e)
{
List<BugInfo> bugInfoList = LoadBugInfo("..\\..\\BugInfo.txt");
DateTime[] dates = new DateTime[bugInfoList.Count];
int[] numberOpen = new int[bugInfoList.Count];
int[] numberClosed = new int[bugInfoList.Count];
for (int i = 0; i < bugInfoList.Count; ++i)
{
dates[i] = bugInfoList[i].date;
numberOpen[i] = bugInfoList[i].numberOpen;
numberClosed[i] = bugInfoList[i].numberClosed;
}
var datesDataSource = new EnumerableDataSource<DateTime>(dates);
datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));
var numberOpenDataSource = new EnumerableDataSource<int>(numberOpen);
numberOpenDataSource.SetYMapping(y => y);
var numberClosedDataSource = new EnumerableDataSource<int>(numberClosed);
numberClosedDataSource.SetYMapping(y => y);
CompositeDataSource compositeDataSource1 = new
CompositeDataSource(datesDataSource, numberOpenDataSource);
CompositeDataSource compositeDataSource2 = new
CompositeDataSource(datesDataSource, numberClosedDataSource);
plotter.AddLineGraph(compositeDataSource1,
new Pen(Brushes.Blue, 2),
new CirclePointMarker { Size = 10.0, Fill = Brushes.Red },
new PenDescription("Number bugs open"));
plotter.AddLineGraph(compositeDataSource2,
new Pen(Brushes.Green, 2),
new TrianglePointMarker { Size = 10.0,
Pen = new Pen(Brushes.Black, 2.0),
Fill = Brushes.GreenYellow },
new PenDescription("Number bugs closed"));
plotter.Viewport.FitToView();
} // Window1_Loaded()
private static List<BugInfo> LoadBugInfo(string fileName)
{
var result = new List<BugInfo>();
FileStream fs = new FileStream(fileName, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string line = "";
while ((line = sr.ReadLine()) != null)
{
string[] pieces = line.Split(':');
DateTime d = DateTime.Parse(pieces[0]);
int numopen = int.Parse(pieces[1]);
int numclosed = int.Parse(pieces[2]);
BugInfo bi = new BugInfo(d, numopen, numclosed);
result.Add(bi);
}
sr.Close();
fs.Close();
return result;
}
} // class Window1
public class BugInfo {
public DateTime date;
public int numberOpen;
public int numberClosed;
public BugInfo(DateTime date, int numberOpen, int numberClosed) {
this.date = date;
this.numberOpen = numberOpen;
this.numberClosed = numberClosed;
}
}} // ns
ここでは、Visual Studio のテンプレートによって生成された、不要な using 名前空間のステートメント (System.Windows.Shapes など) を削除します。そのうえで、DynamicDataDisplay ライブラリの 3 つの名前空間の using ステートメントを追加し、それらの名前を完全に修飾しなくてもすむようにします。次に、Window1 コンストラクターに、メインのプログラム定義のルーチン用のイベントを追加します。
Loaded += new RoutedEventHandler(Window1_Loaded);
メイン ルーチンの最初の処理は次のとおりです。
private void Window1_Loaded(object sender, RoutedEventArgs e)
{
List<BugInfo> bugInfoList = LoadBugInfo("..\\..\\BugInfo.txt");
...
汎用のリスト オブジェクトの bugInfoList を宣言し、LoadBugInfo というプログラム定義のヘルパー メソッド を使用して、BugInfo.txt 内のダミー データをリストに読み込みます。また、バグ情報を整理するため、小さなヘルパー クラス BugInfo を宣言しました (図 5 参照)。
図 5 ヘルパー クラス BugInfo
public class BugInfo {
public DateTime date;
public int numberOpen;
public int numberClosed;
public BugInfo(DateTime date, int numberOpen, int numberClosed) {
this.date = date;
this.numberOpen = numberOpen;
this.numberClosed = numberClosed;
}
}
わかりやすいように、ここでは get および set プロパティを組み合わせて使用するプライベート型ではなく、パブリック型で 3 つのデータ フィールドを宣言しています。この場合、BugInfo はデータにすぎないので、クラスではなく C# 構造体を使用することもできます。LoadBugInfo メソッドは BugInfo.txt ファイルを開いてそのデータを反復処理し、各フィールドを解析して、BugInfo オブジェクトのインスタンスを作成し、各 BugInfo オブジェクトを結果のリストに格納します (図 6 参照)。
図 6 LoadBugInfo メソッド
private static List<BugInfo> LoadBugInfo(string fileName)
{
var result = new List<BugInfo>();
FileStream fs = new FileStream(fileName, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string line = "";
while ((line = sr.ReadLine()) != null)
{
string[] pieces = line.Split(':');
DateTime d = DateTime.Parse(pieces[0]);
int numopen = int.Parse(pieces[1]);
int numclosed = int.Parse(pieces[2]);
BugInfo bi = new BugInfo(d, numopen, numclosed);
result.Add(bi);
}
sr.Close();
fs.Close();
return result;
}
データ ファイルの各行を読み取って処理するのではなく、File.ReadAllLines メソッドを使用してすべての行を読み取って文字列配列にまとめることもできます。ここでは、コードのサイズを抑え、また、わかりやすくなるように、運用環境で実行する通常のエラー チェックは省略しました。
次に、値を宣言して 3 つの配列に割り当てます (図 7 参照)。
図 7 Building 配列
DateTime[] dates = new DateTime[bugInfoList.Count];
int[] numberOpen = new int[bugInfoList.Count];
int[] numberClosed = new int[bugInfoList.Count];
for (int i = 0; i < bugInfoList.Count; ++i)
{
dates[i] = bugInfoList[i].date;
numberOpen[i] = bugInfoList[i].numberOpen;
numberClosed[i] = bugInfoList[i].numberClosed;
}
...
DynamicDataDisplay ライブラリを操作するときは、通常、表示データを 1 次元配列のセットに整理すると便利です。データをリスト オブジェクトに読み取ってから、そのリスト データを配列に渡すというプログラム デザインの代わりに、データを直接配列に読み取ることもできます。
次に、データ配列を特別な EnumerableDataSource 型に変換します。
var datesDataSource = new EnumerableDataSource<DateTime>(dates);
datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));
var numberOpenDataSource = new EnumerableDataSource<int>(numberOpen);
numberOpenDataSource.SetYMapping(y => y);
var numberClosedDataSource = new EnumerableDataSource<int>(numberClosed);
numberClosedDataSource.SetYMapping(y => y);
...
DynamicDataDisplay ライブラリの場合、グラフに展開されるすべてのデータの形式は統一されている必要があります。ここでは、単純に 3 つのデータ配列を汎用の EnumerableDataSource コンストラクターに渡しています。また、各データ ソースにどの軸 (x または y) を関連付けるかをライブラリに指示する必要があります。SetXMapping メソッドと SetYMapping メソッドには、メソッドのデリゲートを引数として指定できます。ここでは、明示的なデリゲートを定義するのではなく、ラムダ式を使用して匿名メソッドを作成しました。DynamicDataDisplay ライブラリの基軸のデータ型は倍精度浮動小数点型 (double) です。SetXMapping メソッドと SetYMapping メソッドは、ここでの特定のデータ型を倍精度浮動小数点型にマップします。
x 軸では、ConvertToDouble メソッドを使用して DateTime データを明示的に倍精度浮動小数点型に変換しています。y 軸では、単純に y => y ("y を y にマップする" という意味) と記述して、暗黙のうちに int 型の y の入力データを倍精度浮動小数点型の y の出力データに変換しています。この場合、SetYMapping(y => Convert.ToDouble(y) と記述して、型マッピングを明示的に行うこともできます。ラムダ式のパラメーターに x と y を使用しているのは特に決まりがあるわけではなく、任意のパラメーター名を使用してかまいません。
次は、x 軸と y 軸のデータ ソースを組み合わせます。
CompositeDataSource compositeDataSource1 = new
CompositeDataSource(datesDataSource, numberOpenDataSource);
CompositeDataSource compositeDataSource2 = new
CompositeDataSource(datesDataSource, numberClosedDataSource);
...
図 1 のスクリーンショットでは、未解決のバグ数と解決済みのバグ数という 2 種類のデータ系列が同じグラフ上にプロットされています。1 つの複合データ ソースによって 1 つのデータ系列が定義されるため、ここでは、未解決のバグ数を表すデータ ソースと解決済みのバグ数を表すデータ ソースの 2 つのデータ ソースが必要です。すべてのデータが準備できたら、たった 1 つのステートメントでデータ要素をプロットできます。
plotter.AddLineGraph(compositeDataSource1,
new Pen(Brushes.Blue, 2),
new CirclePointMarker { Size = 10.0, Fill = Brushes.Red },
new PenDescription("Number bugs open"));
...
AddLineGraph メソッドには、プロットするデータを定義する CompositeDataSource を、データをどのようにプロットするかについての詳細な情報と併せて指定できます。ここでは、plotter というプロッター オブジェクト (Window1.xaml ファイルで定義) に、太さが 2 の青い線を使用してグラフを描画し、枠線が赤で塗りつぶしの色も赤のサイズ 10 の円形のマーカーを配置して、系列のタイトルとして "Number bugs open" を追加するよう指示しています。いい感じですね。また、他にも表現方法は多数ありますが、その 1 つとしては、次を使用できます。
plotter.AddLineGraph(compositeDataSource1, Colors.Red, 1, "Number Open")
この場合は、マーカーがない細い赤線が引かれます。または、次のように実線ではなく点線を作成することもできます。
Pen dashedPen = new Pen(Brushes.Magenta, 3);
dashedPen.DashStyle = DashStyles.DashDot;
plotter.AddLineGraph(compositeDataSource1, dashedPen,
new PenDescription("Open bugs"));
プログラムの最後の処理は、2 つ目のデータ系列のプロットです。
...
plotter.AddLineGraph(compositeDataSource2,
new Pen(Brushes.Green, 2),
new TrianglePointMarker { Size = 10.0,
Pen = new Pen(Brushes.Black, 2.0),
Fill = Brushes.GreenYellow },
new PenDescription("Number bugs closed"));
plotter.Viewport.FitToView();
} // Window1_Loaded()
ここでは、プロッターに緑の線と、枠線が黒で、塗りつぶし色が黄緑の三角形のマーカーを使用するように指示しています。FitToView メソッドは、グラフのサイズを WPF ウィンドウのサイズに合わせて調整します。
Visual Studio から BugGraph プロジェクトをビルドすると、BugGraph.exe 実行可能ファイルが作成されます。このファイルは、いつでも手動でまたはプログラムを使用して起動できます。BugInfo.txt ファイルを編集するだけで、基盤のデータを更新できます。システム全体が .NET Framework コードに基づいているため、テクノロジ間の問題に対処する必要もなく、容易にグラフ作成機能を任意の WPF プロジェクトに統合できます。また、Silverlight 版の DynamicDataDisplay ライブラリもあるため、Web アプリケーションにもプログラムによるグラフ作成機能を追加できます。
散布図
ここまで紹介してきた手法は、テスト関連のデータだけでなく、あらゆる種類のデータに応用できます。簡単ですが、見栄えの良い例をもう 1 つ手短に紹介します。図 8 のスクリーンショットは、13,509 の米国内の都市を示しています。
図 8 散布図の例
おそらく、フロリダ、テキサス、南カリフォルニア、五大湖がどこにあるかおわかりになるでしょう。この散布図のデータは、コンピューター サイエンスにおいて最も有名で広く研究されているトピックの 1 つである、巡回セールスマン問題に使用するためのデータ ライブラリ (www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95、英語) から入手しました。使用したファイル usa13509.tsp.gz は、次のような内容です。
NAME : usa13509
(other header information)
1 245552.778 817827.778
2 247133.333 810905.556
3 247205.556 810188.889
...
13507 489663.889 972433.333
13508 489938.889 1227458.333
13509 490000.000 1222636.111
最初のフィールドは、1 から始まるインデックスの ID です。2 番目と 3 番目のフィールドは、人口が 500 人以上の米国の都市の緯度と経度から成る座標を表しています。前に説明した方法で新しい WPF アプリケーションを作成し、テキスト ファイル項目をプロジェクトに追加して、都市データをこのファイルにコピーします。データ ファイルのヘッダー行は、スラッシュを 2 つ (//) 先頭に付けて、コメント アウトしました。
図 8 の散布図を作成するには、前に紹介した例を少し変更するだけで済みます。MapInfo クラス メンバーを次のように変更します。
public int id;
public double lat;
public double lon;
図 9 に、変更した LoadMapInfo メソッド内の重要な処理ループを示します。
図 9 散布図のループ
while ((line = sr.ReadLine()) != null)
{
if (line.StartsWith("//"))
continue;
else {
string[] pieces = line.Split(' ');
int id = int.Parse(pieces[0]);
double lat = double.Parse(pieces[1]);
double lon = -1.0 * double.Parse(pieces[2]);
MapInfo mi = new MapInfo(id, lat, lon);
result.Add(mi);
}
}
このコードでは、現在の行がプログラム定義のコメント トークンで始まっているかどうかを確認し、始まっている場合はその行をスキップするようにしています。経度を基にしたフィールドに -1.0 を掛けていることに注意してください。これは、経度を x 軸に沿って西から東 (右から左) に展開するためです。-1.0 の倍数がなければ、地図は正しい向きの地図を左右反転させたイメージになるでしょう。
未加工のデータ配列にデータを読み込んだら、確実に緯度と経度を y 軸と x 軸にそれぞれ正しく関連付けるだけです。
for (int i = 0; i < mapInfoList.Count; ++i)
{
ids[i] = mapInfoList[i].id;
xs[i] = mapInfoList[i].lon;
ys[i] = mapInfoList[i].lat;
}
関連付けの順序を逆にすると、結果の地図は横転した状態になるでしょう。データのプロットについては、折れ線グラフではなく散布図が作成されるように 1 箇所小さな変更をするだけでした。
plotter.AddLineGraph(compositeDataSource,
new Pen(Brushes.White, 0),
new CirclePointMarker { Size = 2.0, Fill = Brushes.Red },
new PenDescription("U.S. cities"));
値 0 を Pen コンストラクターに渡すことで、幅が 0 の線を指定しています。これにより、線が消えて、折れ線グラフではなく散布図が作成されます。非常に見栄えがよいグラフを作成でき、しかも、このグラフを生成したプログラムの記述には、わずか数分しかかかりません。地図データをプロットするために本当にさまざまな方法を試しましたが、WPF と DynamicDataDisplay ライブラリを使用する方法が、見つけた中で一番優れたソリューションです。
簡単になったグラフ作成
ここで紹介した手法は、プログラムからグラフを作成するために使用できます。この手法の鍵は、Microsoft Research が提供している DynamicDataDisplay ライブラリです。ソフトウェア運用環境でグラフを作成する単独の手法として使用する場合、この方法が最も有用なのは基盤のデータが頻繁に変更される場合です。グラフを生成するための手法としてアプリケーション内に組み込んで使用する場合は、WPF または Silverlight アプリケーションと使用する場合に最も有用です。WPF と Silverlight が進化するにつれて、間違いなく、この 2 つのテクノロジを基にした素晴らしいビジュアル表示ライブラリが他にも提供されることになるでしょう。
Dr. James McCaffrey は Volt Information Sciences Inc. に勤務し、ワシントン州レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes: A Problem-Solution Approach』(Apress、2006 年) の著者でもあります。連絡先は jammc@microsoft.com (英語のみ) です。