以前のブログで、Ignite UI の igDataChart に任意の Canvas 要素を追加する方法をご紹介しました。Infragistics WPF 製品には、igDataChart と似た構造を持つチャートコントロールの XamDataChart があります。XamDataChart でも同じように、チャートエリアに自由にUI要素を配置いただくことが可能です。
今回は、前回と同様、チャートに最大値と最小値を表すラインとラベル、そしてデータポイントを強調する視覚要素を追加してみたいと思います。
目指すチャートはこちらです。
上のチャートは、柱状シリーズを持つ XamDataChart に、カスタムで作成した ContentControl をオーバーレイして(重ねて)作成されています。この ContentControl の Content に、Max および Min のラインとそのラベル、それから「ここ重要!」のラベルと緑の丸印の要素を配置した Canvas インスタンスが割り当てられています。
まずは、オーバーレイの土台となる ContentControl の実装を見てみます。
publicclassChartOverlay : ContentControl
{
protectedCanvas _overlayCanvas = newCanvas();
public ChartOverlay()
{
Viewport = Rect.Empty;
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
Content = _overlayCanvas;
}
publicXamDataChart Chart
{
get { return (XamDataChart)GetValue(ChartProperty); }
set { SetValue(ChartProperty, value); }
}
publicstaticreadonlyDependencyProperty ChartProperty =
DependencyProperty.Register(
"Chart",
typeof(XamDataChart),
typeof(ChartOverlay),
newPropertyMetadata(
null,
(o, e) => (o asChartOverlay).OnChartChanged(
(XamDataChart)e.OldValue, (XamDataChart)e.NewValue)));
privatevoid OnChartChanged(XamDataChart oldChart, XamDataChart newChart)
{
if (oldChart != null)
{
Detach(oldChart);
}
if (newChart != null)
{
Attach(newChart);
}
}
protectedXamDataChart _chart = null;
privatevoid Attach(XamDataChart newChart)
{
_chart = newChart;
newChart.RefreshCompleted += RefreshCompleted;
}
protectedRect Viewport { get; set; }
privatevoid RefreshCompleted(object sender, EventArgs e)
{
Viewport = GetChartViewport();
DoRefresh();
}
privateRect GetChartViewport()
{
if (_chart == null)
{
returnRect.Empty;
}
var eles =
_chart.Axes.OfType<FrameworkElement>()
.Concat(_chart.Series.OfType<FrameworkElement>());
if (!eles.Any())
{
returnRect.Empty;
}
var first = eles.First();
Point topLeft;
try
{
topLeft = first.TransformToVisual(this).Transform(newPoint(0, 0));
}
catch (Exception e)
{
returnRect.Empty;
}
returnnewRect(
topLeft.X,
topLeft.Y,
_chart.ViewportRect.Width,
_chart.ViewportRect.Height);
}
protectedvoid DoRefresh()
{
DoRefreshOverride();
}
protectedvirtualvoid DoRefreshOverride()
{
}
privatevoid Detach(XamDataChart oldChart)
{
oldChart.RefreshCompleted -= RefreshCompleted;
}
}
上記の ChartOverlay クラスは、Content に空の Canvas が割り当てられている状態です。ターゲットとなる XamDataChart の情報をもち、チャートが再描画された際に発生する RefreshCompleted() イベントでオーバーレイのリフレッシュを行う機能が実装されています。
あとは上記の ChartOverlay クラスを継承し、Canvas に表示したい要素を配置するのみです。
publicclassMyCustomOverlay : ChartOverlay
{
//ここに以下で紹介するコードを追加します。
}
それでは、MyCustomOverlay クラスのコードをご紹介します。
まずはメンバー変数の定義です。ここでは、最大値と最小値(タイプ: double )、また「ここ重要!」を付与する座標軸(タイプ: Point )を保持するプロパティ、それからUI要素である Line や TextBlock、Ellipse の宣言とインスタンス化を行っています。また、軸情報( CategoryXAxis、NumericYAxis )には頻繁にアクセスを行うため、getter を用意しています。
publicdouble MyMaxY { get; set; }
publicdouble MyMinY { get; set; }
publicPoint HighlightP { get; set; }
privateLine maxLine = newLine()
{
Stroke = newSolidColorBrush(Colors.Red),
StrokeThickness = 2
};
privateLine minLine = newLine()
{
Stroke = newSolidColorBrush(Colors.Blue),
StrokeThickness = 2
};
privateTextBlock maxLabel = newTextBlock()
{
Foreground = newSolidColorBrush(Colors.Red),
};
privateTextBlock minLabel = newTextBlock()
{
Foreground = newSolidColorBrush(Colors.Blue),
};
privateEllipse circle = newEllipse()
{
StrokeThickness = 2,
Stroke = newSolidColorBrush(Colors.Green),
Width = 50,
Height = 50
};
privateTextBlock circleLabel = newTextBlock()
{
Foreground = newSolidColorBrush(Colors.Green),
Text = "ここ重要!"
};
protectedCategoryXAxis XAxis
{
get{ return _chart.Axes.OfType<CategoryXAxis>().First(); }
}
protectedNumericYAxis YAxis
{
get{ return _chart.Axes.OfType<NumericYAxis>().First(); }
}
次に、コンストラクタで上記のUI要素を Canvas に追加します。MyMaxY、MyMinY、HighlightP はそれぞれ最大値と最小値の線を描画する Y 軸値とデータを強調表示する X 軸値ですが、ここでハードコードしています。これらは外からアクセス可能なプロパティですので、お好みのタイミングで良いでしょう。
public MyCustomOverlay()
{
_overlayCanvas.Children.Add(maxLine);
_overlayCanvas.Children.Add(minLine);
_overlayCanvas.Children.Add(maxLabel);
_overlayCanvas.Children.Add(minLabel);
_overlayCanvas.Children.Add(circle);
_overlayCanvas.Children.Add(circleLabel);
MyMaxY = 920;
MyMinY = 100;
HighlightP = newPoint(3, 430);
}
これでUI要素は Canvas に追加されました。次に、これらの要素を Canvas.SetTop()、Canvas.SetLeft() を使用して Canvas 上のあるべき位置に移動させます。以下の drawElements() メソッドでこれを行います。
privatevoid drawElements()
{
var maxYVal = YAxis.ScaleValue(MyMaxY) + Viewport.Top;
maxLine.X1 = Viewport.Left;
maxLine.X2 = Viewport.Right;
maxLine.Y1 = maxYVal;
maxLine.Y2 = maxYVal;
var minYVal = YAxis.ScaleValue(MyMinY) + Viewport.Top;
minLine.X1 = Viewport.Left;
minLine.X2 = Viewport.Right;
minLine.Y1 = minYVal;
minLine.Y2 = minYVal;
maxLabel.Text = "Max:" + MyMaxY;
Canvas.SetTop(maxLabel, maxYVal - maxLabel.ActualHeight / 2);
Canvas.SetLeft(maxLabel, Viewport.Right);
minLabel.Text = "Min:" + MyMinY;
Canvas.SetTop(minLabel, minYVal - minLabel.ActualHeight / 2);
Canvas.SetLeft(minLabel, Viewport.Right);
var cicleX = XAxis.ScaleValue(HighlightP.X) + Viewport.Left + (XAxis.ScaleValue(1) - XAxis.ScaleValue(0)) / 2 - circle.ActualWidth / 2;
var cicleY = YAxis.ScaleValue(HighlightP.Y) + Viewport.Top - circle.ActualHeight / 2;
Canvas.SetTop(circle, cicleY);
Canvas.SetLeft(circle, cicleX);
Canvas.SetTop(circleLabel, cicleY - circleLabel.ActualHeight);
Canvas.SetLeft(circleLabel, cicleX);
}
軸の ScaleValue() メソッドや ViewPort 変数を利用して軸の値から Canvas における位置を求めています。ViewPort は ChartOverlay クラスから継承している変数ですが、これは Canvas 上での XamDataChart のプロット領域(軸ラベル等を除く、チャートのプロット可能な矩形領域です)の座標を表す Rectangle です。
これで要素を正しい位置に配置する準備が整いました。
さて、この drawElements() メソッドを実行するタイミングですが、チャートの再描画に合わせてオーバーレイも書き換えが必要です。XamDataChart のサイズが変わったり、ズームレベルが変更されたりして XamDataChart が再描画される都度、Canvas 上におけるUI要素の位置は再計算される必要があるためです。ChartOverlay クラスにはそのためのメソッド DoRefreshOverride() があります。このメソッドを、以下のようにオーバーライドします。
protectedoverridevoid DoRefreshOverride()
{
base.DoRefreshOverride();
if (Viewport.IsEmpty)
{
return;
}
SetClipRectangle();
drawElements();
}
最終行で先ほどの drawElements() メソッドを実行しています。その一行前の SetClipRectangle() ですが、これは Canvas を、表示したい部分を残して切り取る作業です。チャートのプロットエリアのみをオーバーレイの対象とするならばViewPort の大きさでクリップすればよいですが、今回は最大値と最小値のラベルをプロットエリアの右側に表示したいため、右側に少々マージンをとってクリップします。
privatevoid SetClipRectangle()
{
_overlayCanvas.Clip = newRectangleGeometry()
{
Rect = newRect(Viewport.X, Viewport.Y, Viewport.Width + 50, Viewport.Height)
};
}
MyCustomOverlay クラスの実装は以上です。
それでは、実際にMyCustomOverlay を配置してみましょう。
Xaml に、XamDataChart と MyCustomOverlay を定義します。
<ig:XamDataChart x:Name="xamDataChart1" Margin="0,0,50,0" HorizontalZoomable="True" VerticalZoomable="True">
<ig:XamDataChart.Axes>
<ig:CategoryXAxis x:Name="xAxis" ItemsSource="{Binding}" Label="{}{Label}" Gap="1"/>
<ig:NumericYAxis x:Name="yAxis" MinimumValue="0" MaximumValue="1000"/>
</ig:XamDataChart.Axes>
<ig:XamDataChart.Series>
<ig:ColumnSeries XAxis="{Binding ElementName=xAxis}"
YAxis="{Binding ElementName=yAxis}"
ItemsSource="{Binding}"
ValueMemberPath="Value1">
</ig:ColumnSeries>
</ig:XamDataChart.Series>
</ig:XamDataChart>
<local:MyCustomOverlay x:Name="myCustomOverlay" Chart="{Binding ElementName=xamDataChart1}" MyMaxY="920" MyMinY="100">
<local:MyCustomOverlay.HighlightP>
<Point X="3" Y="430" />
</local:MyCustomOverlay.HighlightP>
</local:MyCustomOverlay>
MyCustomOverlay の Chart プロパティにターゲットとなる XamDataChart を指定します。
MyMinY、MyMaxY、HighlightP はコンストラクタでハードコードしましたが、プロパティとしてこのように指定することもできます。XamDataChartにバインドするデータはここでは省略していますので、詳しい内容は下のリンクよりサンプルをダウンロードして内容をご確認ください。
今回のサンプルの実装は以上です。
チャートに自由にアノテーション- Annotations -を追加し、ご要件に合わせたデータ表示を演出してください。
サンプルはこちらから。
(当サンプルは17.1.20171.1000バージョンを使用して作成されました)