Real-time charting with TeeChart for Xamarin.Android

To complete the zooming and panning functionality description in the multi-touch article, I should also speak about one last case which concerns real-time performance. Actually, this is not a zoom/pan style but the lack of it.  It’s the the TChart.Zoom.Style property value not covered on that article, ZoomStyles.None.

So, what does ZoomStyles.None consists of exactly?  It disables zooming and scrolling and its main priority is refreshing the chart at the highest rate possible for real-time purposes. This is achieved via multi-threading and forcing the chart to paint on the UI thread. For this to work the RunOnUiThread delegate need to be implemented in your real-time charting applications.

Before getting too abstract, let’s brake things into pieces. This article is based on the RealTimeCharting demo project shipped with TeeChart for Xamarin.Android evaluation and registered versions and also available at GitHub.  We will start setting up the chart with the minimum elements required to represent our data trying to add as little work for the CPU as possible and therefore get a more responsible application. As you can see in the code snippet below, this involves disabling: 3D, legend, gradient, walls, automatic axes range calculation, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Add the chart
tChart1 = new Steema.TeeChart.TChart(this);
tChart1.Aspect.View3D = false;
tChart1.Zoom.Style = Steema.TeeChart.ZoomStyles.None;
tChart1.Legend.Visible = false;
tChart1.Panel.Gradient.Visible = false;
tChart1.Walls.Back.Gradient.Visible = false;
tChart1.Walls.Back.Visible = false;
tChart1.Axes.Left.Grid.Visible = false;
tChart1.Axes.Bottom.Grid.Visible = false;
tChart1.Axes.Left.Automatic = false;
tChart1.Axes.Bottom.Automatic = false;
tChart1.Axes.Left.SetMinMax(MinValue, MaxValue);
tChart1.Axes.Bottom.SetMinMax(0, NumPoints);
tChart1.ClickSeries += new Steema.TeeChart.TChart.SeriesEventHandler(tChart1_ClickSeries);
 
//Left axis disabled for performance purposes.
tChart1.Axes.Left.Visible = false;
//Add the chart
tChart1 = new Steema.TeeChart.TChart(this);
tChart1.Aspect.View3D = false;
tChart1.Zoom.Style = Steema.TeeChart.ZoomStyles.None;
tChart1.Legend.Visible = false;
tChart1.Panel.Gradient.Visible = false;
tChart1.Walls.Back.Gradient.Visible = false;
tChart1.Walls.Back.Visible = false;
tChart1.Axes.Left.Grid.Visible = false;
tChart1.Axes.Bottom.Grid.Visible = false;
tChart1.Axes.Left.Automatic = false;
tChart1.Axes.Bottom.Automatic = false;
tChart1.Axes.Left.SetMinMax(MinValue, MaxValue);
tChart1.Axes.Bottom.SetMinMax(0, NumPoints);
tChart1.ClickSeries += new Steema.TeeChart.TChart.SeriesEventHandler(tChart1_ClickSeries);

//Left axis disabled for performance purposes.
tChart1.Axes.Left.Visible = false;

Besides setting the Zoom.Style to ZoomStyles.None, disabling some objects’ visibility and automatic axes range calculation, vertical axis is completely disabled to avoid it having to calculate labels or anything else that would require some precious computing time. Next thing to consider is the kind of chart style (we call it series in TeeChart) we will use for that kind of chart. This example uses a FastLine series, a specific line, reduced to the minimum expression for performance purposes.

1
2
3
var fastLine1 = new Steema.TeeChart.Styles.FastLine(tChart1.Chart);
fastLine1.FillSampleValues(NumPoints);
fastLine1.DrawAllPoints = false;
var fastLine1 = new Steema.TeeChart.Styles.FastLine(tChart1.Chart);
fastLine1.FillSampleValues(NumPoints);
fastLine1.DrawAllPoints = false;

It’s also worth mentioning the use of the DrawAllPoints property in FastLine series. This property controls how many points in a FastLine series will be displayed. When True (the default), all points are displayed. When set to False, it will only display points that have a different “X” position in screen pixels. So, when the series has several points that share the same X pixel position, but with different Y position, it will only display the first point (or use another chosen method via DrawAllPointsStyle property). When set to True (the default), only points that have a different X or a different Y pixel position are displayed. In some cases, setting DrawAllPoints can dramatically speed up displaying a FastLine series with lots lots of points. But, as not all points are displayed, the final output might not be as accurate.

After having explained that, I should only add that, in this project initialisation, a timer is being created to add data to the chart at a fixed continuous rate simulating a real-time data capturing environment.

1
2
3
4
timer1 = new System.Timers.Timer();
timer1.Elapsed += new System.Timers.ElapsedEventHandler(timer1_Elapsed);
timer1.Interval = 100;
timer1.Start();
timer1 = new System.Timers.Timer();
timer1.Elapsed += new System.Timers.ElapsedEventHandler(timer1_Elapsed);
timer1.Interval = 100;
timer1.Start();

The event that the timer triggers just implements RunOnUiThread delegate, which calls a method (AnimateSeries) that updates the series in the chart.

1
2
3
4
5
6
7
void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
  RunOnUiThread(delegate
  {
    AnimateSeries(tChart1);
  });
}
void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
  RunOnUiThread(delegate
  {
	AnimateSeries(tChart1);
  });
}

After the chart has been set up for work, the AnimateSeries method is where almost everything happens:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void AnimateSeries(Steema.TeeChart.TChart chart)
{
  var rnd = new Random();
  double newX, newY;
 
  tChart1.AutoRepaint = false;
 
  foreach (Steema.TeeChart.Styles.Series s in chart.Series)
  {
    // show only 50 points - delete the rest
    while (s.Count > NumPoints) s.Delete(0);
    if (s.Count > NumPoints) s.Delete(0);
    newX = s.XValues.Last + 1;
    newY = rnd.Next(MaxValue);
    if ((Math.Abs(newY) > MaxValue) || (Math.Abs(newY) < MinValue)) newY = 0.0;
    s.Add(newX, newY);
  }
 
  tChart1.Axes.Bottom.SetMinMax(tChart1.Axes.Bottom.Minimum + 1, tChart1.Axes.Bottom.Maximum + 1);
  
  tChart1.AutoRepaint = true;
  tChart1.Chart.Invalidate();
}
void AnimateSeries(Steema.TeeChart.TChart chart)
{
  var rnd = new Random();
  double newX, newY;

  tChart1.AutoRepaint = false;

  foreach (Steema.TeeChart.Styles.Series s in chart.Series)
  {
	// show only 50 points - delete the rest
	while (s.Count > NumPoints) s.Delete(0);
	if (s.Count > NumPoints) s.Delete(0);
	newX = s.XValues.Last + 1;
	newY = rnd.Next(MaxValue);
	if ((Math.Abs(newY) > MaxValue) || (Math.Abs(newY) < MinValue)) newY = 0.0;
	s.Add(newX, newY);
  }

  tChart1.Axes.Bottom.SetMinMax(tChart1.Axes.Bottom.Minimum + 1, tChart1.Axes.Bottom.Maximum + 1);
  
  tChart1.AutoRepaint = true;
  tChart1.Chart.Invalidate();
}

Besides generating some random data for the example chart, the most important thing here is the AutoRepaint property, which toggles chart repainting when new data is being added to the chart. To avoid the chart being repainted after every new point being added to it, we will disable it and will only enable it back, followed by a Chart.Invalidate() call, after all data has been added/removed from the series to force the chart being refreshed at this stage. In the meantime, if there are more points in the chart than those being specified by NumPoints constant, first points in the sequential range will be removed and new data will be added to the series in the same fashion. Finally, horizontal axis scale will be updated accordingly.

So we are done with it. In this article we have covered which are the basic TeeChart aspects you should consider modifying to get the most speed out of it. You probably know this is very important when handling large data volumes.

As mentioned at the beginning of the article, the complete example project is included with both TeeChart for Xamarin.Android evaluation and registered versions and also at the GitHub repository for the component. However, for clarity and completeness of purpose, here’s the complete code listing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System;
 
using Android.App;
using Android.OS;
using Android.Views;
using Android.Widget;
 
namespace RealTimeCharting
{
  [Activity(Label = "RealTimeCharting", MainLauncher = true, Icon = "@drawable/icon")]
  public class Activity1 : Activity
  {
    Steema.TeeChart.TChart tChart1;
    const int NumPoints = 50;
    const int MinValue = 0;
    const int MaxValue = 1000;
    System.Timers.Timer timer1;
 
    protected override void OnCreate(Bundle bundle)
    {
      base.OnCreate(bundle);
 
      // Set our view from the "main" layout resource
      SetContentView(Resource.Layout.Main);
 
      // Get our button from the layout resource,
      // and attach an event to it
      Button button = FindViewById<button>(Resource.Id.MyButton);
 
      button.Click += delegate 
      {
        timer1.Enabled = !timer1.Enabled;
        button.Text = (timer1.Enabled) ? Resources.GetString(Resource.String.Stop) : Resources.GetString(Resource.String.Start);
      };
 
      //Add the chart
      tChart1 = new Steema.TeeChart.TChart(this);
      tChart1.Aspect.View3D = false;
      tChart1.Zoom.Style = Steema.TeeChart.ZoomStyles.None;
      tChart1.Legend.Visible = false;
      tChart1.Panel.Gradient.Visible = false;
      tChart1.Walls.Back.Gradient.Visible = false;
      tChart1.Walls.Back.Visible = false;
      tChart1.Axes.Left.Grid.Visible = false;
      tChart1.Axes.Bottom.Grid.Visible = false;
      tChart1.Axes.Left.Automatic = false;
      tChart1.Axes.Bottom.Automatic = false;
      tChart1.Axes.Left.SetMinMax(MinValue, MaxValue);      
      tChart1.Axes.Bottom.SetMinMax(0, NumPoints);
      tChart1.ClickSeries += new Steema.TeeChart.TChart.SeriesEventHandler(tChart1_ClickSeries);
      
      //Left axis disabled for performance purposes.
      tChart1.Axes.Left.Visible = false;
 
      var fastLine1 = new Steema.TeeChart.Styles.FastLine(tChart1.Chart);
      fastLine1.FillSampleValues(NumPoints);
      fastLine1.DrawAllPoints = false;
 
      LinearLayout layout = FindViewById(Resource.Id.linearLayout1);
      layout.AddView(tChart1, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FillParent, 400));
 
      timer1 = new System.Timers.Timer();
      timer1.Elapsed += new System.Timers.ElapsedEventHandler(timer1_Elapsed);
      timer1.Interval = 100;
      timer1.Start();
    }
 
    void tChart1_ClickSeries(object sender, Steema.TeeChart.Styles.Series s, int valueIndex, MotionEvent e)
    {
      Toast
          .MakeText(this, "point " + valueIndex, ToastLength.Short)
          .Show();
    }    
 
    void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
      RunOnUiThread(delegate
      {
        AnimateSeries(tChart1);
      });
    }
 
    void AnimateSeries(Steema.TeeChart.TChart chart)
    {
      var rnd = new Random();
      double newX, newY;
 
      tChart1.AutoRepaint = false;
 
      foreach (Steema.TeeChart.Styles.Series s in chart.Series)
      {
        // show only 50 points - delete the rest
        while (s.Count > NumPoints) s.Delete(0);
        if (s.Count > NumPoints) s.Delete(0);
        newX = s.XValues.Last + 1;
        newY = rnd.Next(MaxValue);
        if ((Math.Abs(newY) > MaxValue) || (Math.Abs(newY) < MinValue)) newY = 0.0;
        s.Add(newX, newY);
      }
 
      tChart1.Axes.Bottom.SetMinMax(tChart1.Axes.Bottom.Minimum + 1, tChart1.Axes.Bottom.Maximum + 1);
      
      tChart1.AutoRepaint = true;
      tChart1.Chart.Invalidate();
    }
  }
 
}</button>
using System;

using Android.App;
using Android.OS;
using Android.Views;
using Android.Widget;

namespace RealTimeCharting
{
  [Activity(Label = "RealTimeCharting", MainLauncher = true, Icon = "@drawable/icon")]
  public class Activity1 : Activity
  {
    Steema.TeeChart.TChart tChart1;
    const int NumPoints = 50;
    const int MinValue = 0;
    const int MaxValue = 1000;
    System.Timers.Timer timer1;

    protected override void OnCreate(Bundle bundle)
    {
      base.OnCreate(bundle);

      // Set our view from the "main" layout resource
      SetContentView(Resource.Layout.Main);

      // Get our button from the layout resource,
      // and attach an event to it
      Button button = FindViewById<button>(Resource.Id.MyButton);

      button.Click += delegate 
      {
        timer1.Enabled = !timer1.Enabled;
        button.Text = (timer1.Enabled) ? Resources.GetString(Resource.String.Stop) : Resources.GetString(Resource.String.Start);
      };

      //Add the chart
      tChart1 = new Steema.TeeChart.TChart(this);
      tChart1.Aspect.View3D = false;
      tChart1.Zoom.Style = Steema.TeeChart.ZoomStyles.None;
      tChart1.Legend.Visible = false;
      tChart1.Panel.Gradient.Visible = false;
      tChart1.Walls.Back.Gradient.Visible = false;
      tChart1.Walls.Back.Visible = false;
      tChart1.Axes.Left.Grid.Visible = false;
      tChart1.Axes.Bottom.Grid.Visible = false;
      tChart1.Axes.Left.Automatic = false;
      tChart1.Axes.Bottom.Automatic = false;
      tChart1.Axes.Left.SetMinMax(MinValue, MaxValue);      
      tChart1.Axes.Bottom.SetMinMax(0, NumPoints);
      tChart1.ClickSeries += new Steema.TeeChart.TChart.SeriesEventHandler(tChart1_ClickSeries);
      
      //Left axis disabled for performance purposes.
      tChart1.Axes.Left.Visible = false;

      var fastLine1 = new Steema.TeeChart.Styles.FastLine(tChart1.Chart);
      fastLine1.FillSampleValues(NumPoints);
      fastLine1.DrawAllPoints = false;

      LinearLayout layout = FindViewById(Resource.Id.linearLayout1);
      layout.AddView(tChart1, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FillParent, 400));

      timer1 = new System.Timers.Timer();
      timer1.Elapsed += new System.Timers.ElapsedEventHandler(timer1_Elapsed);
      timer1.Interval = 100;
      timer1.Start();
    }

    void tChart1_ClickSeries(object sender, Steema.TeeChart.Styles.Series s, int valueIndex, MotionEvent e)
    {
      Toast
          .MakeText(this, "point " + valueIndex, ToastLength.Short)
          .Show();
    }    

    void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
      RunOnUiThread(delegate
      {
        AnimateSeries(tChart1);
      });
    }

    void AnimateSeries(Steema.TeeChart.TChart chart)
    {
      var rnd = new Random();
      double newX, newY;

      tChart1.AutoRepaint = false;

      foreach (Steema.TeeChart.Styles.Series s in chart.Series)
      {
        // show only 50 points - delete the rest
        while (s.Count > NumPoints) s.Delete(0);
        if (s.Count > NumPoints) s.Delete(0);
        newX = s.XValues.Last + 1;
        newY = rnd.Next(MaxValue);
        if ((Math.Abs(newY) > MaxValue) || (Math.Abs(newY) < MinValue)) newY = 0.0;
        s.Add(newX, newY);
      }

      tChart1.Axes.Bottom.SetMinMax(tChart1.Axes.Bottom.Minimum + 1, tChart1.Axes.Bottom.Maximum + 1);
      
      tChart1.AutoRepaint = true;
      tChart1.Chart.Invalidate();
    }
  }

}</button>

 

Leave a Reply

Your email address will not be published. Required fields are marked *