Silverlight Chart with reversed Y axis

Recently on StackOverflow.com I’ve found a question about Silverlight charts:
“Is it possible to display a Y LinearAxis in Silverlight in reversed order? Instead of lower numbers at the bottom increasing to the top of the Y axis, I would like to see lower numbers at the top”.

I can’t imagine where such chart can be used, maybe to display a bad revenue report in the best possible way. The chart on the right looks much better than the left one.

two linear charts comparison

Anyway after lot of efforts I’ve managed to create such axis without changing the source code of the Toolkit library, just by using the inheritance so that you can use it so:

<chart:Chart>
	<cr:ReversedAxisColumnSeries ItemsSource="{Binding Items}" DependentValuePath="Value" IndependentValuePath="Title" />
	<cr:ReversedAxisLineSeries ItemsSource="{Binding Items}" DependentValuePath="Value" IndependentValuePath="Title" />
	<chart:Chart.Axes>
		<cr:ReversedLinearAxis Orientation="Y" />
	</chart:Chart.Axes>
</chart:Chart>

Here is links to the solution and inside the post I’ll describe how I’ve implemented these classes.
Source code: ChartReversedAxisSample.zip
Showcase: TestPage.html


To implement a custom axis we should look how it is implemented in the Silverlight Toolit source code. If it is already installed, you can find its source code at the following path: `C:\Program Files (x86)\Microsoft SDKs\Silverlight\v4.0\Toolkit\Apr10\Source\Source code.zip`.
We must write something similar to the LinearAxis class, so let’s look at it, it is situated at the `Controls.DataVisualization.Toolkit\Charting\Axis\` folder.

At first it may appear that the GetMajorValues method is what we need, but actually it just generates the list of values from minimum to maximum and they are displayed by other method. Also we can’t change the behavior that the Minimum property should be always less than the Maximum property.

So we move to the base class RangeAxis, here is the RenderOriented method which renders the axis. It uses the GetPlotAreaCoordinate method. Let’s create our own class and override this method:

public class ReversedLinearAxis : LinearAxis
{
	protected override UnitValue GetPlotAreaCoordinate(object value, double length)
	{
		return GetPlotAreaCoordinate(value, ActualDoubleRange, length);
	}

	protected override UnitValue GetPlotAreaCoordinate(object value, Range<IComparable> currentRange, double length)
	{
		return GetPlotAreaCoordinate(value, currentRange.ToDoubleRange(), length);
	}

	private static UnitValue GetPlotAreaCoordinate(object value, Range<double> currentRange, double length)
	{
		if (currentRange.HasData)
		{
			double doubleValue = ValueHelper.ToDouble(value);

			double pixelLength = Math.Max(length - 1, 0);
			double rangelength = currentRange.Maximum - currentRange.Minimum;

			// Reversed coordinates of points
			//var coordinate = (doubleValue - currentRange.Minimum) * (pixelLength / rangelength);
			var coordinate = (currentRange.Maximum - doubleValue) * (pixelLength / rangelength);

			return new UnitValue(coordinate, Unit.Pixels);
		}

		return UnitValue.NaN();
	}
}

All of the methods were just copypasted. I’ve added the comment to the line which I’ve changed: instead of the (Value – Minimum) expression I use the reversed expression (Maximum – Value).

But if to use just this class and try to run the application, the chart values will not be displayed correctly:

<chart:Chart>
	<chart:LineSeries ItemsSource="{Binding Items}" DependentValuePath="Value" IndependentValuePath="Title" />
	<chart:Chart.Axes>
		<local:ReversedLinearAxis Orientation="Y" Maximum="50" Minimum="-25" />
	</chart:Chart.Axes>
</chart:Chart>

incorrect chart values with the reversed axis

Obviously, we must change the data displaying algorithm too. For the LineSeries class it can be found at the LineAreaBaseSeries.cs file. The points are displayed by the UpdateDataPoint method and the connecting line is displayed by the UpdateShape method.
The first method is easy to override, the second isn’t because it uses an internal property. But we can still fix the shape after it is generated by using the UpdateShapeFromPoints method.
Here is the class where I’ve overridden these two methods. Again, I’ve changed the code so that it uses the Minimum value instead of the Minimum.

public class ReversedAxisLineSeries : LineSeries
{
	protected override void UpdateDataPoint(DataPoint dataPoint)
	{
		// Now the highest value is Minimum
		// Previous code was: double maximum = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Maximum).Value;
		double minimum = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Minimum).Value;
		if (ValueHelper.CanGraph(minimum))
		{
			double x = ActualIndependentAxis.GetPlotAreaCoordinate(dataPoint.ActualIndependentValue).Value;
			double y = ActualDependentRangeAxis.GetPlotAreaCoordinate(dataPoint.ActualDependentValue).Value;

			if (ValueHelper.CanGraph(x) && ValueHelper.CanGraph(y))
			{
				dataPoint.Visibility = Visibility.Visible;

				double coordinateY = Math.Round(minimum - (y + (dataPoint.ActualHeight / 2)));
				Canvas.SetTop(dataPoint, coordinateY);
				double coordinateX = Math.Round(x - (dataPoint.ActualWidth / 2));
				Canvas.SetLeft(dataPoint, coordinateX);
			}
			else
			{
				dataPoint.Visibility = Visibility.Collapsed;
			}
		}

		if (!UpdatingDataPoints)
		{
			UpdateShape();
		}
	}

	protected override void UpdateShapeFromPoints(IEnumerable<Point> points)
	{
		if (points.Any())
		{
			// I can't change the UpdateShape method, so I'll fix the maximum and mimimum values here
			double maximum = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Maximum).Value;
			double minimum = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Minimum).Value;

			PointCollection pointCollection = new PointCollection();
			foreach (Point point in points)
			{
				pointCollection.Add(new Point(point.X, minimum - (maximum - point.Y)));
			}
			SetValue(PointsProperty, pointCollection);
		}
		else
		{
			SetValue(PointsProperty, null);
		}
	}
}

That’s almost all.
Also there was the issue if to use the overridden axis without explicit Minimum and Maximum values, but now I’ve fixed it. The code is too long and I won’t paste it here, just look inside the zip-archive at the beginning of this post.

It isn’t difficult to override the UpdateDataPoint method for other Series, but so far I’ve implemented only the LineSeries and ColumnSeries.

About these ads

4 Responses to Silverlight Chart with reversed Y axis

  1. MB says:

    Following will assist in ReversedAxisAreaSeries-
    UpdateDataPoint should be same as that of ReversedAxisLineSeries

    protected override void UpdateShapeFromPoints(IEnumerable points)
    {
    UnitValue originCoordinate = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Origin);
    UnitValue minimumCoordinate = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Minimum);
    if (points.Any() && ValueHelper.CanGraph(originCoordinate.Value) && ValueHelper.CanGraph(minimumCoordinate.Value))
    {
    double originY = Math.Floor(originCoordinate.Value);
    var figure = new PathFigure {IsClosed = true, IsFilled = true};

    double minimum = minimumCoordinate.Value;
    IEnumerator pointEnumerator = points.GetEnumerator();
    pointEnumerator.MoveNext();
    var startPoint = new Point(pointEnumerator.Current.X, minimum);
    figure.StartPoint = startPoint;

    Point lastPoint;
    do
    {
    lastPoint = pointEnumerator.Current;;
    figure.Segments.Add(new LineSegment { Point = new Point(pointEnumerator.Current.X, originY + pointEnumerator.Current.Y) });
    }
    while (pointEnumerator.MoveNext());
    figure.Segments.Add(new LineSegment { Point = new Point(lastPoint.X, minimum) });

    if (figure.Segments.Count > 1)
    {
    var geometry = new PathGeometry();
    geometry.Figures.Add(figure);
    SetValue(GeometryProperty, geometry);
    return;
    }
    }
    else
    {
    SetValue(GeometryProperty, null);
    }
    }
    }

  2. MB says:

    The code I commented earlier is buggy. This is what it should really look like-
    protected override void UpdateShapeFromPoints(IEnumerable points)
    {
    double maximum = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Maximum).Value;
    double minimum = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Minimum).Value;

    if (points.Any() && ValueHelper.CanGraph(maximum) && ValueHelper.CanGraph(minimum))
    {
    var figure = new PathFigure {IsClosed = true, IsFilled = true};

    IEnumerator pointEnumerator = points.GetEnumerator();
    pointEnumerator.MoveNext();
    var startPoint = new Point(pointEnumerator.Current.X, minimum);
    figure.StartPoint = startPoint;

    Point lastPoint;
    do
    {
    lastPoint = pointEnumerator.Current;;
    figure.Segments.Add(new LineSegment { Point = new Point(pointEnumerator.Current.X, minimum – (maximum – pointEnumerator.Current.Y)) });
    }
    while (pointEnumerator.MoveNext());
    figure.Segments.Add(new LineSegment { Point = new Point(lastPoint.X, minimum) });

    if (figure.Segments.Count > 1)
    {
    var geometry = new PathGeometry();
    geometry.Figures.Add(figure);
    SetValue(GeometryProperty, geometry);
    return;
    }
    }
    else
    {
    SetValue(GeometryProperty, null);
    }
    }

    Thanks vorrtex for the sample. You saved me quite a few days :)

    • vortexwolf says:

      It seems that the originY variable is important, it is used to calculate the offset from the base line with zero value. I’ve fixed the first and last points of the path figure so that they use `minimum – originY` value.
      The source code is updated now.
      Also the BarSeries class works fine without overriding. So among popular chart types only stacked charts are left to override.

  3. MB says:

    Yes, I missed on the offset.
    Shouldn’t it be “minimum + originY”, otherwise the filled area is reversed again on reversed axis.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: