SciChart library has several built-in animations which you can use to animate your Renderable Series.
NOTE: Please refer to the Animations API article for more details.
Also, you can create a custom animation and have complete control over data appearing on the screen. This tutorial shows how to animate Y-Value changes of SCIStackedColumnRenderableSeries in real-time.
#import <SciChart/SCIBaseRenderPassDataTransformation+Protected.h>
@interface SCDUpdatedPointTransformation : SCIBaseRenderPassDataTransformation
- (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;
}
// this method is called before the animation starts
- (void)saveOriginalData {
if (!self.renderPassData.isValid) return;
// save initial yCoords and prevSeriesYCoords
[SCITransformationHelpers copyDataFromSource:self.renderPassData.yCoords toDest:_originalYCoordinates];
[SCITransformationHelpers copyDataFromSource:self.renderPassData.prevSeriesYCoords toDest:_originalPrevSeriesYCoordinates];
}
// this method is called multiple times in a loop while the animation is in progress.
- (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;
}
// calculate new values for a renderPassData based on original values and the current animator fraction
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];
}
}
// this method is called on clean up after animation end
- (void)discardTransformation {
// reset renderPassData to initial yCoords and prevSeriesYCoords
[SCITransformationHelpers copyDataFromSource:_originalYCoordinates toDest:self.renderPassData.yCoords];
[SCITransformationHelpers copyDataFromSource:_originalPrevSeriesYCoordinates toDest:self.renderPassData.prevSeriesYCoords];
}
- (void)onInternalRenderPassDataChanged {
[self applyTransformation];
}
- (void)onAnimationEnd {
[super onAnimationEnd];
// save start values for future animation
[SCITransformationHelpers copyDataFromSource:_originalYCoordinates toDest:_startYCoordinates];
[SCITransformationHelpers copyDataFromSource:_originalPrevSeriesYCoordinates toDest:_startPrevSeriesYCoordinates];
}
@end
import SciChart.Protected.SCIBaseRenderPassDataTransformation
class UpdatedPointTransformation: SCIBaseRenderPassDataTransformation {
private let startYCoordinates = SCIFloatValues()
private let startPrevSeriesYCoordinates = SCIFloatValues()
private let originalYCoordinates = SCIFloatValues()
private let originalPrevSeriesYCoordinates = SCIFloatValues()
init() {
super.init(renderPassDataType: SCIStackedColumnRenderPassData.self)
}
// this method is called before the animation starts
override func saveOriginalData() {
guard
let renderPassData = self.renderPassData,
renderPassData.isValid
else { return }
// save initial yCoords and prevSeriesYCoords
SCITransformationHelpers.copyData(fromSource: renderPassData.yCoords, toDest: originalYCoordinates)
SCITransformationHelpers.copyData(fromSource: renderPassData.prevSeriesYCoords, toDest: originalPrevSeriesYCoordinates)
}
// this method is called multiple times in a loop while the animation is in progress.
override func applyTransformation() {
guard
let renderPassData = self.renderPassData,
renderPassData.isValid
else { return }
// calculate new values for a renderPassData based on original values and the current animator fraction
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)
}
}
// this method is called on clean up after animation end
override func discardTransformation() {
guard let renderPassData = self.renderPassData else { return }
// reset renderPassData to initial yCoords and prevSeriesYCoords
SCITransformationHelpers.copyData(fromSource: originalYCoordinates, toDest: renderPassData.yCoords)
SCITransformationHelpers.copyData(fromSource: originalPrevSeriesYCoordinates, toDest: renderPassData.prevSeriesYCoords)
}
override func onInternalRenderPassDataChanged() {
applyTransformation()
}
override func onAnimationEnd() {
super.onAnimationEnd()
// save start values for future animation
SCITransformationHelpers.copyData(fromSource: originalYCoordinates, toDest: startYCoordinates)
SCITransformationHelpers.copyData(fromSource: originalPrevSeriesYCoordinates, toDest: startPrevSeriesYCoordinates)
}
}
Animate series
With the transformation created above, all we need to do is just animate our series.
It’s easily achievable with SCIAnimations APIs like below:
// Since we have two Renderable Series in our `SCIVerticallyStackedColumnsCollection`, we need to create separate animators for each series. Please refer to a complete example for more details.
SCIValueAnimator *_animator1 = [self p_SCD_createAnimatorForSeries:_rSeries1];
SCIValueAnimator *_animator2 = [self p_SCD_createAnimatorForSeries:_rSeries2];
- (SCIValueAnimator *)p_SCD_createAnimatorForSeries:(id)rSeries {
SCIValueAnimator *animator = [SCIAnimations createAnimatorForSeries:rSeries withTransformation:[SCDUpdatedPointTransformation new]];
animator.easingFunction = [SCICubicEase new];
return animator;
}
// this method is called in real time based on timer.
- (void)p_SCD_refreshData {
//cancel animators in case they are in progress
[_animator1 cancel];
[_animator2 cancel];
// update our Data Series
__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];
}
}];
// start animation
[_animator1 startWithDuration:animationDuration];
[_animator2 startWithDuration:animationDuration];
}
// Since we have two Renderable Series in our `SCIVerticallyStackedColumnsCollection`, we need to create separate animators for each series. Please refer to a complete example for more details.
var animator1: SCIValueAnimator = createAnimator(series: rSeries1)
var animator2: SCIValueAnimator = createAnimator(series: rSeries2)
// create animator for renderable series with our custom transformation
func createAnimator(series: ISCIRenderableSeries) -> SCIValueAnimator {
let animator = SCIAnimations.createAnimator(for: series, with: UpdatedPointTransformation())
animator.easingFunction = SCICubicEase()
return animator
}
// this method is called in real time based on timer.
func refreshData() {
//cancel animators in case they are in progress
animator1.cancel()
animator2.cancel()
// update our Data Series
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)
}
}
// start animation
animator1.start(withDuration: animationDuration)
animator2.start(withDuration: animationDuration)
}