Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF: 3D graph

, 24 Dec 2009
Rate this:
Please Sign up or sign in to vote.
WPF: A simple pageable 3D graph.

Introduction

As some of you may know, I am a member of an online community called the WPF Disciples, and we are lucky enough to have some very, very smart people in the group. Occasionally, the group grows by a member, and the other day we had a new member join. When someone joins, they share a bit of work with the group so we can see what others are up to.

This new member showed us this amazing 3D app he had worked on, which inspired me to do a bit more 3D. It also tied in nicely with a very large app that I am in a research phase on, which will be my wife's primary tool for her own Nutritional Therapy business. The app is in a conceptual stage right now, but as I think about things, I am also trying things out, and some of these thoughts will make it into articles right here.

So what does this article do? Simply put, it's a 3D bar chart that allows us to navigate through historical data. It has a somewhat limited use, as it so closely matches my wife's business requirements, but could easily be adapted for someone else's uses, and I do feel it is still a nice example of working with 3D and WPF.

Before I start, I would just like to thank one of my regular partners in crime: Fredrik Bornander, who helped me with some of the finer points of Normals/Camera positions, and generally being cool, as is his way.

What Does It Look Like

This is what it looks like when it starts up:

And here is what it looks like when the user hovers their mouse over a particular item within the chart:

Basically, there is some more information shown in a bar at the top, and ScaleTransform3D is applied to the ModelUIElement3D, which you can kind of see with this Yellow bar being a little bigger than its neighbors.

It also makes use of both the TrackballDecorator and the Interactive3DDecorator classes found in the 3D Tools for WPF assembly. What this means is that the third chart may be panned/zoomed using the mouse.

  • Panning: The user is able to pan with the left mouse button.
  • Zooming: The user is able to pan with the right mouse button.

What Does It Do

As I stated, the attached code fulfills a pretty niche requirement that works for my wife's business, but I still feel there is something that could be learned from this article.

But anyway, here was the original brief from my wife gave:

"I want something that I can use to visualize different questions, and answers to those questions over time, where the same five questions will be asked each time I see a client".

So that is what my wife wanted. Sure, you could have visualised this as something like this using standard graphs:

But I feel this chart does not do that much, and I also felt that it just felt a bit static, and did not quite visualize the data in the correct manner. So I set about creating a semi-flexible 3D chart. I say semi-flexible as, it really is fixed to five questions, which was part of my original brief.

So what does it do exactly, Sacha?

Well, it does this:

  • Visualizes five bar graphs, one bar for each of my wife's questions to her client.
  • Allows users to visualize data about the bar by mousing over it.
  • Allows users to view historical data (all in memory at this stage; in the real app, I am building this as persisted to SQL Server).
  • Allows users to pan/zoom around the chart.

So if you want to extend this for your own use, you will need to be aware of that limitation.

How Does It Do It

Like I have stated already, although this demo code may not be the most flexible of things, there is still quite a lot of good 3D stuff in there that may be useful to some folk; so rather than go on a tirade about how limited the functionality of the demo app is, and how useless it will be for most of you, let us instead concentrate on the 3D aspects of it that may help some of you out in your own projects.

So what I am going to do is go through how all the different parts of the demo app are constructed, and that will hopefully show you something.

Viewport3D

The ViewPort3D is required to host any 3D model, and contains some very important children, without which 3D would just not be possible. Let's have a look at what the ViewPort3D looks like in the demo code:

<Viewport3D x:Name="ViewPort">

    <Viewport3D.Camera>
        <PerspectiveCamera x:Name="MainCamera" 
                       FieldOfView="90" 
                       Position="-0.7,1.5,1.2" 
                       LookDirection="0.5,-1.5,-2"/>
    </Viewport3D.Camera>

    <ModelVisual3D>
        <ModelVisual3D.Content>
            <AmbientLight x:Name="Ambient" 
                            Color="#404040"/>
        </ModelVisual3D.Content>
    </ModelVisual3D>
    <ModelVisual3D>
        <ModelVisual3D.Content>
            <DirectionalLight x:Name="Directional" 
              Color="WhiteSmoke" Direction="0.1,-2,-1"/>
        </ModelVisual3D.Content>
    </ModelVisual3D>

</Viewport3D>

