Pre loader

iOS & macOS Animating Line Chart

iOS & macOS charts - Examples

Please note! These examples are new to SciChart iOS v4 release! SciChart’s OpenGL ES and Metal iOS and Metal macOS Chart library ships with hundred of Objective-C and Swift iOS & macOS Chart Examples which you can browse, play with and view the source-code. All of this is possible with the new and improved SciChart iOS Examples Suite and demo application for Mac, which ships as part of the SciChart SDK.

Download Examples

This example demonstrates how to create a custom animation and have complete control over data appearing on the screen. It shows how to animate the last appended point to a Line Series in real-time.

The Swift and Objective-C source code for the iOS and macOS Animating Line Chart example is included below (Scroll down!).

Did you know that we have the source code for all our example available for free on Github?

Clone the SciChart.iOS.Examples from Github.

Also the SciChart iOS and Scichart macOS Trials contain the full source for the examples (link below).

DOWNLOAD THE IOS CHART EXAMPLES

AnimatingLineChartView.m
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// AnimatingLineChartView.m is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************

#import "AnimatingLineChartView.h"
#import <SciChart/SCIBaseRenderPassDataTransformation+Protected.h>
#import "SCDRandomUtil.h"
#import "SCDToolbarItem.h"
#import "SCDToolbarButtonsGroup.h"

@interface SCDAppendedPointTransformation : SCIBaseRenderPassDataTransformation<SCILineRenderPassData *>

- (instancetype)init;

@end

@implementation SCDAppendedPointTransformation {
    SCIFloatValues *_originalXCoordinates;
    SCIFloatValues *_originalYCoordinates;
}

- (instancetype)init {
    self = [super initWithRenderPassDataType:SCILineRenderPassData.class];
    if (self) {
        _originalXCoordinates = [SCIFloatValues new];
        _originalYCoordinates = [SCIFloatValues new];
    }
    return self;
}

- (void)saveOriginalData {
    if (!self.renderPassData.isValid) return;
    
    [SCITransformationHelpers copyDataFromSource:self.renderPassData.xCoords toDest:_originalXCoordinates];
    [SCITransformationHelpers copyDataFromSource:self.renderPassData.yCoords toDest:_originalYCoordinates];
}

- (void)applyTransformation {
    if (!self.renderPassData.isValid) return;

    id<ISCICoordinateCalculator> xCalculator = self.renderPassData.xCoordinateCalculator;
    id<ISCICoordinateCalculator> yCalculator = self.renderPassData.yCoordinateCalculator;

    NSInteger count = self.renderPassData.pointsCount;

    float firstXStart = [xCalculator getCoordinateFrom:0];
    float xStart = count <= 1 ? firstXStart : [_originalXCoordinates getValueAt:count - 2];
    float xFinish = [_originalXCoordinates getValueAt:count - 1];
    float additionalX = xStart + (xFinish - xStart) * self.currentTransformationValue;
    [self.renderPassData.xCoords set:additionalX at:count - 1];

    float firstYStart = [yCalculator getCoordinateFrom:0];
    float yStart = count <= 1 ? firstYStart : [_originalYCoordinates getValueAt:count - 2];
    float yFinish = [_originalYCoordinates getValueAt:count - 1];
    float additionalY = yStart + (yFinish - yStart) * self.currentTransformationValue;
    [self.renderPassData.yCoords set:additionalY at:count - 1];
}

- (void)discardTransformation {
    [SCITransformationHelpers copyDataFromSource:_originalXCoordinates toDest:self.renderPassData.xCoords];
    [SCITransformationHelpers copyDataFromSource:_originalYCoordinates toDest:self.renderPassData.yCoords];
}

- (void)onInternalRenderPassDataChanged {
    [self applyTransformation];
}

@end

static int const fifoCapacicty = 20;
static double const timeInterval = 1;
static double const animationDuration = 0.5;
static double const visibleRangeMax = 10;
static double const maxYValue = 100;

@implementation AnimatingLineChartView {
    SCIXyDataSeries *_dataSeries;
    SCIFastLineRenderableSeries *_rSeries;
    
    NSTimer *_timer;
    BOOL _isRunning;
    double _currentXValue;
}

- (Class)associatedType { return SCIChartSurface.class; }

- (BOOL)showDefaultModifiersInToolbar { return NO; }

- (NSArray<id<ISCDToolbarItem>> *)provideExampleSpecificToolbarItems {
    __weak typeof(self) wSelf = self;

    return @[[[SCDToolbarButtonsGroup alloc] initWithToolbarItems:@[
        [[SCDToolbarItem alloc] initWithTitle:@"Start" image:[SCIImage imageNamed:@"chart.play"] andAction:^{ self->_isRunning = YES; }],
        [[SCDToolbarItem alloc] initWithTitle:@"Pause" image:[SCIImage imageNamed:@"chart.pause"] andAction:^{ self->_isRunning = NO; }],
        [[SCDToolbarItem alloc] initWithTitle:@"Stop" image:[SCIImage imageNamed:@"chart.stop"] andAction:^{
            self->_isRunning = NO;
            [wSelf resetChart];
        }],
    ]]];
}

