Bar Charts and Xamarin.Forms

Intro

I recently experimented with bar charts for Xamarin.Forms. I tried to use Oxyplot, which is a popular open source tool for implementing graph charts. However, I ran into versioning issues between my Xamarin.Forms assemblies and Oxyplot. After an hour of frustration, I searched for an alternative approach. I soon found one. It was Syncfusion.

Bar Charts with Syncfusion

I got started learning about Synfusion’s support for bar charts with an awesome tutorial by James Montemagno.

I implemented the following app after reading his tutorial:

The app maintains a bar chart representation of successes and failures.

Assembly References

Make sure to add the proper assembly references described in the tutorial.

Android

lib/android/Syncfusion.SfChart.Android.dll

lib/android/Syncfusion.SfChart.XForms.dll

lib/android/Syncfusion.SfChart.XForms.Android.dll

Add new SfChartRenderer(); in the MainActivity.cs after Forms.Init (this, bundle);.

Portable Class Library lib/pcl/Syncfusion.SfChart.XForms.dll

Solution Explorer

My Solution Explorer is reflected below:

Stats_solutionExplorer

XAML

The XAML is below:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:ViewStats="clr-namespace:ViewStats;assembly=ViewStats" xmlns:chart="clr-namespace:Syncfusion.SfChart.XForms;assembly=Syncfusion.SfChart.XForms" x:Class="ViewStats.View">
 
	<ContentPage.BindingContext>
		<ViewStats:ViewModel />
	</ContentPage.BindingContext>
 
    <Grid Padding="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
 
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
 
        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Text="Success.Log" TextColor="Silver" />
        <Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding SuccessRate, StringFormat='{}{0:P2}'}" HorizontalOptions="Center" TextColor="Silver" FontSize="36" />
 
        <Label Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Ratio}" HorizontalOptions="Center" TextColor="Silver" FontSize="24" />
 
        <chart:SfChart x:Name="Chart" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" BackgroundColor="Transparent">
 
			<chart:SfChart.PrimaryAxis>
				<chart:CategoryAxis LabelRotationAngle = "-45">
					<chart:CategoryAxis.Title>
						<chart:ChartAxisTitle Text ="Date"/>
					</chart:CategoryAxis.Title>
				</chart:CategoryAxis>
			</chart:SfChart.PrimaryAxis>
 
			<chart:SfChart.SecondaryAxis>
				<chart:NumericalAxis>
					<chart:NumericalAxis.Title>
						<chart:ChartAxisTitle Text ="Result"/>
					</chart:NumericalAxis.Title>
				</chart:NumericalAxis>
			</chart:SfChart.SecondaryAxis>
 
			<chart:SfChart.Series>
 
				<chart:ColumnSeries ItemsSource="{Binding DataPoints}" XBindingPath="Date" YBindingPath="Successes" Label="Successes" DataMarkerPosition = "Center" EnableDataPointSelection = "false" Color="Green">
					
					<chart:ColumnSeries.DataMarker>
						<chart:ChartDataMarker>
							<chart:ChartDataMarker.LabelStyle>
								<chart:DataMarkerLabelStyle LabelPosition = "Center"/>
							</chart:ChartDataMarker.LabelStyle>
						</chart:ChartDataMarker>
					</chart:ColumnSeries.DataMarker>
				</chart:ColumnSeries>
 
				<chart:ColumnSeries ItemsSource="{Binding DataPoints}" XBindingPath="Date" YBindingPath="Failures" Label="Failures" EnableDataPointSelection = "false" Color="Red">
					
					<chart:ColumnSeries.DataMarker>
						<chart:ChartDataMarker>
							<chart:ChartDataMarker.LabelStyle>
								<chart:DataMarkerLabelStyle LabelPosition = "Center"/>
							</chart:ChartDataMarker.LabelStyle>
						</chart:ChartDataMarker>
					</chart:ColumnSeries.DataMarker>
				</chart:ColumnSeries>
 
			</chart:SfChart.Series>
 
		</chart:SfChart>
 
		<Button Grid.Row="4" Grid.Column="0" Text="Failed" BackgroundColor="Red" Command="{Binding Failed}" />
		<Button Grid.Row="4" Grid.Column="1" Text="Succeeded" BackgroundColor="Green" Command="{Binding Success}" />
	</Grid>
 
</ContentPage>

Implementing the ViewModel

I partition my view-models into three layers:

  • ViewModel
  • ViewModel.commands
  • ViewModel.internal

ViewModel

public partial class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
 
    public ViewModel()
    {
        ActivateCommands();
        var dataPoints = new DataPointsRepository().GetDataPoints();
        DataPoints = new ObservableCollection<DataPoint>(dataPoints);
    }
 
    ObservableCollection<DataPoint> _datapoints = new ObservableCollection<DataPoint>();
    public ObservableCollection<DataPoint> DataPoints
    {
        get { return _datapoints; }
        set
        {
            if (_datapoints != value)
            {
                _datapoints = value;
                OnPropertyChanged();
            }
        }
    }
 
    decimal _successRate = 0.00m;
    public decimal SuccessRate
    {
        get { return _successRate; }
        set
        {
            if (_successRate != value)
            {
                _successRate = value;
                OnPropertyChanged();
            }
        }
    }
 
    string _ratio = null;
    public string Ratio
    {
        get { return _ratio; }
        set
        {
            if (_ratio != value)
            {
                _ratio = value;
                OnPropertyChanged();
            }
        }
    }
 
    public void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

ViewModel.commands

ViewModel.commands
public partial class ViewModel
{
    void ActivateCommands()
    {
        Success = new DelegateCommand(obj => AddDataPoint(true));
        Failed = new DelegateCommand(obj => AddDataPoint(false));
        Undo = new DelegateCommand(UndoLast);
    }
 
    public DelegateCommand Success { get; private set; }
    public DelegateCommand Failed { get; private set; }
    public DelegateCommand Undo { get; private set; }
}

ViewModel.internal

public partial class ViewModel
{
    DataPointsRepository _repository = new DataPointsRepository();
 
    void AddDataPoint(bool isSuccessful)
    {
        var encounter = new Encounter() { IsSuccessful = isSuccessful };
        _repository.Add(encounter);
 
        var datapoints = _repository.GetDataPoints();
        DataPoints = new ObservableCollection<DataPoint>(datapoints);
 
        var totalCount = DataPoints.Sum(dp => dp.Successes + dp.Failures);
        var successCount = DataPoints.Sum(dp => dp.Successes);
        SuccessRate = (decimal)successCount / (decimal)totalCount;
        Ratio = $"{successCount} out of {totalCount}";
    }
 
    void UndoLast(object obj)
    {
        _repository.RemoveLast();
    }
}

Repository

I partition my repositories into two layers:

  • Repository
  • Repository.internal
public partial class DataPointsRepository
{
    public IEnumerable<Encounter> GetEncounters(DateTime date)
    {
        var compareDate = date.ToString(Constants.DATE_FORMAT);
 
        var result = _dateEncounters.FirstOrDefault(dp => dp.Key == compareDate);
 
        if (result.Value == null) return new HashSet<Encounter>();
 
        return result.Value;
    }
 
    public IEnumerable<Encounter> GetEncounters()
    {
        var list = new List<Encounter>();
        foreach (var encounter in _dateEncounters.Values)
        {
            list.AddRange(encounter);
        }
 
        return list;
    }
 
    public IEnumerable<DataPoint> GetDataPoints()
    {
        _datapointDictionary.Clear();
 
        var encounters = GetEncounters();
 
        foreach (var encounter in encounters)
        {
            DataPoint foundDatapoint = null;
            var exists = _datapointDictionary.TryGetValue(encounter.Date, out foundDatapoint);
 
            if (exists)
            {
                SetValue(encounter, foundDatapoint);
            }
            else
            {
                var datapoint = new DataPoint(encounter.Date);
 
                SetValue(encounter, datapoint);
                _datapointDictionary.Add(datapoint.Date, datapoint);
            }
        }
 
        return _datapointDictionary.Values;
    }
 
    public void Add(Encounter encounter)
    {
        var encounters = GetEncounters(DateTime.Now);
        var todaysEncounters = new HashSet<Encounter>(encounters);
        todaysEncounters.Add(encounter);
 
        _dateEncounters[DateTime.Now.ToString(Constants.DATE_FORMAT)] = todaysEncounters;
    }
}

Repository.internal

public partial class DataPointsRepository
{
    Dictionary<string, HashSet<Encounter>> _dateEncounters = new Dictionary<string, HashSet<Encounter>>();
    Dictionary<string, DataPoint> _datapointDictionary = new Dictionary<string, DataPoint>();
 
    void SetValue(Encounter encounter, DataPoint datapoint)
    {
        if (encounter.IsSuccessful) datapoint.Successes++;
        else datapoint.Failures++;
    }
}

Unit Tests

The following unit tests were written:

[TestClass]
public class _ViewModel
{
    [TestMethod]
    public void adding_success_and_failures_updates_datapoints()
    {
        // Setup
        var viewModel = new ViewModel();
 
        // Test
        viewModel.Failed.Execute(null);
        viewModel.Failed.Execute(null);
        viewModel.Success.Execute(null);
        viewModel.Success.Execute(null);
        viewModel.Success.Execute(null);
 
        // Verify
        var datapoint = viewModel.DataPoints.Single();
        var expected = datapoint.Successes == 3 &&
                       datapoint.Failures == 2 &&
                       viewModel.SuccessRate == .6m;
 
        Assert.IsTrue(expected);
    }
 
    [TestMethod]
    public void adding_success_updates_datapoints()
    {
        // Setup
        var viewModel = new ViewModel();
 
        // Test
        viewModel.Success.Execute(null);
 
        // Verify
        var datapoint = viewModel.DataPoints.Single();
        var expected = datapoint.Successes == 1 &&
                       datapoint.Failures == 0;
 
        Assert.IsTrue(expected);
    }
 
    [TestMethod]
    public void adding_failure_updates_datapoints()
    {
        // Setup
        var viewModel = new ViewModel();
 
        // Test
        viewModel.Failed.Execute(null);
 
        // Verify
        var datapoint = viewModel.DataPoints.Single();
        var expected = datapoint.Successes == 0 &&
                       datapoint.Failures == 1;
 
        Assert.IsTrue(expected);
    }
}

Conclusion

In conclusion, I have showed an example of most of the code involved for implementing a bar chart. Syncfusion provides a community edition to get started with graph charts integration.

NOTE:

Scott Nimrod is fascinated with Software Craftsmanship.

He loves responding to feedback and encourages people to share his articles.

He can be reached at scott.nimrod @ bizmonger.net

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: