Animating Multiple Data Sources in Xamarin.Forms Data Chart

MARTIN TRELA / Tuesday, August 8, 2017

Introduction

Have you ever wanted to create a dashboard application or learn how to visualize large data sets? If so, this blog post will show you how to create an application that will visualize the U.S. population over time by animating changes of population grouped by age in a pyramid chart, as well as population grouped by generation in a radial chart.

Note that the above animation does not represent actual charts animation, where data animation is faster and it has smooth frame interpolation between data updates.

Dashboard applications are a great data visualization tools for presenting large and complex data sets. You can build these types of applications using UI controls that consume data and display them in easy-to-understand views. The XamDataChart is highly flexible, cross-platform control that you can configure and use with any combination of 60 build-in views to visualize a wide range of data types in Xamarin.Forms applications. This control is part of Ultimate UI for Xamarin and you can try it for free.

Running Application

You can get the full source code for the application that we'll be building in this blog post from this GitHub repository. Once you open the application, you'll need to make sure you have our Trial or RTM Nuget packages in your local repository and restore the Nuget packages for Infragistics components.

Instructions

The following sections will provide instructions on creating a dashboard application using the XamDataChart control.

1. Create Data Model

First, we need to implement a data model that will store demographic information about population such as age, gender, and generation range. The PopulationData class is an example of such a data model that utilizes JSON attributes to de-serialize data from JSON strings.

[JsonObject(MemberSerialization.OptIn)]
public class PopulationData : INotifyPropertyChanged
{
    #region Constructors
    public PopulationData()
    
{
    }
    public PopulationData(int age, int year, double males, double females)
    
{
        this.Age = age;
        this.Year = year;
        this.Males = males;
        this.Females = females;
        Update();
    }

    public PopulationData(int start, int end)
    
{
        this.GenerationStart = start;
        this.GenerationEnd = end;
    }
    #endregion
      
    #region Json Properties
    [JsonProperty(PropertyName = "females")]
    public double Females { get; set; }
    [JsonProperty(PropertyName = "males")]
    public double Males { get; set; }
    [JsonProperty(PropertyName = "age")]
    public int Age { get; set; }
    [JsonProperty(PropertyName = "year")]
    public int Year { get; set; }
    #endregion

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string name)
    
{
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }

    #region Notify Properties
    public string GenerationRange
    {
        get { return GenerationStart + "-" + GenerationEnd; }
    }
    public double Total { get; private set; }

    ///
    /// Gets or sets population of females in MLN with PropertyChanged notification
    ///
    public double FemalesInMillions
    {
        get { return _FemalesInMillions; }
        set { if (_FemalesInMillions == value) return; _FemalesInMillions = value; OnPropertyChanged("FemalesInMillions"); }
    }
    private double _FemalesInMillions;

    ///
    /// Gets or sets population of males in MLN with PropertyChanged notification
    ///
    public double MalesInMillions
    {
        get { return _MalesInMillions; }
        set { if (_MalesInMillions == value) return; _MalesInMillions = value; OnPropertyChanged("MalesInMillions"); }
    }
    private double _MalesInMillions;
    #endregion

    public int BirthYear { get { return Year - Age; } }
    public int GenerationStart { get; set; }
    public int GenerationEnd { get; set; }

    public void Update()
    
{
        if (double.IsNaN(Males) || double.IsNaN(Females)) return;

        Total = Males + Females;
        // converting males to negative values to plot them as opposite values to females
        MalesInMillions = -Math.Round(Males / 1000000, 1);
        FemalesInMillions = Math.Round(Females / 1000000, 1);
        OnPropertyChanged("Total");
    }
    public PopulationData Clone()
    
{
        return new PopulationData(Age, Year, Males, Females);
    }
}
 

Note that this data model implements the INotifyPropertyChanged interface, which ensures that changes to property values will raise a PropertyChanged event and thus inform the Data Chart control to animate those changes.

2. Create View Model

Next, we need a view model to connect to the www.api.population.io service, retrieve population data sets, and finally de-serialize JSON strings into a list of PopulationData objects:

public class PopulationViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string name)
    
{
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

        if (name == "IsUpdatingData" && this.IsUpdatingData)
        {
            Device.StartTimer(TimeSpan.FromMilliseconds(this.UpdateInterval), TimerCallback);
        }
    }
    public PopulationViewModel()
    
{
        PopulationLookup = new Dictionary<int, List>();

        // initialize population with default values for each age of population
        PopulationByAge = new List();
        for (var age = 0; age <= span="" class="hljs-number" style='color: #d19a66;' data-mce-style='color: #d19a66;'>100; age++)
        {
            PopulationByAge.Add(new PopulationData(age, HistoryStart, 0, 0));
        }

        // initialize population with default values for each generation of population
        PopulationByGen = new List();
        PopulationByGen.Add(new PopulationData(1820, 1900));
        PopulationByGen.Add(new PopulationData(1900, 1930));
        PopulationByGen.Add(new PopulationData(1930, 1945));
        PopulationByGen.Add(new PopulationData(1945, 1965));
        PopulationByGen.Add(new PopulationData(1965, 1980));
        PopulationByGen.Add(new PopulationData(1980, 2000));
        PopulationByGen.Add(new PopulationData(2000, 2025));
        PopulationByGen.Add(new PopulationData(2025, 2050));
        PopulationByGen.Add(new PopulationData(2050, 2075));
        PopulationByGen.Add(new PopulationData(2075, 2100));

        GetHistory();
    }
    protected Dictionary<int, List> PopulationLookup;

    private List _PopulationByAge;
    public List PopulationByAge
    {
        get { return _PopulationByAge; }
        set { if (_PopulationByAge == value) return; _PopulationByAge = value; OnPropertyChanged("PopulationByAge"); }
    }
    private List _PopulationByGen;
    public List PopulationByGen
    {
        get { return _PopulationByGen; }
        set { if (_PopulationByGen == value) return; _PopulationByGen = value; OnPropertyChanged("PopulationByGen"); }
    }
    private int _UpdateInterval = 750;
    public int UpdateInterval
    {
        get { return _UpdateInterval; }
        set { if (_UpdateInterval == value) return; _UpdateInterval = value; OnPropertyChanged("UpdateInterval"); }
    }
    private bool _IsUpdatingData = true;
    public bool IsUpdatingData
    {
        get { return _IsUpdatingData; }
        set { if (_IsUpdatingData == value) return; _IsUpdatingData = value; OnPropertyChanged("IsUpdatingData"); OnPropertyChanged("IsEditableYear"); }
    }

    public bool IsEditableYear
    {
        get { return !this.IsUpdatingData; }
    }
    private int _CurrentYear;
    public int CurrentYear
    {
        get { return _CurrentYear; }
        set
        {
            if (value == _CurrentYear) return;
            if (value > HistoryStop) value = HistoryStart;
            if (value < HistoryStart) value = HistoryStart;
            if (PopulationLookup.ContainsKey(value))
            {
                _CurrentYear = value;
                OnPropertyChanged("CurrentYear");
                UpdateData();
            }
        }
    }
    // values controlling range of data retrieved from population service
    protected int HistoryStop = 2100;
    protected int HistoryStart = 1950;
    protected int HistoryInterval = 10;
    protected HttpClient Client = new HttpClient();
    protected string Source = "http://api.population.io/1.0/population/{0}/United%20States/?format=json";

    public async Task<List> GetData(int year)
    {
        var url = string.Format(Source, year);
        var str = await Client.GetStringAsync(url);
        var population = await Task.Run(
            () => JsonConvert.DeserializeObject<List>(str));

        foreach (var item in population)
        {
            item.Update();
        }
        return population;
    }
    private async void GetHistory()
    
