iOS Charting Documentation - SciChart
Tutorial 06 - Adding Realtime Updates

So far all the tutorials in this series have focused on static charts where the data doesn't change. Assuming you have completed  Tutorial 05 - Adding Tooltips and Legends  , we will now make some changes to update the data dynamically.

Tutorials Repository

As it was mentioned previously - we've created Git repository with all Tutorials. So you can clone/download the appropriate project you need!

Updating Data Values

SciChart has the concept of RenderableSeries and DataSeries. RenderableSeries present the data, while DataSeries hold the X,Y data and manage updates.
Our createDataSeries method currently looks like this:

createDataSeries()
Copy Code
      func createDataSeries(){
        // Init line data series
        lineDataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        lineDataSeries.seriesName = "line series"
        for i in 0..<500{
            lineDataSeries.appendX( SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1)))
        }
       
        // Init scatter data series
        scatterDataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        scatterDataSeries.seriesName = "scatter series"
        for i in 0..<500{
            scatterDataSeries.appendX( SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1)))
        }
    }

This creates some DataSeries and appends some static data.

Now let's animate it.

Firstly, we need to update our properties section with adding Timer and phase properties :

createDataSeries()
Copy Code
class ViewController: UIViewController {
    
    ...
    
    var timer: Timer?
    var phase = 0.0

 

We created a Timer property and will initialize it in the overriden viewWillAppear method.

viewWillAppear() and viewWillDisappear()
Copy Code
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
       
        if nil == timer{
            timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: updatingDataPoints)
        }
    }
   
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
       
        if nil != timer{
            timer?.invalidate()
            timer = nil
        }
    }

We have also overriden the viewWillDisappear method, and used it to invalidate the timer, which is not under ARC umbrella

The updatingDataPoints() method is where we're updating the data series and it looks as follow:

updatingDataPoints()
Copy Code
    func updatingDataPoints(timer:Timer){
       
        for i in 0..<500 {
            lineDataSeries.update(at: Int32(i), x: SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1 + phase)))
            scatterDataSeries.update(at: Int32(i), x: SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1 + phase)))
        }
        phase += 0.01
       
        chartSurface?.invalidateElement()
    }

To make things a little bit more readable, we will also make one small changes in viewDidLoad() to add some padding (growBy property) to the YAxis as well as to the XAxis. Also, let's set the chartView autoresizingMask property, so we can rotate simulator/device for a better user experience:

viewDidLoad()
Copy Code
     override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
       
        sciChartSurface = SCIChartSurface(frame: self.view.bounds)
        sciChartSurface?.translatesAutoresizingMaskIntoConstraints = true
        // Set the autoResizingMask property so the chart will fit the screen when we rotate the device
        sciChartSurface?.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        self.view.addSubview(sciChartSurface!)
       
        let xAxis = SCINumericAxis()
        xAxis.growBy = SCIDoubleRange(min: SCIGeneric(0.1), max: SCIGeneric(0.1))
        sciChartSurface?.xAxes.add(xAxis)
       
        let yAxis = SCINumericAxis()
        yAxis.growBy = SCIDoubleRange(min: SCIGeneric(0.1), max: SCIGeneric(0.1))
        sciChartSurface?.yAxes.add(yAxis)
       
        createDataSeries()
        createRenderableSeries()
        addModifiers()
    }

 

Now run the Application. You should see this!

 

Appending Data Values

As well as using DataSeries.Update, you can also use DataSeries.Append to add new data-values to a DataSeries. Make some changes to your updatingDataPoints() as follows:

updatingDataPoints()
Copy Code
    func updatingDataPoints(timer:Timer){
        
        let i = lineDataSeries.count()
        
        lineDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1 + phase)))
        scatterDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1 + phase)))
        
        phase += 0.01
        
        sciChartSurface?.zoomExtents()
    }

You have might noticed that we are also calling zoomExtents method, which zooms-to fit the chart. More information about this method you can find by visiting our Official Documentation Page

Now run the application again, you should now see the series growing larger as new data is appended.

 

 

Appending Data Values

What if you wanted to scroll as new data was appended? You have a few choices.

We're going to show you both techniques below.

Discarding Data when Scrolling using FifoCapacity

The most memory efficient way to achieve scrolling is to use DataSeries.FifoCapacity to set the maximum size of a DataSeries before old points are discarded.

DataSeries in FIFO mode act as a circular (first-in-first-out) buffer. Once the capacity is exceeded, old points are discarded.
You cannot zoom back to see the old points, once they are lost, they are lost. To use FifoCapacity, we adjust our createDataSeries() method as follows:

createDataSeries()
Copy Code
    func createDataSeries(){
        // Init line data series
        lineDataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        lineDataSeries.fifoCapacity = 500
        lineDataSeries.seriesName = "line series"
        
        // Init scatter data series
        scatterDataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        scatterDataSeries.fifoCapacity = 500
        scatterDataSeries.seriesName = "scatter series"
        
        for i in 0...500{
            lineDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1)))
            scatterDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1)))
        }
        
        i = Int(lineDataSeries.count())
    }

Take a look at the "i" property. This is new property we added for counting the last index of element we will be adding.
As a quick walkthrough we now create our XyDataSeries and set FifoCapacity = 500.
This tells SciChart after 500 points to start discarded old points (in a first-in-first-out fashion).

Now, the last thing to do is to update the method where we are adding new data - updatingDataPoints():

updatingDataPoints()
Copy Code
    func updatingDataPoints(timer:Timer){
       
        i += 1
       
        lineDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1 + phase)))
        scatterDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1 + phase)))
       
        phase += 0.01
       
        chartSurface?.zoomExtents()
    }

This should be the result when you run the application:

 

Preserving Old Data and Allowing Zooming

If you want to be able to retain all data on the chart, as well a zoom backwards and view old data, then you cannot use FifoCapacity.
Instead, we will use another technique. Undo the changes you made above. First, remove the FIFO capacity from your createDataSeries() method.

createDataSeries()
Copy Code
    func createDataSeries(){
        // Init line data series
        // Changed series type back to defaultType
        lineDataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        // REMOVE THIS LINE
        lineDataSeries.fifoCapacity = 500
        lineDataSeries.seriesName = "line series"
       
        // Init scatter data series
        // Changed series type back to defaultType
        scatterDataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        // REMOVE THIS LINE
        scatterDataSeries.fifoCapacity = 500
        scatterDataSeries.seriesName = "scatter series"
       
        for i in 0...500{
            lineDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1)))
            scatterDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1)))
        }
       
        i = Int(lineDataSeries.count())
    }

Next, we are going to write a method, which will manipulate the X axis's Visible Range property. This is the main thing right here - we are going to control the X axis Visible range property, e.g. when we do ZoomExtents or use the XAxisDrag modifier.

updatingDataPoints() - after all changes
Copy Code
    func updatingDataPoints(timer:Timer){
        
        i += 1
        
        // appending new data points into the line and scatter data series
        lineDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(sin(Double(i)*0.1 + phase)))
        scatterDataSeries.appendX(SCIGeneric(i), y: SCIGeneric(cos(Double(i)*0.1 + phase)))
        
        phase += 0.01
        
        let minIndex = lineDataSeries.count() - Int32(totalCapacity)
        let maxIndex = lineDataSeries.count() - 1
        
        let max = SCIGenericDouble(lineDataSeries.xValues().value(at: maxIndex))
        let min = SCIGenericDouble(lineDataSeries.xValues().value(at: minIndex))
        
        let visibleRange = sciChartSurface!.xAxes.item(at: 0).visibleRange as! SCIDoubleRange
        let vMin = SCIGenericDouble(visibleRange.min) + 1.0
        let vMax = SCIGenericDouble(visibleRange.max) + totalCapacity * 0.1 + 1.0
        
        // calculating new visible range to simulate the auto scrolling functionality
        if vMin < min && vMax > max{
            visibleRange.min = SCIGeneric(SCIGenericDouble(visibleRange.min) + 1.0)
            visibleRange.max = SCIGeneric(SCIGenericDouble(visibleRange.max) + 1.0)
        }
        
        // as usual - DON'T  forget to call invalidateElement method to update the visual part of SciChart
        sciChartSurface?.invalidateElement()
    }

The result should be like in the following gif: