すてきな ASP.NET
ASP.NET と LINQ を使用してグラフを作成する
K. Scott Allen
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照
目次
準備
グラフの作成
データを挿入する
グラフからダッシュボードへ
学んだグラフ作成技術を活かす
マイクロソフトは最近、ASP.NET 3.5 SP1 向けの新しいグラフ コントロールをリリースしました。グラフ コントロールは使いやすいコントロールです。ASP.NET Web アプリケーションに魅力的なデータ視覚化機能を追加するために使用できます。このコントロールは、折れ線グラフや円グラフなどの標準的なグラフをすべてサポートしているうえに、じょうごグラフやピラミッド グラフなどを視覚化する高度な機能もサポートしています。このコラムでは、グラフ コントロールについて説明し、LINQ to Objects クエリを使用してある種のデータを生成します。このコラムの完全なソース コードは、MSDN コード ギャラリーから入手できます。
図 1 Visual Studio ツール
準備
まず、Microsoft ダウンロード センターからグラフ コントロールをダウンロードします。このダウンロード ファイルによって、グラフの作成に必要な重要なランタイム コンポーネントがインストールされます (たとえば、System.Web.DataVisualization アセンブリがグローバル アセンブリ キャッシュに追加されます)。
Visual Studio 2008 ツール サポートとグラフ作成の際に利用できるサンプル Web サイトもダウンロードすることをお勧めします。このツール サポートをインストールすると、デザイン時にツールボックスが統合され、IntelliSense がサポートされます。また、サンプル Web サイトには数百にのぼるサンプルが含まれており、必要な種類のグラフを作成する際に参考にできます。なお、Microsoft .NET Framework 3.5 ランタイムと Visual Studio 2008 の Service Pack 1 をそれぞれインストールする必要があります。
インストールがすべて完了したら、Visual Studio で新しい ASP.NET プロジェクトを作成できます。そして [ツールボックス] ウィンドウに Chart コントロールが表示されます (図 1 を参照)。デザイン ビューを使用してグラフを ASPX ファイルにドラッグすることも (この場合、必要な構成の変更が web.config ファイルに加えられます)、ASPX ファイルのソース ビューでコントロールを直接操作することもできます (この場合は、構成を手動で変更する必要があります。これについては後ほど説明します)。
Web フォーム デザイナにグラフを追加すると、Visual Studio によって、web.config ファイルの <controls> セクションに次のような新しいエントリが追加されます。
<add tagPrefix="asp"
namespace="System.Web.UI.DataVisualization.Charting"
assembly="System.Web.DataVisualization,
Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
このエントリにより、他の組み込みの ASP.NET コントロールで使用する、おなじみの asp タグ プレフィックスを持つグラフ コントロールを使用できるようになります。このほかにも、新しい HTTP ハンドラ エントリが IIS 7.0 の <handlers> セクションに追加され、同様のエントリが <httpHandlers> セクションに追加されます (IIS 6.0 および Visual Studio Web 開発サーバーで使用するため)。
<add name="ChartImageHandler"
preCondition="integratedMode"
verb="GET,HEAD"
path="ChartImg.axd"
type="System.Web.UI.DataVisualization.Charting.Chart HttpHandler,
System.Web.DataVisualization,
Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
この HTTP ハンドラは、到着した ChartImg.axd の要求を処理します。これは、グラフ コントロールがグラフを提供するために使用する既定のエンドポイントです。HTTP ハンドラについては、後ほど詳しく説明します。図 2 のコードにはグラフの基本要素が示されています。どのグラフにも、データが設定された Series オブジェクトが少なくとも 1 つは含まれます。各系列の ChartType プロパティによって、系列を描画するために使用されるグラフの種類が決まります (既定の種類は縦棒グラフです)。各グラフには、1 つ以上の ChartArea オブジェクト (描画が行われる場所) を含めることもできます。
図 2 基本的なグラフ
<asp:Chart ID="Chart1" runat="server" Height="300" Width="400">
<Series>
<asp:Series BorderColor="180, 26, 59, 105">
<Points>
<asp:DataPoint YValues="45" />
<asp:DataPoint YValues="34" />
<asp:DataPoint YValues="67" />
</Points>
</asp:Series>
</Series>
<ChartAreas>
<asp:ChartArea />
</ChartAreas>
</asp:Chart>
背景、軸、タイトル、凡例、ラベルなど、ASP.NET グラフ コントロールのほとんどすべての視覚要素をカスタマイズできます。グラフ コントロールはカスタマイズ性がきわめて高いので、アプリケーション内で使用するグラフの外観の一貫性を保つために、何らかの対策が必要です。このコラムでは、すべてのグラフに一貫性のあるフォントと色を適用するため、ビルダの手法を使用します。このビルダ クラスにより、ASP.NET ページの外にあるグラフ コントロールを使用できるようにもなります。
グラフの作成
このコラムで使うサンプル データを探しているとき、株式市場の過去のデータを使うことも少し考えたのですが、深刻な経済状況が続いているためにここ 1 年間のデータはひどい有様なので、代わりに米国の運輸統計局 (bts.gov) のデータを使うことにしました。このコラムのサンプル アプリケーションには、2008 年 1 月に筆者の最寄りの空港 (ボルチモア ワシントン国際空港/BWI) を出発したすべての国内便に関する情報を収めたファイルが含まれています。データには、目的地、距離、タキシング時間、遅延時間などがあります。クラスを使用してこのデータを C# で表したものを図 3 に示します。
図 3 フライト データ
public class Flight {
public DateTime Date { get; set; }
public string Airline { get; set; }
public string Origin { get; set; }
public string Destination { get; set; }
public int TaxiOut { get; set; }
public int TaxiIn { get; set; }
public bool Cancelled { get; set; }
public int ArrivalDelay { get; set; }
public int AirTime { get; set; }
public int Distance { get; set; }
public int CarrierDelay { get; set; }
public int WeatherDelay { get; set; }
}
このデータを使用して最初に作成したグラフは、ボルチモア空港を出発する便の目的地のうち、上位の目的地を表示するグラフです (図 4 を参照)。このグラフを作成するために使用したのは、ASPX ファイル内のごくわずかなコードと、関連の分離コード ファイルです。ASPX ファイルにはページ上のグラフ コントロールが含まれていますが、次のように Width プロパティと Height プロパティだけが設定されています。
<form id="form1" runat="server">
<div>
<asp:Chart runat="server" Width="800" Height="600" ID="_chart">
</asp:Chart>
</div>
</form>
図 4 上位の目的地 (クリックすると拡大画像が表示されます)
次に示すように、分離コードは、その Page_Load イベントの処理作業を TopDestinationsChartBuilder クラスにすべて委任します。
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
var builder = new TopDestinationsChartBuilder(_chart);
builder.BuildChart();
}
}
TopDestinationsChartBuilder は ChartBuilder クラスから継承します。この ChartBuilder 基本クラスは、テンプレート メソッド デザイン パターンを使用してグラフのパーツを構成します。このテンプレート メソッドは、見た目に一貫性のある便利なグラフを生成するのに必要な、基本的なアルゴリズムを指定する一方、構成されたパーツをカスタマイズするためのサブクラスのフックを提供します。このテンプレート メソッドの名前は BuildChart です。図 5 を参照してください。
図 5 BuildChart
public void BuildChart() {
_chart.ChartAreas.Add(BuildChartArea());
_chart.Titles.Add(BuildChartTitle());
if (_numberOfSeries > 1) {
_chart.Legends.Add(BuildChartLegend());
}
foreach (var series in BuildChartSeries()) {
_chart.Series.Add(series);
}
}
ChartBuilder クラスの各 Build メソッドは、Customize メソッドに関連付けられています。Build メソッドは、そのグラフのパーツを構成した後、Customize メソッドを呼び出します。派生クラスでは、カスタマイズ メソッドをオーバーライドしてグラフ固有の設定を適用できます。その一例が、図 6 に示す BuildChartTitle メソッドです。TopDestinationsChartBuilder クラスは、CustomizeChartTitle をオーバーライドしてタイトルの特定のテキストを適用します。
protected override void CustomizeChartTitle(Title title)
{
title.Text = "Top Destinations";
}
図 6 BuildChartTitle
private Title BuildChartTitle() {
Title title = new Title() {
Docking = Docking.Top,
Font = new Font("Trebuchet MS", 18.0f, FontStyle.Bold),
};
CustomizeChartTitle(title);
return title;
}
protected virtual void CustomizeChartTitle(Title title) { }
TopDestinationsChartBuilder の処理のほとんどは、表示するすべてのデータ ポイントの追加など、グラフの系列をカスタマイズするためのものです。さいわいにも、LINQ to Objects を使用すると、Flight オブジェクトのコレクションから目的地の上位 5 都市を驚くほど簡単に見つけることができます (図 7 を参照)。このコードでは、まず LINQ の GroupBy 演算子を適用して、フライトを目的地ごとにグループ分けします。次に、OrderByDescending 演算子により、各グループのフライトの数を基にグループのシーケンスを並べ替えます。最後に、Take 演算子を使用して、そのシーケンスから目的地の上位 5 都市を特定します。
図 7 上位 5 都市を取得する
protected override void CustomizeChartSeries(IList<Series> seriesList) {
var repository = FlightRepositoryFactory.CreateRepository();
var query = repository.Flights
.GroupBy(flight => flight.Destination)
.OrderByDescending(group => group.Count())
.Take(5);
Series cities = seriesList.Single();
cities.Name = "Cities";
foreach (var record in query) {
cities.Points.AddXY(record.Key, record.Count());
}
}
LINQ クエリは、グループのシーケンスを生成します。これらのグループをループ処理し、情報をグラフに追加できます。各グループの Key プロパティは、GroupBy 演算子で選択されたキーの値を表します (この場合は、Destination (目的地) の値です)。このコードでは、各データ ポイントの X 値として目的地を使用しています。各グループには、その目的地にグループ分けされている Flight オブジェクトの列挙可能なシーケンスも含まれています。Count 演算子は、各目的地へのフライトの合計数を取得し、その数を各データ ポイントの Y 値として追加するためだけに使用します。
データを挿入する
グラフ系列のポイントにデータを一度に追加する代わりに、グラフの DataPointCollection で DataBindXY メソッドを使用することにより、ループを使用せずにデータ ポイントのシーケンスを挿入できます。この処理は、日付別にすべての遅延の合計時間 (分) を計算する DelaysByDayChartBuilder で行うことができます。このビルダは、天候不順による遅延の合計時間を表示する副次的なデータ系列も生成します。このビルダではまず、フライトを日付別にグループ分けする LINQ クエリを使用します。ここでは、各グループの Key プロパティは日付を表します (1 月の場合は 1 ~ 31)。
var query = repository.Flights
.GroupBy(flight => flight.Date.Day)
.OrderBy(group => group.Key)
.ToList();
図 8 のコードでは、DataBindXY メソッドを使用しています。まず、Select 演算子を使用して各グループの Key 値を取得することで、すべての X 値がリストに収集されます。次に、追加の処理が最初のグループ化クエリに適用され、各グループで ArrivalDelay 値および WeatherDelay 値の合計が計算されます。
図 8 ループを使用せずに値を挿入する
var xValues = query.Select(group => group.Key).ToList();
totalDelaySeries.Points.DataBindXY(
xValues,
query.Select(
group => group.Sum(
flight => flight.ArrivalDelay)).ToList());
weatherDelaySeries.Points.DataBindXY(
xValues,
query.Select(
group => group.Sum(
flight => flight.WeatherDelay)).ToList());
ご覧のように、標準的な LINQ 演算子を使用すると、レポートやグラフに使用するデータを簡単に収集、操作できます。もう 1 つの好例が TaxiTimeChartBuilder です。このクラスは、曜日別に合計タクシング時間を表示するレーダー チャートを構成します。タクシング時間とは、飛行機がゲートを離れてから空港の滑走路を離陸するまでの時間のことです。混雑した空港では、飛行機は滑走路が空くのを待たなければならないため、タクシング時間が非常に長くなる場合があります。TaxiTimeChartBuilder は、タクシング時間がしきい値を超えているデータ ポイントを強調表示します。系列のデータ ポイントに対して LINQ クエリを使用すると、この処理を簡単に行うことができます。
var overThresholdPoints =
taxiOutSeries.Points
.Where(p => p.YValues.First() > _taxiThreshold);
foreach (var point in overThresholdPoints)
{
point.Color = Color.Red;
}
ここでは、指定したしきい値を超過しているすべてのデータ ポイントの色が赤に変更されています。この動作から、皆さんは空港のダッシュボード レポートを思い浮かべるかもしれません。そこで、次はダッシュボードについて見てみましょう。
グラフからダッシュボードへ
さまざまなビジネス インテリジェンス グループで、ビジネスの主要パフォーマンス情報をグラフィックで表示するためにダッシュボードが使用されています。このパフォーマンス情報はさまざまなソースから提供でき、一連のグラフとウィジェットを使用して視覚化できます。ユーザーは、ダッシュボードの表示をちらりと見て、ビジネスに悪影響を及ぼす可能性のある異常をすばやく特定できなければなりません。
ダッシュボードの表示を生成する際の課題の 1 つが、パフォーマンスです。ダッシュボード用のすべての情報を集約する処理により、リレーショナル データベースと多次元データベースに対して多数のクエリが行われる可能性があります。ダッシュボード用の未加工のデータがキャッシュされる場合でも、ダッシュボード上の多数のグラフとウィジェットにより、サーバーに負荷がかかることがあります。
図 9 に、上位の目的地、曜日別のタクシング時間、曜日ごとの合計フライト数、日付別の合計遅延時間を表示する、シンプルな空港用ダッシュボードを示します。このダッシュボードを構築するときのアプローチの 1 つは、1 つの Web ページ上に 4 つのグラフ コントロールをすべて配置し、グラフを構成するために構築したグラフ ビルダ クラスを使用するというものです。ここで、各グラフを構築するのに 2 秒かかる場合を考えてください。一般に、複雑なクエリの場合は 2 秒は決して長い時間ではありません。しかしこのページにはグラフが 4 つあるため (これはダッシュボードとしては少ない数字です)、最初のグラフィックが表示されるまで、ユーザーは少なくとも 8 秒は待つことになります。
図 9 空港活動ダッシュボード
このダッシュボード ページを構築する場合、ページ内にグラフごとのプレースホルダを定義し、グラフのイメージを非同期的に構築するというアプローチもあります。非同期的なアプローチを用いた場合、ユーザーに対し、表示できるようになった最初の結果から順次表示することができます。このソリューションでは、JavaScript で Windows Communication Foundation (WCF) プロキシを利用して、表示可能になったグラフを順次表示できます。
さいわいなことに、筆者がグラフ向けに定義したビルダ クラスにより、グラフ生成ロジックを WCF Web サービスの背後により簡単に移動できます。図 10 に WCF サービス メソッドを示します。このメソッドは、まず空のグラフを構築してから、プロジェクトに実装されている ChartBuilder 派生クラスのいずれかを構築します。ビルダはメソッドのパラメータとして指定されています。コードは既知のビルダのマップとこのパラメータを照合し、(Activator.CreateInstance を使用して) インスタンス化するビルダの実際の型を探します。
図 10 ビルダをインスタンス化する
[OperationContract]
public string GenerateChart(string builderName) {
Chart chart = new Chart() {
Width = 500, Height = 300
};
ChartBuilder builder =
Activator.CreateInstance(_typeMap[builderName], chart)
as ChartBuilder;
builder.BuildChart();
return SaveChart(builderName, chart);
}
既知のビルダの型のマップを使用した場合、メソッドに実際の型名を渡す必要がなくなります。また、インターネットを介した悪意のある入力に対する防御層を設けることもできます。サービスが把握している型だけをインスタンス化します。
Web サービスでグラフ イメージを生成するのは、簡単ではありません。既に説明したように、グラフでは HTTP ハンドラを使用してしてクライアント上にグラフを描画します。具体的には、Chart コントロールがグラフの完全なイメージを表すバイトを ChartHttpHandler クラスに渡します。そして ChartHttpHandler は一意の識別子を返します。Chart コントロールは、描画を行うとき、ChartImg.axd を参照する src 属性を持つ一般的な HTML <img> タグを生成します。この属性には、クエリ文字列内の一意の識別子が含まれます。このイメージの要求が HTTP ハンドラに到達すると、ハンドラは表示する適切なグラフを検索できます。あらゆる構成オプションを含め、このプロセスの詳細については、Delian Tchoparinov のブログを参照してください。
残念なことに、HTTP ハンドラの API はパブリックではないため、Web サービスでは使用できません。そこで、SaveChart メソッドを使用します。図 10 のサービス コードが呼び出す SaveChart メソッドは、Chart コントロールの SaveImage メソッドを使用して、グラフ イメージをファイル システムに PNG 形式で書き込みます。その後、サービスはファイルの名前をクライアントに返します。物理ファイルを生成しておくと、キャッシング手法を使用して、高負荷の間、クエリとイメージの生成を避けることもできます。
図 11 のコードでは、JavaScript から WCF サービスを使用し、各グラフのイメージ プレースホルダの src 属性を設定しています (JavaScript を使用して JavaScript プロキシを生成し、Web サービスを呼び出す方法については、Fritz Onion のコラム「AJAX Extensions を使用してクライアント側から Web サービスを呼び出す」を参照してください)。各グラフのドキュメント オブジェクト モデル (DOM) 識別子およびビルダ クラスは、"コンテキスト" オブジェクトに含まれる JavaScript 配列で定義されます。このコンテキストは、Web サービス プロキシ メソッドの userState パラメータでの Web サービスの連続呼び出しを通じてトンネリングされます。JavaScript は、コンテキスト オブジェクトを使用してダッシュボード グラフの更新処理の進捗状況を追跡します。動的なダッシュボード ページの場合、サーバーは配列を動的に生成できます。
図 11 グラフを更新する
/// <reference name="MicrosoftAjax.js" />
function pageLoad() {
var context = {
index: 0,
client: new ChartingService(),
charts:
[
{ id: "topDestinations", builder: "TopDestinations" },
{ id: "taxiTime", builder: "TaxiTime" },
{ id: "dayOfWeek", builder: "DayOfWeek" },
{ id: "delaysByDay", builder: "DelaysByDay" }
]
};
context.client.GenerateChart(
context.charts[context.index].builder,
updateChart,
displayError,
context);
}
function updateChart(result, context) {
var img = $get(context.charts[context.index].id);
img.src = result;
context.index++;
if (context.index < context.charts.length) {
context.client.GenerateChart(
context.charts[context.index].builder,
updateChart, displayError, context);
}
}
function displayError() {
alert("There was an error creating the dashboard charts");
}
学んだグラフ作成技術を活かす
このコラムでは、テンプレート メソッド デザイン パターン、LINQ 演算子、JavaScript 対応の WCF Web サービスなど、さまざまな技術を取り上げました。このコラムで説明したのは、グラフ コントロールで使用できる機能のごく一部にすぎません。そのため、必ずサンプル Web サイトをチェックし、使用できる豊富な機能を確認してください。このすばらしい視覚化ツールと、柔軟性と表現力に富んだ LINQ を組み合わせて使用すれば、今後、柔軟性、有効性、実用性に優れたグラフ作成アプリケーションを作成できます。
K. Scott Allen は Pluralsight 技術スタッフのメンバであり、OdeToCode の創設者です。彼の連絡先は scott@OdeToCode.com、ブログのアドレスは odetocode.com/blogs/scott です。