How to code a pie? A designer’s adventure in C#

Author
Ruud Renssen
Categories
Publish date

Last time, we looked at the maths of a pie chart and how to draw one with svg. Now that we know all that, we can start programming. For obvious reasons it's way more interesting to generate a pie chart with code instead of drawing it: it's less time consuming, you can show live data and you can add interaction with Javascript. First, we create a pie chart in C# (we'll worry about the interaction and Javascript later).

TL;DR, just give me the Github link

First things first

Before we start writing C# code, let's see what we're dealing with so we have a rough idea about the classes and methods we'll be needing. The pie chart consist of several slices and a container:

  • Every slice has a a value and a name
  • Every slice has a color
  • Every slice has a begin and end point on the outer circle
  • If put together in a container, the slices form a full circle

To make a pie chart out of it, we need to do/process the following:

  • Sort the slices from big to small, so the pie chart is easier to read
  • Calculate the total amount of all slices combined
  • Calculate the coordinates for every slice
  • Assign colours to the slices after they are sorted, so the pie chart looks nice
  • Create an SVG element
  • Draw the slices and enclose the slices name and value in the SVG element so it can be used later

Let's put these elements and processes in an overview so we can start writing our classes. We will create two classes: PieChart and Slice. While PieChart will handle most of the logic, Slice is instantiated for every slice (by the PieChart class) and will mainly be used for storing information about the slice. The outline of our classes will look like this:

PieChart

  • slices:List
  • total:double
    • PieChart(slices:List)
    • SumTotal(slices:List):double
    • SetCoordinates(slices:List):List
    • RenderChart(pieId:string , colors:string[] ):string
    • GetSlice(slice:Slice, color:string, id:int):string

Slice

  • sliceName:string
  • value:double
  • startX:int
  • startY:int
  • endX:int
  • endY:int
    • Slice(sliceName:string, value:double)

The Slice class

We'll start with the easy class: Slice. As mentioned earlier, we only use it to store information. Every slice has a name, a value and 2 coordinates. The name and value are mandatory parameters in the constructor: a slice must have a name and value and both are stored in the object on instantiation. Because the coordinates are determined by the ratio between the slices value and the total value, we create getters and setters so we can store and retrieve the coordinates later on. The class will look like this:

namespace Charts.PieChart
{
     public class Slice
     {
          public string slicename;
          public double value;

          public Slice(string slicename, double value)
          {
               this.slicename = slicename;
               this.value = value;
          }

          public double StartX { get; set; }
          public double EndX { get; set; }
          public double StartY { get; set; }
          public double EndY { get; set; }
     }
}

The PieChart class

This will be the place where most of the magic happens or in other words: it'll handle most of the processes described earlier. This is mainly because the PieChart class is the only class that actually "knows" something about the slices. Let me explain. A slice only has meaning in the bigger picture. For example, it's nice to know there were 12 visitors from Mars but it doesn't really tell us anything. Only when you know there were 22 visitors in total, you know that more than half came from Mars. Because the total amount has nothing to do with a single slice, we keep the logic concerned with the total value away from the Slice class.

Calculating the total value

The calculation of the coordinates will therefor take place in the PieChart class. To calculate the coordinates we must calculate the total amount, so lets write a method for this:

private double SumTotal(List<Slice> slices)
{
     double total = 0;

     foreach (Slice slice in slices)
     {
          //calculate the total value
          total += slice.value;
     }

     return total;
}

This method accepts a list of Slice objects as argument and returns the total amount of the slices in the list.

Calculating the coordinates

To calculate the coordinates we must do a few things first. We already know the total value so we now need to sort the slices and then calculate the coordinates based on the cumulative value. First, let's create a local variable for the cumulative value: cumulative. Then we sort the list of slices from the argument and store those in a new local variable sortedSlices. Now that we have a list of sorted slices, we can start calculating the coordinates for each slices. For the start values we leave the cumulative value alone and use the functions mentioned in Part I. After we set the start coordinate we can add the slices value to the cumulative value and calculate the end coordinate (code 3: line 19 & 20). We then return the sorted slices including the coordinates.

