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 )

Silverlight, WPF

  1. October 20th, 2009 at 15:17 | #1

    Hi Cory,

    The Log10 Axis looks great. I just tweaked your control a bit. For the GetMajorValues method, I used:

    private IEnumerable GetMajorValues
    (
        Size availableSize
    )
    {
        if
        (
            !ActualRange.HasData ||
            ValueHelper.Compare(ActualRange.Minimum, ActualRange.Maximum) == 0 ||
            GetLength(availableSize) == 0.0
        )
        {
          yield break;
        }
    
        int start = (int)Math.Floor(Math.Log10(ActualRange.ToDoubleRange).Minimum));
        int stop = (int)Math.Ceiling(Math.Log10(ActualRange.ToDoubleRange().Maximum));
    
        for (int x = start; x <= stop; x++)
            yield return Math.Pow(10, x);
    }
    

    It is working good.

  2. cplotts
    October 20th, 2009 at 15:47 | #2

    @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!

  3. cplotts
    October 20th, 2009 at 15:52 | #3

    There’s still time for someone to figure out how to make an Interval property work … any takers?

  4. October 21st, 2009 at 09:58 | #4

    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:

    protected override Range<IComparable>
    OverrideDataRange
    (
        Range<IComparable> range
    )
    {
        Range<double> result = range.ToDoubleRange();
        if (!result.HasData)
        {
            return new Range<IComparable>(0.0, 0.0);
        }
        else
        {
            double start = (int)Math.Floor(Math.Log10(result.Minimum));
            double stop = (int)Math.Ceiling(Math.Log10(result.Maximum));
    
            start = Math.Pow(10, start);
            stop = Math.Pow(10, stop);
    
            return new Range<IComparable>(start, stop);
        }
    }
    

    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.

  5. cplotts
    October 21st, 2009 at 10:46 | #5

    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.

  6. October 23rd, 2009 at 10:46 | #6

    Hmm … about the Interval property, I emailed David Anson about my implementation. I got it with ‘another’ override. :)

    protected override
    IEnumerable<UnitValue>
    GetMajorGridLineCoordinates(Size availableSize)
    {
        return
            GetMinorValues(availableSize)
                .Union(GetMajorValues(availableSize))
                .Select(value => GetPlotAreaCoordinate(value))
                .Where(value => value.HasValue)
                .Select(value => value.Value);
    }
    

    where

    private IEnumerable<double>
    GetMinorValues(Size availableSize)
    {
        if
        (
            !ActualRange.HasData ||
            ValueHelper.Compare(ActualRange.Minimum, ActualRange.Maximum) == 0 ||
            GetLength(availableSize) == 0.0
        )
        {
            yield break;
        }
    
        int start = (int)Math.Floor(Math.Log10(ActualRange.ToDoubleRange().Minimum));
        int stop = (int)Math.Ceiling(Math.Log10(ActualRange.ToDoubleRange().Maximum));
    
        for (int x = start; x < stop; x++)
        {
            double begin = Math.Pow(10, x);
    
            for (int y = 2; y < 10; y++)
                yield return begin * y;
        }
    }
    

    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.

  7. cplotts
    October 23rd, 2009 at 19:19 | #7

    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.

  8. cplotts
    October 23rd, 2009 at 19:27 | #8

    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.

  9. October 26th, 2009 at 09:07 | #9

    I understand. Here is how my latest implementation looks:

    http://img24.imageshack.us/img24/3817/logchart.jpg

  10. cplotts
    October 26th, 2009 at 09:30 | #10

    Very nice! Thanks for sharing!

  11. cplotts
    January 19th, 2010 at 09:33 | #11

    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.

  12. Kyle
    January 20th, 2010 at 16:27 | #12

    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!

  13. cplotts
    January 21st, 2010 at 08:05 | #13

    @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.)

  14. Kyle
    January 26th, 2010 at 12:08 | #14

    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?

  15. cplotts
    January 28th, 2010 at 08:21 | #15

    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.

  16. cplotts
    January 28th, 2010 at 08:36 | #16

    Kyle: I would also contact David Anson about this issue. Or … are you only noticing it with my logarithmic axis implementation?

  17. Kyle
    February 2nd, 2010 at 15:23 | #17

    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.

  18. Kyle
    February 2nd, 2010 at 15:26 | #18

    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.

  1. No trackbacks yet.