Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drawing a Basic 3D Cylinder Chart in WPF

0.00/5 (No votes)
4 Oct 2014 7  
A really basic 3D Cylinder chart, drawn on a WPF canvas

Note: The code presented has a little flaw: The ellipse width is statically set in the code, since the bottom wouldn't sit correctly on the X-Axis line.

Introduction

Honor to them who deserve it - This article is based on the article "Drawing a Grid Covered Cylinder in WPF", written by CodeProject user StewBob. Well, I renamed his "DrawingClass" to "CylinderPainter3D" and modified it in order to be able to draw the cylinder with an X / Y offset, without the Grid Lines, with different Radius values, heights and Colors - Finished was the core part I needed for my 3D Cylinder chart.

The only thing I had to do from scratch was to add another class to draw the chart itself - This class, called ChartPainter takes advantage of the previously mentioned CylinderPainter3D class which is basically StewBob's expanded DrawingClass.

In the end, the chart will look somewhat like this:

The resulting classes will enable to draw a chart based on a collection of data, maximum size, offset and caption in order to make your application look more professional.
Even the Color of the chart elements (Cylinders, in this case) is customizable and can be set to every System.Windows.Media.Color available in the System.Windows.Media.Colors collection: System.Windows.Media.Color available in the System.Windows.Media.Colors collection:

Background

About the WPF Canvas - What is it?

Defines an area within which you can explicitly position child elements by using coordinates that are relative to the Canvas area.

This sentence, stolen from the MSDN, says basically that you can forms onto a two dimensional coordinate systems. The special thing you really really need to keep in mind about the Canvas is that the Y Axis is mirrored:

 ------------------------------------  X [10/0]
 |[0/0]  
 |
 |
 |
 |
 |
 |
 |
 |
 | Y [0/8]  

A small graphic of the coordinate system in a System.Windows.Controls.Canvas. The Y Axis appears mirrored.

Drawing Sketches

This chapter is going to show you how the Chart is drawn, and which offsets and constant values were applied. We first did a sketch drawing in Microsoft Visio in order to get an idea of how the chart might be drawn the best way. I'd highly recommend this to everyone to everyone doing drawing of custom elements in any programming language: You get a straightforward idea of how something is done the best way without loosing too much time if the first attempt fails.

An important Use Case was that the Chart can be drawn anywhere on the Canvas element. I did this by specifying an Offset Point - X Offset on the vertical X Axis and Y Offset on the vertical Y Axis (# [1] (X Offset) and [2] (Y Offset). Changing either the value of the X or Y Offset results in the chart being drawn with a bigger or smaller offset.
The Y Axis Line [4] starts at X Offset / Y Offset - It's end is at X Offset / Y Offset + Y Axis Line - The Y Axis Line is equal to the maximum height of the chart in total.
Drawing the X Axis Line of the chart was nearly the same as drawing the Y Axis Line - The start point is at the end point of the Y Axis Line ( X Offset / Y Offset + Y Axis Line), and the end point is simply the same as the start point of the X Axis Line, except that I had to add the length of the X Axis Line to the X coordinate of the end point [5].
Positioning the Cylinders (shown as yellow boxes in the picture above) was pretty easy: I know the starting point of the X Axis Line, and I know the offset each Cylinder needs from there. The said offset is calculated by the formula:

25 + (3r * index) 

'r' is the radius of a single Cylinder - It is calculated by dividing the length of the X Axis through the amount of Cylinders the chart needs to hold. This ensures a variable use of the charts width, and takes the burden of adjusting the width of the chart according to the amount of Cylinders it is going to hold from the Developer's back.
The index is a zero-based number indicating how far a Cylinder is away from the Chart's 0/0 coordinate point (X Offset / Y Offset + Y Axis Line). The index is calculated 'on the fly' while drawing the Cylinders.
Now what is the number 25 used for? Simples: It is a default initial offset on the X Axis because the Cylinder would else stick to the Y Axis Line, destroying the whole decent appearance.

Now, drawing the Cylinder that it will have the correct height is a bit more difficult - Since the Canvas' Y-Axis is mirrored, I needed to use the following formula:

 (Y Axis Maximum Height – Cylinder Height) + Y Offset  

I guess it is quite clear what it means: The Y Axis Maximum Height - Cylinder Height gives you the gap between the start of the drawing point and the top border of the Chart - Adding the Y Offset gives you the exact value to pass on to the Cylinder Drawing class.

To calculate the offset of the Cylinder caption [6] is an easy gain: Just add up the length of the Y Axis Line, the Y Offset and an extra Offset to prevent the caption from sticking to the X Axis line.

Now you know about the whole magic behind drawing the chart, at least in theory. But there was this other thing, right?
Exactly. We now have to add the caption for the X and Y Lines.

In the top left corner of the chart is the Caption of the Y Axis placed. It has a left (X) offset of X Offset + 2 - The 2 is a constant value which keeps the caption from sticking to the Y Axis Line.

The caption for the X Axis Line is placed at Y Offset + Length of the Y Axis Line [3] from top and X Offset + Length of the X Axis Line from left [4].

Using the Code

The chapter "Using the code" provides an overview over the classes used within the project. Every class method is documented with an explanation text and separately explained code samples. The code samples are left out in case of properties - I assume what a property is, what it does and what its code looks like - I just added a text explaining what specific need the property serves.

ChartDataContainer - The Data Container

As for every data handling thingie, we need something to contain the data first. I made a class called ChartDataContainer for this purpose which offers the following data properties:

  • List<double> Data
  • List<string> DataCaptions
  • Point MaxSize
  • Point Offset
  • string XAxisText
  • string YAxisText
  • Color ChartElementColor

Properties

Alright - If you have skipped the "Background" chapter and / or ask yourself now "Wait. Why do I need all this data?" - Here comes the explanation, grouped by each property of the ChartDataContainer.

List<double> Data

The Data property provides getter and setter methods for a list of double values which are the data points to display in the chart. Each double value in the list represents a single Cylinder's height.

List<string> DataCaptions

The DataCaption property is optional and has some restrictions on it: The amount of items in the list must either match the amount of items in the Data list or be zero. Each string in the list represents a caption of an X Axis value (Cylinder) to display in the chart.

Point MaxSize

The MaxSize property is used to get or set an X-Y value pair which defines the maximum size of the chart control. The X-Y value pair is represented by a System.Drawing.Point.

Point Offset

The Offset property is a getter and setter property to define the Offset from the left (X) and from the top of the canvas where the chart is drawn onto. As the MaxSize property it is an X-Y value pair represented by a System.Drawing.Point.

string XAxisText

This property is used to get or set the caption of the chart's X-Axis.

string YAxisText

The YAxisText property is used to get or set the caption of the chart's Y-Axis.

Color ChartElementColor

The ChartElementColor property defines the filling color of a graph element, e.g. the Cylinders.

CylinderPainter3D - Draw a Cylinder

The CylinderPainter3D class is used to draw the 3D Cylinders used in the chart. It is configurable by setting the following properties:

  • Color FillingColor
  • double CylinderHeight
  • double CylinderRadius

I haven't listed all of the properties here - There are actually a few more - but those who I left out are not configurable by the ChartDataContainerClass and therefore useless for explaining the given example.

Properties

Color FillingColor

The FillingColor property defines the filling color of the Cylinder.

double CylinderHeight

The CylinderHeight property defines the height of the Cylinder.

double CylinderRadius

The CylinderRadius property defines the Radius of the Cylinder (CylinderRadius * 2 = Cylinder's diameter).

Methods

public void DrawCylinder(System.Windows.Controls.Canvas cnv, double xOffset, double yOffset) 

Draws a 3D Cylinder with a specified X/Y offset onto a System.Windows.Controls.Canvas. The rest of the data is provided by the class' Properties. A big part of this method was written by StewBob, as mentioned at the top of this article.

The whole drawing method is based on the following seven variables:

 double ellipseHeight = 12;
 Point ptUpperLeft;
 Point ptUpperRight;
 Point ptLowerLeft;
 Point ptLowerRight;
 Point ptC;
 Path pth = new Path();
 
 ptUpperLeft = new Point(xOffset, ellipseHeight * 2);
 ptUpperRight = new Point(xOffset + (cylinderRadius * 2), ptUpperLeft.Y);
 ptLowerLeft = new Point(xOffset, ptUpperLeft.Y + cylinderHeight);
 ptLowerRight = new Point(ptUpperLeft.X + (cylinderRadius * 2), ptUpperLeft.Y + cylinderHeight);
 ptC = new Point(xOffset + cylinderRadius, ptUpperLeft.Y);  

Each of those variables serves a very specific need:

  • ellipseHeight
    • The ellipse on the top of the cylinder has a height which is statically set to 12.
      This gives the impression that you look at the Cylinder from a position which is above the Cylinder - The original version by StewBob had a dynamically calculated height of the ellipse, but I had to leave it out because it would've broken the offset calculation.
  • ptUpperLeft
  • ptUpperRight
  • ptLowerLeft
  • ptLowerRight
    • These four variables define the corners of the Cylinder as X/Y coordinates
  • ptC
    • The ptC variable defines the coordinates of the center of the Cylinder's top ellipse.
  • pth
    • This one will contain the PathFigure and Geometry Segments.

Now that we know about which variables we are going to use drawing the Cylinder is quite easy:

//Draw cylinder body.
LineSegment ln = new LineSegment(ptLowerLeft, true);
ArcSegment arc = new ArcSegment(ptLowerRight, 
new Size(cylinderRadius, ellipseHeight), 0, false, 
System.Windows.Media.SweepDirection.Counterclockwise, true);
 
PathFigure pf = new PathFigure();
pf.StartPoint = ptUpperLeft;
//Add left side of cylinder.
pf.Segments.Add(ln);
//Add bottom arc of cylinder.
pf.Segments.Add(arc);
ln = new LineSegment(ptUpperRight, true);
//Add right side of cylinder.
pf.Segments.Add(ln);
 
PathGeometry pg = new PathGeometry();
pg.Figures.Add(pf);
 
pth.Stroke = new SolidColorBrush
(gridLineColor);//Grid Line Color is also used as Border Color
pth.StrokeThickness = 2;
pth.Fill = new SolidColorBrush(fillingColor);
pth.Data = pg;
cnv.Children.Add(pth);
 
//Add top ellipse.
pth = new Path();
pth.Stroke = new SolidColorBrush
(gridLineColor);//Grid Line Color is also used as Border Color
pth.StrokeThickness = 1;//Border is 1Px thick
pth.Fill = new SolidColorBrush(fillingColor);
pg = new PathGeometry();
pg.AddGeometry(new EllipseGeometry(ptC, cylinderRadius, ellipseHeight));
pth.Data = pg;  
cnv.Children.Add(pth);

The above snippet does nothing else than adding Geometry and PathFigure elements to the path in order to draw the path (pth) on the canvas after everything is colored. Now that we went through the code to draw the Cylinder, there is just one thing left: Drawing the chart & adding several Cylinders in order to make a shiny chart, self-drawn onto a canvas.

ChartPainter - Draw a Chart

The CylinderChartPainter class can be seen as API which is encapsulating the whole functionality of drawing the Chart and Chart Elements (Cylinders) - It is static, and provides only a single static method.

Methods

void DrawChart( System.Windows.Controls.Canvas cnv, ChartDataContainer container) 

This method draws a Cylinder Chart onto the System.Windows.Controls.Canvas "cnv" - The data defining the chart's size, offset, content, content captions and colorization options is stored in the ChartDataContainer "container" .

The first thing is that the X-Axis line of the chart is drawn on to the canvas (I use the class Line from the System.Windows.Shapes namespace to draw Lines):

//Draw the X-Axis line of the chart
Line xLine = new Line();
xLine.Stroke = Brushes.Black; //Line color is black

//Set Start & End points of the line
xLine.X1 = container.Offset.X;
xLine.X2 = container.Offset.X + container.MaxSize.X;
xLine.Y1 = container.Offset.Y + container.MaxSize.Y;
xLine.Y2 = container.Offset.Y + container.MaxSize.Y;
 
xLine.StrokeThickness = 1; //Line's size is set to 1 pixel
xLine.SnapsToDevicePixels = true;
cnv.Children.Add(xLine);  //Add Line to canvas 

The next thing drawn is the Y-Axis Line (Same technique as for the X-Axis line, but vertically aligned):

//Draw the Y-Axis Line of the chart.
Line yLine = new Line();
 
// Set Start & End points of the Line
yLine.Stroke = Brushes.Black;
yLine.X1 = container.Offset.X;
yLine.Y1 = container.Offset.Y;
yLine.X2 = container.Offset.X;
yLine.Y2 = xLine.Y2;
 
yLine.StrokeThickness = 1;<
span style="font-size: 9pt;">//Line's size is set to 1 pixel</span> 
<span style="font-size: 9pt;">yLine.SnapsToDevicePixels = true;
</span><span style="font-size: 9pt;">
cnv.Children.Add(yLine); </span>
<span style="font-size: 9pt;">//Add Line to canvas </span><
//span style="font-size: 9pt;"> </span>

After the X- and Y-Axis Lines are drawn, I need to draw the caption for them, according to the caption supplied in the ChartDataContainer. In order to give you a better idea of what is placed where, I present you the same graphic as I did a bit above again:

The following code does nothing else than inserting two text blocks ("Y Axis Caption" and "X Axis Caption" in the picture above) according to the four offsets [1], [2], [3] and [4]. The text value is provided by the container:

 //Add Y-Axis description text to the chart
TextBlock yAxisDescriptionBlock = new TextBlock();
yAxisDescriptionBlock.Text = container.YAxisText;
Canvas.SetLeft(yAxisDescriptionBlock, yLine.X1 + 5);
Canvas.SetRight(yAxisDescriptionBlock, xLine.X2);
Canvas.SetTop(yAxisDescriptionBlock, container.Offset.Y);
cnv.Children.Add(yAxisDescriptionBlock);
 
//Add X-Axis description text to the chart
TextBlock xAxisDescriptionBlock = new TextBlock();
xAxisDescriptionBlock.Text = container.XAxisText;
Canvas.SetTop(xAxisDescriptionBlock, yLine.Y2 - 12);
Canvas.SetLeft(xAxisDescriptionBlock, xLine.X2 + 3);
Canvas.SetRight(xAxisDescriptionBlock, xLine.X2 + container.YAxisText.Length);
cnv.Children.Add(xAxisDescriptionBlock);  

Afterwards we are ready to use the CylinderPainter3D class to draw the Cylinder elements of the chart.

Each chart is drawn according to the sketches shown in the chapter Drawing Sketches, and the offset (the gap) between the single Cylinders is calculated as:

 CylinderRadius * 2.5 

Another thing which I haven't shown in the sketch above is the fact that I included a scaling mechanism, allowing the chart to automatically resize the height of the cylinders in order to use as much space as possible (and therefore, display the data as detailed as possible). For example, if the biggest data element is 45 and the chart has a maximum height of 100, the method automatically resizes every Cylinder to be twice as high as it would be without the scale. This feature enables the chart to be as readable as possible. The charts are drawn for each data element in the container.

//Add Cylinder graphics
double offsetX = container.Offset.X + 25;//Offset on the X-Axis
double scale =  container.MaxSize.Y / 
    container.Data.Max(); //Scaling value - Needed to make the chart
                                                            //more readable
double radius = (container.MaxSize.X / 
(container.Data.Count + 2.5)) / 2;   //Radius of a single chart element
                                     // = diameter/ 2
CylinderPainter3D cylinderPainter = new CylinderPainter3D();
cylinderPainter.CylinderRadius = 
    radius;                    // Set the radius of the cylinder to be drawn
cylinderPainter.FillingColor = 
    container.ChartElementColor; // Set the Cylinder's color
foreach(double dataelement in container.Data)
{
    double topY = (container.Offset.Y + 
    (container.MaxSize.Y - (dataelement*scale))) - 26;  //Calculate Y-Offset
    cylinderPainter.CylinderHeight = dataelement * scale;   //Apply scale and set height
    cylinderPainter.DrawCylinder(cnv, offsetX, topY);   //Draw the Cylinder
    offsetX += cylinderPainter.CylinderRadius * 2.5;    //Increase offset
}

After the chart's Cylinders are added to the canvas, I only need to add the caption elements for each Cylinder. Since it is entirely voluntary to even add captions to the Cylinders, I check whether there are captions available in the container:

if (container.DataCaptions.Count > 0)//If available, add captions to cylinder graphics
{ 

If there are caption elements, I proceed to add them to the canvas (I add them using a text block, and a horizontal line to connect the caption to a Cylinder. In addition, I defined a zipper variable, which has a value of 10 and is multiplicated with -1 each iteration of the foreach loop, causing the captions to have a slight, zipper-like offset which makes them better readable (the cyptions are not looking crowded anymore).

offsetX = container.Offset.X + 25; //Offset on the X-Axis 
    //is reset because captioning is started from the left
short zipperOffset = 10;
foreach (string caption in container.DataCaptions)
{
    zipperOffset *= (-1);
    //Add line to associate a caption with a single chart element
    Line captionLine = new Line();
    captionLine.Stroke = Brushes.Black;
    captionLine.X1 = offsetX;
    captionLine.X2 = offsetX;
    captionLine.Y1 = yLine.Y2;
    captionLine.Y2 = yLine.Y2 + 40 + zipperOffset;
 
    cnv.Children.Add(captionLine);
 
    //Add chart element description text block
    TextBlock chartElementDescriptionTextBlock = new TextBlock();
    chartElementDescriptionTextBlock.Text = caption;
    Canvas.SetTop(chartElementDescriptionTextBlock, yLine.Y2 + 47 + zipperOffset);
    Canvas.SetLeft(chartElementDescriptionTextBlock, offsetX);
    offsetX += radius * 2.5;
    Canvas.SetRight(chartElementDescriptionTextBlock, offsetX);
    cnv.Children.Add(chartElementDescriptionTextBlock);
} 

The above code gives a caption, connected to a Cylinder element of the chart looking like this:

Using the ChartPainter class to draw a Cylinder Chart

I made up a simple example code which shall show you how the ChartPainter class can be used to draw a chart:

ChartDataContainer container = new ChartDataContainer();
container.Data = new List<double>() {20, 33, 115 , 85, 65 };//Dummy data
container.DataCaptions = new List<string>() 
{"1","2", "3", "4", "5"};//Set Captions
container.MaxSize = new Point(700, 300);//
container.Offset = new Point(10, 150);
container.ChartElementColor = Colors.SlateBlue;
CylinderChartPainter.DrawChart(container, this.Canvas1); 

Executing the above code gives you a chart looking like this:

Points of Interest

I learned how to draw a 3D Cylinder, use the drawn Cylinder to make a 3D Cylinder chart control and learned how to draw Text, Lines and other fancy stuff onto a Canvas - What really grinds my gears so far is the mirrored Y Axis of the WPF Canvas... If anyone has an idea how that comes - feel free to contact me and brighten my mind.

Apart from that, I am happy to announce that the charting routine I presented in this article was already used in production, in another CodeProject article Sandro Lorek and I did together. If you are interested in seeing what the code presented in this article does, download it and try to include it in a real life project. If you have any suggestions, bug findings / fixings or just want to say how happy you are with the code we presented, feel free to drop a comment below.

History

  • 24-Nov-2013
    • First version published

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here