private List<Slice> SetCoordinates(List<Slice> unsortedSlices)
{
     double cumulative = 0;

     //sort the slices
     List<Slice> sortedSlices = unsortedSlices.OrderByDescending(slice => slice.value).ToList();

     //set the coordinates for end and start value
     foreach (Slice slice in sortedSlices)
     {
          //first, set the start position of the slide
          slice.StartX = Math.Round((Math.Cos((2 * (cumulative / this.total)) * Math.PI)) * 100);
          slice.StartY = Math.Round((Math.Sin((2 * (cumulative / this.total)) * Math.PI)) * 100);

          //update cumulative value between setting the start and end
          cumulative += slice.value;

          //now set the end position of the slide
          slice.EndX = Math.Round((Math.Cos((2 * (cumulative / this.total)) * Math.PI)) * 100);
          slice.EndY = Math.Round((Math.Sin((2 * (cumulative / this.total)) * Math.PI)) * 100);
     }

     return sortedSlices;
}

Creating a single slice

In this method we are going to actually draw a slice and it's probably the most complex method of its parent class; the rest is pretty straight forward. The method receives the Slice object, the colour and the ID as argument, makes an XElement out of it and returns it. Let's look at it in more detail. We start by creating the XElement and we'll call it xSlice. We also set the default direction flag to 0 (if you're puzzled by the direction flag, read the previous part for more information). We then go through some conditional statements; the first one is checking whether the total value is equal to the total value. If that's true, we don't want to use the path element, but the circle element (the slice covers the whole pie). A circle is defined by its origin (x and y coordinate) and the radius. If it's not equal, like in most cases, we need to check whether the slice value is more than half of the total value. If that's the case, we set the direction flag to 1, making sure the browser draws the long arc. After setting the direction flag we build the path element by defining the 'd' parameter. A detailed description on how to build of this parameter is discussed in the previous part. At the end of the method we add some other attributes like ID, class name and the colour. We also pass the name and value and place it in data attributes. In the end, the method returns the XElement just created. It'll be used in the RenderChart method which we'll discuss next.

private XElement GetSlice(Slice slice, string color, int id)
{
     XElement xmlSlice; //xml element for the slice
     int directionFlag = 0; //set the direction flag, default to 0

     if (slice.value == this.total)
     {
          //pie consist of only one slice: draw a circle
          xmlSlice = new XElement("circle");
          xmlSlice.Add(new XAttribute("cx", 0)); //x position origin circle
          xmlSlice.Add(new XAttribute("cy", 0)); //y position origin circle
          xmlSlice.Add(new XAttribute("r", 100)); //radius circle
     }
     else
     {
          //pie consist of only several slices: draw slices
          if (slice.value >= this.total / 2)
          {
               //if part is bigger than one half, set direction flag to 1
               directionFlag = 1;
          }

          xmlSlice = new XElement("path");

          string moveTo = "M0,0";
          string lineToStart = "L" + slice.StartX + "," + slice.StartY;
          string arc = "A100,100 0 " + directionFlag + ",1";
          string lineToEnd = slice.EndX + "," + slice.EndY;
          string closePath = "Z";

          string attribute = String.Format("{0} {1} {2} {3} {4}", moveTo, lineToStart, arc, lineToEnd, closePath);

          xmlSlice.Add(new XAttribute("d", attribute));
     }

     xmlSlice.Add(new XAttribute("id", id));
     xmlSlice.Add(new XAttribute("class", "slice"));
     xmlSlice.Add(new XAttribute("style", "fill:" + color));
     xmlSlice.Add(new XAttribute("data-name", slice.slicename));
     xmlSlice.Add(new XAttribute("data-value", slice.value));

     return xmlSlice;
}

Rendering the pie chart

