Hey Dave

David Carron / Friday, August 24, 2012

clip_image002Everybody’s talking about 3D these days. “Hey Dave, could you just put a 3D chart sample together real quick? Make sure you’ve got motion framework, tooltips, legends, markers, all that stuff. You know what I mean. Anytime this week is good.”

Hopefully, the boss wasn’t looking for much more than a proof-of-concept, but that still isn’t a request you want to be getting the week before you’re planning on taking a bit of vacation.

In the Infragistics research department we don't only prototype new stuff; sometimes we like to test the limits of what's already there. So faced with a request for a 3D chart and with the Infragistics data chart to work with, what are the options?

Well, you can think of a 2D chart as a 3D chart with the series all rotated to face the viewer and with no perspective.

In a way, the chart already is 3D: if there was just a way to change the rotation and add a bit of perspective to the series without changing anything else, then we’d be all set.

And this is the key to the whole thing. It turns out that the Projection property does exactly that. Projections were added to Silverlight way back in version 3 and allow a UIElement to be projected to give the appearance of 3D rotation and displacement. The UIElement that is projected into the scene remains fully interactive and isn’t even aware that anything “unusual” is going on.

What’s really cool is that since the data chart’s series already are UIElements, we can just set a projection on each series and we’ll get a 3D effect, automatically picking up all of the data chart’s functionality without any extra work.

3D Projections are usually implemented using a sequence of three or more 4 x 4 matrices working in a homogeneous four dimensional coordinate space. Depending on who you talk to this can all be considered hopelessly complicated or trivially simple. In any case at the price of a loss of flexibility, Silverlight’s PlaneProjection class allows us to side-step the whole question by setting just nine properties.

  1. A translation exposed by the LocalOffsetX, LocalOffsetY and LocalOffsetZ properties.
  2. A rotation represented by the CenterOfRotationX, CenterOfRotationY, CenterOfRotationZ and RotationX, RotationY and RotationZ properties.
  3. Another translation, exposed by the GlobalOffsetX, GlobalOffsetY and GlobalOffsetZ properties
  4. A fixed perspective projection back to normal 2D screen coordinates.

For me, it is easiest to think of PlaneProjection as a wrapper for a 3D scene and specific series of 3D Transforms that you can apply to a UIElement to get it to ‘project’ in 3D space.

The alternative is the MatrixProjection class which at the cost of forcing you to do all the math yourself does basically the same thing but with much finer control over the end result.

Of course, we’re not really doing “full” 3D here – we’re letting Silverlight’s Z ordering take care of depth sorting, and there won’t be any lighting or thickness to the series, but as is common to almost all 3D applications, by restricting the viewing options the illusion should be good.

Building the Sample

Now that we know basically what we’re going to do, let’s take a look at some of the details. First, an outline of the XAML (I’m just showing the layout root part, the xml namespaces are as you’d expect)

<Grid Background="#d0d0d0">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="auto"/>
        <ColumnDefinition Width="auto"/>
    </Grid.ColumnDefinitions>
           
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>
           
    <ig:XamDataChart x:Name="Chart" Grid.Column="0" Grid.Row="0" Margin="4"
        BorderBrush="Black" BorderThickness="1"
        PlotAreaBackground="White"
        SizeChanged="Charts_SizeChanged">
        <ig:XamDataChart.Resources>
           …
        </ig:XamDataChart.Resources>
               
        <ig:XamDataChart.Axes>
            <ig:CategoryXAxis x:Name="XAxis" />
            <ig:NumericYAxis x:Name="YAxis" MinimumValue="0" MaximumValue="2" />
        </ig:XamDataChart.Axes>
    </ig:XamDataChart>

    <Slider x:Name="RX" Grid.Row="0" Grid.Column="1" Margin="0 4 4 4"
        Orientation="Vertical" Minimum="-80" Maximum="80"
        ValueChanged="Slider_ValueChanged"/>

    <ToggleButton Grid.Row="0" Grid.Column="2" Margin="0 4 4 4" VerticalAlignment="Top"
        Content="Update" Click="Button_Click"/>
       
    <ig:Legend x:Name="Legend" Grid.Row="0" Grid.Column="2" VerticalAlignment="Center"
        Margin="0 4 4 4"
        Background="White" BorderBrush="Black" BorderThickness="1" />

    <Slider x:Name="RY" Grid.Row="1" Grid.Column="0" Margin="4 0 4 4"
        Orientation="Horizontal" Minimum="-80" Maximum="80"
        ValueChanged="Slider_ValueChanged"/>
</Grid>

clip_image004There’s a couple of event listeners being added, and you’ll notice that since there’s nothing in the XAML, the series must be being created in page code, but for the moment at least this is just a very standard chart layout, and if you were to run this without the special 3D stuff what you’d see is very standard chart (or perhaps a 3D chart viewed face-on!)

To give this a bit of 3d pizzazz, we’re going to add a function to calculate and set the series Projection property.

Without wanting to spoil the surprise too much, if you take a look at the screen shot below, you’ll see that the series have been individually offset in z and then rotated together around their center.

To implement this we’re going to build a Projection for each series and update it whenever any of the view settings change:

private void Project()
{
    double depth = Math.Min(Chart.ActualHeight, Chart.ActualHeight);
    double localOffsetZ = 0.5 * depth;

    foreach (UIElement series in Chart.Series)
    {
        double rotationX = -RX.Value; // X Rotation slider value
        double rotationY = -RY.Value; // Y Rotation slider value

        series.Projection = new PlaneProjection()
        {
            LocalOffsetZ = localOffsetZ,
            RotationX = rotationX, RotationY = rotationY,
            GlobalOffsetZ = –depth
        };

        localOffsetZ += depth / (Chart.Series.Count - 1);
    }
}

clip_image006

We need to recalculate the projection when either of the rotations change and – since the projections also depend on the width and height of the chart and the number of series – whenever the chart size changes.

The End of the Week

Although setting Projections on the individual series wasn’t something we really thought of during the design of the data chart, the control plays so nicely with the standard Silverlight functionality that given a little lateral thinking there’s almost always going to be way to implement completely unforeseen use-cases by plugging the available bits and pieces together.

If you’re interested you can take a look at a video on you tube, or you could drop me a mail and I’ll send you the project sources. To be honest though, the whole thing’s so simple, the easiest thing would be to just copy-paste the “Project()” function straight into one of your existing projects.

I’ll work on adding lighting and proper support for axes when I get back from vacation boss.