I love controls. Even more … I love graph controls. In fact, graph controls have been one of the main things I have worked on during my tenure at my current place of employment.
A small aside: I think it is what I was meant to do (i.e. work on graph controls). For, my name is … Cory Plotts. Plotts, you know, as in plots. Ok, never mind, that was lame. 😀
So, it was quite natural, and actually part of my job, to take a look at what Microsoft is offering in the WPF and Silverlight toolkits. I have to hand it to David Anson and the other fellows at Microsoft. They have taken a very nice approach.
First, almost everything is a System.Windows.Controls.Control that you can restyle and re-template to your heart’s content. So, that makes it very designable.
Second, I love the data binding model. They have decided to follow an ItemsControl like approach where you have an ItemsSource property that you just plunk your data into. Very nice. It literally takes you seconds to get something up and running.
Third, they are developing it … with Blend in mind. That is, they are trying to provide a positive Blend experience and have gone to pains to make it so. So, not only is it designable … but designers can actually use Blend to do their designing … instead of hacking through xaml in a code editor. Woot! Woot!
Now, this post is not going to be an introduction on how to start using the charting component. Many others have done that already. If that is what you are looking for … I would suggest that you go to this blog post by David Anson where he lists out lots and lots of links to other blog posts and articles. In fact, he is so nice … that he has even separated them out by difficulty level.
No, in this blog post, I am going to show you the fourth thing I love about this charting component: how extensible it is! And, I am going to do that by showing how I created a logarithmic axis by simply deriving from NumericAxis.
So, here’s the code (sorry for just dumping it all in one place … but it gives you a nice place to copy out the entire implementation at once):
using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Shapes; namespace System.Windows.Controls.DataVisualization.Charting { /// <summary> /// An axis that displays numeric values along a logarithmic range. /// </summary> [StyleTypedProperty(Property = "GridLineStyle", StyleTargetType = typeof(Line))] [StyleTypedProperty(Property = "MajorTickMarkStyle", StyleTargetType = typeof(Line))] [StyleTypedProperty(Property = "MinorTickMarkStyle", StyleTargetType = typeof(Line))] [StyleTypedProperty(Property = "AxisLabelStyle", StyleTargetType = typeof(NumericAxisLabel))] [StyleTypedProperty(Property = "TitleStyle", StyleTargetType = typeof(Title))] [TemplatePart(Name = AxisGridName, Type = typeof(Grid))] [TemplatePart(Name = AxisTitleName, Type = typeof(Title))] public class LogarithmicAxis : NumericAxis { /// <summary> /// Instantiates a new instance of the LogarithmicAxis /// </summary> public LogarithmicAxis() { ActualRange = new Range<IComparable>(1.0, 2.0); } /// <summary> /// Returns the plot area coordinate of a value. /// </summary> /// <param name="value">The value to plot.</param> /// <param name="range">The range of values.</param> /// <param name="length">The length of the axis.</param> /// <returns>The plot area coordinate of the value.</returns> protected override UnitValue? GetPlotAreaCoordinate(object value, Range<IComparable> range, double length) { if (value == null) { throw new ArgumentNullException("value"); } if (range.HasData) { double doubleValue = ValueHelper.ToDouble(value); Range<double> actualDoubleRange = range.ToDoubleRange(); return new UnitValue ( length / Math.Log10(actualDoubleRange.Maximum / actualDoubleRange.Minimum) * Math.Log10(doubleValue / actualDoubleRange.Minimum), Unit.Pixels ); } return new UnitValue?(); } /// <summary> /// Returns the value range given a plot area coordinate. /// </summary> /// <param name="value">The plot area position.</param> /// <returns>The value at that plot area coordinate.</returns> protected override IComparable GetValueAtPosition(UnitValue value) { if (ActualRange.HasData && ActualLength != 0.0) { if (value.Unit == Unit.Pixels) { double coordinate = value.Value; Range<double> actualDoubleRange = ActualRange.ToDoubleRange(); double output = Math.Pow ( 10, coordinate * Math.Log10(actualDoubleRange.Maximum / actualDoubleRange.Minimum) / ActualLength ) * actualDoubleRange.Minimum; return output; } else { throw new NotImplementedException(); } } return null; } /// <summary> /// Returns a sequence of values to create major tick marks for. /// </summary> /// <param name="availableSize">The available size.</param> /// <returns>A sequence of values to create major tick marks for. /// </returns> protected override IEnumerable<IComparable> GetMajorTickMarkValues(Size availableSize) { return GetMajorValues(availableSize).Cast<IComparable>(); } /// <summary> /// Returns a sequence of values to plot on the axis. /// </summary> /// <param name="availableSize">The available size.</param> /// <returns>A sequence of values to plot on the axis.</returns> protected override IEnumerable<IComparable> GetLabelValues(Size availableSize) { return GetMajorValues(availableSize).Cast<IComparable>(); } /// <summary> /// Returns a sequence of major axis values. /// </summary> /// <param name="availableSize">The available size.</param> /// <returns>A sequence of major axis values. /// </returns> private IEnumerable<double> GetMajorValues(Size availableSize) { if (!ActualRange.HasData || ValueHelper.Compare(ActualRange.Minimum, ActualRange.Maximum) == 0 || GetLength(availableSize) == 0.0) { yield break; } yield return 125; yield return 250; yield return 500; yield return 1000; yield return 2000; yield return 4000; yield return 8000; } } }
To explain the above code a little bit, I would first direct your attention to two methods. The first is called GetPlotAreaCoordinate. This method is the method responsible for converting from world coordinates into device coordinates. That is, it converts all the data point values in your Series into pixel coordinates … so that your data point is placed where you expect it should be.
The second method is called GetValueAtPosition. This method does the exact opposite as GetPlotAreaCoordinate. It converts from device coordinates into world coordinates. In other words, it takes the mouse position (for example) and it tells you where your mouse is at on the respective axis.
So, the above two methods contain the math behind the axis … and if you look closely, you’ll see the Math.Log10 function getting called. Yep. That’s right. This is a logarithmic axis.
After those two methods, you will see GetMajorTickValues and GetLabelValues. They both delegate to a third method called GetMajorValues. GetMajorTickValues returns the values at which we want major tick marks and GetLabelValues returns the values at which we want grid line labels. Pretty straightforward.
Now, a word about my implementation of GetMajorValues. In it, I have chosen to hardcode very specific values for this axis’ grid lines (i.e. 125, 250, 500, 1000, 2000, 4000, 8000). I did that because I was lazy. For inside of LinearAxis, there is an Interval property which you can use to generate the grid lines in a dynamic fashion … and I didn’t feel like figuring out how to make the Interval property work for the logarithmic axis. (If anyone out there does take the time to do so … please share!)
So, more than likely, you will be wanting to replace those specific values with some of your own … or will be wanting to generate the major values in a much more dynamic way.
Ok, now let me show you how you would use this axis. Check out this xaml (this is just a snippet … I am leaving out all the styling xaml and more):
<charting:Chart x:Name="chart" Width="480" Height="480" Margin="10" Title="Response" BorderBrush="{x:Null}" Style="{StaticResource chartStyle}" > <charting:Chart.Axes> <charting:LogarithmicAxis Orientation="X" ShowGridLines="True" Title="Frequency (Hz)" Minimum="100" Maximum="10000" /> <charting:LinearAxis Orientation="Y" ShowGridLines="True" Title="Response (dB SPL)" Minimum="20" Maximum="120" Interval="10" /> </charting:Chart.Axes> <charting:LineSeries ItemsSource="{StaticResource responseCurve}" IndependentValueBinding="{Binding Frequency}" DependentValueBinding="{Binding Response}" DataPointStyle="{StaticResource lineDataPointStyle}" AnimationSequence="Simultaneous" TransitionDuration="0:0:0" IsSelectionEnabled="True" /> </charting:Chart>
In the above, you can see my new and proud LogarithmicAxis with a Minimum value of 100 and a Maximum value of 10000. Here is a snapshot of the chart that the above xaml creates:
Woot! Doesn’t it look sweet!
Now, go out and create some logarithmic charts!
p.s.
I have had to make these changes in a local edit of the toolkits. However, as David Anson points out in this post … they are unsealing everything! So, in a little while … you will no longer need to do this. You will be free to simply derive a new axis at your leisure. In my mind, that is another point for extensibility!
p.s.s.
Here is the source code for this article … note that it contains David Anson’s development release version 1 … and it contains his DataVisualizationDemos applications. Finally, and most importantly for this article, it contains the sample code for the above … under the title Visibility (I was working on some DataPoint visibility functionality at the time I drafted up that test harness).
p.s.s.s.
David Anson and the fellows at Microsoft have finally released the version of the Silverlight Toolkit that unseals everything. Check out this blog post for more info. When David publishes another development release, I’ll update the source code for this article so that the above is not a local edit.
p.s.s.s.s.
I have finally gotten around to getting the source code together where the above logarithmic axis is just an extension of toolkit … versus the local edit as before. So, here is that source code. It is actually from David Anson’s development release 3 (although I have stripped out the WPF 4 and Silverlight 4 projects). I won’t remove the above source code … as some people might want to see both ways of doing it.
One additional comment. In David’s development release 2, there was an internal NumericAxis constructor. So, even though he unsealed everything, that internal constructor was inhibiting me from inheriting a new numeric axis type via an extension. That meant I had to wait till development release 3 in order to provide this code. (I also had to find some time too. :D)
p.s.s.s.s.s.
I have finally decided to crack out the WPF 4.0 and Silverlight 4.0 version of my logarithmic axis above as several people have expressed confusion and frustration trying to get it to work.
The source code can be downloaded here.
This source code is based on David Anson’s development release 4 version (i.e. here for the core bits and here for the test harnesses).
Please note that you should open up DataVisualizationDemos4.sln from the downloaded code as that solution contains all 4 projects: the 2 core projects and the 2 test harness projects. The 2 test harness projects have project references to the assemblies built in the 2 core projects.
Also note that you should unload either the Silverlight 4 test harness … or the WPF 4 test harness, when running, setting the other one as your startup project … as I have had problems with the built executables/bits for the test harnesses stomping on top of one another.
Finally, once you get it running, there is a ‘Logarithmic Axis’ test harness. Good luck!
Hi Cory,
The Log10 Axis looks great. I just tweaked your control a bit. For the GetMajorValues method, I used:
It is working good.
@Srikanth
I tried your suggestion above and it works nicely. I’m sure readers will like an ‘automatic’ way of doing it … in addition to setting them manually.
Thanks for sharing!
There’s still time for someone to figure out how to make an Interval property work … any takers?
I am not sure what you mean about the Interval property. I just found this interesting method to override. Take a look at the following snippet:
This gives some nice margins required for the plot. Now all I have to figure out is how to handle the situation where I get some 0 or negative numbers to be plotted on the Axis.
I tried overriding the ‘bool CanPlot(object)’ method but it is checking only the first element in the series. So some more work is left before I can use it for real. 🙂
Thanks for the implementation.
Ah, that is a very nice addition (your above code snippet) … such that you don’t have to set the Minimum and Maximum properties. I like it.
About the Interval property: there is an Interval property on the LinearAxis. It allows you to specify the interval between major grid lines. For example, in the above chart, I have it set to 10 and thus there are major grid lines every 10 units, starting from the Minimum (20) and ending with the Maximum (120).
David Anson has done some more work regarding values crossing the 0 boundary. When he has published another development release, I plan on trying this new functionality out. So, stay tuned. I will probably attach this new code to this blog post.
Hmm … about the Interval property, I emailed David Anson about my implementation. I got it with ‘another’ override. 🙂
where
This was drawing the gridlines where ever I want. David says this is the correct way to specify where you want the grid lines to be drawn. I was looking for an option for minor grid lines as it will allow for styling them differently. I could not find any.
I am awaiting your post using the development release 2. I haven’t got time to check for differences this week.
Well, I see what you did … and now I understand your confusion about the Interval property.
For the logarithmic charts that I create, the grid lines just happen to be equidistant from each other (125, 250, …), but for the typical logarithmic chart the grid lines are spaced linearly … to emphasize the logarithmic nature of the axis.
Given that, I’m not sure that an Interval property makes sense for a logarithmic axis … because I would want it one way … and you would want it another way.
In my way, the Interval property could be set to 0.1 and then there would be a grid line for 10^2.0, 10^2.1, 10^2.2, and so on.
In your way, I’m not sure what the Interval property should be set to … or what it would mean.
Again, I think I see your initial confusion.
As to your desire about minor grid lines … I don’t think that David Anson and crew have given us the ability (out-of-the-box) to do minor grid lines.
I understand. Here is how my latest implementation looks:
http://img24.imageshack.us/img24/3817/logchart.jpg
Very nice! Thanks for sharing!
I have just updated this blog post to include the source code that shows the logarithmic axis as an extension of the toolkit (versus a local edit).
See the last postscript above.
When opening the solution using VisualStudio 2008 I see two errors. One of which is:
A value of type ‘LogarithmicAxis’ cannot be added to a collection or dictionary of type ‘Collection`1’.
C:\DataVisualizationDemos\Silverlight\VisibilityTestHarness.xaml 202 5 DataVisualizationDemosWPF
The demo seems to work all the same however I get the same error in my own project after I have put this source in my project … and attempt to utilize the logarithmic axis in the xaml file.
Do you know why this is happening and how to fix it?
The Visual Studio designer cannot render the Window when this error occurs.
Thanks!
@Kyle
As I have mentioned in a private email to you, I do get this error, but it doesn’t seem to affect things … and if I rebuild … the Visual Studio designer surface does come up.
What is interesting is that I don’t get this error in Blend … which makes me less concerned about it really. The Visual Studio designer definitely has its issues. I wish I hadn’t stripped out the WPF 4 project now. It would be interesting to see if that designer error comes up in the new Visual Studio 2010 designer. I would speculate that it wouldn’t … as they have seriously worked on the Visual Studio designer for 2010. If anyone goes to the length of finding this out … let me know.
(Note: this designer error only happens with the version of the source code above … that is an extension of the toolkit … and not with the local edit of the toolkit.)
I was wondering if you have had any luck getting a chart to print using a FixedDocument? My experience in attempting to do so shows that the X and Y axis print, but not the data points or polyline connecting them. I am not talking about printing the chart as a ‘visual’ having already been rendered within a Window, but directly created and placed on a FixedDocument (well in a FixedPage and Canvas but you get the idea).
This is a mystery to me and I thought you might have tried this already?
Kyle: I have never tried that. Interesting.
Very interesting, in fact. As we currently have a bug where the polyline is not showing up in one of our graphs being printed.
I will keep your issue in mind when I tackle our own issue, and if I learn anything that might help you … I will comment back here. Please do the same on your end.
Kyle: I would also contact David Anson about this issue. Or … are you only noticing it with my logarithmic axis implementation?
This failure to see the polyline is not specific to the logarithmic axis. I have not figured this out yet; it sure would be nice to know how to get this to work.
One more hint. The polyline also does not show up when the control is rendered using RenderTargetBitmap; I am attempting to render the control into an image file without first having to actually have a Window associated with the graph.
Hello cplotts,
Thanks for the code. It works well. However, we are facing a small issue while using this code. Let me explain.
Whenever we set min and max for the axis (LogarthmicAxis), the GetMajorTickValues and GetLabelValues are getting called repeatedly. Are they supposed to be called only once whenever we set the min and max? Also, the GetPlotAreaCoordinate method gets called repeatedly (irrespective of GetMajorTickValues and GetLabelValues). This behaviour is more evident (the application hangs even longer) when the different between min and max is minimal.
Do you have any thoughts on this?
I just realized that I forgot to post back here about how I solved the bug where the polyline is not showing up in our printed graphs.
So, here is what was going on …
In LineAreaBaseSeries.OnApplyTemplate … there is some code that creates a DataPoint and bounces it into the visual tree. It then uses the Background brush of the DataPoint to set the Background brush of LineSeries itself. This code never gets called in the situation where the chart isn’t part of the visual tree … and so the Background brush wasn’t getting set on the LineSeries.
I could see no reason for bouncing the DataPoint into the visual tree and changed the code so that it just sets the Style directly.
Now, of course, this is a local edit of the charting component. If you aren’t locally modifying the component, you can also set the Background directly (instead of through a Style). This also worked for me:
Hope that helps someone!
I don’t quite know what is causing you problems @KS and I really don’t have much time to dive in on it. I took a quick look and noticed that the local edit source code … is indeed setting a min and a max on the logarithmic axis, but I am not having any problems (hangs).
GetPlotAreaCoordinate should get called many times (my debugger counted over 600 times for just initialization of the Visibility graph) as it gets called once for every point on the curve … each time the curve gets updated (and the curve has 80 points). However, as I already mentioned, this doesn’t cause the application to hang whatsoever.
For the same initialization, GetMajorTickValues and GetLabelValues were not called that much at all … 11 times for GetMajorTickValues and 6 times for GetLabelValues. So, something funny is going on in your code.
Good luck and post back here if you figure out what is wrong!
Hi,
I was just trying to add this to a Silverlight 4 project and the compiler is throwing an error:
‘…LogarithmicAxis’ does not implement inherited abstract member ‘System.Windows.Controls.DataVisualization.Charting.RangeAxis.GetPlotAreaCoordinate(object, double)’
The code does implement the version of the function with three params (value, range, length) but not the two param version. Any help on this please?
@Bpj: I have not updated the source code associated with this blog post … for the release of Silverlight 4. I will put this on the todo list and post back here when I get it finished.
It should be fairly simple to implement though … if you want to try on your own. Leave a comment back here if you do.
@cplotts Thank you very much for that bugfix – it’s finally fixed a problem that has been tormenting me for hours :-). Hope it gets into the next release of the toolkit.
Nice to hear @David! Glad I could be of help.
I’ve been playing with the Logarithmic Axis in the hopes of using it in a project I’m involved with. However I’ve run into issues when the range of values is <=0. VS2010 seems to hang (very high CPU, no response). Has anyone else run into this?
@John, the simple answer is that the logarithm of zero is undefined. Check out here and here for more info.
@cplotts
Thanks kindly! Sorry about the dumb question. Got that portion of things working well now. I’m currently working on getting independently style-able minor grid lines implemented. Being able to do gradient shading between is another request. Anybody have ideas?
@John, maybe you realize this already, but the chart control doesn’t support minor grid lines out-of-the-box.
I actually had to locally modify the chart to get minor grid lines myself. Unfortunately, I’m not able to share that code. The simple approach I took was to create a collection of what I call ManualGridLine(s) … on the axis itself. If there are any grid lines in this collection, it renders those instead of using the normal logic to render the automatically generated ones.
You could use an AreaSeries for the gradient shading between the grid lines … but that seems sort of like an ugly hack. But, the alternative is yet another modification of the chart control. If you’re already modifying the chart control, then maybe this is not a big deal for you.
As to implementation ideas for the gradient shading, it seems like an elegant solution would be to create/derive an axis which does this shading for you.
@cplotts
Thanks again!
I realize that minor grid lines aren’t available ‘out of the box’. I’ve been running two copies of VS2010 almost constantly; one with my code, the other with the toolkit source loaded so I can dig around. 🙂
I’m intrigued by your idea of ManualGridLine(s). I’ve been working on a implementation where I essentially create a mirror of the MajorGridLine functionality at the LogarithmicAxis level for minor grid lines. How did you get the ManualGridLines on the axis to render on the invalidate event? Is it a question of adding it to the listener?
My original thought was to create the minor grid lines, style them appropriately, then add them to the grid lines collection for rendering. However, I’ve discovered the DisplayAxisGridLines type (and it’s associated GridLines property) are both internal to the DisplayAxis. No love there. 😛
Needless to say, I’m new to WPF and this has been a learning experience. As always, any thoughts are most appreciated. Keep up the great work!
@John … sorry, but I don’t have a lot of time to devote to explaining how I went about things, and besides, I would really need to get permission from my employer (to share this further) before going too much deeper.
Anyways, it sounds like you are taking a different approach.
One last thing I will leave you with: if you’re locally modifying the toolkit source code … you can make those internals … public.
Good luck. 🙂
@cplotts
I totally understand. Thanks for the time you’ve already provided!
I’m trying to avoid too much hacking on the toolkit. I’d hate to get into a position of making breaking changes to the toolkit that I’d have to fix when a new version comes out … but we’ll see.
Thanks! 🙂
@John: Ha! 😀
That is exactly where I’m at … and I still haven’t updated/merged the latest bits from Microsoft (David Anson’s development release v4) … which was released back in April of this year.
So, if you can avoid it … do so …
@cplotts
The magic of software development! Trust me, I’m doing everything I can to avoid ‘mucking about’ in the toolkit. 🙂
By the way, here is an image of the chart I’m working on for your amusement.
I’m taking a break from the minor grid lines and looking to get the legend to appear. 🙂
@John: love the graph! Thanks for forwarding a picture on.
Victory!! 🙂
I inherited from LogarithmicAxis (called it LogarithmicAxisMinor), added a private function called GetMinorValues (essentially GetMajorValues which does the minor positons) and overrode the GetMajorGridLineCoordinates to return the output from GetMinorValues.
Then I added my new LogarithmicAxisMinor to the X axis along with the LogarithmicAxis and suppressed showing the AxisLabel and MajorTickMarks. Best of all it doesn’t make any changes to the Toolkit. 🙂
@John: very nice! And even better that you didn’t have to modify the toolkit.
By the way, just curious … is this a WPF graph or a Silverlight one?
@cplotts
I believe the project is WPF but I don’t see any reason it couldn’t be done in Silverlight.
Quick question: Is there a way built in way to determine the y coordinate of a line series if given the x coordinate? (and vice versa)
@John: as far as I know there is no built in way to do that. You’re on your own there.
@cplotts
Roger that. I’ll let you know when (if) I find anything. 🙂
This thing does not compile here, the ValueHelper and the ToDoubleRange require something you have not said.
After copying and pasting and deleting the line counters I have a useless piece of code. Ideas?
Otherwise, really, rubbish as usual.
@lollo, I have always had code attached to this blog post that you can download and build.
Note, however, that the code has never been updated for Silverlight 4 (as some comments above) point out … so if you are trying to get it to work for that platform, you will need to do some work yourself.
Note, as well, that technologies are never stagnant and move forward. Meaning, I have no idea how this code compiles with the current Silverlight SDK and the like.
But, with a little work, you should be able to get things to work.
@cplotts:
I have to implement a logrithmic X axis for my WPF chart. Currently, I am using a linear axis to display a label for the graph and a category axis to plot the points. I am doing so as the interval between the points is not equal (the intervals are 750, 1k, 1.5k, 2k, etc.). For this, I am using a converter class from double to string in order to display the label.
My requirements seem to be the same as you have given in the above example. That is, I have a response graph where the X axis is Frequency (HZ) and where the Y axis is Response (dB).
My application is developed with Visual Studio 2010 using .NET 4.0.
I tried your approach above, but have been unable to get it to work.
@shubha, I have finally decided to crack out the WPF 4.0 and Silverlight 4.0 version of my logarithmic axis above … given your recent comment.
The source code can be downloaded here.
This source code is based on David Anson’s development release 4 version (i.e. here for the core bits and here for the test harnesses).
Please note that you should open up DataVisualizationDemos4.sln as that solution contains all 4 projects: the 2 core projects and the 2 test harness projects.
Also note that you should unload either the Silverlight 4 test harness … or the WPF 4 test harness, setting the other one as your startup project.
Once you get it running, there is a ‘Logarithmic Axis’ test harness.
@cplotts
Thanks for the quick reply… the code you provided is plotting a static graph. For my application, I need to plot a point on a log graph. I am able to do so, but still need to make more changes. 🙂
@shubha
You need to make changes to the axis? Or changes to the data values being plotted on the graph?
For the former, that is, changes to the axis … you will likely need to modify the LogarithmicAxis class to make it more flexible … since the grid lines for the X axis are hard coded in the GetMajorValues method. Note, however, that you should be able to currently change the Minimum and Maximum values on the LogarithmicAxis … and have it update correctly.
For the latter, that is, changes to the data values … that is completely supported. I only provided a simple example where that data is indeed hard coded to one particular curve. However, you can update those values or data bind a whole different curve.
Your code was a great help, but when I try to set the y axis to be log it reverses the y axis (the min value is on top and the max value is near the origin). Anyone else had this problem?
@Morgan, my bad … that is, if you were using the WPF 4.0/Silverlight 4.0 version of the code that I recently uploaded.
It had a mistake in it that I have corrected.
I have uploaded the correct code to the same link.