IntroductionThe 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 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. BackgroundDrawingFirst, note that the coordinate system as shown in the figure below is used: The parametric equation of ellipse has a form of:
where
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, 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 ( 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 TestingWhen 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 codeThe PieChart solution contains three classes: The
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:
The meaning of all these properties and their possible values can be seen from the demo sample. The The public void Draw(Graphics graphics) { ... }
To display the chart on the screen, 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;
Note that Points of InterestTo 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 Similarly, a user can override the 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 Copyright noticeYou are free to use this code and the accompanying DLL. Please include a reference to this web page in the list of credits. History
|
3D Pie Chart
最新推荐文章于 2023-09-10 02:01:48 发布