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)]
publicclassPopulationData : INotifyPropertyChanged
{
#region Constructors
publicPopulationData()
{
}
publicPopulationData(int age, int year, double males, double females)
{
this.Age = age;
this.Year = year;
this.Males = males;
this.Females = females;
Update();
}
publicPopulationData(int start, int end)
{
this.GenerationStart = start;
this.GenerationEnd = end;
}
#endregion
#region Json Properties
[JsonProperty(PropertyName = "females")]
publicdouble Females { get; set; }
[JsonProperty(PropertyName = "males")]
publicdouble Males { get; set; }
[JsonProperty(PropertyName = "age")]
publicint Age { get; set; }
[JsonProperty(PropertyName = "year")]
publicint Year { get; set; }
#endregion
publicevent PropertyChangedEventHandler PropertyChanged;
publicvoidOnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
#region Notify Properties
publicstring GenerationRange
{
get { return GenerationStart + "-" + GenerationEnd; }
}
publicdouble Total { get; privateset; }
///
/// Gets or sets population of females in MLN with PropertyChanged notification
///
publicdouble FemalesInMillions
{
get { return _FemalesInMillions; }
set { if (_FemalesInMillions == value) return; _FemalesInMillions = value; OnPropertyChanged("FemalesInMillions"); }
}
privatedouble _FemalesInMillions;
///
/// Gets or sets population of males in MLN with PropertyChanged notification
///
publicdouble MalesInMillions
{
get { return _MalesInMillions; }
set { if (_MalesInMillions == value) return; _MalesInMillions = value; OnPropertyChanged("MalesInMillions"); }
}
privatedouble _MalesInMillions;
#endregion
publicint BirthYear { get { return Year - Age; } }
publicint GenerationStart { get; set; }
publicint GenerationEnd { get; set; }
publicvoidUpdate()
{
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()
{
returnnew 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:
publicclassPopulationViewModel : INotifyPropertyChanged
{
publicevent PropertyChangedEventHandler PropertyChanged;
publicvoidOnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
if (name == "IsUpdatingData"&& this.IsUpdatingData)
{
Device.StartTimer(TimeSpan.FromMilliseconds(this.UpdateInterval), TimerCallback);
}
}
publicPopulationViewModel()
{
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"); }
}
privateint _UpdateInterval = 750;
publicint UpdateInterval
{
get { return _UpdateInterval; }
set { if (_UpdateInterval == value) return; _UpdateInterval = value; OnPropertyChanged("UpdateInterval"); }
}
privatebool _IsUpdatingData = true;
publicbool IsUpdatingData
{
get { return _IsUpdatingData; }
set { if (_IsUpdatingData == value) return; _IsUpdatingData = value; OnPropertyChanged("IsUpdatingData"); OnPropertyChanged("IsEditableYear"); }
}
publicbool IsEditableYear
{
get { return !this.IsUpdatingData; }
}
privateint _CurrentYear;
publicint 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
protectedint HistoryStop = 2100;
protectedint HistoryStart = 1950;
protectedint HistoryInterval = 10;
protected HttpClient Client = new HttpClient();
protectedstring Source = "http://api.population.io/1.0/population/{0}/United%20States/?format=json";
publicasync 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;
}
privateasyncvoidGetHistory()
{
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);
}
privateboolTimerCallback()
{
CurrentYear += HistoryInterval;
return IsUpdatingData;
}
privatevoidUpdateData()
{
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:XamDataChartTitle="USA Population by Age"
TitleFontSize="12"TitleTopMargin="10"
TitleTextColor="Gray"
GridMode="BeforeSeries"PlotAreaBackground="#4DE3E3E3"
Legend="{x:Reference Legend}">
<ig:XamDataChart.Axes>
<ig:CategoryYAxisx: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:NumericXAxisx: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:BarSeriesItemsSource="{Binding PopulationByAge}"
XAxis="{x:Reference BarXAxis}"
YAxis="{x:Reference BarYAxis}"
TransitionDuration="200"
Brush="#86009DFF"Outline="#86009DFF"
Thickness="0"Title="Males"
ValueMemberPath="MalesInMillions" />
<ig:BarSeriesItemsSource="{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:XamDataChartGrid.Row="1"
GridMode="BehindSeries"
Title="USA Population by Generation"
TitleFontSize="12"TitleTopMargin="10"
TitleTextColor="Gray">
<ig:XamDataChart.Axes>
<ig:CategoryAngleAxisx:Name="AngleAxis"
MajorStroke="#BB808080"
MajorStrokeThickness=".5"
TickLength="5"TickStroke="#BB808080"
TickStrokeThickness="1"
ItemsSource="{Binding PopulationByGen}"
Label="GenerationRange" />
<ig:NumericRadiusAxisx: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:RadialPieSeriesItemsSource="{Binding PopulationByGen}"
AngleAxis="{x:Reference AngleAxis}"
ValueAxis="{x:Reference RadiusAxis}"
TransitionDuration="200"
Brush="#A1C159F7"Outline="#A1C159F7"Thickness="0"
ValueMemberPath="FemalesInMillions" />
<ig:RadialPieSeriesItemsSource="{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:Legendx: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