- (void)initExample {
    _dataSeries = [[SCIXyDataSeries alloc] initWithXType:SCIDataType_Double yType:SCIDataType_Double];
    _dataSeries.fifoCapacity = fifoCapacicty;
    
    _rSeries = [SCIFastLineRenderableSeries new];
    _rSeries.dataSeries = _dataSeries;
    _rSeries.strokeStyle = [[SCISolidPenStyle alloc] initWithColorCode:0xFF4083B7 thickness:6];
    
    _currentXValue = 0;
    
    id<ISCIAxis> xAxis = [SCINumericAxis new];
    xAxis.visibleRange = [[SCIDoubleRange alloc] initWithMin:-1 max:visibleRangeMax];
    
    id<ISCIAxis> yAxis = [SCINumericAxis new];
    yAxis.growBy = [[SCIDoubleRange alloc] initWithMin:0.0 max:0.1];
    yAxis.visibleRange = [[SCIDoubleRange alloc] initWithMin:0 max:maxYValue];
    
    [SCIUpdateSuspender usingWithSuspendable:self.surface withBlock:^{
        [self.surface.xAxes add:xAxis];
        [self.surface.yAxes add:yAxis];
        [self.surface.renderableSeries add:self->_rSeries];
    }];
    
    [self p_SCD_addPointAnimated];
    _timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(p_SCD_updateData) userInfo:nil repeats:YES];
#if TARGET_OS_OSX
    if (_timer) {
        [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
#endif
    _isRunning = YES;
}

- (void)p_SCD_updateData {
    if (!_isRunning) return;
    
    [self p_SCD_addPointAnimated];
}

- (void)p_SCD_addPointAnimated {
    [SCIUpdateSuspender usingWithSuspendable:self.surface withBlock:^{
        [self->_dataSeries appendX:@(self->_currentXValue) y:@(SCDRandomUtil.nextDouble * maxYValue)];
    }];
    
    [SCIAnimations animateSeries:_rSeries withTransformation:[SCDAppendedPointTransformation new] duration:animationDuration andEasingFunction:[SCICubicEase new]];
    
    _currentXValue += timeInterval;
    
    [self p_SCD_animateVisibleRangeIfNeeded];
}

- (void)p_SCD_animateVisibleRangeIfNeeded {
    if (_currentXValue > visibleRangeMax) {
        id<ISCIAxis> xAxis = [self.surface.xAxes itemAt:0];
        id<ISCIRange> newRange = [[SCIDoubleRange alloc] initWithMin:xAxis.visibleRange.minAsDouble + timeInterval max:xAxis.visibleRange.maxAsDouble + timeInterval];
        [xAxis animateVisibleRangeTo:newRange withDuration:animationDuration];
    }
}

- (void)resetChart {
    [SCIUpdateSuspender usingWithSuspendable:self.surface withBlock:^{
        [self->_dataSeries clear];
    }];
    
    _currentXValue = 0;
    id<ISCIAxis> xAxis = [self.surface.xAxes itemAt:0];
    [xAxis animateVisibleRangeTo:[[SCIDoubleRange alloc] initWithMin:-1 max:visibleRangeMax] withDuration:animationDuration];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    [_timer invalidate];
    _timer = nil;
}

@end
AnimatingLineChartView.swift
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// AnimatingLineChartView.swift is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************

import Foundation
import SciChart.Protected.SCIBaseRenderPassDataTransformation

class AnimatingLineChartView: SCDSingleChartViewController<SCIChartSurface> {
    
    private var timer: Timer?
    private var isRunning = true
    private var currentXValue: Double = 0
    
    private let fifoCapacity = 20
    private let timeInterval: TimeInterval = 1
    private let animationDuration: TimeInterval = 0.5
    private let visibleRangeMax: Double = 10
    private let maxYValue: Double = 100
    
    private let rSeries = SCIFastLineRenderableSeries()
    private let dataSeries = SCIXyDataSeries(xType: .double, yType: .double)
    
    override var associatedType: AnyClass { return SCIChartSurface.self }
    
    override var showDefaultModifiersInToolbar: Bool { false }
    
    override func provideExampleSpecificToolbarItems() -> [ISCDToolbarItem] {
        return [SCDToolbarButtonsGroup(toolbarItems: [
            SCDToolbarButton(title: "Start", image: SCIImage(named: "chart.play"), andAction: { [weak self] in self?.isRunning = true }),
            SCDToolbarButton(title: "Pause", image: SCIImage(named: "chart.pause"), andAction: { [weak self] in self?.isRunning = false }),
            SCDToolbarButton(title: "Stop", image: SCIImage(named: "chart.stop"), andAction: { [weak self] in
                self?.isRunning = false
                self?.resetChart()
            })
        ])]
    }
    
    override func initExample() {
        let xAxis = SCINumericAxis()
        xAxis.visibleRange = SCIDoubleRange(min: -1, max: Double(visibleRangeMax))
        
        let yAxis = SCINumericAxis()
        yAxis.growBy = SCIDoubleRange(min: 0.1, max: 0.1)
        yAxis.visibleRange = SCIDoubleRange(min: 0, max: maxYValue)
        
        dataSeries.fifoCapacity = fifoCapacity
        
        rSeries.dataSeries = dataSeries
        rSeries.strokeStyle = SCISolidPenStyle(color: 0xFF4083B7, thickness: 6)
        
        SCIUpdateSuspender.usingWith(surface) {
            self.surface.xAxes.add(xAxis)
            self.surface.yAxes.add(yAxis)
            self.surface.renderableSeries.add(self.rSeries)
        }
        
        addPointAnimated()
        timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(updateData), userInfo: nil, repeats: true)
        if let timer = timer {
            RunLoop.main.add(timer, forMode: .common)
        }
    }
    
    @objc func updateData() {
        if !isRunning { return }
            
        addPointAnimated()
    }
    
    private func addPointAnimated() {
        SCIUpdateSuspender.usingWith(surface) { [weak self] in
            guard let self = self else { return }
            
            self.dataSeries.append(x: self.currentXValue, y: SCDRandomUtil.nextDouble() * self.maxYValue)
        }
        
        SCIAnimations.animate(
            rSeries,
            with: AppendedPointTransformation(),
            duration: animationDuration,
            andEasingFunction: SCICubicEase()
        )
        
        currentXValue += timeInterval
        
        animateVisibleRangeIfNeeded()
    }
    
    private func animateVisibleRangeIfNeeded() {
        if currentXValue > visibleRangeMax {
            let xAxis = surface.xAxes.item(at: 0)
            let newRange = SCIDoubleRange(
                min: xAxis.visibleRange.minAsDouble + timeInterval,
                max: xAxis.visibleRange.maxAsDouble + timeInterval
            )
            xAxis.animateVisibleRange(to: newRange, withDuration: Float(animationDuration))
        }
    }
    
    private func resetChart() {
        SCIUpdateSuspender.usingWith(self.surface) {
            self.dataSeries.clear()
        }
        self.currentXValue = 0
        self.surface.xAxes.firstOrDefault()?.animateVisibleRange(
            to: SCIDoubleRange(min: -1, max: Double(self.visibleRangeMax)),
            withDuration: Float(self.animationDuration)
        )
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        timer?.invalidate()
        timer = nil
    }
    
    private class AppendedPointTransformation: SCIBaseRenderPassDataTransformation<SCILineRenderPassData> {
        private let originalXCoordinates = SCIFloatValues()
        private let originalYCoordinates = SCIFloatValues()
        
        init() {
            super.init(renderPassDataType: SCILineRenderPassData.self)
        }
        
        override func saveOriginalData() {
            guard let renderPassData = self.renderPassData, renderPassData.isValid else { return }
            
            SCITransformationHelpers.copyData(fromSource: renderPassData.xCoords, toDest: originalXCoordinates)
            SCITransformationHelpers.copyData(fromSource: renderPassData.yCoords, toDest: originalYCoordinates)
        }
        
        override func applyTransformation() {
            guard
                let renderPassData = self.renderPassData,
                    renderPassData.isValid,
                let xCalculator = renderPassData.xCoordinateCalculator,
                let yCalculator = renderPassData.yCoordinateCalculator
            else { return }
             
            let count = renderPassData.pointsCount
            
            let firstXStart = xCalculator.getCoordinate(0)
            let xStart = count <= 1 ? firstXStart : originalXCoordinates.getValueAt(count - 2)
            let xFinish = originalXCoordinates.getValueAt(count - 1)
            let additionalX = xStart + (xFinish - xStart) * currentTransformationValue
            renderPassData.xCoords.set(additionalX, at: count - 1)
            
            let firstYStart = yCalculator.getCoordinate(0)
            let yStart = count <= 1 ? firstYStart : originalYCoordinates.getValueAt(count - 2)
            let yFinish = originalYCoordinates.getValueAt(count - 1)
            let additionalY = yStart + (yFinish - yStart) * currentTransformationValue
            renderPassData.yCoords.set(additionalY, at: count - 1)
        }
        
        override func discardTransformation() {
            guard let renderPassData = self.renderPassData else { return }
            
            SCITransformationHelpers.copyData(fromSource: originalXCoordinates, toDest: renderPassData.xCoords)
            SCITransformationHelpers.copyData(fromSource: originalYCoordinates, toDest: renderPassData.yCoords)
        }
        
        override func onInternalRenderPassDataChanged() {
            applyTransformation()
        }
    }
}
Back to iOS & macOS charts Examples