Category Archives: Charts
WPF TimeLine Custom Control
This is a custom WPF control and not a UserForm or a standard control with some extra properties in XAML. It is a proper class, which you would usually place in a library.
All Controls inherit from the FrameworkElement class. Some inherit from the Control Class, which is derived from the FrameworkElement class. In this case here I am deriving our custom control from the Canvas class, which in turn is derived from the Panel class and inherently FrameworkElement class. Once again I am not re-inventing the wheel and use existing functionality. Therefore the code remains pretty short. I am basically overriding the OnRender() event of the Canvas class.
This effects the design-time and run-time, which means you can see immediate results while you add the control in the XAML/Blend editor and of course when your C# application is running.
Just in case you are interested in further reading for design-time enhancements: WpfTutorial
The code in the MainWindow is not required. This is just a demonstration on how to change the display at run-time. A timer is used to make the change obvious. The code execution would be too fast otherwise. You wouldn’t see the first TimeLine graph, just the second. The method SetTimeSeries() triggers the change by invalidating the Canvas control (method InvalidateVisual()).
You can see a similar initialization in the TimeLineControl constructor. This data can be seen at design-time. You could also code that part in the XAML code, but this is not the purpose here. The control should not need any additional XAML in its ‘distributable’ version. Implement the TimeLineControl as it is. That is the beauty of overriding OnRender() rather than using WPF templates.
The inner control paddings and other settings were hard-coded. You can change these. Hard-coding made the example easier to understand – no redundant code to confuse you. Feel free to replace this part with flexible user settings or calculated values.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="TimeLine.MainWindow" xmlns:local="clr-namespace:TimeLine" Title="TimeLineControl" Height="130" Width="525"> <DockPanel> <local:TimeLineControl x:Name="MyTimeLineControl" /> </DockPanel> </Window>
using System; using System.Collections.Generic; using System.Timers; using System.Windows; using System.Windows.Documents; namespace TimeLine { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Timer lTimer = new Timer(3000.0); lTimer.Elapsed += Timer_Elapsed; lTimer.Start(); } // constructor void Timer_Elapsed(object xSender, ElapsedEventArgs e) { Timer lTimer = xSender as Timer; if (lTimer == null) return; lTimer.Stop(); // demo: how to change the TimeLine List<TimeEvent> lList = new List<TimeEvent>(); AddEvent(lList, new DateTime(2015, 03, 01), ""); AddEvent(lList, new DateTime(2015, 03, 06), "exD Brown-Forman Corp"); AddEvent(lList, new DateTime(2015, 03, 10), "exD UniFirst Corp"); AddEvent(lList, new DateTime(2015, 03, 11), "exD Worthington Industries Inc"); AddEvent(lList, new DateTime(2015, 03, 12), "exD Garmin Ltd"); AddEvent(lList, new DateTime(2015, 03, 18), "exD Republic Bank Corp"); AddEvent(lList, new DateTime(2015, 03, 23), "exD STMicroelectronics NV"); AddEvent(lList, new DateTime(2015, 03, 31), ""); MyTimeLineControl.SetTimeSeries(lList); } // constructor private void AddEvent(List<TimeEvent> xList, DateTime xDate, string xText) { TimeEvent lEvent = new TimeEvent(); lEvent.Date = xDate; lEvent.TextRed = xDate.ToString("dd MMMyy"); lEvent.TextBlack = xText; xList.Add(lEvent); } // } // class } // namespace
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace TimeLine { public class TimeLineControl : Canvas { private IEnumerable<TimeEvent> _TimeSeries; public TimeLineControl() { Background = Brushes.White; List<TimeEvent> lList = new List<TimeEvent>(); lList.Add(new TimeEvent { Date = DateTime.Today, TextRed = ":(", TextBlack = "Today" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(1), TextRed = "", TextBlack = "Tomorrow" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(7), TextRed = "", TextBlack = "Next week" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(14), TextRed = "", TextBlack = "Fortnight" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddMonths(1), TextRed = ":)", TextBlack = "NextMonth" }); _TimeSeries = lList; } // constructor public void SetTimeSeries(IEnumerable<TimeEvent> xList) { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(() => SetTimeSeries(xList)); return; } _TimeSeries = xList; InvalidateVisual(); } // // some hardcoding to keep the example code simple const double cPaddingLeft = 15.0; const double cPaddingRight = 50.0; const double cPaddingBottom = 17.0; const double cMarkerLength = 10.0; const double cTextOffset = 5.0; // distance to rotation point const double cTextSpace = 7.0; // distance between the red and the black text const double cAngle = 45.0; // rotation angle (0.0 <= cAngle <= 180.0) protected override void OnRender(DrawingContext xDrawingContext) { base.OnRender(xDrawingContext); double lLeft = cPaddingLeft; double lRight = ActualWidth - cPaddingRight; double lBottom = ActualHeight - cPaddingBottom; double lWidth = ActualWidth - cPaddingLeft - cPaddingRight; Point p1 = new Point(lLeft, lBottom); Point p2 = new Point(lRight, lBottom); // draw the X-Axis Pen lPen = new Pen(Brushes.Black, 3.0); lPen.DashStyle = DashStyles.Solid; xDrawingContext.DrawLine(lPen, p1, p2); // determine range DateTime lMin = _TimeSeries.Min(x => x.Date); DateTime lMax = _TimeSeries.Max(x => x.Date); double lDateRange = lMax.Subtract(lMin).TotalDays; foreach (TimeEvent t in _TimeSeries) { double lRelativeX = t.Date.Subtract(lMin).TotalDays / lDateRange; double lAbsoluteX = lRelativeX * lWidth + lLeft; // convert to canvas coordinates // draw the X-Axis marker p1 = new Point(lAbsoluteX, lBottom); p2 = new Point(lAbsoluteX, lBottom - cMarkerLength); xDrawingContext.DrawLine(lPen, p1, p2); // write the text with a 45 degrees angle Point lRotationCenter = p2; double lTextWidth = DrawText(xDrawingContext, t.TextRed, lRotationCenter, cTextOffset, Brushes.Red); // red text DrawText(xDrawingContext, t.TextBlack, lRotationCenter, lTextWidth + cTextOffset + cTextSpace, Brushes.Black); // black text } } // /// <returns>the width of the text</returns> private double DrawText(DrawingContext xDrawingContext, string xText, Point xPoint, double xOffset, SolidColorBrush xBrush) { Typeface lTypeface = new Typeface("Arial"); CultureInfo lCultureInfo = CultureInfo.CurrentCulture; FormattedText lText = new FormattedText(xText, lCultureInfo, FlowDirection.LeftToRight, lTypeface, 10.0, xBrush); RotateTransform lRotateTransform = new RotateTransform(-cAngle, xPoint.X, xPoint.Y); xDrawingContext.PushTransform(lRotateTransform); xDrawingContext.DrawText(lText, new Point(xPoint.X + xOffset, xPoint.Y - lText.Height / 2.0)); xDrawingContext.Pop(); //return new Point(xPoint.X + lText.Height / 2.0 * Math.Sin(cAngle) + lText.Width * Math.Cos(cAngle) , xPoint.Y - lText.Width * Math.Sin(cAngle)); return lText.Width; } // private void DrawLine(double x1, double x2, double y1, double y2) { Line lLine = new Line(); lLine.Stroke = Brushes.LightSteelBlue; lLine.X1 = x1; lLine.X2 = x2; lLine.Y1 = y1; lLine.Y2 = y2; lLine.HorizontalAlignment = HorizontalAlignment.Left; lLine.VerticalAlignment = VerticalAlignment.Center; lLine.StrokeThickness = 2; Children.Add(lLine); } // } // class } // namespace
using System; namespace TimeLine { public class TimeEvent { public DateTime Date; public string TextRed; public string TextBlack; } // class } // namespace
WPF Charts (Part 2)
I have been quite busy in the last weeks, I will resume blogging on a regular basis again.
Today, we are enhancing the previous chart Part1 with some new features:
- One curve has circles around all points. These become transparent when you hover over them.
I chose a pretty large size to make them more obvious. - The shared X-Axis text labels are rotated by 90 degrees. Feel free to test other angles like 45 degrees.
- A three second interval timer appends new points to the chart.
- A text field shows the object types while the mouse hovers over the chart elements. This is the entry point to examine chart objects, find positions and generate on the fly ToolTips.
Some code was commented out. Play with these code pieces. You can change the following behavior:
- Hide the legend by setting its width to zero.
- Add a non-shard Axis.
- Change the color to blue rather transparent when hovering over a point.
- Besides the above, play with the code as much as you like – especially the XAML part.
In this example I used the .NET ReadOnlyObservableCollection. This collection is a wrapper around the well known ObservableCollection. The readonly collection cannot be changed (if there was any chart element to eg. cut a point out). But you can access the wrapped read/write ObservableCollection. This is a nice approach. For the outer world you have an encapsulated object. From behind the curtain you still have full access. The chart automatically updates when a new point is added to the read/write ObservableCollection.
Once again the code is pretty much self explanatory. So I am not adding a long post text.
<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"> <!-- hide the legend --> <!--<chart:Chart.LegendStyle> <Style TargetType="datavisualization:Legend"> <Setter Property="Width" Value="0" /> </Style> </chart:Chart.LegendStyle>--> <chart:LineSeries Title="Volkswagen" ItemsSource="{Binding Points}" IndependentValueBinding="{Binding Date}" DependentValueBinding="{Binding PriceVW}" MouseMove="OnMouseMove"> <chart:LineSeries.DependentRangeAxis> <chart:LinearAxis Orientation="Y" Title="Y-Axis Volkswagen" ShowGridLines="True" /> </chart:LineSeries.DependentRangeAxis> <chart:LineSeries.DataPointStyle> <Style TargetType="{x:Type chart:LineDataPoint}"> <Setter Property="Background" Value="Red" /> <Setter Property="Width" Value="20" /> <Setter Property="Height" Value="20" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <!--<Setter Property="Background" Value="Blue"/>--> <Setter Property="Background" Value="Transparent" /> </Trigger> </Style.Triggers> </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="Y-Axis Daimler" /> </chart:LineSeries.DependentRangeAxis> <!--<chart:LineSeries.IndependentAxis > <chart:DateTimeAxis Orientation="X" Title="non-shared axis" /> </chart:LineSeries.IndependentAxis>--> <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.Windows; using System.Windows.Controls.DataVisualization.Charting; using System.Windows.Input; namespace Demo { public partial class MainWindow : Window { private Model _Model; 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) { IInputElement lInputElement = sender as IInputElement; // == Chart, LineSeries ... Chart lChart = sender as Chart; LineSeries lLineSeries = sender as LineSeries; Point lPoint = e.GetPosition(lInputElement); if (lChart != null) { IInputElement lSelection = lChart.InputHitTest(lPoint); if (lSelection == null) return; InfoBox.Text = lSelection.GetType().ToString(); } else if (lLineSeries != null) { IInputElement lSelection = lLineSeries.InputHitTest(lPoint); if (lSelection == null) return; InfoBox.Text = lSelection.GetType().ToString(); } } // } // 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; 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; 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 } // namespace
WPF Charts (Part 1)
I was playing around with techniques and built a short chart demo. There are many tools out there to create charts. I prefer the good old WPF Toolkit solution on codeplex, which adds the namespace ‘System.Windows.Controls.DataVisualization.Chart’ and is supported by Microsoft. You can expect high compatibility at zero costs.
Do not confuse this one with the Extended WPF Toolkit, which is free software, but also offers a commercial solution.
We are going to create various WPF charts in the coming weeks. The programming pattern series will continue at some point afterwards. What topics I choose is always closely related to my personal interests at that time. I find it hard to motivate myself otherwise.
This is a very simple example today. I added two NumericUpDown controls to add some flavor. Well, in the WPF Toolkit they are not called NumericUpDown anymore. There are corresponding DoubleUpDown/ DecimalUpDown/IntegerUpDown controls.
The lower DoubleUpDown control in this demo is linked to the upper one. And in turn the upper one is bound to a DataContext object property. This demonstrates a chain of bindings. Hence three objects are linked together holding the same value.
You can uncomment the prepared block in the XAML code. This will influence the line color and the line thickness. This template has its limits. It does not change the color of related objects. Anyway, it is a good start.
The chart has two curves. You can influence one by using any of the two DoubleUpDown controls.
The used ObservableCollection to store the curve points could be a post on its own. Basically, it is a WPF collection, which notifies WPF when you add or remove items from/to the list. But how do you update a chart, which only changes a value of a single point? The four methods to invalidate the drawing area are somewhat not showing the expected results.
You can set the DataContext to null and then set it back to your source. This is not the fastest way. But practically speaking, changing one value does not happen very often and setting the DataContext is quick and easy. Usually you only add or remove points. If you are looking for animations, they are dealt with differently in WPF. You should have a look into System.Windows.Media.Storyboard for that. In this example I chose to simply remove and add the affected point.
You don’t have to re-insert the point at the right collection position. I just did it to easily find the same point again. A simple Chart.Add() would work as well.
WPF will most likely not show the point removal on the screen. Tell me if I am wrong. I haven’t seen any impact. I guess the Dispatcher thread is blocked while you are using it on the WPF event call. A signal, which may happen right after the event finishes, will trigger the queued removal and addition in one go.
<Window x:Class="Demo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:tool="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit" xmlns:dv="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit" xmlns:local="clr-namespace:Demo" Title="MainWindow" Height="350" Width="525" Initialized="Window_Initialized"> <Grid> <DockPanel LastChildFill="True"> <tool:DoubleUpDown DockPanel.Dock="Top" Name="UpDown1" AllowSpin="True" Minimum="0" Maximum="100.5" Increment="0.5" ClipValueToMinMax="True" DefaultValue="0" Watermark="enter a value" MouseWheelActiveOnFocus="True" MouseWheelActiveTrigger="FocusedMouseOver" FormatString="N3" ShowButtonSpinner="True" TextAlignment="Center" Value="{Binding PriceOfDay3, Mode=OneWayToSource, FallbackValue=55.5 }" /> <tool:DoubleUpDown DockPanel.Dock="Top" Name="UpDown2" AllowSpin="True" Minimum="0" Maximum="100.5" Increment="0.5" ClipValueToMinMax="True" Value="{Binding Value, ElementName=UpDown1, Mode=TwoWay}" MouseWheelActiveOnFocus="True" MouseWheelActiveTrigger="FocusedMouseOver" FormatString="N3" ShowButtonSpinner="True" TextAlignment="Center"/> <dv:Chart Name="Chart1" Title="Test Chart" > <dv:LineSeries Title="Price" ItemsSource="{Binding Points, Delay=2500, IsAsync=False}" IndependentValueBinding="{Binding Day}" DependentValueBinding="{Binding Price}" > <dv:LineSeries.DependentRangeAxis> <dv:LinearAxis Orientation="Y" Title="Price" Minimum="50" Maximum="60" Interval="2" ShowGridLines="True"/> </dv:LineSeries.DependentRangeAxis> <!--<dv:LineSeries.Template> --><!-- change the line color to green and set the thickness --><!-- <ControlTemplate TargetType="dv:LineSeries"> <Canvas x:Name="PlotArea"> <Polyline x:Name="polyline" Points="{TemplateBinding Points}" Style="{TemplateBinding PolylineStyle}" Stroke="Green" StrokeThickness="4" /> </Canvas> </ControlTemplate> </dv:LineSeries.Template>--> </dv:LineSeries> <dv:LineSeries Title="Tax" ItemsSource="{Binding Points, Delay=2500, IsAsync=False}" IndependentValueBinding="{Binding Day}" DependentValueBinding="{Binding Tax}"> <dv:LineSeries.DependentRangeAxis> <dv:LinearAxis Orientation="Y" Title="Tax" Minimum="-10" Maximum="10" Interval="2.5"/> </dv:LineSeries.DependentRangeAxis> </dv:LineSeries> <dv:Chart.Axes> <dv:LinearAxis Orientation="X" Title="X-Axis" Interval="2" ShowGridLines="True"/> </dv:Chart.Axes> </dv:Chart> </DockPanel> </Grid> </Window>
using System; using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls.DataVisualization.Charting; namespace Demo { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } public class DataPoint { public double Day { get; set; } public double Price { get; set; } public double Tax { get; set; } } // class public class ViewModel { private readonly Chart _Chart; public ObservableCollection<DataPoint> Points { get; private set; } public double PriceOfDay3 { get { lock (this) return Points[2].Price; } set { lock (this) { DataPoint p = Points[2]; p.Price = value; Points.Remove(p); Points.Insert(2, p); // same position //Points.Add(p); // append to the end } } } // public ViewModel(Chart xChart) { _Chart = xChart; Points = new ObservableCollection<DataPoint>(); Points.Add(new DataPoint() { Day = 1.0, Price = 55, Tax = 2.0 }); Points.Add(new DataPoint() { Day = 1.5, Price = 54, Tax = 1.0 }); Points.Add(new DataPoint() { Day = 2.0, Price = 58, Tax = -1.0 }); Points.Add(new DataPoint() { Day = 3.0, Price = 55.5, Tax = 0.0 }); Points.Add(new DataPoint() { Day = 4.0, Price = 53, Tax = -2.0 }); } // constructor } // class private void Window_Initialized(object sender, EventArgs e) { ViewModel lViewModel = new ViewModel(Chart1); DataContext = lViewModel; } // } // class } // namespace