The RenderChart is responsible for generating the SVG code. Because SVG is a actually an XML document, we'll be using XDocument to build the chart and we'll convert it to a string at the moment the method returns a value. But let's start with the top of the method. We first define a new XDocument for our SVG. In the parameters we immediately set the viewBox attribute and create a group for the slices and rotate it 90 degrees counter clockwise (for an in depth look, pleas read the previous part). After creating the SVG element we add an ID attribute to it. Now it's time to add the slices. To do this, we loop through the slices and call the GetSlice method for every Slice in the list and provide it with the relevant parameters: the slice, the next colour in the list and the id. The GetSlice method will handle all the complex things and the only thing we need to do now is return the XDocument object as a string so the browser can read it.

public string RenderChart(string pieId, string[] colours)
{
     //xml document for SVG
     XDocument svg;
     svg = new XDocument( new XElement("svg", new XAttribute("viewBox", "-100 -100 200 0"), new XAttribute("class", "pie-chart"), new XElement("g", new XAttribute("transform", "rotate(-90)"))));

     //add id attribute to svg element
     svg.Element("svg").Add(new XAttribute("id", pieId));

     //draw each slice
     int sliceCount = 0;
     foreach (Slice slice in this.slices)
     {
          svg.Element("svg").Element("g").Add(GetSlice(slice, colours[sliceCount], sliceCount));
          sliceCount++;
     }

     return svg.ToString();
}

A thing about colour

To draw the slices we loop through the list of slices and execute the GetSlice method for each slice in the list and add the return value to the string svgChart. The method accepts the slice, the colour and the ID as parameter. The ID is just the number of the slice object in the list, and slice the object itself. The colour is assigned after the sorting process is finished for both aesthetics and readability purposes and is therefor stored in a different list. In the image below it becomes clear why you want to assign the colour after the slices are sorted. The left versions not only looks nicer, it's also more clear because colours that look alike are placed next to each other so you can distinguish them more easily.

Implementation

All the classes are ready for use and all we need to do now is implement it in a Razor page. At the top you let the page know what classes you are using including the namespace, in this case Charts.PieChart. Then you need some data and colours. In this example I just made a list and an array, but in real life the data probably comes out of a database and the colours out of a style guide. After we have the data and the colours we create a new pie chart object and pass the data as argument. Now, all that is left is rendering the chart which is done by a single line: pie.RenderChart("pie-id", colours).

//Include at the top of your Razor file
@using PieChart
@{
     Layout = null;

     //define the colours
     string[] colours = { "#17324f", "#38869c", "#55b7ae", "#b7e0c4", "#f2f2dc", "#d6b598", "#b77462", "#9c3836", "#4f0e33" };

     //define the slices
     var slices = new List<Slice>
     {
          new Slice("Mars", 12),
          new Slice("Venus", 7),
          new Slice("Europa", 2),
          new Slice("Titan", 1)
     };

     PieChart pie = new PieChart(slices);
}

//To implement it in your HTML Razor file, insert:
@Html.Raw(pie.RenderChart("sample-pie", colours))

Improvement

Although we have a working pie, there is still a small problem. What happens when you have more slices than colours? Currently, we'll get an error because the RenderChart method will reference a non existing object in the list of colours. There are a few solutions to this problem:

Blame the lazy designer, he should have added more colours

Probably the most easy, but also the most risky solution. Yes, a designer should provide colours, but it's not always possible for the designer to know how many colours are needed when dealing with dynamic data.

Add a default set of colours to the PieChart class

By creating a very large default set of colours you're pretty sure there will be enough colours for every slice. This set will be added to the custom set of colours. When the loop reaches the last custom colour, it will proceed with the default colours.

Generate new colours based on the given set

Because all colours are actually three hexadecimal values (red, green and blue) you can calculate a variant for every single colour. Let's say we have a bright green in our set. You could create a darker version by subtracting the same amount of all three values. Or you can make it more blue by only subtracting form the red and green or adding to the blue channel. This gives you endless possibilities of creating new colours that fit within the style. However, there is a risk of creating colours that are very similar to each other and are therefor not very useful in a chart.

Adding interaction

Next time we are going to look at how to add interaction with Javascript. Hope to see you soon!

Back to top