Home > Silverlight, WPF > WPF & Silverlight Charting: A Logarithmic Axis

WPF & Silverlight Charting: A Logarithmic Axis

October 9th, 2009

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. :D

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:

image

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!

Share/Save

Silverlight, WPF

  1. Morgan
    August 29th, 2011 at 13:44 | #1

    Thanks for the quick response, I am using WPF4.0/Silverlight 4.0 so hopefully this will fix the problem. The only issue is that you posted a bad link. :)

  2. cplotts
    August 29th, 2011 at 14:20 | #2

    @Morgan, ha :D, I can’t win!

    I fixed the link.

  3. Morgan
    August 30th, 2011 at 15:30 | #3

    Thanks, another quick question, currently if you set a Major value to less the 0.01 it formats it to 0, is there any quick easy way to change the formating?

  4. cplotts
    September 1st, 2011 at 10:06 | #4

    @Morgan, I’m pretty sure you can do that … but I really don’t have the time to solve it for you.

    I did take a quick look, though, and my axis labels were showing up as 0.01 after setting the Minimum to 0.01 and returning a few more grid lines … so this seems like something you might be doing/causing on your side.

  5. Justin
    November 14th, 2011 at 15:11 | #5

    I just recently started playing around with WPF for a work project, and I appear to be having some issues with this axis. My plot is supposed to be a column series with a date/time axis along the X, and a logarithmic axis in the Y direction. When I attempt to render, no values appear on my chart. If I change the column series to another type, such as scatter, area, or line series, my data renders. Do you have any ideas on why my columns don’t appear?

    As I said, I am new to WPF (I am a Java developer mostly), so if there is more info I need to provide let me know. Thanks!

  6. cplotts
    November 14th, 2011 at 16:33 | #6

    I have never used the logarithmic axis in that way, but it should be possible. That being said, I wouldn’t be surprised if there is an arbitrary limitation in the charting code.

    I would debug into it if I were you.

    If you are still hitting a brick wall after debugging into it, compose a simple project for me and I’ll see if I can take a quick look myself.

  7. Justin
    November 15th, 2011 at 13:28 | #7

    @cplotts
    Thanks for the quick reply. I’m looking into it still and haven’t yet come up w/ a reasonable explanation. Consequently, I have also downloaded the DataVisualizationDemos4 project you have linked above, and when selecting to see the “Logarithmic Axis” demonstration, the grid appears but the data line does not. Could the fact no data displays there be related as well?

    As a background, I am using the February 2010 Release of the WPF Toolkit and .Net Framework 4 in Visual Studio 2010.

  8. cplotts
    November 15th, 2011 at 15:30 | #8

    @Justin, two things.

    First … I have updated the test harness that you have downloaded. I was previously only showing a logarithmic axis (in the correct direction) on the Y axis … and not really focusing on the line series. It was working … but the minimum and maximum values weren’t showing a range to where you could see the data. That is, the sample data occurs at values less than 100.

    In the updated harness, I have created a derived logarithmic axis that better shows off the line series.

    So, in other words, the fact that no data displays in my test harness is likely not related.

    Second … the development release 4 of this charting component … may or may not be completely compatible with the February 2010 release of the WPF Toolkit … I do not know, I have not tried this specific incantation of the logarithmic axis against it. If it doesn’t work, it shouldn’t be too hard to fix … so that it does work … but it may not be a drop-in.

  9. cplotts
    November 15th, 2011 at 15:35 | #9

    For fun, here is an image to the new chart:
    Correct Logarithmic Chart

  10. BG
    March 22nd, 2012 at 15:42 | #10

    This is an EXCELLENT article and sample. Thanks!

    Is it possible to have the minimum X axis value be 0? If I set that, then the logarithm doesn’t work and always return NaN. Any way to “trick” things to make this work?

    Thanks again.

  11. cplotts
    March 23rd, 2012 at 12:23 | #11

    @BG, thanks for the compliment on the post.

    Yes, it doesn’t work because the logarithm of 0 is mathematically undefined.

    I would think that there might be a way to trick things … via the grid line labels. I would set a grid line at 0.001 or something like that … and then round the label to 0.

  12. BS
    January 11th, 2013 at 09:47 | #12

    Was there ever a solution for your logarithmic axis not working with ColumnSeries?? It works with Line and Bar but not column. Any thoughts??

  13. cplotts
    January 11th, 2013 at 11:20 | #13

    @BS, boy, I don’t even remember that it couldn’t do that … it’s been forever since I’ve looked at this code.

    If you say it is a limitation, then it probably is. However, I am sure it is a solvable problem. Unfortunately, I don’t have time to dive in on it for you.

    Good luck!

Comment pages
  1. No trackbacks yet.