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:

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

## 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 Julijan Sribar.

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:

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

to `DrawArc`

(s):

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:

at 0° the width is 0:

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°:

in code:

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:

Or you can use the control programmatically:

PieChart pieControl = new PieChart();
pieControl.DisplayDoughnut = true;
pieControl.Items.Add(new PieChartItem(10, Color.Red, "Text", "ToolTipText"));
pieControl.Items.Add(new PieChartItem(5, Color.Blue, "Blue", "BlueTips"));
pieControl.AutoSizePie = true;
pieControl.TextDisplayMode = PieChart.TextDisplayTypes.FitOnly;
pieControl.DrawBorder = false;
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:

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

is:

The *classic *functionalities (+ enhancements) are still there:

## 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`

:

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);
startAngle = UnstretchAngle(startAngle, radius);
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);
startAngle = UnstretchAngle(startAngle, radius);
endAngle = UnstretchAngle(endAngle, radius);
double angleMultiplier = (double)Math.Floor(2 * sweepDirection *
(endAngle - startAngle) / Math.PI) + 1;
angleMultiplier = Math.Min(angleMultiplier, 4);
double calculatedEndAngle = startAngle + angleMultiplier *
Math.PI / 2 * sweepDirection;
calculatedEndAngle = sweepDirection *
Math.Min(sweepDirection * calculatedEndAngle, sweepDirection * endAngle);
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

- 13
^{th} April, 2021: Revision 0: Original version - 14
^{th} April, 2021: Revision 1: Added the .NET5 Version