{
        for (int year = HistoryStart; year <= historystop="" year="" historyinterval="" br="">        {
            var data = await GetData(year);
                
            PopulationLookup.Add(year, data);
        }
        CurrentYear = 1950;
        Device.StartTimer(new TimeSpan(0, 0, 0, 0, 200), TimerCallback);
    }
    private bool TimerCallback()
    
{
        CurrentYear += HistoryInterval;
        return IsUpdatingData;
    }
    private void UpdateData()
    
{
        for (int i = 0; i < PopulationLookup[CurrentYear].Count; i++)
        {
            PopulationByAge[i].Age = PopulationLookup[CurrentYear][i].Age;
            PopulationByAge[i].Year = PopulationLookup[CurrentYear][i].Year;
            PopulationByAge[i].Males = PopulationLookup[CurrentYear][i].Males;
            PopulationByAge[i].Females = PopulationLookup[CurrentYear][i].Females;
            PopulationByAge[i].Update();
        }
        foreach (var generation in PopulationByGen)
        {
            var males = 0.0;
            var females = 0.0;
            foreach (var population in PopulationLookup[CurrentYear])
            {
                if (population.BirthYear > generation.GenerationStart &&
                    population.BirthYear <= generation="" generationend="" br="">                {
                    females += population.Females;
                    males += population.Males;
                }
            }
            generation.MalesInMillions = males / 1000000;
            generation.FemalesInMillions = females / 1000000;
        }
    }
}

3. Create Pyramid Chart

With the back-end implemented, we can move to the fun part of visualizing and animating data. The XamDataChart control supports over 60 types of series. You can create a pyramid chart using two BarSeries views that are stacked next to each other to show population of the U.S. between ages 0 and 100.

<ig:XamDataChart Title="USA Population by Age"
                 TitleFontSize="12" TitleTopMargin="10"
                 TitleTextColor="Gray"
                 GridMode="BeforeSeries" PlotAreaBackground="#4DE3E3E3"
                 Legend="{x:Reference Legend}">

    <ig:XamDataChart.Axes>
                <ig:CategoryYAxis x:Name="BarYAxis"
                                  Gap="0.5"
                                  Overlap="1"
                                  MajorStroke="#BB808080" Stroke="#BB808080"
                                  MajorStrokeThickness="0" StrokeThickness="0"
                                  TickLength="5"
                                  TickStroke="#BB808080"
                                  TickStrokeThickness="1"
                                  ItemsSource="{Binding PopulationByAge}"
                                  Label="Age" />

                <ig:NumericXAxis x:Name="BarXAxis"
                                 MajorStroke="#BB808080" Stroke="#BB808080"
                                 MajorStrokeThickness="1" StrokeThickness="1"
                                 TickLength="5" TickStroke="#BB808080"
                                 TickStrokeThickness="1"
                                 MinimumValue="-5"
                                 MaximumValue="5"
                                 Interval="1" />

            ig:XamDataChart.Axes>
    <ig:XamDataChart.Series>
        <ig:BarSeries ItemsSource="{Binding PopulationByAge}"
                      XAxis="{x:Reference BarXAxis}"
                      YAxis="{x:Reference BarYAxis}"
                      TransitionDuration="200"
                      Brush="#86009DFF" Outline="#86009DFF"
                      Thickness="0" Title="Males"
                      ValueMemberPath="MalesInMillions" />

        <ig:BarSeries ItemsSource="{Binding PopulationByAge}"
                      XAxis="{x:Reference BarXAxis}"
                      YAxis="{x:Reference BarYAxis}"
                      TransitionDuration="200"
                      Brush="#A1C159F7" Outline="#A1C159F7"
                      Thickness="0" Title="Females"
                      ValueMemberPath="FemalesInMillions" />

    ig:XamDataChart.Series>
ig:XamDataChart>

When implemented, the pyramid chart will display the difference between male and female populations grouped by their age, and animate changes in data over time.

Note that you can control the duration of data animation by setting the TransitionDuration property on the individual BarSeries. However, this duration should always be smaller or equal to the time interval at which the view model is updating data, otherwise, data animation will not be smooth.

4. Create Radial Chart

The XamDataChart control can also display grouped data points around a center point of the plot area using one of the RadialSeries types, as demonstrated in the following code snippet:

<ig:XamDataChart Grid.Row="1"
                 GridMode="BehindSeries"
                 Title="USA Population by Generation"
                 TitleFontSize="12" TitleTopMargin="10"
                 TitleTextColor="Gray">

    <ig:XamDataChart.Axes>
        <ig:CategoryAngleAxis x:Name="AngleAxis"
                              MajorStroke="#BB808080"
                              MajorStrokeThickness=".5"
                              TickLength="5" TickStroke="#BB808080"
                              TickStrokeThickness="1"
                              ItemsSource="{Binding PopulationByGen}"
                              Label="GenerationRange" />

        <ig:NumericRadiusAxis x:Name="RadiusAxis"
                              MajorStroke="#BB808080"
                              MajorStrokeThickness=".5"
                              TickLength="5" TickStroke="#BB808080"
                              TickStrokeThickness="1"
                              MinimumValue="0" Interval="40"
                              MaximumValue="80"
                              InnerRadiusExtentScale="0.2"
                              RadiusExtentScale="0.7" />

    ig:XamDataChart.Axes>
    <ig:XamDataChart.Series>
        <ig:RadialPieSeries ItemsSource="{Binding PopulationByGen}"
                            AngleAxis="{x:Reference AngleAxis}"
                            ValueAxis="{x:Reference RadiusAxis}"
                            TransitionDuration="200"
                            Brush="#A1C159F7" Outline="#A1C159F7" Thickness="0"
                            ValueMemberPath="FemalesInMillions" />

        <ig:RadialPieSeries ItemsSource="{Binding PopulationByGen}"
                            AngleAxis="{x:Reference AngleAxis}"
                            ValueAxis="{x:Reference RadiusAxis}"
                            TransitionDuration="200"
                            Brush="#86009DFF" Outline="#86009DFF" Thickness="0"
                            ValueMemberPath="MalesInMillions" />

    ig:XamDataChart.Series>
ig:XamDataChart>

When implemented, the radial chart will display the difference between male and female populations grouped by their birthday, and animate changes in data over time.

5. Create Legend

No chart would be complete without a legend that identifies the data sets. The Legend is a simple control that we can overlay over the XamDataChart control or place in any location outside of the chart:

<ig:Legend x:Name="Legend" Grid.Row="1" Margin="5"
           VerticalOptions="StartAndExpand"
           HorizontalOptions="EndAndExpand" />

Final Thoughts

The above code snippets show the most important elements of implementing a dashboard application that visualizes and animates the U.S. population over time. You can download the full source code for this application from this GitHub repository. I hope you found this blog post interesting and got inspired to create your own dashboard applications using the Ultimate UI for Xamarin product.

Happy coding,

Martin