This is an article about 3D pie charts with transparency.
I needed a pie chart in a small application without the need or the complications of 3rd party software. At first, I thought it would be an easy task till I realized that my math needed a little refreshing and that took a while, though the math’s content is fairly simple with limited trigonometry, it did initially put a strain on the gray matter.
A 2D pie chart is very straight forward, knowing the Start and Sweep angle, DrawPie does this well, but as soon as you apply some perspective to the chart, you soon realize there's a bit of math involved here. The reason for this is that as perspective is introduced, the length of the arch of particular slices changes according to the perspective tilt.
You can see from Fig 2 that the lengths of the arches particularly on the left and right side have changed from that of Fig 1 yet they are both the same pie chart. This is where that stuff we did back at school comes into play. So how do we do it? Well the method is called Cartesian Coordinates’ named after a French guy (well, a Mathematician) Descartes. Mind you, this was back in 1637. Pretty smart when you think about, I mean back in 1637.
Well Microsoft didn’t make things easy as I will point out soon.
Firstly, back to the Cartesian coordinate system.
As you can see in Fig 3, a point on a graph can be plotted in two ways, the first, knowing the angle and length (Radius--Polar Coordinates) or using the X and Y coordinates (Rectangular Coordinates). Note the zero reference in the centre; this is important because when we write code using system drawing, references are from the top left corner of a binding rectangle. To compensate for this (in other words, move the 0 reference), it’s only a matter of subtracting the radius (rectangle width/2) from X and adding the radius to Y.
The top left corner is +Y because it's up from the Cartesian zero Coordinate and it is –X because it’s left of the Cartesian zero coordinate. You will also notice in Fig 3 that there are 4 quadrants in the Cartesian Plane, knowing this is also important because, for example, if say we look at the coordinate X= -5 and Y=5 in the second quadrant, compare this to X= 5 and Y= -5 in the forth quadrant, although the figure values are the same, the fact that the negative and positive signs are different, puts us in a different quadrant, hence it gives us a different angle For example, if we use +X as the base line, and we call this zero degrees, as shown in fig 3 then if we look at anti clock rotation then any angle that lies in the first quadrant is between 0 and 90 (X = +X) and (Y= +Y) and any angle that lies in the second quadrant we be in the range of 90 (Y= +Y) to 180 (X = -X) and angles the third will be between 180 (X= -X) and 270 (Y= -Y) and so on. The importance of this is that when trigonometry is used to calculate X and Y coordinate from a given angle, take the following:
Trig formula X = cos(angle)
Y = sin (angle)
Sounds easy enough, but take two angles for instance 0 degrees and 180 degrees, find Y:
Y = Sin (0) = 0
Y = Sin (180) = 0
Ha, both equal 0. Terrific! How do we tell the difference ?? To answer that, we now look at the X coordinate given the same angle.
Treat for angle = 0 degrees
X = cos(0) = 1
Now treat for angle = 180 degrees
X = cos(180) = - 1
The difference is X = -1 for 180 degrees and X = 1 for 0 degrees.
The reason for this is that the Trig formula is for right angle triangles or to put it into angle perspective 0 to 90 degrees so given a circle has 4 right angle triangles hence 4 quadrants. So back to fig 3, you can now see that when X is negative and Y is positive we are in the second quadrant and if X is negative and Y is negative then we are in the third quadrant
and so on. So by testing for X and Y polarity, we can then determine which quadrant the angle lies, and by knowing this we can the add multiples of 90 as required.
An important thing to remember is that basic Trig formulas require the angle to be radians, E.g. 360 degrees = 6.21318 radians or 2 x pi therefore 180 degrees = pi, hence when you see a formula that includes Math.PI / 180) what this does effectively is convert degrees to radians.
So Math.Pi = 3.14159 plus a few more decimal places.
Now put it into practice.
First let’s look at the Form load event.
Private Sub GraphForm_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
Dim Total As Double = 0
Dim StartAngle As Single, FinishAngle As Single
Me.Width = 340
Me.Height = 340
btnClose.Top = Margin + (Radius - btnClose.Height / 2)
btnClose.Left = Margin + (Radius - btnClose.Width / 2)
For I = 0 To Values.Length - 1
Values(I) = CSng(40)
Dim CurrentYear As Single = Year(Now)
For I = 0 To Values.Length - 1
Strings(I) = CurrentYear
CurrentYear = CurrentYear - 1
For I = 0 To Values.Length - 1
StringsToolTip(i) = "Fiscal Year " & Strings(i)
For I = 0 To Values.Length - 1
Total = Total + Values(I)
For I As Integer = 0 To Values.Length - 1
Angles(I + 1) = 360 * Values(I) / Total
Angles(0) = 0
For I As Integer = 1 To Values.Length
Angles(I) = Angles(I) + Angles(I - 1)
StartAngle = GetAngle(Angle:=Angles(0) + m_Rotation)
FinishAngle = GetAngle(Angle:=Angles(1) + m_Rotation)
Timer1.Enabled = True ‘Start timer of slice animation
Now in the load event we placed values into an array, for convenience they are all equal
angles, 40 degrees. It is done here purely as a test exercise, you would load the array elsewhere based on values suited to your application. The array could be a variable length.
Now let's examine the actual drawing of the pie chart
This is done in the
MakePieChart routine. Firstly because I provided for semi transparent pie charts (Set
m_Transparency2Level variables in the declaration section. Note 255 is totally opaque, and lessor becomes more transparent.
m_Transparency2Level is the top of the pie and
m_TransparencyLevel is the body.) Different effects can be achieved by changing the alpha values, e.g. set Body to 255 and top 55.
The order in which slices are draw is important, this is commented in the
MakePieChart routine. If the order is not correct, then a slice that should be behind may appear in front, to demonstrate this just swap some of the blocks of code in the
MakePieChart routine and set
MakePieChart routine, before each angle in Angles(n) is used to
drawPie it is first sent to the
GetAngle routine, this is where trigonometry is applied. Note that when 3D is applied, the Y radius is different to the X radius, in other words an ellipse has its width radius (Major) larger than its height radius (Minor). So finding the Y coordinant requires the radius to be multiplied by the aspect ratio.
All angles contained in the angles array are sent to
GetAngle before drawing each pie slice. What happens in the
GetAngle routine is first the X and Y coordinants are found, Y having the radius adjusted by the aspect ratio. Once this is done, we have a X and Y coordinant for a given angle on and an ellipse (it is an ellipse because we corrected the radius for Y by the aspect ratio). Now this is great but
drawPie in .NET requires angles (polar coordinants) so we need to translate the X and Y coordinant to an angle. So effectively, we are doing a rectangular to Polar conversion.
Here’s the formula:
GetAngle = ( Math.Atan((x)/Y) * (180/Math.PI))+90
Now check the whole routine below.....
Private Function GetAngle(ByVal Angle As Single) As Single
Dim X as single, Y as single
X = Radius + Math.Cos((Angle) * Math.PI / 180) * Radius
Y = Radius - Math.Sin((Angle) * Math.PI / 180) * Radius _
X = X - Radius
Y = Y – Radius
‘Within the Cartesian plane coordinant we need a point we call zero degrees.
GetAngle = (Math.Atan((X) / Y) * (180 / Math.PI)) + 90
if Y > 0 then that puts us in the top two quadrants so we must add 180
‘otherwise we zero again
If Y >= 0 Then
GetAngle = GetAngle + 180
‘If we go past 360 we start again, so make it equal zero
If GetAngle = 360 Then GetAngle = 0
A similar approach to this is required for
DrawText except this argument requires rectangular coordinants X and Y. So we take an angle, half way between the start and sweep angle and using Trigonometry we obtain an X and Y coordinant, see
Depth is easy to accomplish, it's just a matter of drawing the same pie several times(depending on depth) and displacing Y value each time by say one pixel (hence we have depth). An alternative would be to draw point to point. Note that depth must change as perspective is changed, so as you tilt the pie chart, the depth of the chart must change accordingly.
I have added a little animation to the Pie chart (always looks good) though sometimes it can be overdone and becomes boring.
The pie chart once created, becomes the Background image of a form (however, a picture box can be used). I like this approach because I can display the chart over other controls e.g.
Datagridview, etc. using a transparent form, this approach reduces form clutter (too many forms on the screen), also, users like things simple. I also don't like too many buttons on things, the mouse always makes a good interface, so I have added mouse wheel routines to take care of perspective and depth (less buttons, hate too many buttons).
Use the Vscroll buttons (small, alongside close button) to control luminance. This is done in the Paint Event. Values are changed within the
Just place the mouse over the chart and the wheel controls perspective and placing the mouse over the close button and the wheel controls depth, right clicking rotates the chart, though I just noticed I have a bug here which I shall fix when I get time.
The code is straight forward, read the comments, values are loaded on the load event, so remove and add appropriately. Transparency for both top and body can be set by changing
m_Transparency2Level respectively to provide for different effect (play around as you wish)
I have also provided Tooltip independent for each slice. Once again this required a bit of Trig Maths to get the angle range the mouse pointer is in relative to the pie slices. This is done in the
PieChartForm_MouseMove event, the angle chosen is half way between the start and finish angle on each slice.
StringsToolTip(n) contains the text for each slice, for demo purposes this is also done in the Form load event so in your application move accordingly.
If using the Transparent form approach (make
PieChartForm transparent), it would pay to set the form background colour (and transparency color of course), the same as the background colour of surface it's going to be displayed on, this gives a better outline effect. The approach I used was to dock a form onto another form, so when the form its docked to move around the chart moves with it, but with the feature that if it is pulled of the docked form (Using mouse down) it is no longer anchored, this I believe creates a good effect with versatility.
false in the declaration section will draw the pie body with a HatchStyle effect.
I will eventually update to add more, e.g. highlight slices on mouse enter, etc. a different labelling, etc.