XamDataChart にカスタムのUI要素を追加する

以前のブログで、Ignite UI の igDataChart に任意の Canvas 要素を追加する方法をご紹介しました。Infragistics WPF 製品には、igDataChart と似た構造を持つチャートコントロールの XamDataChart があります。XamDataChart でも同じように、チャートエリアに自由にUI要素を配置いただくことが可能です。

今回は、前回と同様、チャートに最大値と最小値を表すラインとラベル、そしてデータポイントを強調する視覚要素を追加してみたいと思います。

目指すチャートはこちらです。

image

上のチャートは、柱状シリーズを持つ XamDataChart に、カスタムで作成した ContentControl をオーバーレイして(重ねて)作成されています。この ContentControl の Content に、Max および Min のラインとそのラベル、それから「ここ重要!」のラベルと緑の丸印の要素を配置した Canvas インスタンスが割り当てられています。

まずは、オーバーレイの土台となる ContentControl の実装を見てみます。




    public class ChartOverlay : ContentControl

    {

        protected Canvas _overlayCanvas = new Canvas();

        public ChartOverlay()

        {

            Viewport = Rect.Empty;

            HorizontalContentAlignment = HorizontalAlignment.Stretch;

            VerticalContentAlignment = VerticalAlignment.Stretch;

            Content = _overlayCanvas;

        }

 

        public XamDataChart Chart

        {

            get { return (XamDataChart)GetValue(ChartProperty); }

            set { SetValue(ChartProperty, value); }

        }

 

        public static readonly DependencyProperty ChartProperty =

            DependencyProperty.Register(

            "Chart",

            typeof(XamDataChart),

            typeof(ChartOverlay),

            new PropertyMetadata(

                null,

                (o, e) => (o as ChartOverlay).OnChartChanged(

                    (XamDataChart)e.OldValue, (XamDataChart)e.NewValue)));

 

        private void OnChartChanged(XamDataChart oldChart, XamDataChart newChart)

        {

            if (oldChart != null)

            {

                Detach(oldChart);

            }

            if (newChart != null)

            {

                Attach(newChart);

            }

        }

 

        protected XamDataChart _chart = null;

 

        private void Attach(XamDataChart newChart)

        {

            _chart = newChart;

            newChart.RefreshCompleted += RefreshCompleted;

        }

 

        protected Rect Viewport { get; set; }

 

        private void RefreshCompleted(object sender, EventArgs e)

        {

            Viewport = GetChartViewport();

            DoRefresh();

        }

 

        private Rect GetChartViewport()

        {

            if (_chart == null)

            {

                return Rect.Empty;

            }

            var eles =

                _chart.Axes.OfType<FrameworkElement>()

                .Concat(_chart.Series.OfType<FrameworkElement>());

            if (!eles.Any())

            {

                return Rect.Empty;

            }

 

            var first = eles.First();

            Point topLeft;

            try

            {

                topLeft = first.TransformToVisual(this).Transform(new Point(0, 0));

            }

            catch (Exception e)

            {

                return Rect.Empty;

            }

 

            return new Rect(

                topLeft.X,

                topLeft.Y,

                _chart.ViewportRect.Width,

                _chart.ViewportRect.Height);

        }

 

        protected void DoRefresh()

        {

            DoRefreshOverride();

        }

 

        protected virtual void DoRefreshOverride()

        {

        }

 

        private void Detach(XamDataChart oldChart)

        {

            oldChart.RefreshCompleted -= RefreshCompleted;

        }

    }


上記の ChartOverlay クラスは、Content に空の Canvas が割り当てられている状態です。ターゲットとなる XamDataChart の情報をもち、チャートが再描画された際に発生する RefreshCompleted() イベントでオーバーレイのリフレッシュを行う機能が実装されています。

あとは上記の ChartOverlay クラスを継承し、Canvas に表示したい要素を配置するのみです。


    public class MyCustomOverlay : ChartOverlay
    {

          //ここに以下で紹介するコードを追加します。

    }


それでは、MyCustomOverlay クラスのコードをご紹介します。

まずはメンバー変数の定義です。ここでは、最大値と最小値(タイプ: double )、また「ここ重要!」を付与する座標軸(タイプ: Point )を保持するプロパティ、それからUI要素である Line や TextBlock、Ellipse の宣言とインスタンス化を行っています。また、軸情報( CategoryXAxis、NumericYAxis )には頻繁にアクセスを行うため、getter を用意しています。


        public double MyMaxY { get; set; }

        public double MyMinY { get; set; }
        public Point HighlightP { get; set; }

        private Line maxLine = new Line()

        {

            Stroke = new SolidColorBrush(Colors.Red),

            StrokeThickness = 2

        };


        private Line minLine = new Line()

        {

            Stroke = new SolidColorBrush(Colors.Blue),

            StrokeThickness = 2

        };

        private TextBlock maxLabel = new TextBlock()

        {

            Foreground = new SolidColorBrush(Colors.Red),

        };


        private TextBlock minLabel = new TextBlock()

        {

            Foreground = new SolidColorBrush(Colors.Blue),

        };


        private Ellipse circle = new Ellipse()

        {

            StrokeThickness = 2,

            Stroke = new SolidColorBrush(Colors.Green),

            Width = 50,

            Height = 50

        };


        private TextBlock circleLabel = new TextBlock()

        {

            Foreground = new SolidColorBrush(Colors.Green),

            Text = "ここ重要!"

        };

        protected CategoryXAxis XAxis

        {

            get{ return _chart.Axes.OfType<CategoryXAxis>().First(); }

        }

        protected NumericYAxis 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 = new Point(3, 430);

        }


これでUI要素は Canvas に追加されました。次に、これらの要素を Canvas.SetTop()、Canvas.SetLeft() を使用して Canvas 上のあるべき位置に移動させます。以下の drawElements() メソッドでこれを行います。

        private void 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() があります。このメソッドを、以下のようにオーバーライドします。

        protected override void DoRefreshOverride()
       
{

           base.DoRefreshOverride();

            if (Viewport.IsEmpty)
           
{

                return;

            }

            SetClipRectangle();

            drawElements();
       
}


最終行で先ほどの drawElements() メソッドを実行しています。その一行前の SetClipRectangle() ですが、これは Canvas を、表示したい部分を残して切り取る作業です。チャートのプロットエリアのみをオーバーレイの対象とするならばViewPort の大きさでクリップすればよいですが、今回は最大値と最小値のラベルをプロットエリアの右側に表示したいため、右側に少々マージンをとってクリップします。

        private void SetClipRectangle()

        {

           _overlayCanvas.Clip = new RectangleGeometry()

            {

                 Rect = new Rect(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バージョンを使用して作成されました)

Posted: 25 Jul 2017, 15:46

Comments

No Comments

Anonymous comments are disabled