Click here to Skip to main content
Click here to Skip to main content

3D Pie Chart

, 13 Mar 2006 Zlib
Rate this:
Please Sign up or sign in to vote.
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 GraphicsPaths: 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
    double ellipseRadius = 
           (b * a) / Math.Sqrt(b2 * cosFi * cosFi + a2 * sinFi * sinFi);
    return ellipseRadius > r;
  }
  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.

Copyright notice

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.

License

This article, along with any associated source code and files, is licensed under The zlib/libpng License

Share

About the Author

Julijan Sribar
Software Developer (Senior) Syntellect
Croatia 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#.
Follow on   LinkedIn

Comments and Discussions

 
QuestionExcelence Pinmemberpakorro07038615-Jul-11 8:38 
QuestionProblem compiling the demo PinmemberGuillermo Ganem7-May-11 22:37 
AnswerRe: Problem compiling the demo PinmemberJulijan Sribar8-May-11 21:07 
GeneralMy vote of 5 Pinmembersantoshshrest9-Apr-11 19:10 
GeneralMy vote of 5 Pinmemberfuckpenis26-Nov-10 14:00 
Questiondo you have a new update ? PinmemberEdward11111-Oct-10 11:41 
AnswerRe: do you have a new update ? PinmemberJulijan Sribar11-Oct-10 20:57 
GeneralRe: do you have a new update ? PinmemberEdward11112-Oct-10 3:09 
Generalgood work! Pinmemberkechengsheji10-Oct-10 20:51 
GeneralMy vote of 4 Pinmemberfrabusfe27-Sep-10 7:25 
GeneralNew project site PinmemberEnderE10-Aug-10 7:16 
GeneralBasic use question Pinmemberleyroyjenkins21-Jul-10 22:20 
GeneralRe: Basic use question PinmemberEnderE22-Jul-10 3:58 
GeneralRe: Basic use question PinmemberEnderE22-Jul-10 8:47 
GeneralRe: Basic use question PinmemberEnderE22-Jul-10 10:16 
Generalhvala PinmemberMember 358316412-Mar-10 0:09 
Generalanother newbie to C# PinmemberMember 69748214-Mar-10 1:32 
AnswerRe: another newbie to C# PinmemberJulijan Sribar4-Mar-10 9:51 
GeneralSame in VB6 Pinmembersmartstar24-Dec-09 4:52 
GeneralRe: Same in VB6 PinmemberIdris917-Feb-10 23:11 
GeneralGreat Job... BUT... can I generate images from your Library PinmemberHermesSurfer19-Nov-09 3:58 
AnswerRe: Great Job... BUT... can I generate images from your Library PinmemberJulijan Sribar19-Nov-09 20:53 
RantA good example of bad code PinmemberMember 33603652-Apr-09 18:43 
AnswerRe: A good example of bad code PinmemberEnderE13-Apr-09 11:27 
AnswerRe: A good example of bad code PinmemberJulijan Sribar13-Apr-09 23:45 

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.141015.1 | Last Updated 13 Mar 2006
Article Copyright 2004 by Julijan Sribar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid