11,429,855 members (74,720 online)

3D Pie Chart

, 13 Mar 2006 Zlib
 Rate this:
A class library for drawing 3D pie charts.

Introduction

The initial idea was to create a utility class / class library that could be used for drawing 3-D pie charts. At first, this seemed quite simple, since there is a `DrawPie` method already available in the `Graphics` class. This method accepts start angle and sweep angle as arguments, so it should not be a problem to use it: just sum up all values and then calculate the portion for each one, converting it to sweep angle for the corresponding pie slice. And this works for a circular chart. However, if you want to add 3-D perspective (i.e., if a chart is drawn with an ellipse shape) this approach will result in an impression of varying values, as demonstrated in the figure below: pie slices at the left and right side appear larger than those up and down, although all of them have the same sweep angle.

Instead of a direct insertion of the sweep angle, the parametric equation for ellipse has to be used.

The above problem thus solved, adding a real 3-D look to the chart requires only one additional step: drawing a cylinder brink. However, if you want to draw pie slices displaced from the common center, then the slice cut sides become visible and have to be drawn too. Since these sides may partially overlap, the order of drawing is of utmost importance to obtain the correct 3-D appearance.

Background

Drawing

First, note that the coordinate system as shown in the figure below is used:

The parametric equation of ellipse has a form of:

x = a * cos(t)

y = b * sin(t)

where `a` and `b` are major and minor semi-axis, respectively, and `t` is a variable parameter. Note that `t` does not have direct interpretation in terms of an angle but (as anyone familiar with trigonometry will conclude from the figure below) can be related to the polar angle from the ellipse center as:

angle = tan-1(y/x) = tan-1((b * sin(t)) / (a * cos(t)))

Consequently, when initializing individual shapes for rendering, the corresponding start and sweep angles have to be transformed by the following method:

```protected float TransformAngle(float angle) {
double x = m_boundingRectangle.Width * Math.Cos(angle * Math.PI / 180);
double y = m_boundingRectangle.Height * Math.Sin(angle * Math.PI / 180);
float result = (float)(Math.Atan2(y, x) * 180 / Math.PI);
if (result < 0)
return result + 360;
return result;
}```

In the above method, `m_boundingRectangle` is the boundary rectangle of the ellipse from which the pie shape is cut out. The width and height of this rectangle are equal to the major and minor axes of the ellipse, respectively.

When drawing a 3-D pie slice (with some finite height), it is necessary to draw the slice cut sides as well as the outer periphery of the cylinder from which the slice is cut out. For this, the center point and the points on the pie slice periphery (`m_center`, `m_pointStart` and `m_pointEnd` private members of the `PieSlice` class) and their corresponding siblings on the slice bottom side have to be calculated first. These points are used to constitute `GraphicsPath`s: paths for cut sides consist of four lines, while the path for the cylinder periphery consists of two vertical lines and two arcs.

It is worthy to note that the slice side corresponding to the start angle is visible only when the start angle is larger than 90 and less than 270 degrees, while the side corresponding to the end angle is visible only when the angle is between 270 and 90 degrees. Also, the cylinder brink is visible only for angles between 0 and 180 degrees.

As already mentioned, the drawing order is important when the chart contains several slices displaced from the center. The pie shape that is crossing the 270 degrees boundary must be drawn first because it may be (partially) covered by another pie slice. The slice closest to the 270 degrees axis (regardless if it is from the left or the right side) is drawn next, the procedure being repeated for the slices to follow.

To achieve this order, the pie slices are stored into an array starting with the shape that crosses the 270 degrees axis. Consequently, neighboring shapes will be placed in the second and in the last position of the array. Therefore, the search for the next shape to be drawn goes from the start and from the end of the list simultaneously, selecting the shape which is closer to the 270 degrees axis to be drawn first.

