Draw a pie chart with labels and annotations in C#

(My last pie slice example for a while, I promise.) The example Draw a labeled pie chart in C# explains how to draw a pie chart with labeled slices. This example adds annotations attached to the slices by lines. The following code shows how the program draws, labels, and annotates the slices.
// Draw a pie chart.
private static void DrawAnnotatedPieChart(Graphics gr, Rectangle ellipse_rect,
    Rectangle left_rect, Rectangle right_rect, float annotation_radius_scale,
    float initial_angle, Brush[] brushes, Pen[] pens, float[] values,
    string[] annotations, string label_format, Font label_font, Brush label_brush,
    Font annotation_font, Pen annotation_pen, Brush annotation_brush,
    Brush rectangle_brush, Pen rectangle_pen)
{
    ...
// Get the total of all angles.
    float total = values.Sum();

    // Draw the slices.
    float start_angle = initial_angle;
    for (int i = 0; i < values.Length; i++)
    {
        float sweep_angle = values[i] * 360f / total;

        // Fill and outline the pie slice.
        gr.FillPie(brushes[i % brushes.Length], ellipse_rect, start_angle, sweep_angle);
        gr.DrawPie(pens[i % pens.Length], ellipse_rect, start_angle, sweep_angle);

        start_angle += sweep_angle;
    }

    // Draw the rectangles if desired.
    if (rectangle_brush != null)
    {
        gr.FillRectangle(rectangle_brush, left_rect);
        gr.FillRectangle(rectangle_brush, right_rect);
    }
    if (rectangle_pen != null)
    {
        gr.DrawRectangle(rectangle_pen, left_rect);
        gr.DrawRectangle(rectangle_pen, right_rect);
    }

    // Label and annotate the slices.
    // We label the slices after drawing them all so one
    // slice doesn't cover the label on another very thin slice.
    using (StringFormat string_format = new StringFormat())
    {
        // Find the center of the rectangle.
        float cx = (ellipse_rect.Left + ellipse_rect.Right) / 2;
        float cy = (ellipse_rect.Top + ellipse_rect.Bottom) / 2;

        // Place the label about 2/3 of the way out to the edge.
        float radius = (ellipse_rect.Width + ellipse_rect.Height) / 2f * 0.33f;

        // Distances for annotation lines.
        float annotation_rx1 = ellipse_rect.Width / 2;
        float annotation_ry1 = ellipse_rect.Height / 2;
        float annotation_rx2 = annotation_rx1 * annotation_radius_scale;
        float annotation_ry2 = annotation_ry1 * annotation_radius_scale;

        start_angle = start_angle = initial_angle;
        for (int i = 0; i < values.Length; i++)
        {
            float sweep_angle = values[i] * 360f / total;

            // Label the slice.
            string_format.Alignment = StringAlignment.Center;
            string_format.LineAlignment = StringAlignment.Center;
            double label_angle = Math.PI * (start_angle + sweep_angle / 2) / 180;
            float x = cx + (float)(radius * Math.Cos(label_angle));
            float y = cy + (float)(radius * Math.Sin(label_angle));
            gr.DrawString(values[i].ToString(label_format),
                label_font, label_brush, x, y, string_format);

            // Draw a radial line to connect to the annotation.
            float x1 = cx + (float)(annotation_rx1 * Math.Cos(label_angle));
            float y1 = cy + (float)(annotation_rx1 * Math.Sin(label_angle));
            float x2 = cx + (float)(annotation_rx2 * Math.Cos(label_angle));
            float y2 = cy + (float)(annotation_rx2 * Math.Sin(label_angle));
            gr.DrawLine(annotation_pen, x1, y1, x2, y2);

            // Draw a horizontal line to the annotation.
            if (x2 < x1)
            {
                // Draw to the left.
                gr.DrawLine(annotation_pen, x2, y2, left_rect.Right, y2);

                // Draw the annotation right justified.
                string_format.Alignment = StringAlignment.Far;
                gr.DrawString(annotations[i], annotation_font, annotation_brush,
                    left_rect.Right, y2, string_format);
            }
            else
            {
                // Draw to the right.
                gr.DrawLine(annotation_pen, x2, y2, right_rect.Left, y2);

                // Draw the annotation left justified.
                string_format.Alignment = StringAlignment.Near;
                gr.DrawString(annotations[i], annotation_font, annotation_brush,
                    right_rect.Left, y2, string_format);
            }

            start_angle += sweep_angle;
        }
    }
}

See the previous examples for details about the code that draws and labels the slices. This example concentrates on the code that draws the radial and horizontal lines from the slices to their annotations.

Before it starts looping through the slices to draw their labels and annotations, the code calculates values annotation_rx1 and annotation_ry1. These give radii that the code can multiply by sines and cosines of angles to get a point on the edge of the pie slice ellipse.

The code also calculates annotation_rx2 and annotation_ry2, which it can use to get points along the same angle as the points on the ellipse's edge but slightly outside the ellipse.

The program then loops over the slices, labeling them as in the previous example. For each slice it also draws a short radial segment between the points calculated by using the annotation_rx1, annotation_ry1, annotation_rx2, and annotation_ry2 variables. Depending on whether the radial line points more to the left or right, the code draws a horizontal line to the left or right annotation area. It finishes the slice by drawing the slice's annotation text either right or left justified.

This example is a bit finicky and doesn't necessarily produce good results for some data sets. For example, if there are too many very thin slices on the top or bottom of the ellipse, then their annotation text may overlap. In that case you can try to draw lines to the annotations that spreads them apart but that would be tricky to do in a general way and might also produce ugly results. Alternatively you might be able to rearrange the values so thin ones are not placed together or you might be able to change the start angle for the first slice so the groups of thin slices are not at the top or bottom.

Finally, because of the way the FillPie and DrawPie methods measure angles, this example only works well if the ellipse is a circle. If the ellipse is highly elliptical, the slices' radial lines will not be in the centers of their slices and in extreme cases may not be in their slices at all.

Still this example produces a pretty nice picture much of the time.

   

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments
  • No comments exist for this post.
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.