Blog

Visiblox Charts Ranges Demystified

This blog post will introduce the concept of a Range that Visiblox Charts uses to describe the limits of an axis. It will cover the following topics:

 

What is a Range?

In Visiblox Charts we have a notion, described by the IRange interface, of a value range. This is primarily used to tell an axis how much of a value range to show. The IRange interface is really quite simple and basically just has a Maximum and a Minimum (and a few methods to grow the range). There are various implementations of IRange (or rather more specifically IRange<T>) out of the box which provide strongly typed versions of Maximum and Minimum. These are: DoubleRange, DateTimeRange and CategoryRange, which map quite nicely to the various axes which are provided out of the box with Visiblox Charts.

 

Auto-Calculated Ranges

If you just create a chart, assign a chart series (e.g. a LineSeries) to it, add some data to that chart series via the DataSeries class and run your project you will notice that despite never having defined axes, your chart ends up with an X and Y axis. The reason for this is that on first load, the chart makes a best effort at creating axes of the correct type based on the data in the data series. Once the axes are created, they then need to figure out how much of a value range to show, this is done by creating a range of the appropriate type, which can be done by calling the IAxis.CreateRange() method.

The chart then iterates over all the data points in the data series and growing the range to include all those points. Finally a little bit extra is added to allow for a margin. In many cases that works pretty well and is good enough, however sometimes you might want more control over the size of the range or to avoid the auto-range calculation to help performance. In such cases you can manually create a range and assign it to the IAxis.Range property.

 

Setting a Fixed Range

Creating a range is pretty simple. If you know what kind of range you need you can just construct it, for example:

//if your axis is a LinearAxis or a LogarithmicAxis
var range1 = new DoubleRange();
range1.Minimum = 0;
range1.Maximum = 10;
Chart.XAxis.Range = range1;

//if your axis is a DateTimeAxis or DiscontinousDateTimeAxis
var range2 = new DateTimeRange();
range2.Minimum = DateTime.Now;
range2.Maximum = DateTime.Now.AddDays(10);
Chart.XAxis.Range = range2;

//if your axis is a CategoryAxis
var range3 = new CategoryRange();

//for the CategoryRange it's probably better to use the Grow(IComparable) method
//so that you get the full set of categories you want
range3.Grow("Category1");
range3.Grow("Category2");
range3.Grow("Category3");
range3.Grow("Category4");

Chart.XAxis.Range = range3;

Alternatively, and probably more safely, you can just ask the axis for a new Range. To do this just call CreateRange() as so:

//if your axis is a LinearAxis or a LogarithmicAxis you will get a DoubleRange
var range1 = Chart.XAxis.CreateRange();
range1.Minimum = 0;
range1.Maximum = 10;
Chart.XAxis.Range = range1;

//if your axis is a DateTimeAxis or DiscontinousDateTimeAxis you will get a DateTimeRange
var range2 = Chart.XAxis.CreateRange();
range2.Minimum = DateTime.Now;
range2.Maximum = DateTime.Now.AddDays(10);
Chart.XAxis.Range = range2;

//if your axis is a CategoryAxis you will get a CategoryRange
var range3 = Chart.XAxis.CreateRange();

//for the CategoryRange it's probably better to use the Grow(IComparable) method
//so that you get the full set of categories you want
range3.Grow("Category1");
range3.Grow("Category2");
range3.Grow("Category3");
range3.Grow("Category4");

Chart.XAxis.Range = range3;

As you can see we simply assign the newly constructed range object to the IAxis.Range property and that is then adopted as the new range, no questions asked.

 

IAxis.Range vs IAxis.ActualRange

If you look closely at the IAxis interface, however, you will see that there is actually a Range and an ActualRange property. So what's the deal? Well Visiblox Charts follows the same convention as a lot of the rest of the .NET APIs in that the Range is the user settable version and the ActualRange is what is actually applied. What then is the difference?

  • If you're setting a range, never set ActualRange, always set Range. ActualRange may be manipulated or replaced by the chart at any time, but it will never touch the Range set by the user.
  • If you're interested in finding out what the range actually is being used by the chart internally, or in binding to range changes, bind to the ActualRange, it will always be more up-to-date than Range.

If you look even closer, you will see that Range and ActualRange aren't, in fact, the same type. Range is of type IRange, but ActualRange is of type IRangeWithEffectiveLimits.

 

IRange and IRangeWithEffectiveLimits

As we mentioned earlier the IRange interface is really pretty simple. The IRangeWithEffectiveLimits is a good bit more complicated. In particular it adds a new concept of "effective" range. Where the IRange describes the axis minimum and maximum values, the effective range describes the currently visible range after zoom is applied. For example, if you Range is 0 to 10, and you zoom in to show only the first half of that range, then your Range will still be 0 to 10 but your effective range will be 0 to 5.

As a result if you want to be notified of changes in zoom or you want to know what is currently visible, irrespective of what zoom is applied, use the ActualRange.EffectiveMaximum and ActualRange.EffectiveMinimum dependency properties. You can also safely bind to these and be notified of zoom changes on the fly. Beware though, that these properties are updated "live" as the zoom is happening so if you have processing intensive operations happening here, you will slow down your chart's zoom operation.

That is fine for almost all purposes you'll come across, but there is one more twist to the tale of the Visiblox Charts' Range.

 

Exploring Continuous and Discontinuous Ranges

In order to smoothly zoom or pan based on a user's mouse operations, all ranges have to be in a continuous space. With linear ranges like the DoubleRange that's no problem because obviously real numbers are pretty continuous. The problem enters the picture when your value ranges aren't continuous, such as the category range or on a discontinuous date time axis. Here we have a problem because what if the user wants to zoom into somewhere that isn't exactly on the category boundary. For example, look at this image:

Partial Bar Zoom

Here we want to zoom from somewhere halfway between the "Canada" and the "UK" category to somewhere towards the "France" category. The EffectiveMinimum and EffectiveMaximum equivalent of those two would be "UK" and "France" respectively, but it has no notion of how much of each is included. In order to do that we need to have a notion of continuous and discrete value ranges and a way to convert from one to the other.

For this purpose IRangeWithEffectiveLimits has a ContinuousEffectiveMinimum and ContinuousEffectiveMaximum which is the mapping of the EffectiveMinimum and EffectiveMaximum onto some continuous range. In the case of the CategoryRange that works out so each category is an increment of 1 on a double range. So in the above example the ContinousEffectiveMinimum would be something like 1.5 (i.e. just before the second category) and the ContinuousEffectiveMaximum would be 3.7 (i.e. just before the 4th category) or so.

To map arbitrary values between the discontinuous range and the continuous range, there are two methods on IRangeWithEffectiveLimits:

IComparable ToDiscreteValueRange(IComparable continuousValue);

IComparable ToContinuousValueRange(IComparable discreteValue);

Thankfully, unless you are implementing your own axis or doing some very complex range or zoom logic, you should never need to worry about the continuous range, but it's worth knowing that it's there should you ever need it.

 

Conclusion

This article covered a basic overview of the Visiblox Charts Range concept. By using this and associated concepts such as the effective range and the continuous range, you are given full control over defining the range to show as well as determining what range is currently set and which is currently visible.

There are a few points to remember:

  • Set fixed range by assigning IAxis.Range which is of type IRange
  • Read values from IAxis.ActualRange but never set it
  • The dependency properties IAxis.ActualRange.EffectiveMinimum and IAxis.ActualRange.EffectiveMaximum define the limits of the currently visible portion of the axis
  • There is a continuous and a discrete axis range and where necessary you can switch between them using methods on IRangeWithEffectiveLimits

Hopefully you've found this useful information. Why not download Visiblox Charts for free and give this stuff a try now.

Comments

Hey Jesse,

thanks for your post!

Currently im trying to build a chart which gets updated each second. When a new datapoint is added, the chart range is updated so it scrolls to the last point on the xaxis. I also have a checkbox which stops the updating, so you can scroll manualy through the data. Im simply using the panBehaviour for this.

But for some strange reason, the auto-scrolling doesnt work anymore after manualy scrolling to a different position. it still scrolls. but it seems to have an offset, depending on how far away you scrolled from the newest datapoint.

Any solution for this? i guess i have to reset a scaling or offset value, wich gets changed by the panBehaviour...

Thanks! Lutz

 
Posted by Lutz

Hi Lutz,

Yes what is happening is that the PanBehaviour sets the Offset property of the Axis' Zoom object. That offset is always calculated relative to the current ActualRange.Maximum and ActualRange.Minimum. So if you want to "snap" your chart back so you're seeing the new data coming in, you just need to set the Offset for that axis back to 0 and it should do what you want.

Cheers, Jesse

 
Posted by Jesse Beaumont

Thanks!

 
Posted by Lutz

How can I bind to the maximum value of a range? Is it not a DependencyProperty?

 
Posted by Tim

Hi Tim,

Indeed it is not possible to bind a range Minimum or Maximum. You could however easily provide a data bindable version of a range. The IRange interface is very minimal so it should be possible to just implement that with a DependencyObject which makes the Minimum and Maximum properties dependency properties. Also, the Range property of the axes is a dependency property so it is possible to bind to that.

Cheers, Antony

 
Posted by Antony Welsh

Thanks, Antony. I did try that out and was able to get it working, but I didn't like the feel of my view model having a Visiblox-specific range object in there. What I did instead was to create a "MaxRange" event in my view model which fires any time the max value changes, and had my view subscribe to that event and then change the YAxis max value programmatically. I don't know if that is a good way, but it felt cleaner.

 
Posted by Tim

Is it possible to specify only the minimum of a range? I.e., to let Visiblox auto-calculate the maximum while fixing the minimum (at, say, 0). I tried seeing the maximum to double.NaN, double.MaxValue and double.PositiveInfinity, none of those had the desired result. Thanks.

 
Posted by Peter

Hi Peter, Yes, it is possible to do so, although it isn't entirely obvious how. Suppose you want to fix the YAxis minimum to 0. What you need to do is set the YAxis of the series you're plotting to be some new additional axis (e.g. a item in Chart.AdditionalPrimaryYAxes) which has its ShowAxis property set to false - this axis will have the full range of the series but won't be visible. In the visible axis you wish to set the range on, bind its Range to the ActualRange of invisible axis and use a Converter to set the Minimum of the Range to 0.

Hope that helps, Partha

 
Posted by Partha Lal

Hi. I want to display on the Y axis only integer values. The way it is now, the chart is displaying values of: 0.25, 0.5, 0.75, 1 ( so every a quarter ). What I really want is to display integer numbers only ( 0,1,2,3,4 or 0,5,10,15,20).

Do you know if this is possible ? Thanks.

 
Posted by Andrei

Hi Andrei,

The axis displays labels based on what it calculates to be suitable for the range of data being displayed, for example, if the data range is from -1 to 1 then the axis will decide that a TickInterval of 0.25 or 0.5 is appropriate. If the data ranges from 0 to 10, then it will use a TickInterval of 1 (and display values 0, 1, 2, 3, etc.).

You can override this if you choose by setting the MajorTickInterval and MinorTickInterval properties of the Axis.

Hope this helps. Antony

 
Posted by Antony Welsh

Hi. I experienced a strange behaviour when panning the X axis: the intervals for X axis appeared on 2 lines instead of 1. To be more specific, I display data for 24 hours and the X axis displayes intervals for 00:00, 04:00, 08:00 and so on. After I pan on X axis the date also appears on the chart ( 29 March ) at the beginning and 30 March at the end, causing the intervals to be displayed on 2 lines.

Do you know why that happens ? Maybe is there a way to change the size of the text displayed on the X axis in order to fit on the first line ? Or do you have another solution for this ?

Thanks a lot.

 
Posted by Laurentiu

Hi!

The simplest way to avoid the offsetting as you pan is to tailor the label collision avoidance algorithm you're using. You can set that by setting the LabelCollisionDetectionMode on the Axis. There's an enum of layout algorithms so it's worth experimenting with those. Hide might be a decent option.

Hope that's useful,

Jesse

 
Posted by Jesse Beaumont

I cannot find the property on XAxis and I didn't find any example of where it was used. Do you know a link of explaining this property in more detail ? I only found information about this property that it's used for X axis only.

Thanks

 
Posted by Laurentiu

Hi,

If you take a look at the API documentation and take a look at the AxisLabelCollisionDetectionMode enumeration, that details what the different values do. Note that the IAxis interface does not have that property on it, it's only on the axis instances. So if you're trying access it via the chart you need to do something like:

((AxisBase) MyChart.XAxis).LabelCollisionDetectionMode = AxisLabelCollisionDetectionMode .Hide;

You are correct that it only works on the X-Axis. The Y-Axis does not do any automatic label collision avoidance.

Jesse

 
Posted by Jesse Beaumont

AxisBase works with a TComparable parameter: something like this:

AxisBase but it fails to compile. Do you know what generic should be put there? Thanks.

 
Posted by Laurentiu

Oh sorry, yes - it depends on what kind of axis it is. You can also just cast to the right axis subclass (LinearAxis for numbers, DateTimeAxis for DateTimes and CategoryAxis for everything else).

 
Posted by Jesse Beaumont

I tried this way with casting:

((DateTimeAxis)this.CurrentChart.XAxis).LabelCollisionDetectionMode = AxisLabelCollisionDetectionMode .Hide;

but the intellisense doesn't find the LabelCollisionDetectionMode atrribute and neither the AxisLabelCollisionDetectionMode enum and VS fails to compile.

It's pretty strange why that happens.

Laurentiu

 
Posted by Laurentiu

It's my mistake. I've just now realized that I have been using an old version of Visiblox dll. Now I've upgraded to 2.2 and now I can access that property. Will keep you posted with the progress I am making.

Thanks Laurentiu

 
Posted by Laurentiu

After I changed the Visiblox dll I received this error:

Message: Unhandled Error in Silverlight Application Code: 4004 Category: ManagedRuntimeError Message: System.ArgumentOutOfRangeException: The added or subtracted value results in an un-representable DateTime. Parameter name: value at System.DateTime.AddTicks(Int64 value) at System.DateTime.Add(Double value, Int32 scale) at System.DateTime.AddSeconds(Double value) at #=q7n_qU6A7l8n6CLjQlVOhVPJOYM4EQR_J$IJ3g7eEReO$tDkdCX8YsgqUYbNRg8DY.#=qcTE$YNOAHY95lYoPcxCn2Q==(DateTime #=qN_eNNG07GIVFwBP4cO1LYw==, Int32 #=q9OIU0fbH5WXTArELPGw1sA==) at Visiblox.Charts.DateTimeAxis.GrowActualRange() at Visiblox.Charts.DateTimeAxis.GetMajorTickValues() at Visiblox.Charts.AxisBase`1.InvalidateGridlines() at Visiblox.Charts.AxisBase`1.gridlineContainer_SizeChanged(Object #=qfPWDIdWrfmQfw2NGtU7RIA==, SizeChangedEventArgs #=qY$e1zV0vaOJffJLa5WsuMQ==) at System.Windows.SizeChangedEventHandler.Invoke(Object sender, SizeChangedEventArgs e) at System.Windows.FrameworkElement.OnSizeChanged(Object sender, SizeChangedEventArgs e) at MS.Internal.JoltHelper.RaiseEvent(IntPtr target, UInt32 eventId, IntPtr coreEventArgs, UInt32 eventArgsTypeIndex)

I didn't even try to change the LabelCollisionDetectionMode so basically it's the same code before I changed the dll.

I set the X Range dinamically at run time. Maybe this is something that has to do with it ?

Laurentiu

 
Posted by Laurentiu
On a DateTimeAxis, how do I show tickmarks only for the min and max values? Thanks in advance!
 
Posted by Victor
Victor, you can do that either by setting the major tick frequency to exactly your axis range, or you can subclass the axis and override GetMajorTickValues to return just those two values.
 
Posted by Jesse
Is it possible to manually set the viewing window? Chart.XAxis.ActualRange.Effective(Maximum/Minimum) is read only. Also I find that sometimes the ActualRange.Effective(Maximum/Minimum) is incorrect and just returns the ActualRange.(Maximum/Minimum) instead, do you know why this might be?
 
Posted by Alex
Hi Alex, Yes, those properties don't have public setters - if you want to change the EffectiveMaximum and EffectiveMinimum you need to set the Zoom on the axis. You can work out what Zoom you need by using the GetZoom method on the axis. That method works with render positions and so you may need to use the GetDataValueAsRenderPositionWithZoom method to compute the arguments. I think my answer has highlighted how this process could be a bit simpler - we'll investigate and hopefully have something better soon. Regards, Partha
 
Posted by Partha

Post a comment