Blog Archives
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
XML (part 1, basics), file IO
The last days were tough. Writing posts requires a lot of time. Explaining some basic know-how will give me some recreation today.
I will concentrate on the C# parts. There will be no explanations about XML, which is well documented across the internet.
XML stands for eXtensible Markup Language. XML is a versatile and flexible data structure. It is easy to read, learn and understand. The downside is the amount of bytes it needs. You can use XML to transport and/or store data, but it quickly becomes quite inefficient when a lot of datasets are involved.
XML is a well established industry standard. So there is no way around it, you have to know at least something about it.
Here is an XML file that we will use in the loading example:
<?xml version="1.0" encoding="UTF-8"?> <!-- XML EXAMPLE --> <Walmart> <food> <name>Banana</name> <price>1.99</price> <description>Mexican delicious</description> </food> <food> <name>Rice</name> <price>0.79</price> <description>the best you can get</description> </food> <food> <name>Cornflakes</name> <price>3.85</price> <description>buy some milk</description> </food> <food> <name>Milk</name> <price>1.43</price> <description>from happy cows</description> </food> <electronic> <name>Kindle fire</name> <price>100</price> <description>Amazon loves you</description> <somethingElse>the perfect Xmas gift for your kids</somethingElse> </electronic> <food> <name>baked beans</name> <price>1.35</price> <description>very British</description> </food> </Walmart>
Paste the XML structure into a text editor and save it as an XML file (“Walmart.xml”) onto your desktop.
The following class will be used to store datasets in memory.
public class WmItem { public readonly string name; public readonly double price; public readonly string description; public WmItem(string xName, double xPrice, string xDescription) { name = xName; price = xPrice; description = xDescription; } // constructor public override string ToString() { return name.PadRight(12) + price.ToString("#,##0.00").PadLeft(8) + " " + description; } // } // class
Loading XML files is easy. Processing files takes a bit longer. The centerpiece generally is the parsing algorithm.
There is a field called “somethingElse” in the XML file. It does not cause any trouble. The code simply disregards it. I added it to demonstrate the “eXtensible” in “eXtensible Markup Language”.
public static void LoadXml() { string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\"; string lFile = lDesktopPath + "Walmart.xml"; XDocument lXDocument = XDocument.Load(lFile); // food (using WmItem) WmItem[] lFood = (from lData in lXDocument.Descendants("Walmart").Descendants("food") select new WmItem( lData.Element("name").Value, double.Parse(lData.Element("price").Value), lData.Element("description").Value) ).ToArray(); foreach (WmItem lItem in lFood) Console.WriteLine(lItem); Console.WriteLine(); // electronic (quick and dirty, using var) var lElectronic = from lData in lXDocument.Descendants("Walmart").Descendants("electronic") select lData; foreach (var lItem in lElectronic) { Console.WriteLine(lItem); Console.WriteLine(); Console.WriteLine(lItem.Element("name").Value); Console.WriteLine(lItem.Element("price").Value); Console.WriteLine(lItem.Element("description").Value); } Console.ReadLine(); } //
example output:
Banana 1.99 Mexican delicious
Rice 0.79 the best you can get
Cornflakes 3.85 buy some milk
Milk 1.43 from happy cows
baked beans 1.35 very British<electronic>
<name>Kindle fire</name>
<price>100</price>
<description>Amazon loves you</description>
<somethingElse>the perfect Xmas gift for your kids</somethingElse>
</electronic>Kindle fire
100
Amazon loves you
Saving XML is also straight forward. I added a comment and attributes for demonstration purposes. The program generates and saves the XML file “Genesis.xml” on your desktop.
public static void SaveXml() { string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\"; string lFile = lDesktopPath + "Genesis.xml"; WmItem[] lMetals = { new WmItem("Lead", 1.0, "here we go"), new WmItem("Silver", 2.0, "cutlery"), new WmItem("Gold", 3.0, "wife's best friend"), new WmItem("Platinum", 4.0, "posh") }; XDocument lXDocument = new XDocument(); lXDocument.Declaration = new XDeclaration("1.0", "utf-8", "yes"); lXDocument.Add(new XComment("copyfight by Bastian M.K. Ohta")); XElement lLME = new XElement("London_Metal_Exchange", new XAttribute("attribute1", "buy here"), new XAttribute("AreYouSure", "yes")); lXDocument.Add(lLME); foreach (WmItem lMetal in lMetals) { XElement lGroup = new XElement("metal"); lGroup.Add(new XElement("name", lMetal.name)); lGroup.Add(new XElement("price", lMetal.price)); lGroup.Add(new XElement("description", lMetal.description)); lLME.Add(lGroup); } lXDocument.Save(lFile); //Console.ReadLine(); } //