Monthly Archives: October 2014
WPF Charts (Part 4)
Posted by Bastian M.K. Ohta
This post is about zooming.
You can draw a transparent rectangle, which will then determine the coordinates. To achieve this a WPF Canvas is used. It is invisible and is located directly above the chart. You cannot draw custom rectangles into the chart directly. Overlaying is no issue in WPF. The bubbling/tunneling does a fabulous job and avoids disabling any overlaid object. See Routed Events.
First I was planning a real-time zoom with two fingers. Touch-Screens are quite common these days. Performance issues quickly forced me to think about it all over again. In this example you can zoom by using the mouse or two fingers. There is no real-time zoom. You draw the rectangle and when you are done, then the coordinates on the chart are determined. Before this point in time it is just a plain rectangle object on a canvas with no real link to the chart. The escape button resets the axes/unzooms the chart.
I encapsulated the required functionality into a Zoom class. Yes, it maybe should have been in the ViewModel. I got the idea that it was neither fish nor meat – somewhere in between: “fieat”. Whatever, the Zoom class itself is clean and easy to understand.
There are some throttles in the code. They are definitely needed. The amount of touch events can easily become a “no-go” factor. You don’t have to process them all. Your eyes cannot perceive the vast amount of updates anyway. And WPF does not update the screen fast enough to show each value.
Oh, and one more issue here. I chose to not create a separate class to store the finger positions and rather go for a Tuple, which stores two values in an immutable way. The Tuple itself is declared only once. You find the declaration in the usings of the Zoom class.
using myTuple = System.Tuple<int, System.Windows.Point>;
In theory you can simultaneously have a lot of fingers on the screen; 10 if I am not mistaken 😉
You would store these in a dictionary. In this example I only concentrate on 2 fingers.
Personally, I prefer the mouse solution. The touch events are nice, but the performance is jerky. It seems that you would get better result by using only one finger and dragging it like a mouse pointer.
For learning purposes I am displaying the finger manipulation values in today’s example. Uncomment them or uncomment the rectangle code to play with it properly. You might realise that the vertical zoom factor equals the horizontal zoom factor. Call it a bug or a Microsoft flaw. Yes, this is annoying. Anyway, I guess these people don’t do things without any reason. So their sins are forgiven.
<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" PreviewKeyDown="OnKeyDown" Loaded ="Window_Loaded"> <DockPanel LastChildFill="True"> <TextBox Name="InfoBox" Text="{Binding InfoBoxText, Mode=OneWay}" Height="Auto" DockPanel.Dock="Top"/> <Canvas DockPanel.Dock="Top" IsHitTestVisible="True" MouseDown="OnMouseLeftButtonDown" MouseLeftButtonUp="OnMouseLeftButtonUp" MouseMove="OnMouseMove" IsManipulationEnabled="True" ManipulationDelta="OnManipulationDelta" TouchDown="OnTouchDown" TouchMove="OnTouchMove" TouchUp="OnTouchUp" Width="Auto" Height="Auto" MinWidth="400" MinHeight="300"> <chart:Chart Name="myChart" Title="2014" Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}, Path=ActualWidth}" Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}, Path=ActualHeight}"> <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> </Canvas> </DockPanel> </Window>
using System; using System.Windows; using System.Windows.Controls.DataVisualization.Charting; namespace Demo { public class AxisPointLinear : AxisPoint { public readonly LinearAxis Axis; public readonly double Min; public readonly double Max; // larger than Min; public readonly double Range; public readonly double MouseAxisValueAbsolute; public AxisPointLinear(Chart xChart, LinearAxis xAxis, Point xPoint, double xMin, double xMax) : base(xChart, xAxis, xPoint) { Min = xMin; Max = xMax; Range = xMax - xMin; Axis = xAxis; MouseAxisValueAbsolute = xMin + (MouseAxisValueRelative * Range); } // constructor public override string ToString() { string s = "Mouse: "; s += MouseAxisValueRelative.ToString("0.000%"); s += " => "; s += MouseAxisValueAbsolute.ToString("#,##0.000"); s += " EUR for "; s += Axis.Orientation; s += "-Axis "; s += Axis.Title; return s; } // } // class public class AxisPointDateTime : AxisPoint { public readonly DateTimeAxis Axis; public readonly DateTime Min; public readonly DateTime Max; // larger than Min; public readonly TimeSpan Range; public readonly DateTime MouseAxisValueAbsolute; public AxisPointDateTime(Chart xChart, DateTimeAxis xAxis, Point xPoint, DateTime xMin, DateTime xMax) : base(xChart, xAxis, xPoint) { Min = xMin; Max = xMax; Range = xMax - xMin; Axis = xAxis; MouseAxisValueAbsolute = xMin.AddMinutes(MouseAxisValueRelative * Range.TotalMinutes); } // constructor public override string ToString() { string s = "Mouse: "; s += MouseAxisValueRelative.ToString("0.000%"); s += " => "; s += MouseAxisValueAbsolute.ToString("dd MMM yyyy"); s += " for "; s += Axis.Orientation; s += "-Axis "; s += Axis.Title; return s; } // } // class public class AxisPointFactory { public static AxisPoint getAxisPoint(Chart xChart, RangeAxis xAxis, Point xPoint) { if (xAxis == null) return null; if (xAxis is LinearAxis) { // some redundant basic checks LinearAxis lAxis = xAxis as LinearAxis; double? lMin; double? lMax; lMin = lAxis.ActualMinimum; lMax = lAxis.ActualMaximum; if ((!lMin.HasValue) || (!lMax.HasValue)) return null; if (lMin.Value >= lMax.Value) return null; return new AxisPointLinear(xChart, lAxis, xPoint, lMin.Value, lMax.Value); } if (xAxis is DateTimeAxis) { // some redundant basic checks DateTimeAxis lAxis = xAxis as DateTimeAxis; DateTime? lMin; DateTime? lMax; lMin = lAxis.ActualMinimum; lMax = lAxis.ActualMaximum; if ((!lMin.HasValue) || (!lMax.HasValue)) return null; if (lMin.Value >= lMax.Value) return null; return new AxisPointDateTime(xChart, lAxis, xPoint, lMin.Value, lMax.Value); } throw new Exception("Axis type not supported yet."); } // } // class public abstract class AxisPoint { public readonly Chart Chart; public readonly Point MouseAbsoluteLocation; public readonly double MouseAxisValueRelative; // a number between 0% and 100% public readonly double Length; // object pixel display units, larger than zero public AxisPoint(Chart xChart, RangeAxis xAxis, Point xPoint) { if (xAxis.Orientation == AxisOrientation.X) Length = xAxis.ActualWidth; else Length = xAxis.ActualHeight; if (Length <= 0) throw new Exception("Chart object length is zero or less."); MouseAbsoluteLocation = xChart.TranslatePoint(xPoint, xAxis); if (xAxis.Orientation == AxisOrientation.X) MouseAxisValueRelative = MouseAbsoluteLocation.X / Length; else MouseAxisValueRelative = 1.0 - (MouseAbsoluteLocation.Y / Length); if (MouseAxisValueRelative > 1.0) MouseAxisValueRelative = 1.0; else if (MouseAxisValueRelative < 0.0) MouseAxisValueRelative = 0.0; } // constructor } // class } // namespace
using System; using System.Windows; using System.Windows.Input; namespace Demo { public partial class MainWindow : Window { private Model _Model; private ViewModel _ViewModel; private Zoom _Zoom; public MainWindow() { InitializeComponent(); } // constructor private void Window_Loaded(object sender, RoutedEventArgs e) { _Zoom = new Zoom(myChart); _ViewModel = new ViewModel(myChart); DataContext = _ViewModel; _Model = new Model(_ViewModel); } // private void OnKeyDown(object xSender, KeyEventArgs e) { _Zoom.OnKeyDown(xSender, e); } private void OnMouseLeftButtonDown(object xSender, MouseButtonEventArgs e) { _Zoom.OnMouseLeftButtonDown(xSender, e); } private void OnMouseLeftButtonUp(object xSender, MouseButtonEventArgs e) { _Zoom.OnMouseLeftButtonUp(xSender, e); } private void OnMouseMove(object xSender, MouseEventArgs e) { _Zoom.OnMouseMove(xSender, e); } private void OnTouchDown(object xSender, TouchEventArgs e) { _Zoom.OnTouchDown(xSender, e); } private void OnTouchMove(object xSender, TouchEventArgs e) { _Zoom.OnTouchMove(xSender, e); } private void OnTouchUp(object xSender, TouchEventArgs e) { _Zoom.OnTouchUp(xSender, e); } DateTime _LastOnManipulationDelta; private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e) { if (DateTime.Now.Subtract(_LastOnManipulationDelta).TotalMilliseconds < 500) return; // throttle _LastOnManipulationDelta = DateTime.Now; InfoBox.Text = "Expansion: " + e.CumulativeManipulation.Expansion.ToString() + Environment.NewLine + "Rotation: " + e.CumulativeManipulation.Rotation.ToString() + Environment.NewLine + "Scale: " + e.CumulativeManipulation.Scale.ToString() + Environment.NewLine + "Translation: " + e.CumulativeManipulation.Translation.ToString() + Environment.NewLine + "Exp " + e.CumulativeManipulation.Expansion.X + "/" + e.CumulativeManipulation.Expansion.Y; } // } // 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; using System.Windows; using System.Windows.Input; using System.Collections.Generic; namespace Demo { using myTuple = Tuple<AxisPointDateTime, AxisPointLinear>; using System.Windows.Threading; public class ViewModel : DependencyObject { 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
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.DataVisualization.Charting; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using myTuple = System.Tuple<int, System.Windows.Point>; namespace Demo { public class Zoom { private Rectangle _Rectangle = null; private myTuple _PointA = null; private myTuple _PointB = null; // only used for the fingers, not for the mouse private readonly Chart _Chart; public Zoom(Chart xChart) { _Chart = xChart; } // #region draw new rectangle on button or finger down public void OnMouseLeftButtonDown(object xSender, MouseButtonEventArgs e) { Canvas lCanvas = xSender as Canvas; if (lCanvas == null) return; Point lPointA = e.GetPosition(lCanvas); DrawNewRectangle(lCanvas, lPointA, -1); } // public void OnTouchDown(object xSender, TouchEventArgs e) { Canvas lCanvas = xSender as Canvas; if (lCanvas == null) return; TouchPoint lTouchPoint = e.GetTouchPoint(lCanvas); if (lTouchPoint == null) return; myTuple lTuple = _PointA; if (lTuple != null) { if (lTuple.Item1 == e.TouchDevice.Id) return; // this was finger 1, not going to happen anyway as it cannot touchdown twice Point lPointA = lTuple.Item2; // store second finger; we don't care about its ID, so it could also be finger 3, 4 or 5 ... Point lPointB = lTouchPoint.Position; _PointB = new myTuple(e.TouchDevice.Id, lPointB); RedrawRectangle(lPointA, lPointB); return; } // first finger DrawNewRectangle(lCanvas, lTouchPoint.Position, lTouchPoint.TouchDevice.Id); return; } // private void DrawNewRectangle(Canvas xCanvas, Point xPoint, int xPointId) { if (_Rectangle != null) return; _Rectangle = new Rectangle(); Point lPointA = new Point(xPoint.X, xPoint.Y); // clone _PointA = new myTuple(xPointId, lPointA); xCanvas.Children.Add(_Rectangle); Canvas.SetLeft(_Rectangle, xPoint.X); Canvas.SetTop(_Rectangle, xPoint.Y); _Rectangle.Height = 1.0; _Rectangle.Width = 1.0; _Rectangle.Opacity = 0.3; _Rectangle.Fill = new SolidColorBrush(Colors.SteelBlue); _Rectangle.Stroke = new SolidColorBrush(Colors.DarkBlue); _Rectangle.StrokeDashArray = new DoubleCollection(new double[] { 0.5, 1.5 }); _Rectangle.StrokeDashCap = PenLineCap.Round; _Rectangle.StrokeThickness = 2.0; } // #endregion #region resize rectangle on any movement public void OnMouseMove(object xSender, MouseEventArgs e) { if (!(xSender is Canvas)) return; myTuple lTuple = _PointA; if (lTuple == null) return; Point lPointA = lTuple.Item2; Point lPointB = e.GetPosition((Canvas)xSender); RedrawRectangle(lPointA, lPointB); } // DateTime _LastOnTouchMove; public void OnTouchMove(object xSender, TouchEventArgs e) { if (DateTime.Now.Subtract(_LastOnTouchMove).TotalMilliseconds < 300) return; // throttle _LastOnTouchMove = DateTime.Now; Canvas lCanvas = xSender as Canvas; if (lCanvas == null) return; TouchPoint lTouchPoint = e.GetTouchPoint(lCanvas); if (lTouchPoint == null) return; myTuple lTuple = _PointA; if (lTuple == null) return; Point lPointA = lTuple.Item2; if (e.TouchDevice.Id == lTuple.Item1) { // this is the finger we were touching down first lPointA = lTouchPoint.Position; _PointA = new myTuple(e.TouchDevice.Id, lPointA); } lTuple = _PointB; if (lTuple == null) return; // no second finger Point lPointB = lTuple.Item2; if (e.TouchDevice.Id == lTuple.Item1) { // this was the second finger lPointB = lTouchPoint.Position; _PointB = new myTuple(e.TouchDevice.Id, lPointB); } RedrawRectangle(lPointA, lPointB); } // private void RedrawRectangle(Point xPointA, Point xPointB) { Rectangle lRectangle = _Rectangle; if (lRectangle == null) return; Canvas.SetTop(lRectangle, Math.Min(xPointA.Y, xPointB.Y)); lRectangle.Height = Math.Abs(xPointA.Y - xPointB.Y); Canvas.SetLeft(lRectangle, Math.Min(xPointA.X, xPointB.X)); lRectangle.Width = Math.Abs(xPointA.X - xPointB.X); } // #endregion #region remove rectangle and Zoom public void OnMouseLeftButtonUp(object xSender, MouseButtonEventArgs e) { Rectangle lRectangle = _Rectangle; RemoveRectangle(xSender); ProcessZoom(lRectangle); } // public void OnTouchUp(object xSender, TouchEventArgs e) { Rectangle lRectangle = _Rectangle; if (lRectangle == null) return; // do not process any results when the first finger was gone already RemoveRectangle(xSender); ProcessZoom(lRectangle); } // private void RemoveRectangle(object xSender) { Canvas lCanvas = xSender as Canvas; if (lCanvas == null) return; lCanvas.Children.Remove(_Rectangle); _PointA = null; _PointB = null; _Rectangle = null; } // public void ProcessZoom(Rectangle xRectangle) { if (xRectangle == null) return; Point lFrom = new Point(Canvas.GetLeft(xRectangle), Canvas.GetTop(xRectangle)); Point lTo = new Point(lFrom.X + xRectangle.Width, lFrom.Y + xRectangle.Height); foreach (IAxis lAxis in _Chart.ActualAxes) { if (lAxis is LinearAxis) { LinearAxis lLinearAxis = lAxis as LinearAxis; AxisPointLinear a = AxisPointFactory.getAxisPoint(_Chart, lLinearAxis, lFrom) as AxisPointLinear; AxisPointLinear b = AxisPointFactory.getAxisPoint(_Chart, lLinearAxis, lTo) as AxisPointLinear; lLinearAxis.Minimum = Math.Min(a.MouseAxisValueAbsolute, b.MouseAxisValueAbsolute); lLinearAxis.Maximum = Math.Max(a.MouseAxisValueAbsolute, b.MouseAxisValueAbsolute); continue; } if (lAxis is DateTimeAxis) { DateTimeAxis lDateTimeAxis = lAxis as DateTimeAxis; AxisPointDateTime a = AxisPointFactory.getAxisPoint(_Chart, lDateTimeAxis, lFrom) as AxisPointDateTime; AxisPointDateTime b = AxisPointFactory.getAxisPoint(_Chart, lDateTimeAxis, lTo) as AxisPointDateTime; lDateTimeAxis.Minimum = a.MouseAxisValueAbsolute < b.MouseAxisValueAbsolute ? a.MouseAxisValueAbsolute : b.MouseAxisValueAbsolute; lDateTimeAxis.Maximum = a.MouseAxisValueAbsolute > b.MouseAxisValueAbsolute ? a.MouseAxisValueAbsolute : b.MouseAxisValueAbsolute; continue; } } } // #endregion #region reset Zoom public void OnKeyDown(object xSender, KeyEventArgs e) { if (e.Key != Key.Escape) return; ProcessZoomReset(); } // public void ProcessZoomReset() { foreach (IAxis lAxis in _Chart.ActualAxes) { if (lAxis is LinearAxis) { LinearAxis lLinearAxis = lAxis as LinearAxis; lLinearAxis.Minimum = null; lLinearAxis.Maximum = null; continue; } if (lAxis is DateTimeAxis) { DateTimeAxis lDateTimeAxis = lAxis as DateTimeAxis; lDateTimeAxis.Minimum = null; lDateTimeAxis.Maximum = null; continue; } } } // #endregion } // class } // namespace
Posted in Advanced, Basic, C#, Charts, DataBinding, WPF
Tags: advanced, button, C#, C-sharp, chart, DataBinding, datavisualization, draw, OnMouse, programming, Source code, Windows, WPF
WPF Charts (Part 3)
Posted by Bastian M.K. Ohta
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
using System; using System.Windows; using System.Windows.Controls.DataVisualization.Charting; namespace Demo { public class AxisPointLinear : AxisPoint { public readonly LinearAxis Axis; public readonly double Min; public readonly double Max; // larger than Min; public readonly double Range; public readonly double MouseAxisValueAbsolute; public AxisPointLinear(Chart xChart, LinearAxis xAxis, Point xPoint, double xMin, double xMax) : base(xChart, xAxis, xPoint) { Min = xMin; Max = xMax; Range = xMax - xMin; Axis = xAxis; MouseAxisValueAbsolute = xMin + (MouseAxisValueRelative * Range); } // constructor public override string ToString() { string s = "Mouse: "; s += MouseAxisValueRelative.ToString("0.000%"); s += " => "; s += MouseAxisValueAbsolute.ToString("#,##0.000"); s += " EUR for "; s += Axis.Orientation; s += "-Axis "; s += Axis.Title; return s; } // } // class public class AxisPointDateTime : AxisPoint { public readonly DateTimeAxis Axis; public readonly DateTime Min; public readonly DateTime Max; // larger than Min; public readonly TimeSpan Range; public readonly DateTime MouseAxisValueAbsolute; public AxisPointDateTime(Chart xChart, DateTimeAxis xAxis, Point xPoint, DateTime xMin, DateTime xMax) : base(xChart, xAxis, xPoint) { Min = xMin; Max = xMax; Range = xMax - xMin; Axis = xAxis; MouseAxisValueAbsolute = xMin.AddMinutes(MouseAxisValueRelative * Range.TotalMinutes); } // constructor public override string ToString() { string s = "Mouse: "; s += MouseAxisValueRelative.ToString("0.000%"); s += " => "; s += MouseAxisValueAbsolute.ToString("dd MMM yyyy"); s += " for "; s += Axis.Orientation; s += "-Axis "; s += Axis.Title; return s; } // } // class public class AxisPointFactory { public static AxisPoint getAxisPoint(Chart xChart, RangeAxis xAxis, Point xPoint) { if (xAxis is LinearAxis) { // some redundant basic checks LinearAxis lAxis = xAxis as LinearAxis; double? lMin; double? lMax; lMin = lAxis.ActualMinimum; lMax = lAxis.ActualMaximum; if ((!lMin.HasValue) || (!lMax.HasValue)) return null; if (lMin.Value >= lMax.Value) return null; return new AxisPointLinear(xChart, lAxis, xPoint, lMin.Value, lMax.Value); } if (xAxis is DateTimeAxis) { // some redundant basic checks DateTimeAxis lAxis = xAxis as DateTimeAxis; DateTime? lMin; DateTime? lMax; lMin = lAxis.ActualMinimum; lMax = lAxis.ActualMaximum; if ((!lMin.HasValue) || (!lMax.HasValue)) return null; if (lMin.Value >= lMax.Value) return null; return new AxisPointDateTime(xChart, lAxis, xPoint, lMin.Value, lMax.Value); } throw new Exception("Axis type not supported yet."); } // } // class public abstract class AxisPoint { public readonly Chart Chart; public readonly Point MouseAbsoluteLocation; public readonly double MouseAxisValueRelative; // a number between 0% and 100% public readonly double Length; // object pixel display units, larger than zero public AxisPoint(Chart xChart, RangeAxis xAxis, Point xPoint) { if (xAxis.Orientation == AxisOrientation.X) Length = xAxis.ActualWidth; else Length = xAxis.ActualHeight; if (Length <= 0) throw new Exception("Chart object length is zero or less."); MouseAbsoluteLocation = xChart.TranslatePoint(xPoint, xAxis); if (xAxis.Orientation == AxisOrientation.X) MouseAxisValueRelative = MouseAbsoluteLocation.X / Length; else MouseAxisValueRelative = 1.0 - (MouseAbsoluteLocation.Y / Length); if (MouseAxisValueRelative > 1.0) MouseAxisValueRelative = 1.0; else if (MouseAxisValueRelative < 0.0) MouseAxisValueRelative = 0.0; } // constructor } // class } // namespace
Posted in Advanced, Basic, C#, Charts, DataBinding, WPF
Tags: advanced, button, C#, C-sharp, chart, DataBinding, datavisualization, draw, OnMouse, programming, Source code, Windows, WPF