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:
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