Introduction
This is an article about 3D pie charts with transparency.
Background
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.
Fig 1
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.
Fig 2
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.
Figure 3
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)
Next
Dim CurrentYear As Single = Year(Now)
For I = 0 To Values.Length - 1
Strings(I) = CurrentYear CurrentYear = CurrentYear - 1
Next
For I = 0 To Values.Length - 1
StringsToolTip(i) = "Fiscal Year " & Strings(i)
Next
For I = 0 To Values.Length - 1
Total = Total + Values(I)
Next
For I As Integer = 0 To Values.Length - 1
Angles(I + 1) = 360 * Values(I) / Total
Next
Angles(0) = 0 For I As Integer = 1 To Values.Length
Angles(I) = Angles(I) + Angles(I - 1)
Next
StartAngle = GetAngle(Angle:=Angles(0) + m_Rotation)
FinishAngle = GetAngle(Angle:=Angles(1) + m_Rotation)
SetDisplacement(StartAngle, FinishAngle)
Call MakePieChart()
Timer1.Enabled = True ‘Start timer of slice animation
End Sub
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_TransparencyLevel
and 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 m_Transparency2Level
accordingly.
In the 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 _
* m_AspectRatio
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
End If
‘If we go past 360 we start again, so make it equal zero
If GetAngle = 360 Then GetAngle = 0
Return GetAngle
End Function
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 DrawString
routine.
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. Listview
, 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 ColorMatrix
.
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_TransparencyLevel
and 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.
Setting m_DepthModeTranparent
to 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.
Cheers,
Joe Mifsud