Pre loader

iOS & macOS Animating Stacked Column 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 Y-Value changes of Stacked Column Renderable Series in real-time.

The Swift and Objective-C source code for the iOS and macOS Animating Stacked Column 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

AnimatingStackedColumnChartView.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
//
// AnimatingStackedColumnChartView.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 "AnimatingStackedColumnChartView.h"
#import <SciChart/SCIBaseRenderPassDataTransformation+Protected.h>
#import "SCDRandomUtil.h"
#import "SCDToolbarButton.h"
#import "SCDToolbarButtonsGroup.h"

@interface SCDUpdatedPointTransformation : SCIBaseRenderPassDataTransformation<SCIStackedColumnRenderPassData *>

- (instancetype)init;

@end

@implementation SCDUpdatedPointTransformation {
    SCIFloatValues *_startYCoordinates;
    SCIFloatValues *_startPrevSeriesYCoordinates;
    
    SCIFloatValues *_originalYCoordinates;
    SCIFloatValues *_originalPrevSeriesYCoordinates;
}

- (instancetype)init {
    self = [super initWithRenderPassDataType:SCIStackedColumnRenderPassData.class];
    if (self) {
        _startYCoordinates = [SCIFloatValues new];
        _startPrevSeriesYCoordinates = [SCIFloatValues new];
        
        _originalYCoordinates = [SCIFloatValues new];
        _originalPrevSeriesYCoordinates = [SCIFloatValues new];
    }
    return self;
}

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

- (void)applyTransformation {
    if (!self.renderPassData.isValid) return;
    
    NSInteger count = self.renderPassData.pointsCount;
    
    if (_startPrevSeriesYCoordinates.count != count ||
        _startYCoordinates.count != count ||
        _originalYCoordinates.count != count ||
        _originalPrevSeriesYCoordinates.count != count) {
        return;
    }
    
    float currentTransformationValue = self.currentTransformationValue;

    for (NSInteger i = 0; i < count; i++) {
        float startYCoord = [_startYCoordinates getValueAt:i];
        float originalYCoordinate = [_originalYCoordinates getValueAt:i];
        float additionalY = startYCoord + (originalYCoordinate - startYCoord) * currentTransformationValue;
        
        float startPrevSeriesYCoords = [_startPrevSeriesYCoordinates getValueAt:i];
        float originalPrevSeriesYCoordinate = [_originalPrevSeriesYCoordinates getValueAt:i];
        float additionalPrevSeriesY = startPrevSeriesYCoords + (originalPrevSeriesYCoordinate - startPrevSeriesYCoords) * currentTransformationValue;

        [self.renderPassData.yCoords set:additionalY at:i];
        [self.renderPassData.prevSeriesYCoords set:additionalPrevSeriesY at:i];
    }
}

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

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

- (void)onAnimationEnd {
    [super onAnimationEnd];
    
    [SCITransformationHelpers copyDataFromSource:_originalYCoordinates toDest:_startYCoordinates];
    [SCITransformationHelpers copyDataFromSource:_originalPrevSeriesYCoordinates toDest:_startPrevSeriesYCoordinates];
}

@end

static double const timeInterval = 1;
static double const animationDuration = 0.5;
static int const xValuesCount = 12;
static double const maxYValue = 50;

@interface AnimatingStackedColumnChartView ()
@property (nonatomic, strong) SCDToolbarButton *refreshDataButton;
@end

@implementation AnimatingStackedColumnChartView {
    SCIXyDataSeries *_dataSeries1;
    SCIXyDataSeries *_dataSeries2;
    
    SCIStackedColumnRenderableSeries *_rSeries1;
    SCIStackedColumnRenderableSeries *_rSeries2;
    
    SCIValueAnimator *_animator1;
    SCIValueAnimator *_animator2;
    
    NSTimer *_timer;
    BOOL _isRunning;
}

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

- (BOOL)showDefaultModifiersInToolbar { return NO; }

- (SCDToolbarButton *)refreshDataButton {
    if (!_refreshDataButton) {
        __weak typeof(self) wSelf = self;
        _refreshDataButton = [[SCDToolbarButton alloc] initWithTitle:@"Refresh data" image:nil andAction:^{
            if (self->_isRunning) {
                [self->_timer invalidate];
                [wSelf p_SCD_refreshData];
                self->_timer = [wSelf p_SCD_createTimer];
            } else {
                [wSelf p_SCD_refreshData];
            }
        }];
    }
    return _refreshDataButton;
}

- (NSArray<id<ISCDToolbarItem>> *)provideExampleSpecificToolbarItems {
    NSMutableArray<id<ISCDToolbarItem>> *items = [NSMutableArray arrayWithArray:@[[[SCDToolbarButtonsGroup alloc] initWithToolbarItems:@[
        [[SCDToolbarButton alloc] initWithTitle:@"Start" image:[SCIImage imageNamed:@"chart.play"] andAction:^{ self->_isRunning = YES; }],
        [[SCDToolbarButton alloc] initWithTitle:@"Pause" image:[SCIImage imageNamed:@"chart.pause"] andAction:^{ self->_isRunning = NO; }]
    ]]]];
    
#if TARGET_OS_OSX
    [items insertObject:self.refreshDataButton atIndex:0];
#endif
    
    return items;
}

#if TARGET_OS_IOS
- (SCIView *)providePanel {
    return [self.refreshDataButton createView];
}
#endif

- (void)initExample {
    _dataSeries1 = [[SCIXyDataSeries alloc] initWithXType:SCIDataType_Double yType:SCIDataType_Double];
    _dataSeries2 = [[SCIXyDataSeries alloc] initWithXType:SCIDataType_Double yType:SCIDataType_Double];
    
    _rSeries1 = [SCIStackedColumnRenderableSeries new];
    _rSeries2 = [SCIStackedColumnRenderableSeries new];
    
    _animator1 = [self p_SCD_createAnimatorForSeries:_rSeries1];
    _animator2 = [self p_SCD_createAnimatorForSeries:_rSeries2];
    
    [self p_SCD_configureRenderableSeries:_rSeries1 dataSeries:_dataSeries1 fillColor:0xff47bde6];
    [self p_SCD_configureRenderableSeries:_rSeries2 dataSeries:_dataSeries2 fillColor:0xffe8c667];
    
    [self p_SCD_fillWithInitialData];
    
    SCIVerticallyStackedColumnsCollection *columnCollection = [SCIVerticallyStackedColumnsCollection new];
    [columnCollection add:_rSeries1];
    [columnCollection add:_rSeries2];
    
    id<ISCIAxis> xAxis = [SCINumericAxis new];
    xAxis.growBy = [[SCIDoubleRange alloc] initWithMin:0.1 max:0.1];
    
    id<ISCIAxis> yAxis = [SCINumericAxis new];
    yAxis.growBy = [[SCIDoubleRange alloc] initWithMin:0.1 max:0.1];
    yAxis.visibleRange = [[SCIDoubleRange alloc] initWithMin:0 max:maxYValue * 2];

    [SCIUpdateSuspender usingWithSuspendable:self.surface withBlock:^{
        [self.surface.xAxes add:xAxis];
        [self.surface.yAxes add:yAxis];
        [self.surface.renderableSeries add:columnCollection];
    }];

    _timer = [self p_SCD_createTimer];
#if TARGET_OS_OSX
    if (_timer) {
        [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
#endif
    _isRunning = YES;
}

- (void)p_SCD_configureRenderableSeries:(SCIStackedColumnRenderableSeries *)rSeries dataSeries:(SCIXyDataSeries *)dataSeries fillColor:(unsigned int)fillColor {
    rSeries.dataSeries = dataSeries;
    rSeries.fillBrushStyle = [[SCISolidBrushStyle alloc] initWithColorCode:fillColor];
    rSeries.strokeStyle = [[SCISolidPenStyle alloc] initWithColorCode:fillColor thickness:1.0];
}

- (void)p_SCD_fillWithInitialData {
    __weak typeof(self) wSelf = self;
    [SCIUpdateSuspender usingWithSuspendable:self.surface withBlock:^{
        for (NSInteger i = 0, count = xValuesCount; i < count; i++) {
            [self->_dataSeries1 appendX:@(i) y:@([wSelf p_SCD_getRandomYValue])];
            [self->_dataSeries2 appendX:@(i) y:@([wSelf p_SCD_getRandomYValue])];
        }
    }];
    
    [self p_SCD_refreshData];
}

- (NSTimer *)p_SCD_createTimer {
    return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(p_SCD_updateData) userInfo:nil repeats:YES];
}

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

- (void)p_SCD_refreshData {
    [_animator1 cancel];
    [_animator2 cancel];
    
    __weak typeof(self) wSelf = self;
    [SCIUpdateSuspender usingWithSuspendable:self.surface withBlock:^{
        for (NSInteger i = 0, count = xValuesCount; i < count; i++) {
            [self->_dataSeries1 updateY:@([wSelf p_SCD_getRandomYValue]) at:i];
            [self->_dataSeries2 updateY:@([wSelf p_SCD_getRandomYValue]) at:i];
        }
    }];
    
    [_animator1 startWithDuration:animationDuration];
    [_animator2 startWithDuration:animationDuration];
}

- (double)p_SCD_getRandomYValue {
    return SCDRandomUtil.nextDouble * maxYValue;
}

- (SCIValueAnimator *)p_SCD_createAnimatorForSeries:(id<ISCIRenderableSeries>)rSeries {
    SCIValueAnimator *animator = [SCIAnimations createAnimatorForSeries:rSeries withTransformation:[SCDUpdatedPointTransformation new]];
    animator.easingFunction = [SCICubicEase new];
    
    return animator;
}

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

@end
AnimatingStackedColumnChartView.swift
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2019. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// AnimatingStackedColumnChartView.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 SciChart.Protected.SCIBaseRenderPassDataTransformation
import SciChart

class AnimatingStackedColumnChartView: SCDSingleChartWithTopPanelViewController<SCIChartSurface> {
    private var timer: Timer?
    private var isRunning = true
    private let timeInterval: TimeInterval = 1
    
    private let animationDuration: TimeInterval = 0.5
    private let xValuesCount = 12
    private let maxYValue: Double = 50
    
    private let dataSeries1 = SCIXyDataSeries(xType: .double, yType: .double)
    private let dataSeries2 = SCIXyDataSeries(xType: .double, yType: .double)
    
    private let rSeries1 = SCIStackedColumnRenderableSeries()
    private let rSeries2 = SCIStackedColumnRenderableSeries()
    
    private lazy var animator1: SCIValueAnimator = createAnimator(series: rSeries1)
    private lazy var animator2: SCIValueAnimator = createAnimator(series: rSeries2)
    
    private lazy var refreshDataButton: SCDToolbarButton = {
        SCDToolbarButton(title: "Refresh data", image: nil, andAction: { [weak self] in
            guard let self = self else { return }
            
            if self.isRunning {
                self.timer?.invalidate()
                self.refreshData()
                self.timer = self.createTimer()
            } else {
                self.refreshData()
            }
        })
    }()
    
    override func provideExampleSpecificToolbarItems() -> [ISCDToolbarItem] {
        var items: [ISCDToolbarItem] = [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 })
        ])]
        
#if os(OSX)
        items.insert(refreshDataButton, at: 0)
#endif
        return items
    }
    
#if os(iOS)
    override func providePanel() -> SCIView {
        return refreshDataButton.createView()
    }
#endif
    
    override var associatedType: AnyClass { return SCIChartSurface.self }
    
    override var showDefaultModifiersInToolbar: Bool { false }
    
    override func initExample() {
        configureRenderableSeries(rSeries: rSeries1, dataSeries: dataSeries1, fillColor: 0xff47bde6)
        configureRenderableSeries(rSeries: rSeries2, dataSeries: dataSeries2, fillColor: 0xffe8c667)
        
        fillWithInitialData()
                
        let columnCollection = SCIVerticallyStackedColumnsCollection()
        columnCollection.add(rSeries1)
        columnCollection.add(rSeries2)
        
        let xAxis = SCINumericAxis()
        xAxis.growBy = SCIDoubleRange(min: 0.1, max: 0.1)
        
        let yAxis = SCINumericAxis()
        yAxis.growBy = SCIDoubleRange(min: 0.1, max: 0.1)
        yAxis.visibleRange = SCIDoubleRange(min: 0, max: maxYValue * 2)
        
        SCIUpdateSuspender.usingWith(surface) {
            self.surface.xAxes.add(xAxis)
            self.surface.yAxes.add(yAxis)
            self.surface.renderableSeries.add(columnCollection)
        }
        
        timer = createTimer()
#if os(OSX)
        if let _timer = timer {
            RunLoop.main.add(_timer, forMode: .common)
        }
#endif
    }
    
    private func configureRenderableSeries(
        rSeries: SCIStackedColumnRenderableSeries,
        dataSeries: SCIXyDataSeries,
        fillColor: UInt32
    ) {
        rSeries.dataSeries = dataSeries
        rSeries.fillBrushStyle = SCISolidBrushStyle(color: fillColor)
        rSeries.strokeStyle = SCISolidPenStyle(color: fillColor, thickness: 1.0)
    }
    
    private func fillWithInitialData() {
        SCIUpdateSuspender.usingWith(surface) { [weak self] in
            guard let self = self else { return }
        
            for i in 0..<self.xValuesCount {
                self.dataSeries1.append(x: i, y: self.getRandomYValue())
                self.dataSeries2.append(x: i, y: self.getRandomYValue())
            }
        }
        
        refreshData()
    }
    
    private func createTimer() -> Timer {
        return Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(updateData), userInfo: nil, repeats: true)
    }
    
    @objc private func updateData() {
        if !isRunning { return }
        
        refreshData()
    }
    
    private func refreshData() {
        animator1.cancel()
        animator2.cancel()
        
        SCIUpdateSuspender.usingWith(surface) { [weak self] in
            guard let self = self else { return }
            
            for i in 0..<self.xValuesCount {
                self.dataSeries1.update(y: self.getRandomYValue(), at: i)
                self.dataSeries2.update(y: self.getRandomYValue(), at: i)
            }
        }
        
        animator1.start(withDuration: animationDuration)
        animator2.start(withDuration: animationDuration)
    }
    
    private func getRandomYValue() -> Double {
        SCDRandomUtil.nextDouble() * maxYValue
    }
    
    private func createAnimator(series: ISCIRenderableSeries) -> SCIValueAnimator {
        let animator = SCIAnimations.createAnimator(for: series, with: UpdatedPointTransformation())
        animator.easingFunction = SCICubicEase()
        
        return animator
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        timer?.invalidate()
        timer = nil
    }
    
    private class UpdatedPointTransformation: SCIBaseRenderPassDataTransformation<SCIStackedColumnRenderPassData> {
        private let startYCoordinates = SCIFloatValues()
        private let startPrevSeriesYCoordinates = SCIFloatValues()

        private let originalYCoordinates = SCIFloatValues()
        private let originalPrevSeriesYCoordinates = SCIFloatValues()
        
        init() {
            super.init(renderPassDataType: SCIStackedColumnRenderPassData.self)
        }
        
        override func saveOriginalData() {
            guard
                let renderPassData = self.renderPassData,
                renderPassData.isValid
            else { return }
            
            SCITransformationHelpers.copyData(fromSource: renderPassData.yCoords, toDest: originalYCoordinates)
            SCITransformationHelpers.copyData(fromSource: renderPassData.prevSeriesYCoords, toDest: originalPrevSeriesYCoordinates)
        }
        
        override func applyTransformation() {
            guard
                let renderPassData = self.renderPassData,
                    renderPassData.isValid
            else { return }
            
            let count = renderPassData.pointsCount
            
            if startPrevSeriesYCoordinates.count != count ||
                startYCoordinates.count != count ||
                originalYCoordinates.count != count ||
                originalPrevSeriesYCoordinates.count != count {
                return
            }

            for i in 0..<count {
                let startYCoord = startYCoordinates.getValueAt(i)
                let originalYCoordinate = originalYCoordinates.getValueAt(i)
                let additionalY = startYCoord + (originalYCoordinate - startYCoord) * currentTransformationValue
                
                let startPrevSeriesYCoords = startPrevSeriesYCoordinates.getValueAt(i)
                let originalPrevSeriesYCoordinate = originalPrevSeriesYCoordinates.getValueAt(i)
                let additionalPrevSeriesY = startPrevSeriesYCoords + (originalPrevSeriesYCoordinate - startPrevSeriesYCoords) * currentTransformationValue

                renderPassData.yCoords.set(additionalY, at: i)
                renderPassData.prevSeriesYCoords.set(additionalPrevSeriesY, at: i)
            }
        }
        
        override func discardTransformation() {
            guard let renderPassData = self.renderPassData else { return }
            
            SCITransformationHelpers.copyData(fromSource: originalYCoordinates, toDest: renderPassData.yCoords)
            SCITransformationHelpers.copyData(fromSource: originalPrevSeriesYCoordinates, toDest: renderPassData.prevSeriesYCoords)
        }
        
        override func onInternalRenderPassDataChanged() {
            applyTransformation()
        }
        
        override func onAnimationEnd() {
            super.onAnimationEnd()
            
            SCITransformationHelpers.copyData(fromSource: originalYCoordinates, toDest: startYCoordinates)
            SCITransformationHelpers.copyData(fromSource: originalPrevSeriesYCoordinates, toDest: startPrevSeriesYCoordinates)
        }
    }
}
Back to iOS & macOS charts Examples