Pre loader

Android Vital Signs ECG/EKG Medical Demo

Android Chart - Examples

SciChart Android ships with ~90 Android Chart Examples which you can browse, play with, view the source-code and even export each SciChart Android Chart Example to a stand-alone Android Studio project. All of this is possible with the new and improved SciChart Android Examples Suite, which ships as part of our Android Charts SDK.

Download Scichart Android

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

There are four channels of data simulated, showing how real-time, high performance ECG/EKG charts & graphs can be drawn with SciChart Android to monitor blood pressure, SPO2 blood oxygen, and volumetric flow enabling you to create medical apps using an Android phone or Embedded systems.

SciChart helps you shortcut development of medical applications by providing rich, real time, high performance & reliable Android 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 Android Chart library.

The full source code for the Android Vital Signs ECG/EKG Medical Demo example is included below (Scroll down!).

Did you know you can also view the source code from one of the following sources as well?

  1. Clone the SciChart.Android.Examples from Github.
  2. Or, view source and export each example to an Android Studio project from the Java version of the SciChart Android Examples app.
  3. Also the SciChart Android Trial contains the full source for the examples (link below).

DOWNLOAD THE ANDROID CHART EXAMPLES

Kotlin: VitalSignsMonitorShowcaseFragment.kt
View source code
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2021. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// VitalSignsMonitorShowcaseFragment.kt is part of SCICHART®, High Performance Scientific Charts
// For full terms and conditions of the license, see http://www.scichart.com/scichart-eula/
//
// This source code is protected by international copyright law. Unauthorized
// reproduction, reverse-engineering, or distribution of all or any portion of
// this source code is strictly prohibited.
//
// This source code contains confidential and proprietary trade secrets of
// SciChart Ltd., and should at no time be copied, transferred, sold,
// distributed or made available without express written permission.
//******************************************************************************

package com.scichart.examples.fragments.featuredApps.medicalCharts.kt.vitalSignsMonitor

import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import com.scichart.charting.layoutManagers.ChartLayoutState
import com.scichart.charting.layoutManagers.DefaultLayoutManager
import com.scichart.charting.layoutManagers.VerticalAxisLayoutStrategy
import com.scichart.charting.model.dataSeries.IDataSeries
import com.scichart.charting.model.dataSeries.XyDataSeries
import com.scichart.charting.visuals.axes.AutoRange.Never
import com.scichart.charting.visuals.axes.NumericAxis
import com.scichart.charting.visuals.renderableSeries.FastLineRenderableSeries
import com.scichart.charting.visuals.renderableSeries.IRenderableSeries
import com.scichart.charting.visuals.renderableSeries.XyRenderableSeriesBase
import com.scichart.charting.visuals.renderableSeries.XyScatterRenderableSeries
import com.scichart.charting.visuals.renderableSeries.paletteProviders.IStrokePaletteProvider
import com.scichart.charting.visuals.renderableSeries.paletteProviders.PaletteProviderBase
import com.scichart.core.model.IntegerValues
import com.scichart.data.model.DoubleRange
import com.scichart.drawing.utility.ColorUtil
import com.scichart.examples.R
import com.scichart.examples.databinding.ExampleVitalSignsMonitorFragmentBinding
import com.scichart.examples.fragments.base.ShowcaseExampleBaseFragment
import com.scichart.examples.fragments.featuredApps.medicalCharts.vitalSignsMonitor.DefaultVitalSignsDataProvider
import com.scichart.examples.fragments.featuredApps.medicalCharts.vitalSignsMonitor.EcgDataBatch
import com.scichart.examples.fragments.featuredApps.medicalCharts.vitalSignsMonitor.VitalSignsData
import com.scichart.examples.fragments.featuredApps.medicalCharts.vitalSignsMonitor.VitalSignsIndicatorsProvider
import com.scichart.examples.utils.scichartExtensions.*
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.roundToInt

class VitalSignsMonitorShowcaseFragment : ShowcaseExampleBaseFragment<ExampleVitalSignsMonitorFragmentBinding>() {
    private val ecgDataSeries = newDataSeries(FIFO_CAPACITY)
    private val ecgSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private val bloodPressureDataSeries = newDataSeries(FIFO_CAPACITY)
    private val bloodPressureSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private val bloodVolumeDataSeries = newDataSeries(FIFO_CAPACITY)
    private val bloodVolumeSweepDataSeries = newDataSeries(FIFO_CAPACITY)
    private val bloodOxygenationDataSeries = newDataSeries(FIFO_CAPACITY)
    private val bloodOxygenationSweepDataSeries = newDataSeries(FIFO_CAPACITY)

    private val lastEcgSweepDataSeries = newDataSeries(1)
    private val lastBloodPressureDataSeries = newDataSeries(1)
    private val lastBloodVolumeDataSeries = newDataSeries(1)
    private val lastBloodOxygenationSweepDataSeries = newDataSeries(1)

    private val indicatorsProvider = VitalSignsIndicatorsProvider()

    private val dataBatch = EcgDataBatch()

    override fun inflateBinding(inflater: LayoutInflater): ExampleVitalSignsMonitorFragmentBinding {
        return ExampleVitalSignsMonitorFragmentBinding.inflate(inflater)
    }

    override fun initExample(binding: ExampleVitalSignsMonitorFragmentBinding) {
        val dataProvider = DefaultVitalSignsDataProvider(requireContext())

        setUpChart(dataProvider)

        dataProvider.data.buffer(50, TimeUnit.MILLISECONDS).doOnNext { ecgData: List<VitalSignsData> ->
            if (ecgData.isEmpty()) return@doOnNext

            dataBatch.updateData(ecgData)

            binding.surface.suspendUpdates {
                val xValues = dataBatch.xValues

                ecgDataSeries.append(xValues, dataBatch.ecgHeartRateValuesA)
                ecgSweepDataSeries.append(xValues, dataBatch.ecgHeartRateValuesB)

                bloodPressureDataSeries.append(xValues, dataBatch.bloodPressureValuesA)
                bloodPressureSweepDataSeries.append(xValues, dataBatch.bloodPressureValuesB)

                bloodOxygenationDataSeries.append(xValues, dataBatch.bloodOxygenationA)
                bloodOxygenationSweepDataSeries.append(xValues, dataBatch.bloodOxygenationB)

                bloodVolumeDataSeries.append(xValues, dataBatch.bloodVolumeValuesA)
                bloodVolumeSweepDataSeries.append(xValues, dataBatch.bloodVolumeValuesB)

                val lastVitalSignsData = dataBatch.lastVitalSignsData
                val xValue = lastVitalSignsData.xValue

                lastEcgSweepDataSeries.append(xValue, lastVitalSignsData.ecgHeartRate)
                lastBloodPressureDataSeries.append(xValue, lastVitalSignsData.bloodPressure)
                lastBloodOxygenationSweepDataSeries.append(xValue, lastVitalSignsData.bloodOxygenation)
                lastBloodVolumeDataSeries.append(xValue, lastVitalSignsData.bloodVolume)
            }
        }.compose(bindToLifecycle()).subscribe()

        updateIndicators(0)
        Observable.interval(0, 1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
            .doOnNext(::updateIndicators)
            .compose(bindToLifecycle()).subscribe()
    }

    private fun setUpChart(dataProvider: DefaultVitalSignsDataProvider) {
        val context = requireContext()
        val heartRateColor = ContextCompat.getColor(context, R.color.heart_rate_color)
        val bloodPressureColor = ContextCompat.getColor(context, R.color.blood_pressure_color)
        val bloodVolumeColor = ContextCompat.getColor(context, R.color.blood_volume_color)
        val bloodOxygenation = ContextCompat.getColor(context, R.color.blood_oxygenation_color)

        binding.surface.theme = R.style.SciChart_NavyBlue

        binding.surface.suspendUpdates {
            xAxes { numericAxis {
                visibleRange = DoubleRange(0.0, 10.0)
                autoRange = Never
                drawMinorGridLines = false
                drawMajorBands = false
                visibility = View.GONE
            }}
            yAxes {
                axis(generateYAxis(ECG_ID, dataProvider.ecgHeartRateRange))
                axis(generateYAxis(BLOOD_PRESSURE_ID, dataProvider.bloodPressureRange))
                axis(generateYAxis(BLOOD_VOLUME_ID, dataProvider.bloodVolumeRange))
                axis(generateYAxis(BLOOD_OXYGENATION_ID, dataProvider.bloodOxygenationRange))
            }

            renderableSeries {
                rSeries(generateLineSeries(ECG_ID, ecgDataSeries, heartRateColor))
                rSeries(generateLineSeries(ECG_ID, ecgSweepDataSeries, heartRateColor))
                rSeries(generateScatterForLastAppendedPoint(ECG_ID, lastEcgSweepDataSeries))

                rSeries(generateLineSeries(BLOOD_PRESSURE_ID, bloodPressureDataSeries, bloodPressureColor))
                rSeries(generateLineSeries(BLOOD_PRESSURE_ID, bloodPressureSweepDataSeries, bloodPressureColor))
                rSeries(generateScatterForLastAppendedPoint(BLOOD_PRESSURE_ID, lastBloodPressureDataSeries))

                rSeries(generateLineSeries(BLOOD_VOLUME_ID, bloodVolumeDataSeries, bloodVolumeColor))
                rSeries(generateLineSeries(BLOOD_VOLUME_ID, bloodVolumeSweepDataSeries, bloodVolumeColor))
                rSeries(generateScatterForLastAppendedPoint(BLOOD_VOLUME_ID, lastBloodVolumeDataSeries))

                rSeries(generateLineSeries(BLOOD_OXYGENATION_ID, bloodOxygenationDataSeries, bloodOxygenation))
                rSeries(generateLineSeries(BLOOD_OXYGENATION_ID, bloodOxygenationSweepDataSeries, bloodOxygenation))
                rSeries(generateScatterForLastAppendedPoint(BLOOD_OXYGENATION_ID, lastBloodOxygenationSweepDataSeries))
            }

            layoutManager = DefaultLayoutManager.Builder()
                .setRightOuterAxesLayoutStrategy(RightAlignedOuterVerticallyStackedYAxisLayoutStrategy())
                .build()
        }
    }

    private fun updateIndicators(time: Long) {
        binding.heartRateIndicator.heartIcon.visibility = if (time % 2 == 0L) View.VISIBLE else View.INVISIBLE

        if (time % 5 == 0L) {
            indicatorsProvider.update()
            binding.heartRateIndicator.bpmValueLabel.text = indicatorsProvider.bpmValue

            binding.bloodPressureIndicator.bloodPressureValue.text = indicatorsProvider.bpValue
            binding.bloodPressureIndicator.bloodPressureBar.progress = indicatorsProvider.bpbValue

            binding.bloodVolumeIndicator.bloodVolumeValueLabel.text = indicatorsProvider.bvValue
            binding.bloodVolumeIndicator.svBar1.progress = indicatorsProvider.bvBar1Value
            binding.bloodVolumeIndicator.svBar2.progress = indicatorsProvider.bvBar2Value

            binding.bloodOxygenationIndicator.spoValueLabel.text = indicatorsProvider.spoValue
            binding.bloodOxygenationIndicator.spoClockLabel.text = indicatorsProvider.spoClockValue
        }
    }

    private fun generateYAxis(id: String, visibleRange: DoubleRange): NumericAxis {
        return NumericAxis(requireContext()).apply {
            axisId = id
            visibility = View.GONE
            this.visibleRange = visibleRange
            autoRange = Never
            drawMajorBands = false
            drawMinorGridLines = false
            drawMajorGridLines = false
        }
    }

    private fun generateLineSeries(yAxisId: String, ds: IDataSeries<*, *>, color: Int): IRenderableSeries {
        return FastLineRenderableSeries().apply {
            dataSeries = ds
            this.yAxisId = yAxisId
            this.strokeStyle = SolidPenStyle(color)
            paletteProvider = DimTracePaletteProvider()
        }
    }

    private fun generateScatterForLastAppendedPoint(yAxisId: String, ds: IDataSeries<*, *>): IRenderableSeries {
        return XyScatterRenderableSeries().apply {
            dataSeries = ds
            this.yAxisId = yAxisId
            ellipsePointMarker {
                setSize(4)
                fillStyle = SolidBrushStyle(ColorUtil.White)
                strokeStyle = SolidPenStyle(ColorUtil.White)
            }
        }
    }

    private fun newDataSeries(fifoCapacity: Int): XyDataSeries<Double, Double> {
        return XyDataSeries<Double, Double>().apply {
            this.fifoCapacity = fifoCapacity
            acceptsUnsortedData = true
        }
    }

    private class RightAlignedOuterVerticallyStackedYAxisLayoutStrategy : VerticalAxisLayoutStrategy() {
        override fun measureAxes(availableWidth: Int, availableHeight: Int, chartLayoutState: ChartLayoutState) {
            for (i in 0 until axes.size) {
                val axis = axes[i]
                axis.updateAxisMeasurements()

                chartLayoutState.rightOuterAreaSize = max(getRequiredAxisSize(axis.axisLayoutState), chartLayoutState.rightOuterAreaSize)
            }
        }

        override fun layoutAxes(left: Int, top: Int, right: Int, bottom: Int) {
            val size = axes.size
            val height = bottom - top

            val axisHeight = height / size
            var topPlacement = top

            for (i in 0 until size) {
                val axis = axes[i]
                val axisLayoutState = axis.axisLayoutState

                val bottomPlacement = (topPlacement + axisHeight).toFloat().roundToInt()
                axis.layoutArea(left, topPlacement, left + getRequiredAxisSize(axisLayoutState), bottomPlacement)

                topPlacement = bottomPlacement
            }
        }
    }

    private class DimTracePaletteProvider : PaletteProviderBase<XyRenderableSeriesBase>(XyRenderableSeriesBase::class.java), IStrokePaletteProvider {
        private val colors = IntegerValues()

        private val startOpacity = 0.2
        private val diffOpacity = 1 - startOpacity

        override fun getStrokeColors(): IntegerValues = colors

        override fun update() {
            val defaultColor = renderableSeries!!.strokeStyle.color
            val size = renderableSeries!!.currentRenderPassData.pointsCount()

            colors.setSize(size)

            val colorsArray = colors.itemsArray
            for (i in 0 until size) {
                val faction = i / size.toDouble()
                val opacity = (startOpacity + faction * diffOpacity).toFloat()

                colorsArray[i] = ColorUtil.argb(defaultColor, opacity)
            }
        }
    }

    companion object {
        private const val FIFO_CAPACITY = 7850
        private const val ECG_ID = "ecgId"
        private const val BLOOD_PRESSURE_ID = "bloodPressureId"
        private const val BLOOD_VOLUME_ID = "bloodVolumeId"
        private const val BLOOD_OXYGENATION_ID = "bloodOxygenationId"
    }
}
Java: VitalSignsMonitorShowcaseFragment.java
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
//
// VitalSignsMonitorShowcaseFragment.java 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.
//******************************************************************************

package com.scichart.examples.fragments.featuredApps.medicalCharts.vitalSignsMonitor;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;

import com.scichart.charting.layoutManagers.ChartLayoutState;
import com.scichart.charting.layoutManagers.DefaultLayoutManager;
import com.scichart.charting.layoutManagers.VerticalAxisLayoutStrategy;
import com.scichart.charting.model.dataSeries.IDataSeries;
import com.scichart.charting.model.dataSeries.XyDataSeries;
import com.scichart.charting.visuals.SciChartSurface;
import com.scichart.charting.visuals.axes.AutoRange;
import com.scichart.charting.visuals.axes.AxisLayoutState;
import com.scichart.charting.visuals.axes.IAxis;
import com.scichart.charting.visuals.axes.NumericAxis;
import com.scichart.charting.visuals.pointmarkers.EllipsePointMarker;
import com.scichart.charting.visuals.renderableSeries.IRenderableSeries;
import com.scichart.charting.visuals.renderableSeries.XyRenderableSeriesBase;
import com.scichart.charting.visuals.renderableSeries.paletteProviders.IStrokePaletteProvider;
import com.scichart.charting.visuals.renderableSeries.paletteProviders.PaletteProviderBase;
import com.scichart.core.framework.UpdateSuspender;
import com.scichart.core.model.DoubleValues;
import com.scichart.core.model.IntegerValues;
import com.scichart.data.model.DoubleRange;
import com.scichart.drawing.utility.ColorUtil;
import com.scichart.examples.R;
import com.scichart.examples.databinding.ExampleVitalSignsMonitorFragmentBinding;
import com.scichart.examples.fragments.base.ShowcaseExampleBaseFragment;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;

public class VitalSignsMonitorShowcaseFragment extends ShowcaseExampleBaseFragment<ExampleVitalSignsMonitorFragmentBinding> {
    private static final int FIFO_CAPACITY = 7850;

    private static final String ECG_ID = "ecgId";
    private static final String BLOOD_PRESSURE_ID = "bloodPressureId";
    private static final String BLOOD_VOLUME_ID = "bloodVolumeId";
    private static final String BLOOD_OXYGENATION_ID = "bloodOxygenationId";

    private final XyDataSeries<Double, Double> ecgDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> ecgSweepDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> bloodPressureDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> bloodPressureSweepDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> bloodVolumeDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> bloodVolumeSweepDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> bloodOxygenationDataSeries = newDataSeries(FIFO_CAPACITY);
    private final XyDataSeries<Double, Double> bloodOxygenationSweepDataSeries = newDataSeries(FIFO_CAPACITY);

    private final XyDataSeries<Double, Double> lastEcgSweepDataSeries = newDataSeries(1);
    private final XyDataSeries<Double, Double> lastBloodPressureDataSeries = newDataSeries(1);
    private final XyDataSeries<Double, Double> lastBloodVolumeDataSeries = newDataSeries(1);
    private final XyDataSeries<Double, Double> lastBloodOxygenationSweepDataSeries = newDataSeries(1);

    private final VitalSignsIndicatorsProvider indicatorsProvider = new VitalSignsIndicatorsProvider();

    private final EcgDataBatch dataBatch = new EcgDataBatch();

    @NonNull
    @Override
    protected ExampleVitalSignsMonitorFragmentBinding inflateBinding(@NonNull LayoutInflater inflater) {
        return ExampleVitalSignsMonitorFragmentBinding.inflate(inflater);
    }

    @Override
    protected void initExample(@NonNull ExampleVitalSignsMonitorFragmentBinding binding) {
        final DefaultVitalSignsDataProvider dataProvider = new DefaultVitalSignsDataProvider(requireContext());

        setUpChart(dataProvider);

        dataProvider.getData().buffer(50, TimeUnit.MILLISECONDS).doOnNext(ecgData -> {
            if (ecgData.isEmpty()) return;

            dataBatch.updateData(ecgData);

            UpdateSuspender.using(binding.surface, () -> {
                final DoubleValues xValues = dataBatch.xValues;

                ecgDataSeries.append(xValues, dataBatch.ecgHeartRateValuesA);
                ecgSweepDataSeries.append(xValues, dataBatch.ecgHeartRateValuesB);

                bloodPressureDataSeries.append(xValues, dataBatch.bloodPressureValuesA);
                bloodPressureSweepDataSeries.append(xValues, dataBatch.bloodPressureValuesB);

                bloodOxygenationDataSeries.append(xValues, dataBatch.bloodOxygenationA);
                bloodOxygenationSweepDataSeries.append(xValues, dataBatch.bloodOxygenationB);

                bloodVolumeDataSeries.append(xValues, dataBatch.bloodVolumeValuesA);
                bloodVolumeSweepDataSeries.append(xValues, dataBatch.bloodVolumeValuesB);

                final VitalSignsData lastVitalSignsData = dataBatch.lastVitalSignsData;
                final double xValue = lastVitalSignsData.xValue;

                lastEcgSweepDataSeries.append(xValue, lastVitalSignsData.ecgHeartRate);
                lastBloodPressureDataSeries.append(xValue, lastVitalSignsData.bloodPressure);
                lastBloodOxygenationSweepDataSeries.append(xValue, lastVitalSignsData.bloodOxygenation);
                lastBloodVolumeDataSeries.append(xValue, lastVitalSignsData.bloodVolume);
            });
        }).compose(bindToLifecycle()).subscribe();

        updateIndicators(0);
        Observable.interval(0, 1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
                .doOnNext(this::updateIndicators)
                .compose(bindToLifecycle()).subscribe();
    }

    private void setUpChart(DefaultVitalSignsDataProvider dataProvider) {
        final NumericAxis xAxis = sciChartBuilder.newNumericAxis()
                .withVisibleRange(0, 10)
                .withAutoRangeMode(AutoRange.Never)
                .withDrawMinorGridLines(false)
                .withDrawMajorBands(false)
                .withVisibility(View.GONE)
                .build();

        final Context context = requireContext();
        final int heartRateColor = ContextCompat.getColor(context, R.color.heart_rate_color);
        final int bloodPressureColor = ContextCompat.getColor(context, R.color.blood_pressure_color);
        final int bloodVolumeColor = ContextCompat.getColor(context, R.color.blood_volume_color);
        final int bloodOxygenation = ContextCompat.getColor(context, R.color.blood_oxygenation_color);

        final SciChartSurface surface = binding.surface;
        surface.setTheme(R.style.SciChart_NavyBlue);
        UpdateSuspender.using(surface, () -> {
            Collections.addAll(surface.getXAxes(), xAxis);
            Collections.addAll(surface.getYAxes(),
                    generateYAxis(ECG_ID, dataProvider.getEcgHeartRateRange()),
                    generateYAxis(BLOOD_PRESSURE_ID, dataProvider.getBloodPressureRange()),
                    generateYAxis(BLOOD_VOLUME_ID, dataProvider.getBloodVolumeRange()),
                    generateYAxis(BLOOD_OXYGENATION_ID, dataProvider.getBloodOxygenationRange())
            );

            Collections.addAll(surface.getRenderableSeries(),
                    generateLineSeries(ECG_ID, ecgDataSeries, heartRateColor),
                    generateLineSeries(ECG_ID, ecgSweepDataSeries, heartRateColor),
                    generateScatterForLastAppendedPoint(ECG_ID, lastEcgSweepDataSeries),

                    generateLineSeries(BLOOD_PRESSURE_ID, bloodPressureDataSeries, bloodPressureColor),
                    generateLineSeries(BLOOD_PRESSURE_ID, bloodPressureSweepDataSeries, bloodPressureColor),
                    generateScatterForLastAppendedPoint(BLOOD_PRESSURE_ID, lastBloodPressureDataSeries),

                    generateLineSeries(BLOOD_VOLUME_ID, bloodVolumeDataSeries, bloodVolumeColor),
                    generateLineSeries(BLOOD_VOLUME_ID, bloodVolumeSweepDataSeries, bloodVolumeColor),
                    generateScatterForLastAppendedPoint(BLOOD_VOLUME_ID, lastBloodVolumeDataSeries),

                    generateLineSeries(BLOOD_OXYGENATION_ID, bloodOxygenationDataSeries, bloodOxygenation),
                    generateLineSeries(BLOOD_OXYGENATION_ID, bloodOxygenationSweepDataSeries, bloodOxygenation),
                    generateScatterForLastAppendedPoint(BLOOD_OXYGENATION_ID, lastBloodOxygenationSweepDataSeries)
            );

            surface.setLayoutManager(new DefaultLayoutManager.Builder().setRightOuterAxesLayoutStrategy(new RightAlignedOuterVerticallyStackedYAxisLayoutStrategy()).build());
        });
    }

    private void updateIndicators(long time) {
        binding.heartRateIndicator.heartIcon.setVisibility(time % 2 == 0 ? View.VISIBLE : View.INVISIBLE);

        if (time % 5 == 0) {
            indicatorsProvider.update();
            binding.heartRateIndicator.bpmValueLabel.setText(indicatorsProvider.getBpmValue());

            binding.bloodPressureIndicator.bloodPressureValue.setText(indicatorsProvider.getBpValue());
            binding.bloodPressureIndicator.bloodPressureBar.setProgress(indicatorsProvider.getBpbValue());

            binding.bloodVolumeIndicator.bloodVolumeValueLabel.setText(indicatorsProvider.getBvValue());
            binding.bloodVolumeIndicator.svBar1.setProgress(indicatorsProvider.getBvBar1Value());
            binding.bloodVolumeIndicator.svBar2.setProgress(indicatorsProvider.getBvBar2Value());

            binding.bloodOxygenationIndicator.spoValueLabel.setText(indicatorsProvider.getSpoValue());
            binding.bloodOxygenationIndicator.spoClockLabel.setText(indicatorsProvider.getSpoClockValue());
        }
    }

    private NumericAxis generateYAxis(String id, DoubleRange visibleRange) {
        return sciChartBuilder.newNumericAxis()
                .withAxisId(id)
                .withVisibility(View.GONE)
                .withVisibleRange(visibleRange)
                .withAutoRangeMode(AutoRange.Never)
                .withDrawMajorBands(false)
                .withDrawMinorGridLines(false)
                .withDrawMajorGridLines(false)
                .build();
    }

    private IRenderableSeries generateLineSeries(String yAxisId, IDataSeries<?, ?> ds, @ColorInt Integer color) {
        return sciChartBuilder.newLineSeries()
                .withDataSeries(ds)
                .withYAxisId(yAxisId)
                .withStrokeStyle(color)
                .withPaletteProvider(new DimTracePaletteProvider())
                .build();
    }

    private IRenderableSeries generateScatterForLastAppendedPoint(String yAxisId, IDataSeries<?, ?> ds) {
        final EllipsePointMarker pm = sciChartBuilder.newPointMarker(new EllipsePointMarker())
                .withSize(4)
                .withFill(ColorUtil.White)
                .withStroke(ColorUtil.White, 1f)
                .build();

        return sciChartBuilder.newScatterSeries()
                .withDataSeries(ds)
                .withYAxisId(yAxisId)
                .withPointMarker(pm)
                .build();
    }

    private static XyDataSeries<Double, Double> newDataSeries(int fifoCapacity) {
        final XyDataSeries<Double, Double> ds = new XyDataSeries<>(Double.class, Double.class);
        ds.setFifoCapacity(fifoCapacity);
        ds.setAcceptsUnsortedData(true);
        return ds;
    }

    private static class RightAlignedOuterVerticallyStackedYAxisLayoutStrategy extends VerticalAxisLayoutStrategy {
        @Override
        public void measureAxes(int availableWidth, int availableHeight, ChartLayoutState chartLayoutState) {
            for (int i = 0, size = axes.size(); i < size; i++) {
                final IAxis axis = axes.get(i);
                axis.updateAxisMeasurements();

                chartLayoutState.rightOuterAreaSize = Math.max(getRequiredAxisSize(axis.getAxisLayoutState()), chartLayoutState.rightOuterAreaSize);
            }
        }

        @Override
        public void layoutAxes(int left, int top, int right, int bottom) {
            final int size = axes.size();
            final int height = bottom - top;

            final int axisHeight = height / size;
            int topPlacement = top;

            for (int i = 0; i < size; i++) {
                final IAxis axis = axes.get(i);
                final AxisLayoutState axisLayoutState = axis.getAxisLayoutState();

                final int bottomPlacement = Math.round(topPlacement + axisHeight);
                axis.layoutArea(left, topPlacement, left + getRequiredAxisSize(axisLayoutState), bottomPlacement);

                topPlacement = bottomPlacement;
            }
        }
    }

    private static class DimTracePaletteProvider extends PaletteProviderBase<XyRenderableSeriesBase> implements IStrokePaletteProvider {
        private final IntegerValues colors = new IntegerValues();

        private final double startOpacity;
        private final double diffOpacity;

        public DimTracePaletteProvider() {
            super(XyRenderableSeriesBase.class);

            this.startOpacity = 0.2;
            this.diffOpacity = 1 - startOpacity;
        }

        @Override
        public IntegerValues getStrokeColors() {
            return colors;
        }

        @Override
        public void update() {
            final int defaultColor = renderableSeries.getStrokeStyle().getColor();
            final int size = renderableSeries.getCurrentRenderPassData().pointsCount();

            colors.setSize(size);

            final int[] colorsArray = colors.getItemsArray();

            for (int i = 0; i < size; i++) {
                final double faction = i / (double)size;
                final float opacity = (float) (startOpacity + faction * diffOpacity);

                colorsArray[i] = ColorUtil.argb(defaultColor, opacity);
            }
        }
    }
}
Back to Android Chart Examples