Draw Your Own Pie Chart dynamically

In this tutorial, I’ll show you how to draw your own pie chart just using lines and a list of data items. The code itself is in C#, but the concept can be used on any platform that has a drawing library and ArcSegments.

You can view the entire solution in this GitHub Gist. Okay, let’s get started!

Setting Up

The first thing you’ll need to understand is how the platform’s drawing library makes arcs. In this code, I’ll be using RadPath from Telerik UI for Xamarin most libraries work the same way. RadPath lets you use a custom Geometry which has RadArcSegment object, which has helpful StartAngle and SweepAngle properties.

Let’s start with the data model, with simple Title and Value properties:

public class ChartDataPoint
{
    public string Title { get; set; }
    public double Value { get; set; }
}

Next, let’s create a list that is populated with some activities to represent a 24 hour period.

var dataPoints = new List<ChartDataPoint>
{
    new ChartDataPoint { Title = "Work", Value = 9 },
    new ChartDataPoint { Title = "Commute", Value = 1.5 },
    new ChartDataPoint { Title = "Leisure", Value = 6 },
    new ChartDataPoint { Title = "Sleep", Value = 7.5 },
};

Finally, to finish the setup, create a list of colors we can use for the pie slices.

var colors = new List<Color>
{
    Color.FromHex("#BAB65A"),
    Color.FromHex("#6196D1"),
    Color.FromHex("#3D4268"),
    Color.FromHex("#8A56E2"),
};

Part 1 – Creating the Slices

Now that we have some items, lets move on to generating and drawing the slices. First, we’ll create a container to put the pie and legend into. A Grid is convenient option because we can have two rows, one for the pie and one for the legend.

// Root container to hold the chart and any legend
var container = new Grid();
container.RowDefinitions.Add(new RowDefinition { Height = new GridLength(3, GridUnitType.Star) });
container.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

Now we can start calulating the slice sizes, to do this, we need a total value to get a percentage of that total. We also need a variable to hold the current position on the 360 degree arc where the slices are rendered.

// Sum up all the values to be displayed
var totalValue = dataPoints.Sum(d => d.Value);

// Variable to keep track of where each slice ended.
double currentPosition = 0;

Now for the guts of the operation. We need iterate over the data points and create the arc segments using the data point’s Value property. The code comments will guide you through what each line does, in a nustshell here’s the lifecycle

  • Calculate the data item’s percentage of the total sum
  • Use that percentage to get what percent of 360 degree pie that slice needs
  • Create the RadArcSegment using the current position for the StartAngle and the angle percentage as the SweepAngle
  • Construct the RadPath using the ArgSegment’s geometry and a color from the colors list
// Iterate over the data points to create slices.
for (int i = 0; i < dataPoints.Count; i++)
{
    // Determine the what percentage that data item's value is of the whole
    double slicePercentage = dataPoints[i].Value / totalValue;

    // Calculate the sweep angle using that percentage amount.
    double sweep = slicePercentage * 360;

    // Create the ArcSegment using the current position and sweep
    var segment = new RadArcSegment
    {
        Center = new Point(0.5, 0.5),
        Size = new Size(1, 1),
        StartAngle = currentPosition,
        SweepAngle = sweep,
    };

    // Important - Calculate the last segment's ending angle in order to have a valid start angle for the next loop.
    currentPosition = currentPosition + sweep - 360;

    // Prepare the required PathFigure and add the ArcSegment
    var figure = new RadPathFigure { StartPoint = new Point(0.5, 0.5) };
    figure.Segments.Add(segment);

    // Create the PathGeometry and add the PathFigure
    var geometry = new RadPathGeometry();
    geometry.Figures.Add(figure);

    // Construct the RadPath
    // - Select a Fill color from the brushes parameter (important: use a modulus to wrap to the beginning)
    // - Use the Geometry created from the value
    var slice = new RadPath
    {
        Fill = new RadSolidColorBrush(colors[i % colors.Count]),
        Geometry = geometry,
        HorizontalOptions = LayoutOptions.Center,
        VerticalOptions = LayoutOptions.Center,
        WidthRequest = 100,
        HeightRequest = 100,
        Margin = new Thickness(0, 20, 0, 0)
    };

    // This isn't necessary, but added for completion.
    Grid.SetRow(slice, 0);

    // Finally, add it to the container.
    container.Children.Add(slice);
}

At this point, you now have a full 360 degree chart, with colored slices for each of the data points representing their percentage of the whole.

Part 2 – Creating the Legend

The next phase of the operation is to create the legend. This needs to create text for each slice, as well as a marker that matches the same color of that slice. This could have been done in the same loop as creating the slice, but having a separate loop lets you decide to use a legend or not.

Again, we iterate over the data points. This time we use the Title property of the data point to create the text. To match the color, we use the same modulus to get index and assign it to a RadBorder that creates a thick bar underneath the text.

// Create a horizontal StackLayout to hold the legend items
var legendPanel = new StackLayout
{
    Orientation = StackOrientation.Horizontal,
    HorizontalOptions = LayoutOptions.Center,
    VerticalOptions = LayoutOptions.Center,
    Margin = new Thickness(0, 16, 0, 0),
    Spacing = 5
};

// Iterate over the data points and create a legend item with a matching color
for (int i = 0; i < dataPoints.Count; i++)
{
    // Use a RadBorder with only a bottom thickness and match the color to the slice
    var legendItem = new RadBorder
    {
        BorderColor = colors[i % colors.Count],
        BorderThickness = new Thickness(0, 0, 0, 2)
    };

    // Create a Label for each data point and use the Title property
    var label = new Label
    {
        Text = dataPoints[i].Title,
        FontSize = 12,
        Margin = new Thickness(0, 0, 0, 2),
        TextColor = Color.DimGray
    };

    legendItem.Content = label;

    legendPanel.Children.Add(legendItem);
}

// Insert the legend panel in the root container's 2nd row.
Grid.SetRow(legendPanel, 1);
container.Children.Add(legendPanel);

The last thing to do is add the entire container to the UI. In this example, I’m just setting the entire page’s content to the container Grid..

this.Content = container;

Wrapping Up

I hope this is useful for those times when you need ultimate control over rendering of a chart. If you need more complex setup, I recommend the RadPieChart itself, which is far more feature complete than drawing a few arcs ūüôā

Talk to the View


There are many scenarios where a UI component might not have a Command or bindable property available for certain features that you need to access from the view model. There is a simple, but powerful, thing you can do to allow this and not break MVVM or testing capability, leverage the Interface.

Scenario

Let’s imagine the scenario where we have a special ListView control that has a HighlightItem method that can only be called with a direct reference to that control. There isn’t a Command or DependencyProperty (aka BindableProperty in Xamarin.Forms) alternative, so it has to be done in the code behind.

To keep this simple, here’s the page XAML…

<Page x:Class="MyPage">
    <Page.DataContext>
        <MyPageViewModel x:Name="ViewModel" />
    </Page.DataContext>

    <SpecialListView x:Name="MyListView" />
</Page>

…and here is the page code behind.

public partial class MyPage : Page
{
    public MyPage()
    {
        InitializeComponent();

        // The only way to use HighlightItem method is in the code-behind
        MyListView.HighlightItem(itemToHighlight);
    }
}

As you can see, the HighlightItem method has to be used in the code behind because it has a reference to ‘MyListView’. However, you need to use it in the view model, like this:

public class MyPageViewModel
{
    private void PleaseHighlightItemNow()
    {
        // But, you need to highlight the item from view model
    }
}

Solution

One solution for this is to define an interface.

public interface IHighlightableView
{
    void Highlight(object item);
}

With this, you can add a property to the view model and invoke the method:

public class MyPageViewModel
{
    public IHighlightableView View { get; set; }

    private void PleaseHighlightItem()
    {
        // Call the method on the interface!
        View?.Highlight(itemToHighlight);
    }
}

Now, the interface can be implemented on the page to complete the circle

public partial class MyPage : Page, IHighlightableView
{
    public MyPage()
    {
        InitializeComponent();

        // Assign this page instance to the view model
        ViewModel.View = this;
    }

    private void Highlight(object itemToHighlight)
    {
       // Now you can directly use the UI component's method
        MyListView.HighlightItem(itemToHighlight);
    }
}

Summary

This is a simple approach that will work if you don’t want to (or are not allowed to) use an existing MVVM framework’s built-in DependencyInjection and IoC features. Enjoy!

Migrating from PCLStorage to .NET Standard 2.0

If you’re a Xamarin Forms developer, you’ve likely used PCLStorage (or other Dependency Service) to interact with the target platform’s file system in the portable class library’s code. However, since November 2017, Xamarin.Forms now uses a .NET Standard 2.0 class library and PCLStorage is no longer supported.

System.IO.File

This isn’t a problem because in .NET Core 2.0 you now have access to¬†System.IO.File’s GetFolderPath and SpecialFolder methods (see¬†System.IO.File in .NET Core 2.0 docs).

Thus, you can get a path to the app’s local folder (the one that the app can save to) without having to write a Dependency Service (which is what PCLStorage does) by using:

var localFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);

 

IMPORTANT: Make sure you always use Path.Combine to create the file path because different platforms use different path separators

var filePath = Path.Combine(LocalFolder, "notes.txt");

 

With a reference to the file path,  you can now access the file. For example reading the text:

var notes = File.ReadAllText(filePath);

Functional Demo

As a very simple example, I created a couple extension methods for Stream and Byte[] in the below FileExtensions class.

To test it, I created a ContentPage that downloads an image, saves it using the extension method and set a FileImageSource to confirm that it’s a file (instead of just using a StreamImageSource).

 

https://gist.github.com/LanceMcCarthy/693ab82aa498cadf75e9fe778c4242ad

 

Note that the extension methods are very basic and shouldn’t be used in production as-is (i.e. no defensive programming code).

Here is the result at runtime on UWP:

2018-04-19_1134

Denied?

So, you got an email from DVLUP stating that your challenge submission was denied.¬† Why were you denied? What did you miss?¬† I wanted to write this post to¬†share the top reasons for denial and how to fix it… 90% of these are due to the fact you didn’t edit your WMAppManifest file.

Here are the top offenders:

  • Not all Live Tiles Sizes
  • No animated Tiles
  • Not all WP8 resolutions

#1- You need to have all three tile sizes enabled for your app. Here is a list of the tile sizes from the MSDN Documentation. To rectify this problem, simply toggle the “Support for¬†large Tiles” property in your WMAppManifest file. See the image in answer #2 for more details.

Tile Sizes

#2- Your app failed because the tiles were static. You need to bring your pinned tiles to life with one of the tile templates. Below is an example, find more here in the MSDN Documentation:

Cyclic Template– This template rotates between 1 to 9 images for your pinned tile. To meet the challenge’s requirement you need to have at least 2 images. Here is a quick and easy way to setup a tile from an event handler:


CycleTileData cycleTile = new CycleTileData()
{
Title = &quot;DVLUP Rules&quot;;
Count = 2;
SmallBackgroundImage = new Uri(&quot;/Images/smallBackgroundImage.jpg&quot;, UriKind.Relative);

// An array of URIs will do the trick
CycleImages = new Uri[]
{ // You can have up to 9 images
new Uri(&quot;/Images/cycleImage1.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage2.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage3.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage4.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage5.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage6.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage7.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage8.jpg&quot;, UriKind.Relative),
new Uri(&quot;/Images/cycleImage9.jpg&quot;, UriKind.Relative),
}
};

Another way to setup the Cyclic Template is directly in your WMAppManifest file by assigning an image directly like this:

2012-12-13_1150

#3- Your app needs to support all three Windows Phone resolutions. This link will take you to the MSDN documentation on how to target different resolutions. Here is a screenshot of the new resolutions for Windows Phone from the docs and also my WMAppManifest file.

2012-12-13_1143

2012-12-13_0927

Great, now you’re armed with the information you need to resubmit your application. Update your app through DevCenter, once it’s live in the Store go back into DVLUP and resubmit the app to the challenge. If you have any questions, send me an email¬†to ext-lance.mccarthy(at)nokia(dot)com.