WPF Charts (Part 3)

Chart3

In my last post about charts I added a small feature to obtain object information of any chart element under the mouse cursor.

Today we are doing the next logical step. I added proper coordinates in axis units (here EUROs), time and percent units. You hover over the chart and you don’t have to stop at a specific point. The program tells you the position in relation to each axis. We have three of them, so make sure you read the right output. I believe this is more useful than just being able to read curve point hover events. Sometimes you want to know some values in between – in the middle of nowhere. And we are solving this problem now.

I kept the code flexible. There is barely any hard-coding. You might also realize some redundant checks. They may be stupid at this stage. But think practically; many people copy the code and change it according to their needs. You quickly forget implementing the little checks, which were not necessary in the first place.

Like in the last chart posts, we are dealing with two line curves. There is a third one today, which is generated at runtime. Have a look at the corresponding C# code. In hindsight it always gives a better understanding of XAML code. WPF is not as straight forward as WinForms. It requires some training here and there. For instance, you cannot simply change the height property of an object. You need to use the hard way via setters.

The third curve is a manual line. Use the left mouse button (OnMouseLeftButtonDown event) to draw it. When you do this for the first time, the line is added to the chart. It did not exist before and was not just hidden. The events OnMouseMove and OnMouseLeftButtonUp are used to complete the logic. All three are required to draw the line while the chart keeps on adding points in the background. A small bonus are the proper axis coordinates. Despite subsequent new points, which are timer based, the manual line remains scaled. The new curve is linked to the same axes as the first curve (search for: LineSeries lLineSeries = lChart.Series[0] as LineSeries; …. lRangeAxis = xLineSeries.ActualDependentRangeAxis as RangeAxis; … lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint); ).

 

<Window x:Class="Demo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Demo"
    xmlns:datavisualization="clr-namespace:System.Windows.Controls.DataVisualization;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    xmlns:chart="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    Title="Demo Window"
    Loaded ="Window_Loaded">

    <Grid>
        <TextBox Name="InfoBox" Text="" Height="Auto"/>
        <chart:Chart Name="myChart" Title="2014" Width="Auto" Height="Auto" MinWidth="400" MinHeight="300"
                     MouseMove="OnMouseMove"
                     MouseLeftButtonDown="OnMouseLeftButtonDown"
                     MouseLeftButtonUp="OnMouseLeftButtonUp">
            <chart:LineSeries Title="Volkswagen"
                              ItemsSource="{Binding Points}"
                              IndependentValueBinding="{Binding Date}"
                              DependentValueBinding="{Binding PriceVW}"
                              MouseMove="OnMouseMove">
                <chart:LineSeries.DependentRangeAxis>
                    <chart:LinearAxis Orientation="Y" Title="Volkswagen" ShowGridLines="True" />
                </chart:LineSeries.DependentRangeAxis>
                <chart:LineSeries.DataPointStyle>
                    <Style TargetType="{x:Type chart:LineDataPoint}">
                        <Setter Property="Background" Value="Red" />
                        <Setter Property="Height" Value="0"/>
                        <Setter Property="Width" Value="0"/>
                    </Style>
                </chart:LineSeries.DataPointStyle>
            </chart:LineSeries>

            <chart:LineSeries Title="Daimler"
                              ItemsSource="{Binding Points}"
                              IndependentValueBinding="{Binding Date}"
                              DependentValueBinding="{Binding PriceDaimler}">
                <chart:LineSeries.DependentRangeAxis>
                    <chart:LinearAxis Orientation="Y" Title="Daimler" />
                </chart:LineSeries.DependentRangeAxis>
                <chart:LineSeries.DataPointStyle>
                    <Style TargetType="{x:Type chart:LineDataPoint}">
                        <Setter Property="Background" Value="Green" />
                        <Setter Property="Height" Value="0"/>
                        <Setter Property="Width" Value="0"/>
                    </Style>
                </chart:LineSeries.DataPointStyle>
            </chart:LineSeries>

            <chart:Chart.Axes>
                <chart:DateTimeAxis Name="SharedXAxis"
                                   Orientation="X"
                                  Title="shared X-Axis"
                                  ShowGridLines="True">

                    <!--rotate the X-Axis labels -->
                    <chart:DateTimeAxis.AxisLabelStyle>
                        <Style TargetType="chart:DateTimeAxisLabel">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="chart:DateTimeAxisLabel">
                                        <TextBlock Text="{TemplateBinding FormattedContent}">
                                            <TextBlock.LayoutTransform>
                                                <RotateTransform Angle="90" CenterX = "40" CenterY = "30"/>
                                            </TextBlock.LayoutTransform>
                                        </TextBlock>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </chart:DateTimeAxis.AxisLabelStyle>

                </chart:DateTimeAxis>
            </chart:Chart.Axes>
        </chart:Chart>

    </Grid>
</Window>

 

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.DataVisualization.Charting;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;

namespace Demo {
    public partial class MainWindow : Window {

        private Model _Model;
        private bool _DrawNewLine = false;

        public MainWindow() {
            InitializeComponent();
        } // constructor

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            ViewModel lViewModel = new ViewModel(myChart);
            DataContext = lViewModel;
            _Model = new Model(lViewModel);
        } // 

        private void OnMouseMove(object sender, MouseEventArgs e) {
            if ((!_DrawNewLine) && (e.LeftButton == MouseButtonState.Pressed)) {
                MouseButtonEventArgs lMouseButtonEventArgs = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, MouseButton.Left);
                OnMouseLeftButtonDown(sender, lMouseButtonEventArgs);
                return;
            }

            IInputElement lInputElement = sender as IInputElement; // == basically Chart or LineSeries
            Point lPoint = e.GetPosition(lInputElement);

            Chart lChart = sender as Chart;
            if (lChart != null) {
                string s = string.Empty;

                // iterate through all axes
                lock (lChart.ActualAxes) {
                    foreach (IAxis lAxis in lChart.ActualAxes) {
                        RangeAxis lRangeAxis = lAxis as RangeAxis;
                        if (lRangeAxis == null) continue; // won't happen
                        AxisPoint lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint);

                        s += lAxisPoint.ToString() + Environment.NewLine;
                    }
                }

                InfoBox.Text = s;
                return;
            }

            LineSeries lLineSeries = sender as LineSeries;
            if (lLineSeries != null) {
                IInputElement lSelection = lLineSeries.InputHitTest(lPoint);
                if (lSelection == null) return;
                InfoBox.Text = "LineSeries object: " + lSelection.GetType().ToString();
                return;
            }
        } //

        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
            Chart lChart = sender as Chart;
            if (lChart == null) return;

            AxisPoint lAxisPointX;
            AxisPoint lAxisPointY;
            LineSeries lLineSeries = lChart.Series[0] as LineSeries;
            if (lLineSeries == null) return;   // won't happen anyway
            if (!getAxisPointsToDrawALine(sender, e, lLineSeries, out lAxisPointX, out lAxisPointY)) return;

            AxisPointDateTime lAxisPointDateTimeX = lAxisPointX as AxisPointDateTime;
            AxisPointLinear lAxisPointLinearY = lAxisPointY as AxisPointLinear;

            if (lAxisPointDateTimeX == null) return;  // Quick and dirty test. Should not happen anyway.
            if (lAxisPointLinearY == null) return;  // Might be helpful when you change the code and forget about this requirement in another scenario.

            DrawCurve_WPF_Runtime(lChart, lAxisPointDateTimeX, lAxisPointLinearY);
        } //

        // add a new curve and its first point
        private ObservableCollection<PriceClusterSimple> ManualPoints = null;
        private void DrawCurve_WPF_Runtime(Chart lChart, AxisPointDateTime lAxisPointDateTimeX, AxisPointLinear lAxisPointLinearY) {
            PriceClusterSimple lDataPoint = new PriceClusterSimple(lAxisPointDateTimeX.MouseAxisValueAbsolute, lAxisPointLinearY.MouseAxisValueAbsolute);

            if (ManualPoints != null) {
                if (_DrawNewLine) {
                    _DrawNewLine = false;
                    ManualPoints.Clear();
                    ManualPoints.Add(lDataPoint); // from
                    ManualPoints.Add(lDataPoint); // to
                }
                else {
                    ManualPoints.RemoveAt(1);
                    ManualPoints.Add(lDataPoint); // to
                }
                return;
            }

            ManualPoints = new ObservableCollection<PriceClusterSimple>();
            ManualPoints.Add(lDataPoint);
            ManualPoints.Add(lDataPoint);
            LineSeries lNewLineSeries = new LineSeries();
            lNewLineSeries.Title = "manually added curve";
            lNewLineSeries.SetBinding(LineSeries.ItemsSourceProperty, new Binding());
            lNewLineSeries.ItemsSource = ManualPoints;
            lNewLineSeries.IndependentValueBinding = new Binding("Date");
            lNewLineSeries.DependentValueBinding = new Binding("Price");
            lNewLineSeries.DependentRangeAxis = lAxisPointLinearY.Axis;
            Setter lHeightSetter = new Setter(FrameworkElement.HeightProperty, 0.0);
            Setter lWidthSetter = new Setter(FrameworkElement.WidthProperty, 0.0);
            Setter lColor = new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Black));
            Style lStyle = new Style(typeof(Control));
            lNewLineSeries.DataPointStyle = lStyle;
            lStyle.Setters.Add(lHeightSetter);
            lStyle.Setters.Add(lWidthSetter);
            lStyle.Setters.Add(lColor);
            lChart.Series.Add(lNewLineSeries);
        } //

        private bool getAxisPointsToDrawALine(object xSender, MouseButtonEventArgs xMouseButtonEventArgs, LineSeries xLineSeries, out AxisPoint xAxisPointX, out AxisPoint xAxisPointY) {
            IInputElement lInputElement = xSender as IInputElement; // == basically Chart or LineSeries
            Point lPoint = xMouseButtonEventArgs.GetPosition(lInputElement);
            xAxisPointX = null;
            xAxisPointY = null;

            Chart lChart = xSender as Chart;
            if (lChart == null) return false;

            RangeAxis lRangeAxis;
            AxisPoint lAxisPoint;

            lRangeAxis = xLineSeries.ActualDependentRangeAxis as RangeAxis;
            if (lRangeAxis == null) return false; // won't happen in our example
            lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint);
            if (lRangeAxis.Orientation == AxisOrientation.X) xAxisPointX = lAxisPoint;
            else xAxisPointY = lAxisPoint;

            lRangeAxis = xLineSeries.ActualIndependentAxis as RangeAxis;
            if (lRangeAxis == null) return false; // won't happen in our example
            lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint);
            if (lRangeAxis.Orientation == AxisOrientation.X) xAxisPointX = lAxisPoint;
            else xAxisPointY = lAxisPoint;

            if ((xAxisPointX == null) || (xAxisPointY == null)) return false;
            return true;
        } //

        private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
            _DrawNewLine = true;
        } //

    } // class
} // namespace

 

using System;
using System.Linq;
using System.Windows.Threading;

namespace Demo {
	public class Model {
		private ViewModel _ViewModel;

		public Model(ViewModel xViewModel) {
			_ViewModel = xViewModel;
			DispatcherTimer lTimer = new DispatcherTimer();
			lTimer.Interval = new TimeSpan(0, 0, 3);
			lTimer.Tick += new EventHandler(Timer_Tick);
			lTimer.Start();
		} // constructor

		void Timer_Tick(object sender, EventArgs e) {
			Random r = new Random();
			PriceCluster lPriceCluster = _ViewModel.Points.Last();
			double lVW = lPriceCluster.PriceVW * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0));
			double lDaimler = lPriceCluster.PriceDaimler * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0));
			_ViewModel.AddPoint(lPriceCluster.Date.AddDays(1), lVW, lDaimler);
		} //

	} // class
} // namespace

 

using System;

namespace Demo {

    public class PriceCluster {
		public DateTime Date { get; set; }
		public double PriceVW { get; set; }
		public double PriceDaimler { get; set; }

		public PriceCluster(DateTime xDate, double xPriceVW, double xPriceDaimler) {
			Date = xDate;
			PriceVW = xPriceVW;
			PriceDaimler = xPriceDaimler;
		} // constructor
	} // class

    public class PriceClusterSimple {
        public DateTime Date { get; set; }
        public double Price { get; set; }

        public PriceClusterSimple(DateTime xDate, double xPrice) {
            Date = xDate;
            Price = xPrice;
        } // constructor
    } // class

} // namespace

 

using System;
using System.Collections.ObjectModel;
using System.Windows.Controls.DataVisualization.Charting;

namespace Demo {
	public class ViewModel {
		private readonly Chart _Chart;
		public ReadOnlyObservableCollection<PriceCluster> Points { get; private set; }
		private ObservableCollection<PriceCluster> _Points = new ObservableCollection<PriceCluster>();

		public ViewModel(Chart xChart) {
			_Chart = xChart;

			AddPoint(new DateTime(2014, 04, 10), 67.29, 13.85);
			AddPoint(new DateTime(2014, 04, 11), 66.15, 13.66);
			AddPoint(new DateTime(2014, 04, 14), 66.22, 13.67);
			AddPoint(new DateTime(2014, 04, 15), 63.99, 13.49);
			AddPoint(new DateTime(2014, 04, 16), 65.32, 13.62);
			AddPoint(new DateTime(2014, 04, 17), 67.29, 13.73);
			AddPoint(new DateTime(2014, 04, 22), 68.72, 13.91);
			AddPoint(new DateTime(2014, 04, 23), 67.85, 13.84);
			AddPoint(new DateTime(2014, 04, 24), 67.75, 13.78);
			AddPoint(new DateTime(2014, 04, 25), 66.29, 13.60);
			AddPoint(new DateTime(2014, 04, 28), 66.99, 13.73);
			AddPoint(new DateTime(2014, 04, 29), 67.79, 13.91);
			AddPoint(new DateTime(2014, 04, 30), 66.73, 13.79);
			AddPoint(new DateTime(2014, 05, 02), 66.24, 13.10);
			AddPoint(new DateTime(2014, 05, 05), 65.90, 13.08);
			AddPoint(new DateTime(2014, 05, 06), 65.16, 13.04);
			AddPoint(new DateTime(2014, 05, 07), 64.80, 13.18);
			AddPoint(new DateTime(2014, 05, 08), 65.00, 13.45);
			AddPoint(new DateTime(2014, 05, 09), 64.52, 13.42);
			AddPoint(new DateTime(2014, 05, 12), 65.28, 13.58);
			AddPoint(new DateTime(2014, 05, 13), 66.48, 13.40);
			AddPoint(new DateTime(2014, 05, 14), 66.74, 13.26);
			AddPoint(new DateTime(2014, 05, 15), 66.00, 12.97);
			AddPoint(new DateTime(2014, 05, 16), 65.21, 13.08);
			AddPoint(new DateTime(2014, 05, 19), 66.02, 13.38);
			AddPoint(new DateTime(2014, 05, 20), 66.46, 13.42);
			AddPoint(new DateTime(2014, 05, 21), 67.15, 13.84);
			AddPoint(new DateTime(2014, 05, 22), 67.52, 13.84);
			AddPoint(new DateTime(2014, 05, 23), 68.14, 14.06);
			AddPoint(new DateTime(2014, 05, 26), 69.61, 14.17);
			AddPoint(new DateTime(2014, 05, 27), 69.56, 14.15);
			AddPoint(new DateTime(2014, 05, 28), 69.29, 14.17);
			AddPoint(new DateTime(2014, 05, 29), 69.65, 14.18);
			AddPoint(new DateTime(2014, 05, 30), 69.70, 14.29);
			AddPoint(new DateTime(2014, 06, 02), 69.32, 14.31);
			AddPoint(new DateTime(2014, 06, 03), 69.68, 14.32);
			AddPoint(new DateTime(2014, 06, 04), 69.31, 14.31);
			AddPoint(new DateTime(2014, 06, 05), 70.31, 14.34);
			AddPoint(new DateTime(2014, 06, 06), 70.24, 14.42);
			AddPoint(new DateTime(2014, 06, 09), 70.09, 14.42);
			AddPoint(new DateTime(2014, 06, 10), 70.08, 14.47);
			AddPoint(new DateTime(2014, 06, 11), 69.66, 14.30);
			AddPoint(new DateTime(2014, 06, 12), 69.49, 14.26);
			AddPoint(new DateTime(2014, 06, 13), 69.12, 14.42);
			AddPoint(new DateTime(2014, 06, 16), 69.05, 14.44);
			AddPoint(new DateTime(2014, 06, 17), 69.65, 14.43);
			AddPoint(new DateTime(2014, 06, 18), 69.62, 14.62);
			AddPoint(new DateTime(2014, 06, 19), 70.10, 14.93);
			AddPoint(new DateTime(2014, 06, 20), 70.08, 14.93);
			AddPoint(new DateTime(2014, 06, 23), 69.46, 14.97);
			AddPoint(new DateTime(2014, 06, 24), 69.04, 15.06);
			AddPoint(new DateTime(2014, 06, 25), 68.71, 14.89);
			AddPoint(new DateTime(2014, 06, 26), 68.14, 15.12);
			AddPoint(new DateTime(2014, 06, 27), 68.33, 15.17);
			AddPoint(new DateTime(2014, 06, 30), 68.40, 15.08);
			AddPoint(new DateTime(2014, 07, 01), 69.19, 15.21);
			AddPoint(new DateTime(2014, 07, 02), 69.72, 15.20);
			AddPoint(new DateTime(2014, 07, 03), 70.44, 15.31);
			AddPoint(new DateTime(2014, 07, 04), 70.44, 15.16);
			AddPoint(new DateTime(2014, 07, 07), 69.28, 14.95);
			AddPoint(new DateTime(2014, 07, 08), 68.15, 14.84);
			AddPoint(new DateTime(2014, 07, 09), 68.16, 14.73);
			AddPoint(new DateTime(2014, 07, 10), 67.05, 14.43);
			AddPoint(new DateTime(2014, 07, 11), 66.68, 14.50);
			AddPoint(new DateTime(2014, 07, 14), 67.61, 14.60);
			AddPoint(new DateTime(2014, 07, 15), 67.28, 14.70);
			AddPoint(new DateTime(2014, 07, 16), 67.77, 14.89);
			AddPoint(new DateTime(2014, 07, 17), 66.56, 14.53);
			AddPoint(new DateTime(2014, 07, 18), 65.40, 14.52);
			AddPoint(new DateTime(2014, 07, 21), 64.84, 14.49);
			AddPoint(new DateTime(2014, 07, 22), 66.09, 14.83);
			AddPoint(new DateTime(2014, 07, 23), 65.58, 14.74);
			AddPoint(new DateTime(2014, 07, 24), 66.30, 14.92);
			AddPoint(new DateTime(2014, 07, 25), 65.15, 14.65);
			AddPoint(new DateTime(2014, 07, 28), 63.08, 14.61);
			AddPoint(new DateTime(2014, 07, 29), 63.89, 14.71);
			AddPoint(new DateTime(2014, 07, 30), 63.07, 14.43);
			AddPoint(new DateTime(2014, 07, 31), 61.88, 14.13);
			AddPoint(new DateTime(2014, 08, 01), 60.85, 13.60);
			AddPoint(new DateTime(2014, 08, 04), 61.17, 13.58);
			AddPoint(new DateTime(2014, 08, 05), 60.43, 13.61);
			AddPoint(new DateTime(2014, 08, 06), 59.82, 13.40);
			AddPoint(new DateTime(2014, 08, 07), 58.95, 13.16);
			AddPoint(new DateTime(2014, 08, 08), 59.27, 13.16);
			AddPoint(new DateTime(2014, 08, 11), 60.71, 13.36);
			AddPoint(new DateTime(2014, 08, 12), 59.85, 13.17);
			AddPoint(new DateTime(2014, 08, 13), 60.66, 13.80);
			AddPoint(new DateTime(2014, 08, 14), 61.07, 13.77);
			AddPoint(new DateTime(2014, 08, 15), 59.71, 13.65);
			AddPoint(new DateTime(2014, 08, 18), 60.99, 13.72);
			AddPoint(new DateTime(2014, 08, 19), 61.60, 13.72);
			AddPoint(new DateTime(2014, 08, 20), 61.33, 13.82);
			AddPoint(new DateTime(2014, 08, 21), 62.20, 13.86);
			AddPoint(new DateTime(2014, 08, 22), 61.65, 13.70);
			AddPoint(new DateTime(2014, 08, 25), 62.88, 13.88);
			AddPoint(new DateTime(2014, 08, 26), 63.49, 13.87);
			AddPoint(new DateTime(2014, 08, 27), 63.15, 13.89);
			AddPoint(new DateTime(2014, 08, 28), 62.16, 13.77);
			AddPoint(new DateTime(2014, 08, 29), 62.24, 13.83);
			AddPoint(new DateTime(2014, 09, 01), 61.88, 13.92);
			AddPoint(new DateTime(2014, 09, 02), 61.82, 13.92);
			AddPoint(new DateTime(2014, 09, 03), 62.90, 14.17);
			AddPoint(new DateTime(2014, 09, 04), 64.14, 14.34);
			AddPoint(new DateTime(2014, 09, 05), 65.17, 14.40);

			Points = new ReadOnlyObservableCollection<PriceCluster>(_Points);
		} // constructor

		// only to be called from the dispatcher thread!
		public void AddPoint(DateTime xDate, double xPriceVW, double xPriceDaimler) {
			_Points.Add(new PriceCluster(xDate, xPriceVW, xPriceDaimler));
		} //

	} // class
} // namespace

 

Advertisements

About Bastian M.K. Ohta

Happiness only real when shared.

Posted on October 7, 2014, in Advanced, Basic, C#, Charts, DataBinding, WPF and tagged , , , , , , , , , , , , . Bookmark the permalink. Leave a comment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: