Pre loader

iOS & macOS Vital Signs ECG/EKG Medical Demo

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

The Vital Signs demo showcases how to use SciChart iOS Charts in a medical context.

Four channels of data are simulated, showing how SciChart iOS can be used to draw real-time, high performance ECG/EKG charts & graphs to monitor blood pressure, SPO2 blood oxygen, and volumetric flow enabling you to create medical apps using an iPad or iPhone device.

SciChart helps you shortcut development of medical applications by providing rich, real time, high performance & reliable iOS charts for use in Vital-signs monitors, blood pressure monitors, Electro Cardiogram, medical Ventilators, patient monitors, digital stethoscopes and more.

If you are creating an app that needs to visualize body temperature, pulse rate, respiration rate, blood pressure, or similar, choose SciChart to shortcut your development time & get to market faster with our well-tested, reliable iOS Chart library.

The Swift and Objective-C source code for the iOS and macOS Multiple Axis Demo 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

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

class DefaultVitalSignsDataProvider: DataProviderBase<VitalSignsData> {
    // 1. Heart rate or pulse rate (ECG HR)
    // 2. Blood Pressure (NI BP)
    // 3. Blood Volume (SV ml)
    // 4. Blood Oxygenation (SPo2)
    
    let SAMPLE_RATE: Double = 800
    let ECG_TRACES = "EcgTraces.csv"
    
    private var currentIndex = 0
    private var totalIndex = 0
    private var isATrace = false
    
    let xValues = SCIDoubleValues()
    let ecgHeartRate = SCIDoubleValues()
    let bloodPressure = SCIDoubleValues()
    let bloodVolume = SCIDoubleValues()
    let bloodOxygenation = SCIDoubleValues()
    
    init() {
        super.init(dispatchTimeInterval: .microseconds(1000))
        
        do {
            let rawData = try String.init(contentsOfFile: SCDDataManager.getBundleFilePath(from: ECG_TRACES), encoding: .utf8)
            let lines = rawData.components(separatedBy: "\n")
            for i in 0 ..< lines.count - 1 {
                let split = lines[i].components(separatedBy: ",") as [NSString]
                
                xValues.add(split[0].doubleValue)
                ecgHeartRate.add(split[1].doubleValue)
                bloodPressure.add(split[2].doubleValue)
                bloodVolume.add(split[3].doubleValue)
                bloodOxygenation.add(split[4].doubleValue)
            }
        } catch {
            print("Load ECG Error: Failed to load ECG. \(error.localizedDescription)")
        }
    }
    
    override func onNext() -> VitalSignsData {
        if currentIndex >= xValues.count {
            currentIndex = 0;
        }

        let time = (Double(totalIndex) / SAMPLE_RATE).truncatingRemainder(dividingBy: 10)
        let ecgHeartRate = self.ecgHeartRate.getValueAt(currentIndex)
        let bloodPressure = self.bloodPressure.getValueAt(currentIndex)
        let bloodVolume = self.bloodVolume.getValueAt(currentIndex)
        let bloodOxygenation = self.bloodOxygenation.getValueAt(currentIndex)
        
        let data = VitalSignsData(xValue: time,
                                  ecgHeartRate: ecgHeartRate,
                                  bloodPressure: bloodPressure,
                                  bloodVolume: bloodVolume,
                                  bloodOxygenation: bloodOxygenation,
                                  isATrace: isATrace)

        currentIndex += 1
        totalIndex += 1

        if totalIndex % 8000 == 0 {
            isATrace = !isATrace
        }
        
        return data;
    }    
}

struct VitalSignsData {
    let xValue: Double
    let ecgHeartRate: Double
    let bloodPressure: Double
    let bloodVolume: Double
    let bloodOxygenation: Double
    let isATrace: Bool
}
EcgDataBatch.swift
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// EcgDataBatch.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

class EcgDataBatch {
    let xValues = SCIDoubleValues()
    
    let ecgHeartRateValuesA = SCIDoubleValues()
    let bloodPressureValuesA = SCIDoubleValues()
    let bloodVolumeValuesA = SCIDoubleValues()
    let bloodOxygenationA = SCIDoubleValues()
    
    let ecgHeartRateValuesB = SCIDoubleValues()
    let bloodPressureValuesB = SCIDoubleValues()
    let bloodVolumeValuesB = SCIDoubleValues()
    let bloodOxygenationB = SCIDoubleValues()
    
    var lastVitalSignsData: VitalSignsData!
    
    func updateData(_ vitalSignsDataList: [VitalSignsData]) {
        xValues.clear()
        ecgHeartRateValuesA.clear()
        ecgHeartRateValuesB.clear()
        bloodPressureValuesA.clear()
        bloodPressureValuesB.clear()
        bloodVolumeValuesA.clear()
        bloodVolumeValuesB.clear()
        bloodOxygenationA.clear()
        bloodOxygenationB.clear()
        
        let count = vitalSignsDataList.count
        for i in 0 ..< count {
            let vitalSignsData = vitalSignsDataList[i]
            xValues.add(vitalSignsData.xValue)
            
            if vitalSignsData.isATrace {
                ecgHeartRateValuesA.add(vitalSignsData.ecgHeartRate)
                bloodPressureValuesA.add(vitalSignsData.bloodPressure)
                bloodVolumeValuesA.add(vitalSignsData.bloodVolume)
                bloodOxygenationA.add(vitalSignsData.bloodOxygenation)

                ecgHeartRateValuesB.add(Double.nan)
                bloodPressureValuesB.add(Double.nan)
                bloodVolumeValuesB.add(Double.nan)
                bloodOxygenationB.add(Double.nan)
            } else {
                ecgHeartRateValuesB.add(vitalSignsData.ecgHeartRate)
                bloodPressureValuesB.add(vitalSignsData.bloodPressure)
                bloodVolumeValuesB.add(vitalSignsData.bloodVolume)
                bloodOxygenationB.add(vitalSignsData.bloodOxygenation)

                ecgHeartRateValuesA.add(Double.nan)
                bloodPressureValuesA.add(Double.nan)
                bloodVolumeValuesA.add(Double.nan)
                bloodOxygenationA.add(Double.nan)
            }
        }
        lastVitalSignsData = vitalSignsDataList[count - 1]
    }
}
RightAlignedOuterVerticallyStackedYAxisLayoutStrategy.swift
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// RightAlignedOuterVerticallyStackedYAxisLayoutStrategy.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

class RightAlignedOuterVerticallyStackedYAxisLayoutStrategy: SCIVerticalAxisLayoutStrategy {
    override func measureAxes(withAvailableWidth width: CGFloat, height: CGFloat, andChartLayoutState chartLayoutState: SCIChartLayoutState) {
        for i in 0 ..< axes.count {
            let axis = axes[i] as! ISCIAxis
            axis.updateMeasurements()
            
            let axisLayoutState = axis.axisLayoutState
            chartLayoutState.rightOuterAreaSize = max(SCIVerticalAxisLayoutStrategy.getRequiredAxisSize(from: axisLayoutState), chartLayoutState.rightOuterAreaSize)
        }
    }
    
    override func layout(withLeft left: CGFloat, top: CGFloat, right: CGFloat, bottom: CGFloat) {
        let count = axes.count
        let height = bottom - top
        let axisHeight = height / CGFloat(count)
        
        var topPlacement = top
        
        for i in 0 ..< count {
            let axis = axes[i] as! ISCIAxis
            let axisLayoutState = axis.axisLayoutState
            let bottomPlacement = round(topPlacement + axisHeight)
            axis.layoutArea(withLeft: left, top: topPlacement, right: left + SCIVerticalAxisLayoutStrategy.getRequiredAxisSize(from: axisLayoutState), bottom: bottomPlacement)
            topPlacement = bottomPlacement
        }
    }
}
SCDDimTracePaletteProvider.m
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// SCDDimTracePaletteProvider.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 "SCDDimTracePaletteProvider.h"

@implementation SCDDimTracePaletteProvider {
    SCIUnsignedIntegerValues *_colors;
    double _startOpacity;
    double _diffOpacity;
}

- (instancetype)init {
    self = [super initWithRenderableSeriesType:SCIFastLineRenderableSeries.class];
    if (self) {
        _colors = [SCIUnsignedIntegerValues new];
        _startOpacity = 0.2;
        _diffOpacity = 1 - _startOpacity;
    }
    return self;
}

- (void)update {
    unsigned int defaultColor = self.renderableSeries.strokeStyle.color.colorARGBCode;
    NSInteger count = self.renderableSeries.currentRenderPassData.pointsCount;
    _colors.count = count;
    
    unsigned int *colorsArray = _colors.itemsArray;
    for (int i = 0; i < count; i++) {
        double fraction = (double)i / count;
        double opacity = _startOpacity + fraction * _diffOpacity;
        unsigned int color = [SCIColor argb:defaultColor withOpacity:opacity];
        
        colorsArray[i] = color;
    }
}

- (SCIUnsignedIntegerValues *)strokeColors {
    return _colors;
}

@end
SCDVitalSignsLayoutViewController.m
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// SCDVitalSignsLayoutViewController.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 "SCDVitalSignsLayoutViewController.h"
#import "SCIStackView.h"

@implementation SCDVitalSignsLayoutViewController {
    SCIView *_topView;
    SCIStackView *topStackView;
    
    SCIView *_bottomView;
    SCIStackView *bottomStackView;
    
    BOOL isPortrait;
    
    NSLayoutConstraint *_topViewHeightAnchorPortrait;
    NSLayoutConstraint *_topViewWidthAnchorLandscape;
    
    SCD_ECGView *_ecgView;
    SCD_NIBPView *_nibpView;
    SCD_SVView *_svView;
    SCD_SPO2View *_spo2View;
}

- (void)tryUpdateChartTheme:(SCIChartTheme)theme {
    [SCIThemeManager applyTheme:theme toThemeable:self.surface];
    self.view.platformBackgroundColor = self.surface.backgroundBrushStyle.color;
}

- (void)loadView {
    [super loadView];
    
    self.view = [SCIView new];
    self.view.autoresizingMask = SCIAutoresizingFlexible;
    
    _topView = [self p_SCD_createTopView];
    _topView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:_topView];
    
    _bottomView = [self p_SCD_createBottomView];
    _bottomView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:_bottomView];
    
    _surface = [[self.associatedType alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
    _surface.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:_surface];
    
#if TARGET_OS_OSX
    isPortrait = NO;
#elif TARGET_OS_IOS
    isPortrait = UIInterfaceOrientationIsPortrait(UIApplication.sharedApplication.statusBarOrientation);
#endif
    
    [self p_SCD_updateConstraints];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    self.view.platformBackgroundColor = _surface.backgroundBrushStyle.color;
}

#if TARGET_OS_IOS
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    
    isPortrait = size.height > size.width;
    
    [self p_SCD_updateConstraints];
}
#endif

- (SCIImageView *)heartImageView {
    return _ecgView.heartImageView;
}

- (SCILabel *)bpmValueLabel {
    return _ecgView.bpmValueLabel;
}

- (SCDStepProgressBar *)bpBar {
    return _nibpView.bpBar;
}

- (SCILabel *)bpValueLabel {
    return _nibpView.bpValueLabel;
}

- (SCILabel *)bvValueLabel {
    return _svView.bvValueLabel;
}

- (SCDStepProgressBar *)svBar1 {
    return _svView.svBar1;
}

- (SCDStepProgressBar *)svBar2 {
    return _svView.svBar2;
}

- (SCILabel *)spoClockValueLabel {
    return _spo2View.spoClockValueLabel;
}

- (SCILabel *)spoValueLabel {
    return _spo2View.spoValueLabel;
}

- (void)p_SCD_updateConstraints {
    [self p_SCD_removeAllConstraints];
    [self p_SCD_updateStackViewsAxis];
    
    //Portrait
    _topViewHeightAnchorPortrait = [_topView.heightAnchor constraintEqualToConstant:131];
    _topViewHeightAnchorPortrait.active = isPortrait;
    
    [_topView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor].active = isPortrait;
    [_surface.topAnchor constraintEqualToAnchor:_topView.bottomAnchor].active = isPortrait;
    [_surface.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor].active = isPortrait;
    [_surface.bottomAnchor constraintEqualToAnchor:_bottomView.topAnchor].active = isPortrait;
    
    //Landscape
    _topViewWidthAnchorLandscape = [_topView.widthAnchor constraintEqualToConstant:200];
    _topViewWidthAnchorLandscape.active = !isPortrait;
    
    [_surface.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor].active = !isPortrait;
    [_surface.trailingAnchor constraintEqualToAnchor:_topView.leadingAnchor].active = !isPortrait;
    [_surface.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor].active = !isPortrait;
    [_bottomView.topAnchor constraintEqualToAnchor:_topView.bottomAnchor].active = !isPortrait;
    
    //Common
    [_topView.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = YES;
    [_topView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor].active = YES;
    [_surface.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor].active = YES;
    [_bottomView.heightAnchor constraintEqualToAnchor:_topView.heightAnchor].active = YES;
    [_bottomView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor].active = YES;
    [_bottomView.trailingAnchor constraintEqualToAnchor:_topView.trailingAnchor].active = YES;
    [_bottomView.leadingAnchor constraintEqualToAnchor:_topView.leadingAnchor].active = YES;
}

- (void)p_SCD_updateStackViewsAxis {
    topStackView.axis = [self p_SCD_getStackViewConstraintAxis];
    bottomStackView.axis = [self p_SCD_getStackViewConstraintAxis];
}

- (void)p_SCD_removeAllConstraints {
    for (NSLayoutConstraint *constraint in self.view.constraints) {
        if (constraint != nil) {
            [self.view removeConstraint:constraint];
        }
    }
    
    if (_topViewHeightAnchorPortrait != nil) {
        [_topView removeConstraint:_topViewHeightAnchorPortrait];
    }
    
    if (_topViewWidthAnchorLandscape != nil) {
        [_topView removeConstraint:_topViewWidthAnchorLandscape];
    }
}

- (SCIView *)p_SCD_createTopView {
    _ecgView = [self p_SCD_createECGView];
    _nibpView = [self p_SCD_createNIBPView];
    
    topStackView = [SCIStackView new];
    [topStackView addArrangedSubview:_ecgView];
    [topStackView addArrangedSubview:_nibpView];

    return [self createContainerViewWithStackView:topStackView];
}

- (SCIView *)p_SCD_createBottomView {
    _svView = [self p_SCD_createSVView];
    _spo2View = [self p_SCD_createSPO2View];
    
    bottomStackView = [SCIStackView new];
    [bottomStackView addArrangedSubview:_svView];
    [bottomStackView addArrangedSubview:_spo2View];

    return [self createContainerViewWithStackView:bottomStackView];
}

- (SCD_ECGView *)p_SCD_createECGView {
    SCIColor *color = [SCIColor fromARGBColorCode:0xFF42B649];
    
    SCILabel *ecgLabel = [SCILabel new];
    ecgLabel.text = @"ECG";
    ecgLabel.textColor = color;
    ecgLabel.font = [SCIFont systemFontOfSize:18];
    
    SCIImage *image = [SCIImage imageNamed:@"icon.heart" inBundleWithIdentifier:@"com.scichart.examples.sources"];
    SCIImageView *heartImageView = [SCIImageView imageViewWithImage:image];
    heartImageView.translatesAutoresizingMaskIntoConstraints = NO;
    [heartImageView addConstraints:@[
        [heartImageView.widthAnchor constraintEqualToConstant:24],
        [heartImageView.heightAnchor constraintEqualToConstant:24],
    ]];
    
    SCILabel *bottomLeftLabel = [SCILabel new];
    bottomLeftLabel.numberOfLines = 0;
    bottomLeftLabel.text = @"V1- 1.4MM\nST |+ 0.6 ||+ 0.9";
    bottomLeftLabel.textColor = color;
    bottomLeftLabel.font = [SCIFont systemFontOfSize:10];
    
    SCILabel *bottomRightLabel = [SCILabel new];
    bottomRightLabel.text = @"87";
    bottomRightLabel.textColor = color;
    bottomRightLabel.font = [SCIFont italicSystemFontOfSize:36];
    
    return [[SCD_ECGView alloc] initWithTopLeftView:ecgLabel topRightView:heartImageView bottomLeftView:bottomLeftLabel bottomRightView:bottomRightLabel];
}

- (SCD_NIBPView *)p_SCD_createNIBPView {
    SCIColor *color = [SCIColor fromARGBColorCode:0xFFFFFF00];
    
    SCILabel *topLeftLabel = [SCILabel new];
    topLeftLabel.text = @"NIBP";
    topLeftLabel.textColor = color;
    topLeftLabel.font = [SCIFont systemFontOfSize:18];
    
    SCILabel *topRightLabel = [SCILabel new];
    topRightLabel.numberOfLines = 0;
    topRightLabel.text = @"AUTO\n145/95";
    topRightLabel.textColor = color;
    topRightLabel.font = [SCIFont systemFontOfSize:18];
    
    SCDStepProgressBar *bpBar = [SCDStepProgressBar new];
    bpBar.translatesAutoresizingMaskIntoConstraints = NO;
    [bpBar addConstraints:@[
        [bpBar.heightAnchor constraintEqualToConstant:20],
    ]];
    
    SCILabel *bottomRightLabel = [SCILabel new];
    bottomRightLabel.text = @"87";
    bottomRightLabel.textColor = color;
    bottomRightLabel.font = [SCIFont italicSystemFontOfSize:36];
    
    return [[SCD_NIBPView alloc] initWithTopLeftView:topLeftLabel topRightView:topRightLabel bottomLeftView:bpBar bottomRightView:bottomRightLabel];
}

- (SCD_SVView *)p_SCD_createSVView {
    SCIColor *color = [SCIColor fromARGBColorCode:0xFFB0C4DE];
    
    SCILabel *topLeftLabel = [SCILabel new];
    topLeftLabel.text = @"SV";
    topLeftLabel.textColor = color;
    topLeftLabel.font = [SCIFont systemFontOfSize:18];
    
    SCILabel *topRightLabel = [SCILabel new];
    topRightLabel.numberOfLines = 0;
    topRightLabel.text = @"ML    100\n%*****  55";
    topRightLabel.textColor = color;
    topRightLabel.font = [SCIFont systemFontOfSize:18];
    
    SCDStepProgressBar *svBar1 = [SCDStepProgressBar new];
    svBar1.translatesAutoresizingMaskIntoConstraints = NO;
    [svBar1 addConstraints:@[
        [svBar1.widthAnchor constraintEqualToConstant:20]
    ]];
    
    SCDStepProgressBar *svBar2 = [SCDStepProgressBar new];
    svBar2.translatesAutoresizingMaskIntoConstraints = NO;
    [svBar2 addConstraints:@[
        [svBar2.widthAnchor constraintEqualToConstant:20]
    ]];
    
    SCIStackView *stackView = [SCIStackView new];
    stackView.axis = SCILayoutConstraintAxisHorizontal;
    stackView.distribution = SCIStackViewDistributionFillEqually;
    stackView.spacing = 3;
    [stackView addArrangedSubview:svBar1];
    [stackView addArrangedSubview:svBar2];
    
    SCILabel *bottomRightLabel = [SCILabel new];
    bottomRightLabel.text = @"87";
    bottomRightLabel.textColor = color;
    bottomRightLabel.font = [SCIFont italicSystemFontOfSize:36];
    
    return [[SCD_SVView alloc] initWithTopLeftView:topLeftLabel topRightView:topRightLabel bottomLeftView:stackView bottomRightView:bottomRightLabel];
}

- (SCD_SPO2View *)p_SCD_createSPO2View {
    SCIColor *color = [SCIColor fromARGBColorCode:0xFF6495ED];
    
    SCILabel *topLeftLabel = [SCILabel new];
    topLeftLabel.text = @"SPO2";
    topLeftLabel.textColor = color;
    topLeftLabel.font = [SCIFont systemFontOfSize:18];
    
    SCILabel *topRightLabel = [SCILabel new];
    topRightLabel.text = @"14:35";
    topRightLabel.textColor = color;
    topRightLabel.font = [SCIFont systemFontOfSize:14];
    
    SCILabel *bottomLeftLabel = [SCILabel new];
    bottomLeftLabel.numberOfLines = 0;
    bottomLeftLabel.text = @"71-\nRESP";
    bottomLeftLabel.textColor = color;
    bottomLeftLabel.font = [SCIFont systemFontOfSize:10];
    
    SCILabel *bottomRightLabel = [SCILabel new];
    bottomRightLabel.text = @"87";
    bottomRightLabel.textColor = color;
    bottomRightLabel.font = [SCIFont italicSystemFontOfSize:36];
    
    return [[SCD_SPO2View alloc] initWithTopLeftView:topLeftLabel topRightView:topRightLabel bottomLeftView:bottomLeftLabel bottomRightView:bottomRightLabel];
}

- (SCILayoutConstraintAxis)p_SCD_getStackViewConstraintAxis {
    return isPortrait ? SCILayoutConstraintAxisHorizontal : SCILayoutConstraintAxisVertical;
}

- (SCIView *)createContainerViewWithStackView:(SCIStackView *)stackView {
    stackView.distribution = SCIStackViewDistributionFillEqually;
    stackView.axis = SCILayoutConstraintAxisHorizontal;
    stackView.spacing = 0;
    
    SCIView *container = [SCIView new];
    
    stackView.translatesAutoresizingMaskIntoConstraints = NO;
    [container addSubview:stackView];
    [container addConstraints:@[
        [stackView.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],
        [stackView.topAnchor constraintEqualToAnchor:container.topAnchor],
        [stackView.trailingAnchor constraintEqualToAnchor:container.trailingAnchor],
        [stackView.bottomAnchor constraintEqualToAnchor:container.bottomAnchor]
    ]];
    
    return container;
}

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

class VitalSignsIndicatorsProvider {
    private let timeFormat = "HH:mm"
    
    private let BPM_VALUES = ["67", "69", "72", "74"]
    private let BP_VALUES = ["120/70", "115/70", "115/75", "120/75"]
    private let BPB_VALUES = [5, 6, 7]
    private let BV_VALUES = ["13.1", "13.2", "13.3", "13.0"]
    private let BVB_VALUES = [9, 10, 11]
    private let BO_VALUES = ["93", "95", "96", "97"]
    
    var bpmValue = ""
    var bpValue = ""
    var bpbValue = 0
    var bvValue = ""
    var bvBar1Value = 0
    var bvBar2Value = 0
    var spoValue = ""
    var spoClockValue = ""
    
    func update() {
        bpmValue = randomString(BPM_VALUES)
        bpValue = randomString(BP_VALUES)
        bpbValue = randomInt(BPB_VALUES)
        bvValue = randomString(BV_VALUES)
        bvBar1Value = randomInt(BVB_VALUES)
        bvBar2Value = randomInt(BVB_VALUES)
        spoValue = randomString(BO_VALUES)
        spoClockValue = getTimeString()
    }
    
    private func randomString(_ values: [String]) -> String {
        return randomElement(values) ?? ""
    }
    
    private func randomInt(_ values: [Int]) -> Int {
        return randomElement(values) ?? 0
    }
    
    private func randomElement<T>(_ values: [T]) -> T? {
        return values.randomElement()
    }
    
    private func getTimeString() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = timeFormat
        return dateFormatter.string(from: Date())
    }
}
VitalSignsMonitor/VitalSignsMonitorChartView.swift
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// VitalSignsMonitorChartView.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 RxSwift

class VitalSignsMonitorChartView: SCDVitalSignsLayoutViewController<SCIChartSurface> {
    
    override var associatedType: AnyClass { return SCIChartSurface.self }
    
    static let FIFO_CAPACITY = 7850
    
    private let ecgDataSeries = newDataSeries(FIFO_CAPACITY)
    private let ecgSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private let bloodPressureDataSeries = newDataSeries(FIFO_CAPACITY)
    private let bloodPressureSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private let bloodVolumeDataSeries = newDataSeries(FIFO_CAPACITY)
    private let bloodVolumeSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private let bloodOxygenationDataSeries = newDataSeries(FIFO_CAPACITY)
    private let bloodOxygenationSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private let lastEcgSweepDataSeries = newDataSeries(1)
    private let lastBloodPressureDataSeries = newDataSeries(1)
    private let lastBloodVolumeDataSeries = newDataSeries(1)
    private let lastBloodOxygenationSweepDataSeries = newDataSeries(1)
    
    private let indicatorsProvider = VitalSignsIndicatorsProvider()
    private var dataBatch: EcgDataBatch = EcgDataBatch()
    private var chartDisposable: Disposable?
    private var indicatorsDisposable: Disposable?
        
    deinit {
        chartDisposable?.dispose()
        indicatorsDisposable?.dispose()
    }
    
    override func initExample() {
        chartDisposable?.dispose()
        indicatorsDisposable?.dispose()

        setUpChart()
        setupIndicators()

        let dataProvider = DefaultVitalSignsDataProvider()
        chartDisposable = dataProvider
            .getData()
            .buffer(timeSpan: .milliseconds(16), count: Int.max, scheduler: MainScheduler.instance)
            .do(onNext: { [weak self] ecgData in
                guard let self = self else { return }
                if ecgData.isEmpty { return }

                self.dataBatch.updateData(ecgData)
                SCIUpdateSuspender.usingWith(self.surface) {
                    self.updateDataSeries()
                }
            })
            .subscribe()
        
        updateIndicators(time: 0)
        
        indicatorsDisposable = Observable
            .interval(.seconds(1), scheduler: MainScheduler.instance)
            .do(onNext: { [weak self] in
                self?.updateIndicators(time: $0)
            })
            .subscribe()
    }
    
    private func setupIndicators() {
        let progressBackgroundColor = #colorLiteral(red: 0.3921568627, green: 0.3921568627, blue: 0.3921568627, alpha: 1)
        let bpBarStyle = SCDProgressBarStyle(isVertical: false, max: 10, spacing: 2, progressBackgroundColor: progressBackgroundColor, progressColor: SCIColor.fromARGBColorCode(0xFFFFFF00), barSize: 3)
        
        bpBar.setStyle(bpBarStyle)
        
        let svBarStyle = SCDProgressBarStyle(isVertical: true, max: 12, spacing: 2, progressBackgroundColor: .clear, progressColor: SCIColor.fromARGBColorCode(0xFFB0C4DE), barSize: 3)
        
        svBar1.setStyle(svBarStyle)
        svBar2.setStyle(svBarStyle)
    }
    
    private func updateIndicators(time: Int) {
        heartImageView.isHidden = time % 2 == 0

        if (time % 5 == 0) {
            indicatorsProvider.update();

            bpmValueLabel.text = indicatorsProvider.bpmValue
            bpValueLabel.text = indicatorsProvider.bpValue
            bpBar.setProgress(indicatorsProvider.bpbValue)

            bvValueLabel.text = indicatorsProvider.bvValue
            svBar1.setProgress(indicatorsProvider.bvBar1Value)
            svBar2.setProgress(indicatorsProvider.bvBar2Value)

            spoValueLabel.text = indicatorsProvider.spoValue
            spoClockValueLabel.text = indicatorsProvider.spoClockValue
        }
    }
    
    private func updateDataSeries() {
        let xValues = dataBatch.xValues;

        ecgDataSeries.append(x: xValues, y: dataBatch.ecgHeartRateValuesA)
        ecgSweepDataSeries.append(x: xValues, y: dataBatch.ecgHeartRateValuesB)

        bloodPressureDataSeries.append(x: xValues, y: dataBatch.bloodPressureValuesA)
        bloodPressureSweepDataSeries.append(x: xValues, y: dataBatch.bloodPressureValuesB)

        bloodOxygenationDataSeries.append(x: xValues, y: dataBatch.bloodOxygenationA)
        bloodOxygenationSweepDataSeries.append(x: xValues, y: dataBatch.bloodOxygenationB)

        bloodVolumeDataSeries.append(x: xValues, y: dataBatch.bloodVolumeValuesA)
        bloodVolumeSweepDataSeries.append(x: xValues, y: dataBatch.bloodVolumeValuesB)

        guard let lastVitalSignsData = dataBatch.lastVitalSignsData else {
            return
        }
        let xValue = lastVitalSignsData.xValue

        lastEcgSweepDataSeries.append(x: xValue, y: lastVitalSignsData.ecgHeartRate)
        lastBloodPressureDataSeries.append(x: xValue, y: lastVitalSignsData.bloodPressure)
        lastBloodOxygenationSweepDataSeries.append(x: xValue, y: lastVitalSignsData.bloodOxygenation)
        lastBloodVolumeDataSeries.append(x: xValue, y: lastVitalSignsData.bloodVolume)
    }
    
    private func setUpChart() {
        let xAxis = SCINumericAxis()
        xAxis.visibleRange = SCIDoubleRange(min: 0, max: 10)
        xAxis.autoRange = .never
        xAxis.drawMinorGridLines = false
        xAxis.drawMajorGridLines = false
        xAxis.isVisible = false
        
        let ecgId = "ecgId"
        let bloodPressureId = "bloodPressureId"
        let bloodVolumeId = "bloodVolumeId"
        let bloodOxygenationId = "bloodOxygenationId"
        
        let yAxisEcg: SCINumericAxis = generateYAxis(ecgId)
        let yAxisPressure: SCINumericAxis = generateYAxis(bloodPressureId)
        let yAxisVolume: SCINumericAxis = generateYAxis(bloodVolumeId)
        let yAxisOxygenation: SCINumericAxis = generateYAxis(bloodOxygenationId)
        
        let heartRateColor: UInt32 = 0xFF42B649
        let bloodPressureColor: UInt32 = 0xFFFFFF00
        let bloodVolumeColor: UInt32 = 0xFFB0C4DE
        let bloodOxygenation: UInt32 = 0xFF6495ED
        
        SCIUpdateSuspender.usingWith(surface) {
            self.surface.xAxes.add(xAxis)
            self.surface.yAxes.add(yAxisEcg)
            self.surface.yAxes.add(yAxisPressure)
            self.surface.yAxes.add(yAxisVolume)
            self.surface.yAxes.add(yAxisOxygenation)
            
            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: ecgId, dataSeries: self.ecgDataSeries, strokeStyle: SCISolidPenStyle(color: heartRateColor, thickness: 2)))
            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: ecgId, dataSeries: self.ecgSweepDataSeries, strokeStyle: SCISolidPenStyle(color: heartRateColor, thickness: 2)))
            self.surface.renderableSeries.add(self.generateScatterForLastAppendedPoint(yAxisId: ecgId, dataSeries: self.lastEcgSweepDataSeries))
            
            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: bloodPressureId, dataSeries: self.bloodPressureDataSeries, strokeStyle: SCISolidPenStyle(color: bloodPressureColor, thickness: 2)))
            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: bloodPressureId, dataSeries: self.bloodPressureSweepDataSeries, strokeStyle: SCISolidPenStyle(color: bloodPressureColor, thickness: 2)))
            self.surface.renderableSeries.add(self.generateScatterForLastAppendedPoint(yAxisId: bloodPressureId, dataSeries: self.lastBloodPressureDataSeries))

            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: bloodVolumeId, dataSeries: self.bloodVolumeDataSeries, strokeStyle: SCISolidPenStyle(color: bloodVolumeColor, thickness: 2)))
            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: bloodVolumeId, dataSeries: self.bloodVolumeSweepDataSeries, strokeStyle: SCISolidPenStyle(color: bloodVolumeColor, thickness: 2)))
            self.surface.renderableSeries.add(self.generateScatterForLastAppendedPoint(yAxisId: bloodVolumeId, dataSeries: self.lastBloodVolumeDataSeries))

            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: bloodOxygenationId, dataSeries: self.bloodOxygenationDataSeries, strokeStyle: SCISolidPenStyle(color: bloodOxygenation, thickness: 2)))
            self.surface.renderableSeries.add(self.generateLineSeries(yAxisId: bloodOxygenationId, dataSeries: self.bloodOxygenationSweepDataSeries, strokeStyle: SCISolidPenStyle(color: bloodOxygenation, thickness: 2)))
            self.surface.renderableSeries.add(self.generateScatterForLastAppendedPoint(yAxisId: bloodOxygenationId, dataSeries: self.lastBloodOxygenationSweepDataSeries))
            
            let layoutManager = SCIDefaultLayoutManager()
            layoutManager.rightOuterAxisLayoutStrategy = RightAlignedOuterVerticallyStackedYAxisLayoutStrategy()
            self.surface.layoutManager = layoutManager
        }
    }
    
    private func generateScatterForLastAppendedPoint(yAxisId: String, dataSeries: ISCIDataSeries) -> ISCIRenderableSeries {
        let pointMarker = SCIEllipsePointMarker()
        pointMarker.size = CGSize(width: 5, height: 5)
        pointMarker.strokeStyle = SCISolidPenStyle(color: .white, thickness: 1)
        pointMarker.fillStyle = SCISolidBrushStyle(color: .white)
        
        let scatterSeries = SCIXyScatterRenderableSeries()
        scatterSeries.dataSeries = dataSeries
        scatterSeries.yAxisId = yAxisId
        scatterSeries.pointMarker = pointMarker
        
        return scatterSeries
    }
    
    private func generateLineSeries(yAxisId: String, dataSeries: ISCIDataSeries, strokeStyle: SCIPenStyle) -> ISCIRenderableSeries {
        let series = SCIFastLineRenderableSeries()
        series.dataSeries = dataSeries
        series.yAxisId = yAxisId
        series.strokeStyle = strokeStyle
        
        // Uncomment to allow trace to dim when the trace is old giving an ECG effect
        // However this is slow in simulator (fast on device)
        // series.paletteProvider = SCDDimTracePaletteProvider()
        
        return series
    }
    
    private func generateYAxis(_ id: String) -> SCINumericAxis {
        let yAxis = SCINumericAxis()
        yAxis.axisId = id
        yAxis.growBy = SCIDoubleRange(min: 0.05, max: 0.05)
        yAxis.autoRange = .always
        yAxis.drawMajorBands = false
        yAxis.drawMinorGridLines = false
        yAxis.drawMajorGridLines = false
        yAxis.axisAlignment = .right
        yAxis.isVisible = false
        
        return yAxis
    }
    
    private static func newDataSeries(_ fifoCapacity: Int) -> SCIXyDataSeries {
        let dataSeries = SCIXyDataSeries(xType: .double, yType: .double)
        dataSeries.fifoCapacity = fifoCapacity
        dataSeries.acceptsUnsortedData = true
        
        return dataSeries
    }
}
SCDVitalSignsComponentView.m
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// SCDVitalSignsComponentView.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 "SCDVitalSignsComponentView.h"

#if TARGET_OS_OSX

#elif TARGET_OS_IOS

#import <UIKit/NSLayoutAnchor.h>

#endif

@implementation SCDVitalSignsComponentView {
    SCIView *_topLeftView;
    SCIView *_topRightView;
    SCIView *_bottomLeftView;
    SCIView *_bottomRightView;
}

- (instancetype)initWithTopLeftView:(SCIView *)topLeftView topRightView:(SCIView *)topRightView bottomLeftView:(SCIView *)bottomLeftView bottomRightView:(SCIView *)bottomRightView {
    self = [super initWithFrame:CGRectZero];
    if (self) {
        _topLeftView = topLeftView;
        _topRightView = topRightView;
        _bottomLeftView = bottomLeftView;
        _bottomRightView = bottomRightView;
        
        [self p_SCD_setupViews];
    }
    return self;
}

- (void)p_SCD_setupViews {
    for (SCIView *view in @[_topLeftView, _topRightView, _bottomLeftView, _bottomRightView]) {
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:view];
    }
    
    [self addConstraints:@[
        [_topLeftView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:8],
        [_topLeftView.topAnchor constraintEqualToAnchor:self.topAnchor constant:8],

        [_topRightView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-8],
        [_topRightView.topAnchor constraintEqualToAnchor:self.topAnchor constant:8],

        [_bottomLeftView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:8],
        [_bottomLeftView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-8],

        [_bottomRightView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-8],
        [_bottomRightView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:0],
    ]];
}

@end
Back to iOS & macOS charts Examples