Source code for biosppy.plotting

# -*- coding: utf-8 -*-
"""
biosppy.plotting
----------------

This module provides utilities to plot data.

:copyright: (c) 2015-2023 by Instituto de Telecomunicacoes
:license: BSD 3-clause, see LICENSE for more details.
"""

# Imports
# compat
from __future__ import absolute_import, division, print_function
from six.moves import range, zip
import six

# built-in
import os

# 3rd party
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.patches as patches
import matplotlib.lines as lines
import numpy as np
from mock import MagicMock

# local
from . import utils
from .signals import tools as st

# Globals
MAJOR_LW = 1.5
MED_LW = 1.25
MINOR_LW = 1.0
MAX_ROWS = 10
LOGOS_FOLDER = '.logos'
BIOSPPY_LOGO = 'biosppy.png'
SCIENTISST_LOGO = 'scientisst.png'

def _get_params():
    """Get matplotlib parameters.

    Returns
    -------
    params : dict
        Dictionary of matplotlib parameters.

    """

    params = {
        'text.color': '#495057',
        'axes.labelcolor': '#495057',
        'axes.labelsize': 10,
        'axes.edgecolor': '#495057',
        'axes.facecolor': '#f8f9fa',
        'axes.titlesize': 11,
        'xtick.color': '#495057',
        'ytick.color': '#495057',
        'grid.color': '#ebeef0',
        'axes.spines.top': False,
        'axes.spines.right': False,
        'axes.grid': True,
        'grid.linestyle': '-',
        'grid.color': '#ebeef0',
        'legend.facecolor': 'white',
        'legend.framealpha': 0.75,
        'legend.loc': 'upper right',
        'legend.fontsize': 8
    }

    return params

[docs]def color_palette(idx): """Color palette to use throughout the biosppy package Parameters ---------- idx: str or int identifier of color to use Returns ------- color_id: str hexadecimal color code chosen """ color_dict = { 'blue': '#71a7cc', 'dark-blue': '#184169', 'light-blue': '#a5c8e0', 'green': '#329352', 'dark-green': '#154d28', 'light-green': '#7ac77d', 'red': '#D62839', 'dark-red': '#B14343', 'light-red': '#FF5A5F', 'yellow': '#D19C2F', 'dark-yellow': '#FFC300', 'light-yellow': '#FFDD55', 'violet': '#9D4EDD', 'dark-violet': '#3C096C', 'light-violet': '#E0AAFF', 'orange': '#f3883e', 'dark-orange': '#b2400a', 'light-orange': '#f7d1ab', 'grey': '#ADB5BD', 'dark-grey': '#495057', 'light-grey': '#E6E6E6' } if type(idx) == int: color_id = list(color_dict.values())[idx] else: if idx in color_dict.keys(): color_id = color_dict[idx] else: raise ValueError(f'Please choose one color from {color_dict.keys()}' f' or give an index') return color_id
def _plot_filter(b, a, sampling_rate=1000., nfreqs=4096, log_xscale=True, ax=None): """Compute and plot the frequency response of a digital filter. Parameters ---------- b : array Numerator coefficients. a : array Denominator coefficients. sampling_rate : int, float, optional Sampling frequency (Hz). nfreqs : int, optional Number of frequency points to compute. log_xscale : bool, optional Whether to use log scale for x-axis. ax : axis, optional Plot Axis to use. Returns ------- fig : Figure Figure object. """ # compute frequency response freqs, resp = st._filter_resp(b, a, sampling_rate=sampling_rate, nfreqs=nfreqs) # get matplotlib parameters plt.rcParams.update(_get_params()) # plot if ax is None: fig = plt.figure(figsize=(8, 4)) ax = fig.add_subplot(211) ax2 = fig.add_subplot(212) else: ax2 = ax.twinx() # set layout fig.subplots_adjust(top=0.88, bottom=0.17, hspace=0.2, left=0.16, right=0.94) # title fig.suptitle('Filter Frequency Response', fontsize=12, fontweight='bold') # amplitude pwr = 20. * np.log10(np.abs(resp)) if log_xscale: ax.semilogx(freqs, pwr, color=color_palette('blue'), linewidth=MAJOR_LW) else: ax.plot(freqs, pwr, color=color_palette('blue'), linewidth=MAJOR_LW) ax.set_ylabel('Amplitude (dB)') ax.grid(True) ax.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax.spines['bottom'].set_visible(False) # phase angles = np.unwrap(np.angle(resp)) * 180. / np.pi # radians to degrees if log_xscale: ax2.semilogx(freqs, angles, color=color_palette('dark-blue'), linewidth=MAJOR_LW) else: ax2.plot(freqs, angles, color=color_palette('dark-blue'), linewidth=MAJOR_LW) ax2.set_ylabel('Angle (degrees)') ax2.set_xlabel('Frequency (Hz)') ax2.grid(True) # align y-axis labels fig.align_ylabels() # add logo add_logo(fig) return fig
[docs]def plot_filter(ftype='FIR', band='lowpass', order=None, frequency=None, sampling_rate=1000., log_xscale=True, path=None, show=True, **kwargs): """Plot the frequency response of the filter specified with the given parameters. Parameters ---------- ftype : str Filter type: * Finite Impulse Response filter ('FIR'); * Butterworth filter ('butter'); * Chebyshev filters ('cheby1', 'cheby2'); * Elliptic filter ('ellip'); * Bessel filter ('bessel'). band : str Band type: * Low-pass filter ('lowpass'); * High-pass filter ('highpass'); * Band-pass filter ('bandpass'); * Band-stop filter ('bandstop'). order : int Order of the filter. frequency : int, float, list, array Cutoff frequencies; format depends on type of band: * 'lowpass' or 'bandpass': single frequency; * 'bandpass' or 'bandstop': pair of frequencies. sampling_rate : int, float, optional Sampling frequency (Hz). log_xscale : bool, optional Whether to use log scale for x-axis. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. ``**kwargs`` : dict, optional Additional keyword arguments are passed to the underlying scipy.signal function. """ # get filter b, a = st.get_filter(ftype=ftype, band=band, order=order, frequency=frequency, sampling_rate=sampling_rate, **kwargs) # plot fig = _plot_filter(b, a, sampling_rate, log_xscale=log_xscale) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_spectrum(signal=None, sampling_rate=1000., path=None, show=True): """Plot the power spectrum of a signal (one-sided). Parameters ---------- signal : array Input signal. sampling_rate : int, float, optional Sampling frequency (Hz). path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ freqs, power = st.power_spectrum(signal, sampling_rate, pad=0, pow2=False, decibel=True) # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() ax = fig.add_subplot(111) ax.plot(freqs, power, linewidth=MAJOR_LW) ax.set_xlabel('Frequency (Hz)') ax.set_ylabel('Power (dB)') ax.grid() # make layout tight fig.tight_layout() # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_acc(ts=None, raw=None, vm=None, sm=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.acc.acc. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw ACC signal. vm : array Vector Magnitude feature of the signal. sm : array Signal Magnitude feature of the signal units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ raw_t = np.transpose(raw) acc_x, acc_y, acc_z = raw_t[0], raw_t[1], raw_t[2] # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(10, 5)) fig.suptitle('ACC Summary', fontsize=12, fontweight='bold') gs = gridspec.GridSpec(6, 2) fig.subplots_adjust(top=0.85, hspace=0.7, wspace=0.34, left=0.13, right=0.96, bottom=0.18) # raw signal (acc_x) ax1 = fig.add_subplot(gs[:2, 0]) ax1.set_title("Signal") ax1.plot(ts, acc_x, linewidth=MINOR_LW, label='X', color=color_palette('dark-blue')) ax1.legend(loc='upper right') ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # raw signal (acc_y) ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1) ax2.plot(ts, acc_y, linewidth=MINOR_LW, label='Y', color=color_palette('blue')) ax2.set_ylabel('Amplitude' if units is None else 'Amplitude ($%s$)' % units) ax2.legend(loc='upper right') ax2.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax2.spines['bottom'].set_visible(False) # raw signal (acc_z) ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1) ax3.plot(ts, acc_z, linewidth=MINOR_LW, label='Z', color=color_palette('light-blue')) ax3.set_xlabel('Time (s)') ax3.legend(loc='upper right') # vector magnitude ax4 = fig.add_subplot(gs[:3, 1], sharex=ax1) ax4.set_title('Features') ax4.plot(ts, vm, linewidth=MINOR_LW, label='Vector Magnitude', color=color_palette('orange')) ax4.set_ylabel('Amplitude' if units is None else 'Amplitude ($%s$)' % units) ax4.legend(loc='upper right') ax4.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax4.spines['bottom'].set_visible(False) # signal magnitude ax5 = fig.add_subplot(gs[3:, 1], sharex=ax1) ax5.plot(ts, sm, linewidth=MINOR_LW, label='Signal Magnitude', color=color_palette('light-orange')) ax5.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax5.set_xlabel('Time (s)') ax5.legend(loc='upper right') # align y-axis labels fig.align_ylabels([ax1, ax2, ax3]) fig.align_ylabels([ax4, ax5]) # add logo add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_ppg(ts=None, raw=None, filtered=None, peaks=None, templates_ts=None, templates=None, heart_rate_ts=None, heart_rate=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.ppg.ppg. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw PPG signal. filtered : array Filtered PPG signal. peaks : array Indices of PPG pulse peaks. templates_ts : array Templates time axis reference (seconds). templates : array Extracted PPG templates. heart_rate_ts : array Heart rate time axis reference (seconds). heart_rate : array Instantaneous heart rate (bpm). units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(10, 5)) fig.suptitle('PPG Summary', fontsize=12, fontweight='bold') gs = gridspec.GridSpec(6, 2) fig.subplots_adjust(top=0.88, bottom=0.12, hspace=0.9, wspace=0.3, left=0.1, right=0.96) # raw signal ax1 = fig.add_subplot(gs[:2, 0]) ax1.set_title('Signal') ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', color=color_palette('light-blue'), alpha=0.6) ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered', color=color_palette('blue')) ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax1.legend() ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # signal with onsets ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1) ax2.set_title('Systolic Peak Detection') ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) ax2.plot(ts[peaks], filtered[peaks], ls='None', marker='x', color=color_palette('dark-red'), label='Peaks') ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax2.legend() ax2.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax2.spines['bottom'].set_visible(False) # heart rate ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1) ax3.set_title('Heart Rate') ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate', color=color_palette('blue')) ax3.set_xlabel('Time (s)') ax3.set_ylabel('Heart Rate (bpm)') # align y-axis labels fig.align_ylabels([ax1, ax2, ax3]) # templates ax4 = fig.add_subplot(gs[1:5, 1]) ax4.plot(templates_ts, templates, linewidth=MINOR_LW, alpha=0.5, color=color_palette('blue')) ax4.set_xlabel('Time (s)') ax4.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax4.set_title('Templates') add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_bvp(ts=None, raw=None, filtered=None, onsets=None, heart_rate_ts=None, heart_rate=None, path=None, show=False): """Create a summary plot from the output of signals.bvp.bvp. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw BVP signal. filtered : array Filtered BVP signal. onsets : array Indices of BVP pulse onsets. heart_rate_ts : array Heart rate time axis reference (seconds). heart_rate : array Instantaneous heart rate (bpm). path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() fig.suptitle('BVP Summary') # raw signal ax1 = fig.add_subplot(311) ax1.plot(ts, raw, linewidth=MAJOR_LW, label='Raw') ax1.set_ylabel('Amplitude') ax1.legend() ax1.grid() # filtered signal with onsets ax2 = fig.add_subplot(312, sharex=ax1) ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MAJOR_LW, label='Filtered') ax2.vlines(ts[onsets], ymin, ymax, color='m', linewidth=MINOR_LW, label='Onsets') ax2.set_ylabel('Amplitude') ax2.legend() ax2.grid() # heart rate ax3 = fig.add_subplot(313, sharex=ax1) ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate') ax3.set_xlabel('Time (s)') ax3.set_ylabel('Heart Rate (bpm)') ax3.legend() ax3.grid() # make layout tight fig.tight_layout() # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_abp(ts=None, raw=None, filtered=None, onsets=None, heart_rate_ts=None, heart_rate=None, path=None, show=False): """Create a summary plot from the output of signals.abp.abp. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw ABP signal. filtered : array Filtered ABP signal. onsets : array Indices of ABP pulse onsets. heart_rate_ts : array Heart rate time axis reference (seconds). heart_rate : array Instantaneous heart rate (bpm). path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() fig.suptitle('ABP Summary') # raw signal ax1 = fig.add_subplot(311) ax1.plot(ts, raw, linewidth=MAJOR_LW, label='Raw') ax1.set_ylabel('Amplitude') ax1.legend() ax1.grid() # filtered signal with onsets ax2 = fig.add_subplot(312, sharex=ax1) ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MAJOR_LW, label='Filtered') ax2.vlines(ts[onsets], ymin, ymax, color='m', linewidth=MINOR_LW, label='Onsets') ax2.set_ylabel('Amplitude') ax2.legend() ax2.grid() # heart rate ax3 = fig.add_subplot(313, sharex=ax1) ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate') ax3.set_xlabel('Time (s)') ax3.set_ylabel('Heart Rate (bpm)') ax3.legend() ax3.grid() # make layout tight fig.tight_layout() # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_eda(ts=None, raw=None, filtered=None, edr=None, edl=None, onsets=None, peaks=None, amplitudes=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.eda.eda. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw EDA signal. filtered : array Filtered EDA signal. edr : array Electrodermal response signal. edl : array Electrodermal level signal. onsets : array Events onsets indices. peaks : array Events peaks indices. amplitudes : array Amplitudes location indices. units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(10, 5)) fig.suptitle('EDA Summary', fontsize=14, fontweight='bold') gs = gridspec.GridSpec(6, 2) fig.subplots_adjust(top=0.85, hspace=0.7, wspace=0.34, left=0.13, right=0.96, bottom=0.18) # signal ax1 = fig.add_subplot(gs[:2, 0]) ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', color=color_palette('light-blue'), alpha=0.6) ax1.plot(ts, filtered, linewidth=MINOR_LW, label='Filtered', color=color_palette('blue')) ax1.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax1.set_title('Signal') ax1.legend() ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # event detection ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1, sharey=ax1) ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) ax2.scatter(ts[onsets], filtered[onsets], marker='|', lw=1, s=100, color=color_palette('dark-red'), label='Onsets', zorder=3) ax2.scatter(ts[peaks], filtered[peaks], marker='x', color=color_palette('dark-red'), label='Peaks', zorder=3) ax2.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax2.set_title('Event Detection') ax2.legend() ax2.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax2.spines['bottom'].set_visible(False) # amplitudes ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1) ax3.plot(ts[peaks], amplitudes, linewidth=MAJOR_LW, color=color_palette('blue'), label='Amplitude') ax3.set_xlabel('Time (s)') ax3.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax3.set_title('Event Amplitudes') # align y axis labels fig.align_ylabels() # decomposition EDL ax4 = fig.add_subplot(gs[0:3, 1], sharex=ax1, sharey=ax1) ax4.plot(ts, filtered, linewidth=MAJOR_LW, color=color_palette('light-blue')) ax4.plot(ts, edl, linewidth=MINOR_LW, color=color_palette('dark-orange'), label="EDL") ax4.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax4.set_title('EDA Decomposition') ax4.legend() ax4.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax4.spines['bottom'].set_visible(False) # decomposition EDR ax5 = fig.add_subplot(gs[3:, 1], sharex=ax1) ax5.plot(ts[1:], edr, linewidth=MINOR_LW, color=color_palette('dark-orange'), label="EDR") ax5.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax5.set_xlabel('Time (s)') ax5.legend() # logo add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_emg(ts=None, sampling_rate=None, raw=None, filtered=None, onsets=None, processed=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.emg.emg. Parameters ---------- ts : array Signal time axis reference (seconds). sampling_rate : int, float Sampling frequency (Hz). raw : array Raw EMG signal. filtered : array Filtered EMG signal. onsets : array Indices of EMG pulse onsets. processed : array, optional Processed EMG signal according to the chosen onset detector. units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(10, 5)) fig.suptitle('EMG Summary', fontsize=12, fontweight='bold') fig.subplots_adjust(top=0.85, hspace=0.25, wspace=0.34, left=0.1, right=0.96, bottom=0.18) if processed is not None: ax1 = fig.add_subplot(311) ax2 = fig.add_subplot(312, sharex=ax1) ax3 = fig.add_subplot(313) # processed signal L = len(processed) T = (L - 1) / sampling_rate ts_processed = np.linspace(0, T, L, endpoint=True) ax3.plot(ts_processed, processed, linewidth=MINOR_LW, label='Processed', color=color_palette('blue')) ax3.set_xlabel('Time (s)') ax3.set_ylabel('Amplitude') ax3.legend(loc='upper right') else: ax1 = fig.add_subplot(211) ax2 = fig.add_subplot(212, sharex=ax1) # signal ax1.set_title('Signal') ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', alpha=0.6, color=color_palette('light-blue')) ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered', color=color_palette('blue')) ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax1.legend(loc='upper right') ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # filtered signal with onsets ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.set_title('Event Detection') ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) ax2.vlines(ts[onsets], ymin, ymax, color=color_palette('orange'), linewidth=MINOR_LW, label='Onsets') ax2.set_xlabel('Time (s)') ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax2.legend(loc='upper right') # align y axis labels fig.align_ylabels() # add logo add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_resp(ts=None, raw=None, filtered=None, zeros=None, resp_rate_ts=None, resp_rate=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.ppg.ppg. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw Resp signal. filtered : array Filtered Resp signal. zeros : array Indices of Respiration zero crossings. resp_rate_ts : array Respiration rate time axis reference (seconds). resp_rate : array Instantaneous respiration rate (Hz). units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(10, 5)) fig.suptitle('RESP Summary', fontsize=12, fontweight='bold') fig.subplots_adjust(top=0.85, hspace=0.35, wspace=0.34, left=0.13, right=0.96, bottom=0.18) # raw signal ax1 = fig.add_subplot(311) ax1.set_title('Signal') ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', alpha=0.6, color=color_palette('light-blue')) ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered', color=color_palette('blue')) ax1.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax1.legend(loc='upper right') ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # filtered signal with zeros ax2 = fig.add_subplot(312, sharex=ax1) ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) ax2.vlines(ts[zeros], ymin, ymax, color=color_palette('dark-red'), linewidth=MINOR_LW, label='Zero crossings') ax2.set_ylabel('Amplitude' if units is None else 'Amplitude \n (%s)' % units) ax2.legend(loc='upper right') ax2.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax2.spines['bottom'].set_visible(False) # respiration rate ax3 = fig.add_subplot(313, sharex=ax1) ax3.set_title('Respiration Rate') ax3.plot(resp_rate_ts, resp_rate, linewidth=MAJOR_LW, color=color_palette('blue')) ax3.set_xlabel('Time (s)') ax3.set_ylabel('Respiration Rate \n(Hz)') # align y axes labels fig.align_ylabels() # add logo add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_eeg(ts=None, raw=None, filtered=None, labels=None, features_ts=None, theta=None, alpha_low=None, alpha_high=None, beta=None, gamma=None, plf_pairs=None, plf=None, path=None, show=False): """Create a summary plot from the output of signals.eeg.eeg. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw EEG signal. filtered : array Filtered EEG signal. labels : list Channel labels. features_ts : array Features time axis reference (seconds). theta : array Average power in the 4 to 8 Hz frequency band; each column is one EEG channel. alpha_low : array Average power in the 8 to 10 Hz frequency band; each column is one EEG channel. alpha_high : array Average power in the 10 to 13 Hz frequency band; each column is one EEG channel. beta : array Average power in the 13 to 25 Hz frequency band; each column is one EEG channel. gamma : array Average power in the 25 to 40 Hz frequency band; each column is one EEG channel. plf_pairs : list PLF pair indices. plf : array PLF matrix; each column is a channel pair. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ nrows = MAX_ROWS alpha = 2. # Get number of channels nch = raw.shape[1] figs = [] # raw fig = _plot_multichannel(ts=ts, signal=raw, labels=labels, nrows=nrows, alpha=alpha, title='EEG Summary - Raw', xlabel='Time (s)', ylabel='Amplitude') figs.append(('_Raw', fig)) # filtered fig = _plot_multichannel(ts=ts, signal=filtered, labels=labels, nrows=nrows, alpha=alpha, title='EEG Summary - Filtered', xlabel='Time (s)', ylabel='Amplitude') figs.append(('_Filtered', fig)) # band-power names = ('Theta Band', 'Lower Alpha Band', 'Higher Alpha Band', 'Beta Band', 'Gamma Band') args = (theta, alpha_low, alpha_high, beta, gamma) for n, a in zip(names, args): fig = _plot_multichannel(ts=features_ts, signal=a, labels=labels, nrows=nrows, alpha=alpha, title='EEG Summary - %s' % n, xlabel='Time (s)', ylabel='Power') figs.append(('_' + n.replace(' ', '_'), fig)) # Only plot/compute plf if there is more than one channel if nch > 1: # PLF plf_labels = ['%s vs %s' % (labels[p[0]], labels[p[1]]) for p in plf_pairs] fig = _plot_multichannel(ts=features_ts, signal=plf, labels=plf_labels, nrows=nrows, alpha=alpha, title='EEG Summary - Phase-Locking Factor', xlabel='Time (s)', ylabel='PLF') figs.append(('_PLF', fig)) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: ext = '.png' for n, fig in figs: path = root + n + ext fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close for _, fig in figs: plt.close(fig)
def _yscaling(signal=None, alpha=1.5): """Get y axis limits for a signal with scaling. Parameters ---------- signal : array Input signal. alpha : float, optional Scaling factor. Returns ------- ymin : float Minimum y value. ymax : float Maximum y value. """ mi = np.min(signal) m = np.mean(signal) mx = np.max(signal) if mi == mx: ymin = m - 1 ymax = m + 1 else: ymin = m - alpha * (m - mi) ymax = m + alpha * (mx - m) return ymin, ymax def _plot_multichannel(ts=None, signal=None, labels=None, nrows=10, alpha=2., title=None, xlabel=None, ylabel=None): """Plot a multi-channel signal. Parameters ---------- ts : array Signal time axis reference (seconds). signal : array Multi-channel signal; each column is one channel. labels : list, optional Channel labels. nrows : int, optional Maximum number of rows to use. alpha : float, optional Scaling factor for y axis. title : str, optional Plot title. xlabel : str, optional Label for x axis. ylabel : str, optional Label for y axis. Returns ------- fig : Figure Figure object. """ # ensure numpy signal = np.array(signal) nch = signal.shape[1] # check labels if labels is None: labels = ['Ch. %d' % i for i in range(nch)] if nch < nrows: nrows = nch ncols = int(np.ceil(nch / float(nrows))) # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() # title if title is not None: fig.suptitle(title) gs = gridspec.GridSpec(nrows, ncols, hspace=0, wspace=0.2) # reference axes ax0 = fig.add_subplot(gs[0, 0]) ax0.plot(ts, signal[:, 0], linewidth=MAJOR_LW, label=labels[0]) ymin, ymax = _yscaling(signal[:, 0], alpha=alpha) ax0.set_ylim(ymin, ymax) ax0.legend() ax0.grid() axs = {(0, 0): ax0} for i in range(1, nch - 1): a = i % nrows b = int(np.floor(i / float(nrows))) ax = fig.add_subplot(gs[a, b], sharex=ax0) axs[(a, b)] = ax ax.plot(ts, signal[:, i], linewidth=MAJOR_LW, label=labels[i]) ymin, ymax = _yscaling(signal[:, i], alpha=alpha) ax.set_ylim(ymin, ymax) ax.legend() ax.grid() # last plot i = nch - 1 a = i % nrows b = int(np.floor(i / float(nrows))) ax = fig.add_subplot(gs[a, b], sharex=ax0) axs[(a, b)] = ax ax.plot(ts, signal[:, -1], linewidth=MAJOR_LW, label=labels[-1]) ymin, ymax = _yscaling(signal[:, -1], alpha=alpha) ax.set_ylim(ymin, ymax) ax.legend() ax.grid() if xlabel is not None: ax.set_xlabel(xlabel) for b in range(0, ncols - 1): a = nrows - 1 ax = axs[(a, b)] ax.set_xlabel(xlabel) if ylabel is not None: # middle left a = nrows // 2 ax = axs[(a, 0)] ax.set_ylabel(ylabel) # make layout tight gs.tight_layout(fig) return fig
[docs]def plot_ecg(ts=None, raw=None, filtered=None, rpeaks=None, templates_ts=None, templates=None, heart_rate_ts=None, heart_rate=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.ecg.ecg. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw ECG signal. filtered : array Filtered ECG signal. rpeaks : array R-peak location indices. templates_ts : array Templates time axis reference (seconds). templates : array Extracted heartbeat templates. heart_rate_ts : array Heart rate time axis reference (seconds). heart_rate : array Instantaneous heart rate (bpm). units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(10, 5)) fig.suptitle('ECG Summary', fontsize=12, fontweight='bold') gs = gridspec.GridSpec(6, 2) fig.subplots_adjust(top=0.88, bottom=0.12, hspace=0.9, wspace=0.3, left=0.1, right=0.96) # signal ax1 = fig.add_subplot(gs[:2, 0]) ax1.set_title('Signal') ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', alpha=0.6, color=color_palette('light-blue')) ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered', color=color_palette('blue')) ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax1.legend(loc='upper right') ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # filtered signal with rpeaks ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1) ax2.set_title('R-Peak Detection') ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) ax2.plot(ts[rpeaks], filtered[rpeaks], ls='None', marker='x', color=color_palette('dark-red'), label='Peaks') ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax2.legend(loc='upper right') ax2.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax2.spines['bottom'].set_visible(False) # heart rate ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1) ax3.set_title('Heart Rate') ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, color=color_palette('blue')) ax3.set_xlabel('Time (s)') ax3.set_ylabel('Heart Rate (bpm)') # align y axis labels fig.align_ylabels([ax1, ax2, ax3]) # templates ax4 = fig.add_subplot(gs[1:5, 1]) ax4.set_title('Templates') ax4.plot(templates_ts, templates.T, linewidth=MINOR_LW, alpha=0.5, color=color_palette('blue')) ax4.set_xlabel('Time (s)') ax4.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) # add logo add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_bcg(ts=None, raw=None, filtered=None, jpeaks=None, templates_ts=None, templates=None, heart_rate_ts=None, heart_rate=None, path=None, show=False): """Create a summary plot from the output of signals.bcg.bcg. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw ECG signal. filtered : array Filtered ECG signal. ipeaks : array I-peak location indices. templates_ts : array Templates time axis reference (seconds). templates : array Extracted heartbeat templates. heart_rate_ts : array Heart rate time axis reference (seconds). heart_rate : array Instantaneous heart rate (bpm). path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() fig.suptitle('BCG Summary') gs = gridspec.GridSpec(6, 2) # raw signal ax1 = fig.add_subplot(gs[:2, 0]) ax1.plot(ts, raw, linewidth=MAJOR_LW, label='Raw') ax1.set_ylabel('Amplitude') ax1.legend() ax1.grid() # filtered signal with rpeaks ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1) ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MAJOR_LW, label='Filtered') ax2.vlines(ts[jpeaks], ymin, ymax, color='m', linewidth=MINOR_LW, label='J-peaks') ax2.set_ylabel('Amplitude') ax2.legend() ax2.grid() # heart rate ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1) ax3.plot(heart_rate_ts, heart_rate, linewidth=MAJOR_LW, label='Heart Rate') ax3.set_xlabel('Time (s)') ax3.set_ylabel('Heart Rate (bpm)') ax3.legend() ax3.grid() # templates ax4 = fig.add_subplot(gs[1:5, 1]) ax4.plot(templates_ts, templates.T, 'm', linewidth=MINOR_LW, alpha=0.7) ax4.set_xlabel('Time (s)') ax4.set_ylabel('Amplitude') ax4.set_title('Templates') ax4.grid() # make layout tight gs.tight_layout(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_pcg(ts=None, raw=None, filtered=None, peaks=None, heart_sounds=None, heart_rate_ts=None, inst_heart_rate=None, units=None, path=None, show=False): """Create a summary plot from the output of signals.pcg.pcg. Parameters ---------- ts : array Signal time axis reference (seconds). raw : array Raw PCG signal. filtered : array Filtered PCG signal. peaks : array Peak location indices. heart_sounds : array Classification of peaks as S1 or S2 heart_rate_ts : array Heart rate time axis reference (seconds). inst_heart_rate : array Instantaneous heart rate (bpm). units : str, optional Units of the vertical axis. If provided, the plot title will include the units information. Default is None. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(12, 5)) fig.suptitle('PCG Summary', fontsize=12, fontweight='bold') gs = gridspec.GridSpec(6, 2) fig.subplots_adjust(top=0.88, bottom=0.12, hspace=0.9, wspace=0.3, left=0.1, right=0.96) # signal ax1 = fig.add_subplot(gs[:2, 0]) ax1.set_title('Signal') ax1.plot(ts, raw, linewidth=MED_LW, label='Raw', color=color_palette('light-blue')) ax1.plot(ts, filtered+np.mean(raw), linewidth=MINOR_LW, label='Filtered', color=color_palette('blue')) ax1.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax1.legend(loc='upper right') ax1.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax1.spines['bottom'].set_visible(False) # filtered signal with sounds ax2 = fig.add_subplot(gs[2:4, 0], sharex=ax1) ax2.set_title('Heart Sound Detection') ymin = np.min(filtered) ymax = np.max(filtered) alpha = 0.1 * (ymax - ymin) ymax += alpha ymin -= alpha ax2.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) ax2.vlines(ts[peaks], ymin, ymax, color=color_palette('dark-red'), linewidth=MED_LW, label='Heart Sounds') ax2.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax2.legend(loc='upper right') ax2.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) ax2.spines['bottom'].set_visible(False) # heart rate ax3 = fig.add_subplot(gs[4:, 0], sharex=ax1) ax3.set_title('Heart Rate') ax3.plot(heart_rate_ts, inst_heart_rate, linewidth=MAJOR_LW, color=color_palette('blue')) ax3.set_xlabel('Time (s)') ax3.set_ylabel('Heart Rate (bpm)') # align y axis labels fig.align_ylabels([ax1, ax2, ax3]) # heart sounds ax4 = fig.add_subplot(gs[1:5, 1], sharex=ax1) ax4.plot(ts, filtered, linewidth=MINOR_LW, color=color_palette('blue')) for i in range(0, len(peaks)): text = "S" + str(int(heart_sounds[i])) plt.annotate(text, (ts[peaks[i]], ymax - alpha), ha='center', va='center', size=9, color=color_palette('dark-grey')) ax4.set_xlabel('Time (s)') ax4.set_ylabel('Amplitude' if units is None else 'Amplitude (%s)' % units) ax4.set_title('Heart Sounds Classification') # add logo add_logo(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
def _plot_rates(thresholds, rates, variables, lw=1, colors=None, alpha=1, eer_idx=None, labels=False, ax=None): """Plot biometric rates. Parameters ---------- thresholds : array Classifier thresholds. rates : dict Dictionary of rates. variables : list Keys from 'rates' to plot. lw : int, float, optional Plot linewidth. colors : list, optional Plot line color for each variable. alpha : float, optional Plot line alpha value. eer_idx : int, optional Classifier reference index for the Equal Error Rate. labels : bool, optional If True, will show plot labels. ax : axis, optional Plot Axis to use. Returns ------- fig : Figure Figure object. """ # get matplotlib parameters plt.rcParams.update(_get_params()) if ax is None: fig = plt.figure() ax = fig.add_subplot(111) else: fig = ax.figure if colors is None: x = np.linspace(0., 1., len(variables)) colors = plt.get_cmap('rainbow')(x) if labels: for i, v in enumerate(variables): ax.plot(thresholds, rates[v], colors[i], lw=lw, alpha=alpha, label=v) else: for i, v in enumerate(variables): ax.plot(thresholds, rates[v], colors[i], lw=lw, alpha=alpha) if eer_idx is not None: x, y = rates['EER'][eer_idx] ax.vlines(x, 0, 1, 'r', lw=lw) ax.set_title('EER = %0.2f %%' % (100. * y)) return fig
[docs]def plot_biometrics(assessment=None, eer_idx=None, path=None, show=False): """Create a summary plot of a biometrics test run. Parameters ---------- assessment : dict Classification assessment results. eer_idx : int, optional Classifier reference index for the Equal Error Rate. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() fig.suptitle('Biometrics Summary') c_sub = ['#008bff', '#8dd000'] c_global = ['#0037ff', 'g'] ths = assessment['thresholds'] auth_ax = fig.add_subplot(121) id_ax = fig.add_subplot(122) # subject results for sub in six.iterkeys(assessment['subject']): auth_rates = assessment['subject'][sub]['authentication']['rates'] _ = _plot_rates(ths, auth_rates, ['FAR', 'FRR'], lw=MINOR_LW, colors=c_sub, alpha=0.4, eer_idx=None, labels=False, ax=auth_ax) id_rates = assessment['subject'][sub]['identification']['rates'] _ = _plot_rates(ths, id_rates, ['MR', 'RR'], lw=MINOR_LW, colors=c_sub, alpha=0.4, eer_idx=None, labels=False, ax=id_ax) # global results auth_rates = assessment['global']['authentication']['rates'] _ = _plot_rates(ths, auth_rates, ['FAR', 'FRR'], lw=MAJOR_LW, colors=c_global, alpha=1, eer_idx=eer_idx, labels=True, ax=auth_ax) id_rates = assessment['global']['identification']['rates'] _ = _plot_rates(ths, id_rates, ['MR', 'RR'], lw=MAJOR_LW, colors=c_global, alpha=1, eer_idx=eer_idx, labels=True, ax=id_ax) # set labels and grids auth_ax.set_xlabel('Threshold') auth_ax.set_ylabel('Authentication') auth_ax.grid() auth_ax.legend() id_ax.set_xlabel('Threshold') id_ax.set_ylabel('Identification') id_ax.grid() id_ax.legend() # make layout tight fig.tight_layout() # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_clustering(data=None, clusters=None, path=None, show=False): """Create a summary plot of a data clustering. Parameters ---------- data : array An m by n array of m data samples in an n-dimensional space. clusters : dict Dictionary with the sample indices (rows from `data`) for each cluster. path : str, optional If provided, the plot will be saved to the specified file. show : bool, optional If True, show the plot immediately. """ # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure() fig.suptitle('Clustering Summary') ymin, ymax = _yscaling(data, alpha=1.2) # determine number of clusters keys = list(clusters) nc = len(keys) if nc <= 4: nrows = 2 ncols = 4 else: area = nc + 4 # try to fit to a square nrows = int(np.ceil(np.sqrt(area))) if nrows > MAX_ROWS: # prefer to increase number of columns nrows = MAX_ROWS ncols = int(np.ceil(area / float(nrows))) # plot grid gs = gridspec.GridSpec(nrows, ncols, hspace=0.2, wspace=0.2) # global axes ax_global = fig.add_subplot(gs[:2, :2]) # cluster axes c_grid = np.ones((nrows, ncols), dtype='bool') c_grid[:2, :2] = False c_rows, c_cols = np.nonzero(c_grid) # generate color map x = np.linspace(0., 1., nc) cmap = plt.get_cmap('rainbow') for i, k in enumerate(keys): aux = data[clusters[k]] color = cmap(x[i]) label = 'Cluster %s' % k ax = fig.add_subplot(gs[c_rows[i], c_cols[i]], sharex=ax_global) ax.set_ylim([ymin, ymax]) ax.set_title(label) ax.grid() if len(aux) > 0: ax_global.plot(aux.T, color=color, lw=MINOR_LW, alpha=0.7) ax.plot(aux.T, color=color, lw=MAJOR_LW) ax_global.set_title('All Clusters') ax_global.set_ylim([ymin, ymax]) ax_global.grid() # make layout tight gs.tight_layout(fig) # save to file if path is not None: path = utils.normpath(path) root, ext = os.path.splitext(path) ext = ext.lower() if ext not in ['png', 'jpg']: path = root + '.png' fig.savefig(path, dpi=200, bbox_inches='tight') # show if show: plt.show() else: # close plt.close(fig)
[docs]def plot_rri(rri, rri_trend=None, legends=None, ax=None, show=False): """Plot a series of RR intervals. Parameters ---------- rri : array RR-intervals (ms). rri_trend : array, optional RR-intervals trend (ms). legends : dict, optional Dictionary of features to add to the plot legend. ax : axis, optional Plot Axis to use. show : bool, optional If True, show the plot immediately. """ # time axis t = np.cumsum(rri) / 1000. # plot if ax is None: fig, ax = plt.subplots(1, 1, figsize=(8, 4)) fig.suptitle('HRV - RR Intervals', fontsize=12, fontweight='bold') fig.subplots_adjust(bottom=0.17) add_logo(fig) # plot signal ax.plot(t, rri, color=color_palette('blue'), linewidth=MAJOR_LW, label='RR Intervals') ax.set_ylabel('RRI (ms)') ax.set_xlabel('Time (s)') if rri_trend is not None: ax.plot(t, rri_trend, color=color_palette('dark-red'), alpha=0.5, linewidth=MED_LW, label='Trend') # plot legend if legends is not None: handles, labels = ax.get_legend_handles_labels() for key, value in legends.items(): new_patch = patches.Patch(color='white', alpha=0) handles.extend([new_patch]) labels.extend(['%s = %.2f %s' % (key, value[0], value[1])]) try: pos = ax.get_position() ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height]) ax.legend(handles=handles, labels=labels, loc='upper left', bbox_to_anchor=(1.01, 1.02), frameon=False) except ValueError: pass else: ax.legend(loc='upper right') # show if show: plt.show()
[docs]def plot_poincare(rri=None, s=None, sd1=None, sd2=None, legends=None, ax=None, show=False): """Plot a Poincaré plot of a series of RR intervals (RRI[i+1] vs. RRI[i]) from the output of signals.hrv.compute_poincare. Parameters ---------- rri : array RR-intervals (ms). s : float S - Area of the ellipse of the Poincaré plot (ms^2). sd1 : float SD1 - Poincaré plot standard deviation perpendicular to the identity line (ms). sd2 : float SD2 - Poincaré plot standard deviation along the identity line (ms). legends : dict, optional Dictionary of features to add to the plot legend. ax : axis, optional Plot Axis to use. show : bool, optional If True, show the plot immediately. """ x, y = rri[:-1], rri[1:] rr_mean = rri.mean() if ax is None: fig, ax = plt.subplots(figsize=(7, 5)) fig.suptitle('HRV - Poincaré Plot', fontsize=12, fontweight='bold') # add logo add_logo(fig) ax.set_xlabel('$RR_i$ (ms)') ax.set_ylabel('$RR_{i+1}$ (ms)') # plot Poincaré data points ax.scatter(x, y, marker='.', color=color_palette('blue'), alpha=0.5, s=100, zorder=1) ax.set_xlim([np.min(rri) - 50, np.max(rri) + 50]) ax.set_ylim([np.min(rri) - 50, np.max(rri) + 50]) ax.set_aspect(1. / ax.get_data_ratio()) # draw identity line (RRi+1=RRi) lims = [np.min([ax.get_xlim(), ax.get_ylim()]), # min of both axes np.max([ax.get_xlim(), ax.get_ylim()])] # max of both axes ax.plot(lims, lims, linewidth=0.7, color=color_palette('grey'), linestyle='--', zorder=2, label='Identity line') # draw ellipse ellipse = patches.Ellipse((rr_mean, rr_mean), sd1 * 2, sd2 * 2, angle=-45, linewidth=1, edgecolor=color_palette('dark-grey'), facecolor='None', label='S = %.1f ms$^2$' % s, zorder=3) ax.add_artist(ellipse) # draw SD1 and SD2 ax.arrow(rr_mean, rr_mean, sd1 * np.cos(3 * np.pi / 4), sd1 * np.sin(3 * np.pi / 4), facecolor=color_palette('dark-red'), edgecolor=color_palette('dark-red'), linewidth=2, length_includes_head=True, head_width=4, head_length=4, label='SD1 = %.1f ms' % sd1, zorder=3) ax.arrow(rr_mean, rr_mean, sd2 * np.cos(np.pi / 4), sd2 * np.sin(np.pi / 4), facecolor=color_palette('yellow'), edgecolor=color_palette('yellow'), linewidth=2, length_includes_head=True, head_width=4, head_length=4, label='SD2 = %.1f ms' % sd2, zorder=3) # draw SD1 and SD2 axes f = 1.25 # scaling factor ax.add_artist( lines.Line2D([rr_mean - f * sd1 * np.cos(3 * np.pi / 4), rr_mean + f * sd1 * np.cos(3 * np.pi / 4)], [rr_mean + f * sd1 * np.cos(3 * np.pi / 4), rr_mean - f * sd1 * np.cos(3 * np.pi / 4)], lw=1, color='0.2')) ax.add_artist(lines.Line2D([rr_mean - f * sd2 * np.cos(np.pi / 4), rr_mean + f * sd2 * np.cos(np.pi / 4)], [rr_mean - f * sd2 * np.cos(np.pi / 4), rr_mean + f * sd2 * np.cos(np.pi / 4)], lw=1, color='0.2')) # adjust grid ax.set_axisbelow(True) # add extra labels # update figure legend handles, labels = ax.get_legend_handles_labels() if legends is not None: for key, value in legends.items(): if value is not None: new_patch = patches.Patch(color='white', alpha=0) handles.extend([new_patch]) labels.extend(['%s = %.2f' % (key, value)]) # change handle for ellipse and arrows handles[1] = plt.Line2D([], [], color=color_palette('dark-grey'), marker="o", markersize=10, linewidth=0, markerfacecolor='none') # ellipse handles[2] = plt.Line2D([], [], color=color_palette('dark-red'), linestyle="-", linewidth=1) # SD1 handles[3] = plt.Line2D([], [], color=color_palette('yellow'), linestyle="-", linewidth=1) # SD2 # create legend pos = ax.get_position() ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height]) ax.legend(handles=handles, labels=labels, loc='upper left', bbox_to_anchor=(1.01, 1.02), frameon=False) # show if show: plt.show()
[docs]def plot_hrv_hist(rri=None, bins=None, q_hist=None, hti=None, tinn=None, ax=None, show=False): """Plots the RRI histogram with the corresponding geometrical HRV features from the output of signals.hrv.compute_geometrical. Parameters ---------- rri : array RR-intervals (ms). bins : array Histogram bins. q_hist : array Multilinear function fitted to the histogram. hti : float HTI - HRV triangular index - Integral of the density of the RR interval histogram divided by its height. tinn : float TINN - Baseline width of RR interval histogram (ms). ax : axis, optional Plot Axis to use. show : bool, optional If True, show the plot immediately. """ # plot histogram and triangle if ax is None: fig, ax = plt.subplots() fig.suptitle('HRV - RRI Distribution', fontsize=12, fontweight='bold') # add logo add_logo(fig) ax.hist(rri, bins, facecolor=color_palette('light-blue'), edgecolor=color_palette('dark-grey'), label='HTI: %.1f' % hti) ax.plot(bins, q_hist, color=color_palette('dark-red'), linewidth=1.5, label='TINN: %.1f ms' % tinn) ax.set_xlabel('RR Interval (ms)') ax.set_ylabel('Count') ax.locator_params(axis='y', integer=True) pos = ax.get_position() ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height]) ax.legend(loc='upper left', bbox_to_anchor=(1.01, 1.02), frameon=False) # adjust grid to be in the background ax.set_axisbelow(True) # show if show: plt.show()
[docs]def plot_hrv_fbands(frequencies=None, powers=None, fbands=None, method_name=None, legends=None, ax=None, show=False): """Plots the power spectrum and highlights the defined frequency bands from the output of signals.hrv.compute_fbands. Parameters ---------- frequencies : array Frequency axis. powers : array Power spectrum values for the frequency axis. fbands : dict, optional Dictionary containing the limits of the frequency bands. method_name : str, optional Method that was used to compute the power spectrum. legends : dict, optional Additional legend elements. ax : axis, optional Plot Axis to use. show : bool, optional If True, show the plot immediately. """ spectrum_colors = {'ulf': '#e6eff6', 'vlf': '#89b4c4', 'lf': '#548999', 'hf': '#f1d3a1', 'vhf': '#e3dbd9' } # convert power values powers = powers / (10 ** 6) # to s^2/Hz # initialize plot if ax is None: fig, ax = plt.subplots() fig.suptitle('HRV - Power Spectral Density', fontsize=12, fontweight='bold') # add logo add_logo(fig) # figure attributes if method_name is not None: ax.set_title(f'({method_name})') ax.set_xlabel('Frequency (Hz)') ax.set_ylabel('Power (s$^2$/Hz)') ax.set_xlim([0, 0.5]) # plot spectrum ax.plot(frequencies, powers, linewidth=1, color='0.2') ax.margins(0) # plot frequency bands for fband in fbands.keys(): band = np.argwhere((frequencies >= fbands[fband][0]) & (frequencies <= fbands[fband][-1])).reshape(-1) color = spectrum_colors[fband] if len(band) > 0: ax.fill_between(frequencies[band], powers[band], color=color, label=f'{fband.upper()}: {legends[fband+"_rpwr"]*100:.1f}%') # update figure legend handles, labels = ax.get_legend_handles_labels() if legends.__len__() != 0: for key, value in legends.items(): if not key.endswith('_rpwr') and value is not None: new_patch = patches.Patch(color='white', alpha=0) handles.extend([new_patch]) labels.extend(['%s = %.2f' % (key, value)]) pos = ax.get_position() ax.set_position([pos.x0, pos.y0, pos.width * 0.9, pos.height]) ax.legend(handles=handles, labels=labels, loc='upper left', bbox_to_anchor=(1.01, 1.02), frameon=False) # adjust grid ax.set_axisbelow(True) # show if show: plt.show()
[docs]def plot_hrv(rri, rri_trend=None, td_out=None, nl_out=None, fd_out=None, show=False): """Create a summary plot of a HRV analysis. Parameters ---------- rri : array RR-intervals (ms). rri_trend : array, optional RR-intervals trend (ms). td_out : dict Output of signals.hrv.timedomain. nl_out : dict Output of signals.hrv.nonlinear. fd_out : dict Output of signals.hrv.frequencyomain. show : bool, optional If True, show the plot immediately. """ # convert ReturnTuple to dict if td_out is not None: td_out = td_out.as_dict() if nl_out is not None: nl_out = nl_out.as_dict() if fd_out is not None: fd_out = fd_out.as_dict() # plot # get matplotlib parameters plt.rcParams.update(_get_params()) fig = plt.figure(figsize=(12, 6)) fig.suptitle('HRV Summary', fontsize=12, fontweight='bold') gs = gridspec.GridSpec(6, 2) # plot rri and display time domain features ax1 = fig.add_subplot(gs[:2, 0]) time_legends = {'Mean': (td_out['rr_mean'], 'ms'), 'Median': (td_out['rr_median'], 'ms'), 'MinMax': (td_out['rr_minmax'], 'ms'), 'SDNN': (td_out['sdnn'], 'ms'), 'RMSSD': (td_out['rmssd'], 'ms'), 'pNN50': (td_out['pnn50'], '%'), } plot_rri(rri=rri, rri_trend=rri_trend, legends=time_legends, ax=ax1, show=False) ax1.set_title('Time Domain', fontweight='bold') # plot hrv hist ax2 = fig.add_subplot(gs[2:, 0]) bins = td_out['bins'] q_hist = td_out['q_hist'] hti = td_out['hti'] tinn = td_out['tinn'] plot_hrv_hist(rri=rri, bins=bins, q_hist=q_hist, hti=hti, tinn=tinn, ax=ax2, show=False) # plot pointcare and display non-linear features ax3 = fig.add_subplot(gs[:3, 1]) ax3.set_title('Non-linear Domain', fontweight='bold') rri_pc = rri - rri_trend if rri_trend is not None else rri s = nl_out['s'] sd1 = nl_out['sd1'] sd2 = nl_out['sd2'] poincare_legend = {'SD1/SD2': nl_out['sd12'], 'SD2/SD1': nl_out['sd21'], 'SampEn': nl_out['sampen'] if 'sampen' in nl_out.keys() else None, 'AppEn': nl_out['appen'] if 'appen' in nl_out.keys() else None, } plot_poincare(rri=rri_pc, s=s, sd1=sd1, sd2=sd2, legends=poincare_legend, ax=ax3, show=False) # plot hrv fbands ax4 = fig.add_subplot(gs[3:, 1]) frequencies = fd_out['frequencies'] powers = fd_out['powers'] fbands = fd_out['fbands'] method_name = fd_out['freq_method'] freq_legends = {'LF/HF': fd_out['lf_hf']} for key in fd_out.keys(): if key.endswith('_rpwr'): freq_legends[key] = fd_out[key] plot_hrv_fbands(frequencies=frequencies, powers=powers, fbands=fbands, method_name=method_name, legends=freq_legends, ax=ax4, show=False) ax4.set_title('Frequency Domain', fontweight='bold') # tight layout gs.tight_layout(fig) # adjust margins gs.update(right=0.88, bottom=0.14) # add logo add_logo(fig) # show if show: plt.show()