Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

A Control to Display Pie (and Doughtnut) Charts with Highly Customizable Formatting

4.95/5 (12 votes)
14 Apr 2021CPOL3 min read 12.3K   526  
An extension of A control to display pie charts with highly customizable formatting from mattsj1984 that also renders doughnut.
In this article, you will learn about a control that can display pie (and doughnut) charts with formatting that is highly customizable.

Version .NET Framework (4) running:

Image 1

Version .NET 5 running (chart part of the window):

Image 2

Introduction

After more than a decade, I updated the original project extending the functionality to doughnuts as well.

This code and part of this article are heavily taken from the article: A control to display pie charts with highly customizable formatting made by mattsj1984 which was previously taken from this article: 3D Pie Chart - CodeProject by .

WinForms should be dead ... but it is still alive!

To give a future to this control, I've also made the .NET 5 version of it.

Background

I needed a doughnut control for a project I'm working on, and a colleague of mine said: "well, just take a pie and draw a cylinder on it" ... and a couple of days ago, everything started (no, this is not the approach I've taken...).

Basically, I had to move from the "simple" DrawPie Directive:

C#
public void DrawPie (System.Drawing.Pen pen, float x, float y, float width, 
                     float height, float startAngle, float sweepAngle);

to DrawArc(s):

C#
public void DrawArc (System.Drawing.Pen pen, System.Drawing.Rectangle rect, 
                     float startAngle, float sweepAngle);

DrawPie is a single command and therefore the slice (top or bottom) is dedigned in one step, the you have to calculate the edges to connect the two slices.

Using DrawArc, you have to design the outer ellipse (made of single arcs, like the pieslices) and the inner one with a "distance" that is the doughtnut width, connected together.

This "distance" is changing according to the inclination, in fact, you have to take care of the perspective (projection), at 90° is full width:

Image 3

at 0° the width is 0:

Image 4

This is a good example of how to use the Math.Sin(Double) function, it does exactly this: value 1 for 90° and 0 for 0°:

Image 5

in code:

C#
float bottomInternalY = ((bottomExternalY) + 
      (donutSize / 2) * (float)Math.Sin(Control.pieStyle.Inclination));
float topInternalY = ((topExternalY) + 
      (donutSize / 2) * (float)Math.Sin(Control.pieStyle.Inclination));
 

Then comes another tricky part: you have to connect multiple Arcs and Lines to obtain a GraphicPath.

To achive this (have a connected path), you have to take care of the angles of drawing, and, usually, the second arc has to be drawed with negative angles.

Connecting Arcs also means that you have to have the StartingPoints and the EndPoints (ref. Points of Interest).

Using the Code

The control is structured much like a standard Windows Forms control, in terms of use. The PieChart itself contains an Items property, which is of type PieChart.ItemCollection. This collection stores objects of type PieChartItem. Each PieChartItem is comprised of the Text, ToolTipText, Color, Offset, and Weight properties.

To create and use the control, use the Windows Forms designer in Visual Studio to add a PieChart to a form and play with the chart properties:

Image 6

Or you can use the control programmatically:

C#
PieChart pieControl = new PieChart();

//set as doughtnut if you wish
pieControl.DisplayDoughnut = true;

// add an item with weight 10, color Red,
// text "Text", and tool-tip text "ToolTipText"

pieControl.Items.Add(new PieChartItem(10, Color.Red, "Text", "ToolTipText"));

// add another item with weight 5, color Blue, text "Blue",
// tool-tip text "BlueTips", and an offset
// of 25 pixels from the center of the pie
pieControl.Items.Add(new PieChartItem(5, Color.Blue, "Blue", "BlueTips"));

// set the control to automatically
// fit the pie inside the control
pieControl.AutoSizePie = true;

// set the control to only show the
// text if it fits inside its slice
pieControl.TextDisplayMode = PieChart.TextDisplayTypes.FitOnly;

// set control border
pieControl.DrawBorder = false;

// set a graph title
pieControl.GraphTitle = null;

I've added the property ItemTextTemplate, where with key-words #VALUE, #PERCENTAGE and #ITEMTEXT , the relative information can be displayed, triggered via the boolean UseItemTextTemplate.

(The decimal places for the percentage are managed via PecentageDecimals property.)

This is the result for:

C#
pieControl.ItemTextTemplate = "Val: #VALUE - Perc: #PERCENTAGE - Text: #ITEMTEXT";
pieControl.UseItemTextTemplate = true;
pieControl.PecentageDecimals = 2;

is:

Image 7

The classic functionalities (+ enhancements) are still there:

Image 8

Points of Interest

Because I'm drawing arcs via GDI+, I needed to find "end points" and "starting point" of those arcs in order to create a path to fill or to draw (edges).

After searching, I went on a post (Getting End Point in ArcSegment with Start X/Y and Start+Sweep Angles) with the answer (for end point) from BlueRaja - Danny Pflughoeft.

I've made the helper class below that you can use with the signature of Graphics.DrawArc:

C#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ModernUI.Charting
{
    public static class ChartHelper
    {
        public static PointF GetStartingPoint
        (float x, float y, double width, double height, double startAngle, double sweepAngle)
        {
            return GetStartingPoint(new PointF(x, y), width, height, startAngle, sweepAngle);
        }

        public static PointF GetStartingPoint(PointF startPoint, double width, 
                      double height, double startAngle, double sweepAngle)
        {
            Point radius = new Point((int)width / 2, (int)height / 2);

            //Adjust the angles for the radius width/height
            startAngle = UnstretchAngle(startAngle, radius);

            //Calculate the final point
            return new PointF
            {
                X = (float)(Math.Cos(startAngle) + 1) * radius.X + startPoint.X,
                Y = (float)(Math.Sin(startAngle) + 1) * radius.Y + startPoint.Y,
            };
        }

        public static PointF GetFinalPoint
        (float x, float y, double width, double height, double startAngle, double sweepAngle)
        {
            return GetFinalPoint(new PointF(x, y), width, height, startAngle, sweepAngle);
        }

        public static PointF GetFinalPoint(PointF startPoint, double width, 
                      double height, double startAngle, double sweepAngle)
        {
            Point radius = new Point((int)width / 2, (int)height / 2);
            double endAngle = startAngle + sweepAngle;
            double sweepDirection = (sweepAngle < 0 ? -1 : 1);

            //Adjust the angles for the radius width/height
            startAngle = UnstretchAngle(startAngle, radius);
            endAngle = UnstretchAngle(endAngle, radius);

            //Determine how many times to add the sweep-angle to the start-angle
            double angleMultiplier = (double)Math.Floor(2 * sweepDirection * 
                                     (endAngle - startAngle) / Math.PI) + 1;
            angleMultiplier = Math.Min(angleMultiplier, 4);

            //Calculate the final resulting angle after sweeping
            double calculatedEndAngle = startAngle + angleMultiplier * 
                                        Math.PI / 2 * sweepDirection;
            calculatedEndAngle = sweepDirection * 
            Math.Min(sweepDirection * calculatedEndAngle, sweepDirection * endAngle);

            //Calculate the final point
            return new PointF
            {
                X = (float)(Math.Cos(calculatedEndAngle) + 1) * radius.X + startPoint.X,
                Y = (float)(Math.Sin(calculatedEndAngle) + 1) * radius.Y + startPoint.Y,
            };
        }

        private static double UnstretchAngle(double angle, Point radius)
        {
            double radians = Math.PI * angle / 180.0;

            if (Math.Abs(Math.Cos(radians)) < 0.00001 || 
                                  Math.Abs(Math.Sin(radians)) < 0.00001)
                return radians;

            double stretchedAngle = Math.Atan2(Math.Sin(radians) / Math.Abs(radius.Y), 
                                    Math.Cos(radians) / Math.Abs(radius.X));
            double rotationOffset = (double)Math.Round(radians / (2.0 * Math.PI), 
                                    MidpointRounding.AwayFromZero) -
                                    (double)Math.Round(stretchedAngle / (2.0 * Math.PI), 
                                    MidpointRounding.AwayFromZero);
            return stretchedAngle + rotationOffset * Math.PI * 2.0;
        }
    }
} 

History

  • 13th April, 2021: Revision 0: Original version
  • 14th April, 2021: Revision 1: Added the .NET5 Version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)