Probably the most important child here is the PerspectiveCamera, as it's the camera that we point at our 3D constructed model. No camera, no 3D: it's that simple. We also need some light to make sure our 3D scene looks correct. Of course, lighting only works if the Models that are added to the ViewPort3D are constructed well and have TextureCoordinates and Normals.

So with this ViewPort3D in place, we can start to create our 3D model, but by bit (that works best for me).

Creating a BasePlate That Can Host 2D Content

So the first thing I want to show you is how we can construct a base plate that can be used to host 3D content or even 2D controls such as Grid, StackPanel, or even interactive controls such as Button.

Here is what we are trying to build:

You can see that this is indeed a 3D model that looks like it is made up of some standard 3D meshes, and also looks like it's hosting some 2D control.

How does it do that? Well, the trick lies in creating a 3D cube which is between -0.5 / 0.5 on all axis and is centered around 0.0, and is then scaled to create whatever shape (as long as it is rectangular) we want.

Let's look at how the demo app creates this 3D part:

Creating the Base

private void CreateBasePlateAndBarContainer()
{
    //build base plate
    graphBaseModel = CubeBuilder.Build(
        Brushes.WhiteSmoke,
        Brushes.WhiteSmoke,
        Brushes.WhiteSmoke,
        Brushes.WhiteSmoke,
        graphSurface,
        Brushes.WhiteSmoke);

    Transform3DGroup trans = new Transform3DGroup();
    trans.Children.Add(new ScaleTransform3D(1.0, 
                       CubeBuilder.BASE_HEIGHT, 1.0));
    graphBaseModel.Transform = trans;
    ViewPort.Children.Add(graphBaseModel);

    //And add Bar container
    ViewPort.Children.Add(barsContainer);
}

It can be seen that this makes use of a Factory method on a CubeBuilder class. So let us examine that CubeBuilder class, shall we?

Here it is in its entirety. The idea is simple enough: build a cube side by side where each side is a Visual3D which internally uses the following MeshGeometry3D as a Mesh. Then, each side is rotated in 3D space to be positioned as the correct face of the cube.

<MeshGeometry3D x:Key="CubeMesh"
  TriangleIndices = "0,1,2     2,3,0  
    4,7,6     6,5,4
    8,11,10   10,9,8  
    12,13,14  14,15,12  
    16,17,18  18,19,16
    20,23,22  22,21,20"
  Positions      = "-0.5,-0.5,0.5   -0.5,-0.5,-0.5  0.5,-0.5,-0.5  0.5,-0.5,0.5
       -0.5,0.5,0.5    -0.5,0.5,-0.5   0.5,0.5,-0.5   0.5,0.5,0.5
       -0.5,-0.5,0.5   -0.5,0.5,0.5    0.5,0.5,0.5    0.5,-0.5,0.5
       -0.5,-0.5,-0.5  -0.5,0.5,-0.5   0.5,0.5,-0.5   0.5,-0.5,-0.5
       -0.5,-0.5,0.5   -0.5,0.5,0.5   -0.5,0.5,-0.5  -0.5,-0.5,-0.5
       0.5,-0.5,0.5    0.5,0.5,0.5    0.5,0.5,-0.5   0.5,-0.5,-0.5" />