Pie slices crossing the 270 degrees axis have a unique feature: both cut sides (corresponding to the start and the end angle) are visible - c.f. figure below left. Moreover, if both the start and the end angles are within 0 and 180 degrees range, the slice will have its cylinder brink consisting of two parts (figure below right). To handle this, the slice is split into two sub-slices in the course of drawing, with the common top side.

This splitting comes into play with drawing charts like the one shown below: if the blue slice was drawn first and completely, the green slice would completely overlap it, resulting in an irregular illusion. The numbers on each shape indicate the correct order of drawing.

Hit Testing

When the first version of the article was published, several readers suggested to add tool tips and pie slice highlighting when the mouse is over it. This feature has been implemented in version 1.1.

The main problem was to find and implement the algorithm that searches for the pie slice currently under the mouse cursor. The search order for the entire chart is the reverse of the drawing order, starting from the foremost slice. However, processing of individual slices is cumbersome because of their irregular shapes.

To test if a pie is hit, the pie slice shape has to be decomposed into several surfaces as shown on the figure below, and each of these surfaces is tested if it contains the hit point.

Note that the cylinder outer periphery hitting is not tested directly (in fact, I have no idea how it could be done simply), but is covered by testing the top (1) and the bottom (2) pie surfaces and the quadrilateral defined by the periphery points (3).

Hit testing for the top and bottom slice surfaces is straightforward - the distance of the point from the center of the ellipse is compared to the ellipse radius for the corresponding angle:

```private bool PieSliceContainsPoint(PointF point,
float xBoundingRectangle, float yBoundingRectangle,
float widthBoundingRectangle, float heightBoundingRectangle,
float startAngle, float sweepAngle) {
double a = widthBoundingRectangle / 2;
double b = heightBoundingRectangle / 2;
double x = point.X - xBoundingRectangle - a;
double y = point.Y - yBoundingRectangle - b;
double angle = Math.Atan2(y, x);
if (angle < 0)
angle += 2 * Math.PI;
double angleDegrees = angle * 180 / Math.PI;
// point is inside the pie slice only if between start and end angle
if (angleDegrees >= startAngle &&
angleDegrees <= startAngle + sweepAngle) {
// distance of the point from the ellipse centre
double r = Math.Sqrt(y * y + x * x);
double a2 = a * a;
double b2 = b * b;
double cosFi = Math.Cos(angle);
double sinFi = Math.Sin(angle);
// distance of the ellipse perimeter point
(b * a) / Math.Sqrt(b2 * cosFi * cosFi + a2 * sinFi * sinFi);
}
return false;
}```

For quadrilaterals, a well know algorithm for testing if a point is inside a polygon is used: a ray is traced from the point to test and the number of intersections of this ray with the polygon is counted. If the number is odd, the point is inside the polygon, if it is even, the point is outside (c.f. figure below).

Consequently, all polygon sections are passed, counting intersections with the ray:

```public bool Contains(PointF point, PointF[] cornerPoints) {
int intersections = 0;
float x0 = point.X;
float y0 = point.Y;
for (int i = 1; i < cornerPoints.Length; ++i) {
if (DoesIntersect(point, cornerPoints[i], cornerPoints[i - 1]))
++intersections;
}
if (DoesIntersect(point, cornerPoints[cornerPoints.Length - 1],
cornerPoints[0]))
++intersections;
return (intersections % 2 != 0);
}

private bool DoesIntersect(PointF point, PointF point1, PointF point2) {
float x2 = point2.X;
float y2 = point2.Y;
float x1 = point1.X;
float y1 = point1.Y;
if ((x2 < point.X && x1 >= point.X) ||
(x2 >= point.X && x1 < point.X)) {

float y = (y2 - y1) / (x2 - x1) * (point.X - x1) + y1;
return y > point.Y;
}
return false;
}```

Using the code

The PieChart solution contains three classes: `PieSlice`, `PieChart3D` and `PieChartControl` (derived from the `System.Windows.Forms.Panel` control). The `PieSlice` class provides all the functionality required to draw a 3-D pie slice with given a start and sweep angle, color, height and shadow style.

The `PieChart3D` represents the entire chart. There are several constructors available, all of them taking a bounding rectangle and an array of values. Some constructors also accept:

• array of colors used to represent values,
• array of slice displacements,
• slice thickness.

Slice displacement is expressed as a ratio of the slice "depth" and ellipse radius; minimum value of 0 means that there is no displacement, while 1 (largest allowed value) means that the shape is completely taken out of the ellipse.

Slice thickness represents the ratio of pie slice thickness and the ellipse's vertical, minor axis; largest allowed value being 0.5.

It is also possible to set any of the above parameters using public properties. Note that if the number of colors provided is less than the number of values, colors will be re-used. Similarly, if the number of displacements is exhausted, the last displacement will be used for all the remaining pie slices.

There are also additional public properties that can be set:

• `Texts`,
• `Font`,
• `ForeColor`,
• `ShadowStyle`,
• `EdgeColorType`,
• `EdgeLineWidth`,
• `InitialAngle`,
• `FitToBoundingRectangle`.

The meaning of all these properties and their possible values can be seen from the demo sample. The `Texts` property is an array of strings that are displayed on corresponding slices. Default implementation places text near the center of the slice's top, but the user may override the `PlaceTexts` method of the `PieChart3D` class to implement her/his own placement logic. `Font` and `ForeColor` properties define the font and the color that is used to display these texts.

The `PieChart3D` class can be used for printing: it is only necessary to initialize the chart object and then call its `Draw` method, providing the corresponding `Graphics` object:

`public void Draw(Graphics graphics) { ... }`

To display the chart on the screen, `PieChartControl` is more appropriate: it encapsulates the chart into a panel that is responsible for chart (re)painting. The user only has to place it on the form and set the required values. For example:

```private System.Drawing.PieChart.PieChartControl panelDrawing =
new System.Drawing.PieChart.PieChartControl();
panelDrawing.Values = new decimal[] { 10, 15, 5, 35};
int alpha = 80;
panelDrawing.Colors = new Color[] { Color.FromArgb(alpha, Color.Red),
Color.FromArgb(alpha, Color.Green),
Color.FromArgb(alpha, Color.Yellow),
Color.FromArgb(alpha, Color.Blue) };
panelDrawing.SliceRelativeDisplacements = new float[] { 0.1F, 0.2F, 0.2F, 0.2F };
panelDrawing.Texts = new string[] { "red",
"green",
"blue",
"yellow" };
panelDrawing.ToolTips = new string[] { "Peter",
"Paul",
"Mary",
"Brian" };
panelDrawing.Font = new Font("Arial", 10F);
panelDrawing.ForeColor = SystemColors.WindowText
panelDrawing.LeftMargin = 10F;
panelDrawing.RightMargin = 10F;
panelDrawing.TopMargin = 10F;
panelDrawing.BottomMargin = 10F;
panelDrawing.SliceRelativeHeight = 0.25F;
panelDrawing.InitialAngle = -90F;```

`PieChartControl` overrides both `OnPaint` and `OnResize` events, taking care of correct chart redrawing.

Note that `PieChartControl` has an additional `ToolTips` property accepting an array of strings that are displayed when the corresponding pie slice is hit. If any string in this array is empty, the corresponding value will be displayed instead.

Points of Interest

To achieve a better 3-D perspective, I have introduced a "gradual" shadow, changing the brightness of the slice cut sides depending on their angles. To achieve this effect on the cylinder brink, a gradient fill is used for painting the periphery. I used an empirical formula for this. However, the user may change this by deriving a class from `PieSlice` and overriding the `CreateBrushForSide` and the `CreateBrushForPeriphery` methods in the `PieSlice` class, implementing her/his own logic.

Similarly, a user can override the `CreatePieSliceHighlighted` method in the `PieChart` class; the default implementation draws the highlighted pie slice in a slightly lighter color.

From version 1.4, a simple pie chart printing is included in the demo program; the user just has to click the Print button on the demo form. The printing code is provided in the `PrintChart` class of the Test project.

You are free to use this code and the accompanying DLL. Please include a reference to this web page in the list of credits.

History

• June 1, 2004 - Initial submission of the article.
• June 22, 2004 - ver. 1.1: Tool tip and pie slice highlighting added. Also (credits for these go to Andreas Krohn), flickering on resize has been removed and assertion failure bug when control is made very small has been fixed.
• November 11, 2004 - ver. 1.2: bug fixes.
• March 21, 2005 - ver. 1.3: color transparency support added (thanks to Bogdan Pietroiu for this suggestion), and description text for each slice (as suggested by ccarlinx) added.
• November 10, 2005 - ver. 1.4: Pie chart control crashing for angle of 270 degrees bug (found by gabbyr and rafabgood) and "Crash when all slices have 0 value" has been fixed. A simple pie chart printing sample has been included into the demo project (please note that the quality of the printout depends on the capabilities of the printer and is usually far behind the screen display quality).
• March 12, 2006 - ver. 1.5: Control crashing bug (as noticed by jianingy) has been fixed.

Share

Software Developer (Senior) Syntellect
Croatia
Graduated at the Faculty of Electrical Engineering and Computing, University of Zagreb (Croatia) and received M.Sc. degree in electronics. For several years he was research and lecturing assistant in the fields of solid state electronics and electronic circuits, published several scientific and professional papers, as well as a book "Physics of Semiconductor Devices - Solved Problems with Theory" (in Croatian).
During that work he gained interest in C++ programming language and have co-written "C++ Demystified" (in Croatian), 1st edition published in 1997, 2nd in 2001, 3rd in 2010, 4th in 2014.
After book publication, completely switched to software development, programming mostly in C++ and in C#.

 First PrevNext
 fixed flickering tooltips Member 39573964-Oct-14 13:50 Member 3957396 4-Oct-14 13:50
 3D Pie Chart in AltSketch Graphics Library Demid Korneev26-Jul-14 7:45 Demid Korneev 26-Jul-14 7:45
 Re: 3D Pie Chart in AltSketch Graphics Library Julijan Sribar26-Jul-14 14:12 Julijan Sribar 26-Jul-14 14:12
 Saving as image explosives24-Nov-12 23:56 explosives 24-Nov-12 23:56
 properties Aggelos Tzitzifas2-Oct-12 13:59 Aggelos Tzitzifas 2-Oct-12 13:59
 Noobish note Armando1812-Sep-12 4:50 Armando18 12-Sep-12 4:50
 Message Automatically Removed 15-Aug-12 2:37 Programminfree 15-Aug-12 2:37
 My vote of 5 srfox20-Jun-12 17:37 srfox 20-Jun-12 17:37
 Different slice heights in 3D amanbe21-Sep-11 4:38 amanbe 21-Sep-11 4:38
 My vote of 5 ubaid.engr16-Aug-11 14:30 ubaid.engr 16-Aug-11 14:30
 It is exactly what i needed. excellent work. thanks
 Excelence pakorro07038615-Jul-11 9:38 pakorro070386 15-Jul-11 9:38
 Problem compiling the demo Guillermo Ganem7-May-11 23:37 Guillermo Ganem 7-May-11 23:37
 Re: Problem compiling the demo Julijan Sribar8-May-11 22:07 Julijan Sribar 8-May-11 22:07
 My vote of 5 santoshshrest9-Apr-11 20:10 santoshshrest 9-Apr-11 20:10
 do you have a new update ? Edward11111-Oct-10 12:41 Edward111 11-Oct-10 12:41
 Re: do you have a new update ? Julijan Sribar11-Oct-10 21:57 Julijan Sribar 11-Oct-10 21:57
 Re: do you have a new update ? Edward11112-Oct-10 4:09 Edward111 12-Oct-10 4:09
 good work! kechengsheji10-Oct-10 21:51 kechengsheji 10-Oct-10 21:51
 My vote of 4 frabusfe27-Sep-10 8:25 frabusfe 27-Sep-10 8:25
 New project site EnderE10-Aug-10 8:16 EnderE 10-Aug-10 8:16
 Basic use question leyroyjenkins21-Jul-10 23:20 leyroyjenkins 21-Jul-10 23:20
 Re: Basic use question EnderE22-Jul-10 4:58 EnderE 22-Jul-10 4:58
 Re: Basic use question EnderE22-Jul-10 9:47 EnderE 22-Jul-10 9:47
 Re: Basic use question EnderE22-Jul-10 11:16 EnderE 22-Jul-10 11:16
 hvala Member 358316412-Mar-10 1:09 Member 3583164 12-Mar-10 1:09
 another newbie to C# Member 69748214-Mar-10 2:32 Member 6974821 4-Mar-10 2:32
 Re: another newbie to C# Julijan Sribar4-Mar-10 10:51 Julijan Sribar 4-Mar-10 10:51
 Same in VB6 smartstar24-Dec-09 5:52 smartstar 24-Dec-09 5:52
 Re: Same in VB6 Idris918-Feb-10 0:11 Idris9 18-Feb-10 0:11
 Great Job... BUT... can I generate images from your Library HermesSurfer19-Nov-09 4:58 HermesSurfer 19-Nov-09 4:58
 Re: Great Job... BUT... can I generate images from your Library Julijan Sribar19-Nov-09 21:53 Julijan Sribar 19-Nov-09 21:53
 A good example of bad code Member 33603652-Apr-09 19:43 Member 3360365 2-Apr-09 19:43
 Re: A good example of bad code EnderE13-Apr-09 12:27 EnderE 13-Apr-09 12:27
 Re: A good example of bad code Julijan Sribar14-Apr-09 0:45 Julijan Sribar 14-Apr-09 0:45
 Re: A good example of bad code EnderE14-Apr-09 5:42 EnderE 14-Apr-09 5:42
 Re: A good example of bad code EnderE7-May-09 8:18 EnderE 7-May-09 8:18
 Re: A good example of bad code Mark Thesing7-Dec-09 14:22 Mark Thesing 7-Dec-09 14:22
 Can i use this control in MFC? yejiang12524-Mar-09 16:53 yejiang125 24-Mar-09 16:53
 Fantastic Member 25289315-Mar-09 1:54 Member 2528931 5-Mar-09 1:54
 great 3D pie chart acton10112-Jan-09 14:10 acton101 12-Jan-09 14:10
 Perspective Angle? Rich768-Dec-08 2:55 Rich76 8-Dec-08 2:55
 Re: Perspective Angle? Julijan Sribar8-Dec-08 5:32 Julijan Sribar 8-Dec-08 5:32
 do you have the same codes for asp.net application ? Member 40839703-Dec-08 20:38 Member 4083970 3-Dec-08 20:38
 Re: do you have the same codes for asp.net application ? Julijan Sribar3-Dec-08 22:12 Julijan Sribar 3-Dec-08 22:12
 Re: do you have the same codes for asp.net application ? Member 40839703-Dec-08 22:20 Member 4083970 3-Dec-08 22:20
 Great code but why in System.Drawing namespace? paulsudipto17-Nov-08 22:01 paulsudipto 17-Nov-08 22:01
 Tooltip flickering Bozjak13-Nov-08 21:17 Bozjak 13-Nov-08 21:17
 Re: Tooltip flickering Julijan Sribar13-Nov-08 22:05 Julijan Sribar 13-Nov-08 22:05
 Re: Tooltip flickering MazMatic13-Dec-09 21:20 MazMatic 13-Dec-09 21:20
 Re: Tooltip flickering MazMatic14-Dec-09 1:13 MazMatic 14-Dec-09 1:13
 Last Visit: 31-Dec-99 19:00     Last Update: 3-May-15 17:23 Refresh 123456 Next »