And here is the CubeBuilder class. It can be seen that each side is a single Visual3D which may or may not be a 2D control.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace CubeDemo
{
    class CubeBuilder
    {
        public const Double BASE_HEIGHT = 0.1;

        public enum Side
        {
            Front,
            Back,
            Left,
            Right,
            Top,
            Bottom
        }
        
        private static readonly MeshGeometry3D quadMesh;
        private static readonly Material visualHostMaterial;

        static CubeBuilder()
        {
            quadMesh = new MeshGeometry3D();
            quadMesh.Positions.Add(new Point3D(-0.5, 0.5, 0));
            quadMesh.Positions.Add(new Point3D(-0.5, -0.5, 0));
            quadMesh.Positions.Add(new Point3D(0.5, -0.5, 0));
            quadMesh.Positions.Add(new Point3D(0.5, 0.5, 0));

            quadMesh.TextureCoordinates.Add(new Point(0, 0));
            quadMesh.TextureCoordinates.Add(new Point(0, 1));
            quadMesh.TextureCoordinates.Add(new Point(1, 1));
            quadMesh.TextureCoordinates.Add(new Point(1, 0));

            quadMesh.Normals.Add(new Vector3D(0, 0, 1));
            quadMesh.Normals.Add(new Vector3D(0, 0, 1));
            quadMesh.Normals.Add(new Vector3D(0, 0, 1));
            quadMesh.Normals.Add(new Vector3D(0, 0, 1));

            quadMesh.TriangleIndices.Add(0);
            quadMesh.TriangleIndices.Add(1);
            quadMesh.TriangleIndices.Add(2);
            quadMesh.TriangleIndices.Add(0);
            quadMesh.TriangleIndices.Add(2);
            quadMesh.TriangleIndices.Add(3);

            visualHostMaterial = new DiffuseMaterial(
                new SolidColorBrush(Colors.White));
            visualHostMaterial.SetValue(
                Viewport2DVisual3D.IsVisualHostMaterialProperty, true);
        }


        private static Visual3D CreateSide(object material, Rotation3D rotation)
        {
            Transform3DGroup transform = new Transform3DGroup();
            Transform3D translation = new TranslateTransform3D(0, 0, 0.5);

            transform.Children.Add(translation);
            transform.Children.Add(new RotateTransform3D(rotation));


            if (material is Visual)
            {
                return new Viewport2DVisual3D
                            {
                                Geometry = quadMesh,
                                Visual = (Visual)material,
                                Material = visualHostMaterial,
                                Transform = transform
                            };
            }
            else
            {

                GeometryModel3D model = new GeometryModel3D(quadMesh, 
                    new DiffuseMaterial((Brush)material));

                return new ModelVisual3D { Content=model, Transform=transform};
            }
        }

        public static ModelVisual3D Build(object front, object back, 
            object left, object right, object top, object bottom)
        {
            IDictionary<Side, Visual3D> sides = 
                            new Dictionary<Side, Visual3D>();

            sides[Side.Front] = CreateSide(front, new 
                AxisAngleRotation3D(new Vector3D(1, 0, 0), 0));
            sides[Side.Back] = CreateSide(back, new 
                AxisAngleRotation3D(new Vector3D(1, 0, 0), 180));
            sides[Side.Left] = CreateSide(left, new 
                AxisAngleRotation3D(new Vector3D(0, 1, 0), 90));
            sides[Side.Right] = CreateSide(right, new 
                AxisAngleRotation3D(new Vector3D(0, 1, 0), -90));
            sides[Side.Top] = CreateSide(top, new 
                AxisAngleRotation3D(new Vector3D(1, 0, 0), -90));
            sides[Side.Bottom] = CreateSide(bottom, new 
                AxisAngleRotation3D(new Vector3D(1, 0, 0), 90));

            ModelVisual3D cube = new ModelVisual3D();
            foreach (Visual3D side in sides.Values)
            {
                cube.Children.Add(side);
            }

            return cube;
        }
    }
}

Creating the 2D Content

As already shown in the diagram above, all we need to do now is create a 2D control and pass it to the CubeBuilder and it takes care of the rest. Just to revisit this code, have another look, see how CubeBuilder.Build() takes a "graphSurface" as one of the parameters.

private void CreateBasePlateAndBarContainer()
{
    //build base plate
    graphBaseModel = CubeBuilder.Build(
        Brushes.WhiteSmoke,
        Brushes.WhiteSmoke,
        Brushes.WhiteSmoke,
        Brushes.WhiteSmoke,
        graphSurface,
        Brushes.WhiteSmoke);

    Transform3DGroup trans = new Transform3DGroup();
    trans.Children.Add(new ScaleTransform3D(1.0, 
                       CubeBuilder.BASE_HEIGHT, 1.0));
    graphBaseModel.Transform = trans;
    ViewPort.Children.Add(graphBaseModel);

    //And add Bar container
    ViewPort.Children.Add(barsContainer);
}

If we look into this a bit further, we can see that "graphSurface" is an instance of a 2D control of type GraphSurface. Which is a standard WPF control whose XAML looks like this:

<UserControl x:Class="CubeDemo.GraphSurface"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto">
    
    <Border BorderBrush="Black" 
          BorderThickness="3" Background="CornflowerBlue">
        <Grid x:Name="gridItems" Margin="3" 
               Width="390" Height="360">
            <Grid.RowDefinitions>
                <RowDefinition Height="60"/>
                <RowDefinition Height="60"/>
                <RowDefinition Height="60"/>
                <RowDefinition Height="60"/>
                <RowDefinition Height="60"/>
                <RowDefinition Height="60"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="90"/>
                <ColumnDefinition Width="60"/>
                <ColumnDefinition Width="60"/>
                <ColumnDefinition Width="60"/>
                <ColumnDefinition Width="60"/>
                <ColumnDefinition Width="60"/>
            </Grid.ColumnDefinitions>

           
            <!-- Questions -->
            <Canvas Grid.Row="5" Grid.Column="1" 
                     Width="60" Height="60" Margin="0">
                <Ellipse Width="50" Height="50" 
                         Fill="Black"  Margin="5" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center"/>
                <Ellipse Width="40" Height="40" 
                         Fill="White" Margin="10" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center"/>
                <Label  Width="40" Height="40" 
                        Content="1" Margin="10" 
                        FontSize="30" FontWeight="Bold" 
                        FontFamily="Arial"  
                        HorizontalAlignment="Center" 
                        HorizontalContentAlignment="Center" 
                        VerticalAlignment="Center" 
                        VerticalContentAlignment="Center"/>
            </Canvas>

            <Canvas Grid.Row="5" Grid.Column="2" 
                         Width="60" Height="60" Margin="0">
                <Ellipse Width="50" Height="50" 
                         Fill="Black" Margin="5" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center"/>
                <Ellipse Width="40" Height="40" 
                         Fill="White" Margin="10" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center"/>
                <Label Width="40" Height="40" 
                        Content="2" Margin="10" 
                        FontSize="30" FontWeight="Bold" 
                        FontFamily="Arial"  
                        HorizontalAlignment="Center" 
                        HorizontalContentAlignment="Center" 
                        VerticalAlignment="Center" 
                        VerticalContentAlignment="Center"/>
            </Canvas>

            <Canvas Grid.Row="5" Grid.Column="3" 
                          Width="60" Height="60" 
                          Margin="0">
                <Ellipse Width="50" Height="50" 
                         Fill="Black" Margin="5" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center"/>
                <Ellipse Width="40" Height="40" 
                         Fill="White" Margin="10" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center"/>
                <Label Width="40" Height="40" 
                        Content="3" Margin="10" 
                        FontSize="30" FontWeight="Bold" 
                        FontFamily="Arial"  
                        HorizontalAlignment="Center" 
                        HorizontalContentAlignment="Center" 
                        VerticalAlignment="Center" 
                        VerticalContentAlignment="Center"/>
            </Canvas>


            <Canvas Grid.Row="5" Grid.Column="4" 
                     Width="60" Height="60" Margin="0">
                <Ellipse Width="50" Height="50" 
                     Fill="Black" Margin="5" 
                     HorizontalAlignment="Center" 
                     VerticalAlignment="Center"/>
                <Ellipse Width="40" Height="40" 
                     Fill="White" Margin="10" 
                     HorizontalAlignment="Center" 
                     VerticalAlignment="Center"/>
                <Label  Width="40" Height="40" 
                     Content="4" Margin="10" 
                     FontSize="30" 
                     FontWeight="Bold" FontFamily="Arial"  
                     HorizontalAlignment="Center" 
                     HorizontalContentAlignment="Center" 
                     VerticalAlignment="Center" 
                     VerticalContentAlignment="Center"/>
            </Canvas>

            <Canvas Grid.Row="5" Grid.Column="5" 
                     Width="60" Height="60" Margin="0">
                <Ellipse Width="50" Height="50" 
                     Fill="Black" Margin="5" 
                     HorizontalAlignment="Center" 
                     VerticalAlignment="Center"/>
                <Ellipse Width="40" Height="40" 
                     Fill="White" Margin="10" 
                     HorizontalAlignment="Center" 
                     VerticalAlignment="Center"/>
                <Label  Width="40" Height="40" 
                     Content="5" Margin="10" 
                     FontSize="30" FontWeight="Bold" 
                     FontFamily="Arial"  
                     HorizontalAlignment="Center" 
                     HorizontalContentAlignment="Center" 
                     VerticalAlignment="Center" 
                     VerticalContentAlignment="Center"/>
            </Canvas>

        </Grid>
    </Border>
</UserControl>

There is some additional code-behind to draw the square outlines, but that is not that important. The important thing here is to see how this 2D control gets hosted in 3D by the CubeBuilder.Build() method.

Creating Interactive Bars

So right now, we have this:

Now we need to create some actual bars. How do we do that?

Well, that is done once we have the correct page of data, and once we have that, it really is just a matter of creating some more 3D Models. Only this time, we want the Models to be ModelUIElement3D, which are actual elements that can work in 3D in WPF. They allow events such as MouseDown/MouseLeave etc. The only thing with them is that they must be hosted as children in a ContainerUIElement3D.

Anyway, assuming we have a page worth of reading to display as bars, how do we do that? This is the code that does the job:

for (int readingDates = 0; readingDates < readingSetList.Count; readingDates++)
{
    for (int r = 0; r < readingSetList[readingDates].Readings.Count; r++)
    {
        Reading reading = readingSetList[readingDates].Readings[r];

        ModelUIElement3D bar1 = BarBuilder.CreateBarModelUIElement(
            reading.Value,
            readingDates,
            reading.QuestionNumberIndex,
            (MeshGeometry3D)this.Resources["CubeMesh"],
            brushes[r]);
        bar1.SetValue(BarBuilderBehaviors.AssociatedReadingProperty, reading);
        bar1.SetValue(BarBuilderBehaviors.AssociatedReadingDateProperty, 
                      readingSetList[readingDates].ReadingsDate);
        bar1.MouseEnter += Bar_MouseEnter;
        bar1.MouseLeave += Bar_MouseLeave;

        barsContainer.Children.Add(bar1);
    }
}

Again, notice the Factory BarBuilder.CreateBarModelUIElement(); this is used to create a ModelUIElement3D bar. So, let's have a look at that code in its entirety; it's very simple.

public static class BarBuilder
{
    #region Private Properties
    private static Double[] QuestionNumberIndexPositions = 
    {
        -0.18,  -0.04,   0.11,  0.26,   0.40
    };

    private static Double[] DateOfReadingPositions = 
    {
        0.24,  0.08,   -0.08,  -0.24,   -0.40
    };
    #endregion

    #region Public Methods
    public static T TryFindChild<T>(Transform3DGroup parent)
      where T : DependencyObject
    {
        foreach (DependencyObject child in parent.Children)
        {
            if (child is T)
            {
                return child as T;
            }
        }
        return null;
    }

    public static ModelUIElement3D CreateBarModelUIElement(
        Double readingValue,
        Int32 dateOfReading,
        Int32 questionNumberIndex,
        MeshGeometry3D mesh, Brush brush)
    {

        ModelUIElement3D modelUIElement3D = new ModelUIElement3D()
        {
            Model = new GeometryModel3D(mesh, new DiffuseMaterial(brush))
        };

        Transform3DGroup transform = new Transform3DGroup();

        //ScaleY is ranged between 0.0 : 1.0, for 0% to 100%
        transform.Children.Add(new ScaleTransform3D(0.1, readingValue, 0.1));

        //OffsetY value should be 1/2 widtch of baseplate (which is 0.1) plus 1/2 
        //the height of the value which is scaled between 0.0 - 1.0
        transform.Children.Add(
            new TranslateTransform3D(
                QuestionNumberIndexPositions[questionNumberIndex-1],
                (CubeBuilder.BASE_HEIGHT/2) + (readingValue/2),
                DateOfReadingPositions[dateOfReading]));

        modelUIElement3D.Transform = transform;
        return modelUIElement3D;

    }
    #endregion

}

Now, since the bars are of type ModelUIElement3D, we can listen to their events just like regular 2D elements. Here is an example of the mouse handlers for a single bar ModelUIElement3D:

private void Bar_MouseEnter(object sender, MouseEventArgs e)
{
    ModelUIElement3D modelUIElement3D = (ModelUIElement3D)sender;
    GeometryModel3D model = (GeometryModel3D)modelUIElement3D.Model;
    ScaleTransform3D scale = BarBuilder.TryFindChild<ScaleTransform3D>(
        modelUIElement3D.Transform as Transform3DGroup);
    scale.ScaleX = 0.13;
    scale.ScaleZ = 0.13;

    Reading reading = (Reading)modelUIElement3D.GetValue(
        BarBuilderBehaviors.AssociatedReadingProperty);

    DateTime datetime = (DateTime)modelUIElement3D.GetValue(
        BarBuilderBehaviors.AssociatedReadingDateProperty);
    lblQuestion.Content = String.Format("Question{0} : {1}",
    reading.QuestionNumberIndex, reading.QuestionText);
    lblDate.Content = 
      String.Format("Date : {0}", datetime.ToShortDateString());

    rateValue.Value = (Decimal)(reading.Value * 10);
    lblValue.Content = String.Format("Percentage Value : {0} % ",
    ((Int32)(reading.Value * 100)).ToString());

    bordCurrentItem.Visibility = Visibility.Visible;
}

private void Bar_MouseLeave(object sender, MouseEventArgs e)
{
    bordCurrentItem.Visibility = Visibility.Collapsed;
    ModelUIElement3D modelUIElement3D = (ModelUIElement3D)sender;
    GeometryModel3D model = (GeometryModel3D)modelUIElement3D.Model;
    ScaleTransform3D scale = BarBuilder.TryFindChild<ScaleTransform3D>(
        modelUIElement3D.Transform as Transform3DGroup);
    scale.ScaleX = 0.1;
    scale.ScaleZ = 0.1;
}

Basically, on MouseEnter, the current ModelUIElement3D is scaled up a bit to show it is selected, and then some attached property data is retrieved and used to show the user the data about the current bar.

On MouseLeave, the data about the current bar is hidden.

Note: The star rating control is from one of my previous articles: WPFStarRating.aspx.

Panning/Zooming

As I previously stated, panning and zooming are accomplished using the TrackballDecorator and the Interactive3DDecorator classes found in the 3D Tools for WPF assembly.

But how do we use these classes? Well, it is very easy actually; all we do is adorn our ViewPort3D as follows, and that's it, job done.

<inter3D:TrackballDecorator x:Name="inter3d" >
    <inter3D:Interactive3DDecorator>
        <Viewport3D x:Name="ViewPort">

    ....
    ....
    ....
    ....

        </Viewport3D>
    </inter3D:Interactive3DDecorator>
</inter3D:TrackballDecorator>

Historical Data

As I stated earlier on in the article, I do allow the user to page through historical data. Now, ordinarily, this data would be stored in SQL Server (which it is for the actual app), but for this demo app, I wanted readers to be able to just run the attached demo app without hindrance and without setting up a new DB etc.

So although I do allow paging through historical data, this is all done by having the entire dataset in memory and then using some LINQ to grab the page of data that is required. I decided a page size would be 5.

The graph data is created like this, where there is a MockData class that creates some dummy data for the chart:

public static void SetupMockData(Int32 numberOfHistoricalDates)
{
    DataReadings.Instance.ReadingSets = new List<ReadingSet>();

    for (int dates = 0; dates < numberOfHistoricalDates; dates++)
    {
        List<Reading> readings = new List<Reading>();
        for (int reading = 0; reading < 5; reading++)
        {
            readings.Add(new Reading(rand.NextDouble(), reading + 1));    
        }
        DataReadings.Instance.ReadingSets.Add(
            new ReadingSet(DateTime.Now.AddDays(dates),readings));
        readings = new List<Reading>();
    }
}

Which is set as the chart's data as follows:

MockData.SetupMockData(16);

Where the data structures that represent data for the chart looks like this:

/// <summary>
/// singleton collection of ALL data for Graph
/// which includes <c>ReadingSet</c>s
/// </summary>
public class DataReadings
{
    #region Data
    //This can not be changed
    public const Int32 MAX_HISTORICAL_BARS_ALLOWS = 5;

    private static DataReadings instance;
    private List<ReadingSet> readingSets { get; set; }
    private String[] questions =
    {
        "Are you enjoying the supplement plan, between 0-100% ?",
        "Any you finding that you are still hungry, between 0-100% ?",
        "Do you feel weak or unwell, between 0-100% ?",
        "How are your concentration levels, between 0-100% ?",
        "Do you feel less tired since you last visit, between 0-100% ?"
    };
    #endregion

    #region Ctor
    private DataReadings() 
    {
        CurrentPageNumber = 1;
    }
    #endregion

    #region Public Properties
    public static DataReadings Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new DataReadings();
            }
            return instance;
        }
    }

    public Int32 CurrentPageNumber { get; set; }

    public List<ReadingSet> ReadingSets
    {
        get { return readingSets;  }
        set 
        {
            if (value == null)
                throw new NullReferenceException("Readings can not be null");

            readingSets = value;
        }
    }

    public string this[int questionNumber]
    {
        get { return questions[questionNumber]; }
    }
    #endregion
}

/// <summary>
/// Represents a set of <c>Reading</c>. For this demo
/// there are exactly 5 <c>Reading</c> required
/// </summary>
public class ReadingSet
{

    #region Ctor
    public ReadingSet(DateTime readingsDate, List<Reading> readings)
    {
        if (readings == null)
            throw new NullReferenceException("Readings can not be null");

        if (readings.Count != 5)
            throw new InvalidOperationException(
              "Readings must contain exactly 5 elements");

        Readings = readings;
        ReadingsDate = readingsDate;

    }
    #endregion

    #region Public Properties
    public List<Reading> Readings { get; private set; }
    public DateTime ReadingsDate { get; private set; }
    #endregion
}

/// <summary>
/// Represents a single reading with a Value/Question Number
/// </summary>
public class Reading
{
    #region Ctor
    public Reading(Double value, Int32 questionNumberIndex)
    {
        if (value > MAX) 
            Value = MAX; 
        else 
            Value = value;

        if (value < MIN)
            Value = MIN;
        else
            Value = value;

        QuestionNumberIndex = questionNumberIndex;
    }
    #endregion

    #region Public Properties
    public const Double MAX = 1.0;
    public const Double MIN = 0.0;
    public Double Value { get; private set; }
    public Int32 QuestionNumberIndex { get; private set; }
    public String QuestionText
    {
        get { return DataReadings.Instance[QuestionNumberIndex-1]; }
    }
    #endregion
}

And the bars for the current chart are created by paging through the single set of data, which is done as follows:

private void BuildBarsForReadings(Int32 pageNumber)
{

    float pages = (float)DataReadings.Instance.ReadingSets.Count / 
                         DataReadings.MAX_HISTORICAL_BARS_ALLOWS;
    Int32 roundedUpPages = (Int32)pages;
    if (pages % 1 > 0)
        ++roundedUpPages;



    lblPaging.Content = String.Format("Page {0} of {1}",
        pageNumber, roundedUpPages);

    barsContainer.Children.Clear();

    IEnumerable<ReadingSet> readingSets;

    //if there are still more than we need grab MAX_HISTORICAL_BARS_ALLOWS many
    if (DataReadings.Instance.ReadingSets.Count >=
        pageNumber * DataReadings.MAX_HISTORICAL_BARS_ALLOWS)
    {
        readingSets = DataReadings.Instance.ReadingSets.Skip(
                     (pageNumber - 1) * DataReadings.MAX_HISTORICAL_BARS_ALLOWS).Take(
                      DataReadings.MAX_HISTORICAL_BARS_ALLOWS);
    }
    //otherwise grab whats left
    else
    {
        readingSets = DataReadings.Instance.ReadingSets.Skip((pageNumber - 1) * 
                      DataReadings.MAX_HISTORICAL_BARS_ALLOWS);
    }

    List<ReadingSet> readingSetList = readingSets.ToList();
    graphSurface.CurrentReadingSets = readingSetList;

    ....
    ....
    ....
    ....
}

private void Prev_Click(object sender, RoutedEventArgs e)
{
    if (DataReadings.Instance.CurrentPageNumber-1 > 0)
    {
        DataReadings.Instance.CurrentPageNumber -= 1;
        BuildBarsForReadings(DataReadings.Instance.CurrentPageNumber);
    }
}

private void Next_Click(object sender, RoutedEventArgs e)
{
    if (DataReadings.Instance.CurrentPageNumber *
        DataReadings.MAX_HISTORICAL_BARS_ALLOWS < 
        DataReadings.Instance.ReadingSets.Count())
    {
        DataReadings.Instance.CurrentPageNumber += 1;
        BuildBarsForReadings(DataReadings.Instance.CurrentPageNumber);
    }
}

Known Limitations

Only works for five bars; as this was my original requirement, I am not too fussed about this.

That's it, Hope You Liked It

Anyway, there you go, hope you liked it. I know this is a very small article, but I am hoping it may be useful to someone.

Thanks

As always, votes / comments are welcome.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 2 Pinmembereolson2225-Jan-10 13:15 
GeneralRe: My vote of 2 PinmvpSacha Barber25-Jan-10 21:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140926.1 | Last Updated 24 Dec 2009
Article Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid